From 4ba47d2d2163ea77aacc9f719db91af2d7298905 Mon Sep 17 00:00:00 2001 From: guybe7 Date: Sat, 11 Mar 2023 09:14:16 +0100 Subject: [PATCH] Add reply_schema to command json files (internal for now) (#10273) Work in progress towards implementing a reply schema as part of COMMAND DOCS, see #9845 Since ironing the details of the reply schema of each and every command can take a long time, we would like to merge this PR when the infrastructure is ready, and let this mature in the unstable branch. Meanwhile the changes of this PR are internal, they are part of the repo, but do not affect the produced build. ### Background In #9656 we add a lot of information about Redis commands, but we are missing information about the replies ### Motivation 1. Documentation. This is the primary goal. 2. It should be possible, based on the output of COMMAND, to be able to generate client code in typed languages. In order to do that, we need Redis to tell us, in detail, what each reply looks like. 3. We would like to build a fuzzer that verifies the reply structure (for now we use the existing testsuite, see the "Testing" section) ### Schema The idea is to supply some sort of schema for the various replies of each command. The schema will describe the conceptual structure of the reply (for generated clients), as defined in RESP3. Note that the reply structure itself may change, depending on the arguments (e.g. `XINFO STREAM`, with and without the `FULL` modifier) We decided to use the standard json-schema (see https://json-schema.org/) as the reply-schema. Example for `BZPOPMIN`: ``` "reply_schema": { "oneOf": [ { "description": "Timeout reached and no elements were popped.", "type": "null" }, { "description": "The keyname, popped member, and its score.", "type": "array", "minItems": 3, "maxItems": 3, "items": [ { "description": "Keyname", "type": "string" }, { "description": "Member", "type": "string" }, { "description": "Score", "type": "number" } ] } ] } ``` #### Notes 1. It is ok that some commands' reply structure depends on the arguments and it's the caller's responsibility to know which is the relevant one. this comes after looking at other request-reply systems like OpenAPI, where the reply schema can also be oneOf and the caller is responsible to know which schema is the relevant one. 2. The reply schemas will describe RESP3 replies only. even though RESP3 is structured, we want to use reply schema for documentation (and possibly to create a fuzzer that validates the replies) 3. For documentation, the description field will include an explanation of the scenario in which the reply is sent, including any relation to arguments. for example, for `ZRANGE`'s two schemas we will need to state that one is with `WITHSCORES` and the other is without. 4. For documentation, there will be another optional field "notes" in which we will add a short description of the representation in RESP2, in case it's not trivial (RESP3's `ZRANGE`'s nested array vs. RESP2's flat array, for example) Given the above: 1. We can generate the "return" section of all commands in [redis-doc](https://redis.io/commands/) (given that "description" and "notes" are comprehensive enough) 2. We can generate a client in a strongly typed language (but the return type could be a conceptual `union` and the caller needs to know which schema is relevant). see the section below for RESP2 support. 3. We can create a fuzzer for RESP3. ### Limitations (because we are using the standard json-schema) The problem is that Redis' replies are more diverse than what the json format allows. This means that, when we convert the reply to a json (in order to validate the schema against it), we lose information (see the "Testing" section below). The other option would have been to extend the standard json-schema (and json format) to include stuff like sets, bulk-strings, error-string, etc. but that would mean also extending the schema-validator - and that seemed like too much work, so we decided to compromise. Examples: 1. We cannot tell the difference between an "array" and a "set" 2. We cannot tell the difference between simple-string and bulk-string 3. we cannot verify true uniqueness of items in commands like ZRANGE: json-schema doesn't cover the case of two identical members with different scores (e.g. `[["m1",6],["m1",7]]`) because `uniqueItems` compares (member,score) tuples and not just the member name. ### Testing This commit includes some changes inside Redis in order to verify the schemas (existing and future ones) are indeed correct (i.e. describe the actual response of Redis). To do that, we added a debugging feature to Redis that causes it to produce a log of all the commands it executed and their replies. For that, Redis needs to be compiled with `-DLOG_REQ_RES` and run with `--reg-res-logfile --client-default-resp 3` (the testsuite already does that if you run it with `--log-req-res --force-resp3`) You should run the testsuite with the above args (and `--dont-clean`) in order to make Redis generate `.reqres` files (same dir as the `stdout` files) which contain request-response pairs. These files are later on processed by `./utils/req-res-log-validator.py` which does: 1. Goes over req-res files, generated by redis-servers, spawned by the testsuite (see logreqres.c) 2. For each request-response pair, it validates the response against the request's reply_schema (obtained from the extended COMMAND DOCS) 5. In order to get good coverage of the Redis commands, and all their different replies, we chose to use the existing redis test suite, rather than attempt to write a fuzzer. #### Notes about RESP2 1. We will not be able to use the testing tool to verify RESP2 replies (we are ok with that, it's time to accept RESP3 as the future RESP) 2. Since the majority of the test suite is using RESP2, and we want the server to reply with RESP3 so that we can validate it, we will need to know how to convert the actual reply to the one expected. - number and boolean are always strings in RESP2 so the conversion is easy - objects (maps) are always a flat array in RESP2 - others (nested array in RESP3's `ZRANGE` and others) will need some special per-command handling (so the client will not be totally auto-generated) Example for ZRANGE: ``` "reply_schema": { "anyOf": [ { "description": "A list of member elements", "type": "array", "uniqueItems": true, "items": { "type": "string" } }, { "description": "Members and their scores. Returned in case `WITHSCORES` was used.", "notes": "In RESP2 this is returned as a flat array", "type": "array", "uniqueItems": true, "items": { "type": "array", "minItems": 2, "maxItems": 2, "items": [ { "description": "Member", "type": "string" }, { "description": "Score", "type": "number" } ] } } ] } ``` ### Other changes 1. Some tests that behave differently depending on the RESP are now being tested for both RESP, regardless of the special log-req-res mode ("Pub/Sub PING" for example) 2. Update the history field of CLIENT LIST 3. Added basic tests for commands that were not covered at all by the testsuite ### TODO - [x] (maybe a different PR) add a "condition" field to anyOf/oneOf schemas that refers to args. e.g. when `SET` return NULL, the condition is `arguments.get||arguments.condition`, for `OK` the condition is `!arguments.get`, and for `string` the condition is `arguments.get` - https://github.com/redis/redis/issues/11896 - [x] (maybe a different PR) also run `runtest-cluster` in the req-res logging mode - [x] add the new tests to GH actions (i.e. compile with `-DLOG_REQ_RES`, run the tests, and run the validator) - [x] (maybe a different PR) figure out a way to warn about (sub)schemas that are uncovered by the output of the tests - https://github.com/redis/redis/issues/11897 - [x] (probably a separate PR) add all missing schemas - [x] check why "SDOWN is triggered by misconfigured instance replying with errors" fails with --log-req-res - [x] move the response transformers to their own file (run both regular, cluster, and sentinel tests - need to fight with the tcl including mechanism a bit) - [x] issue: module API - https://github.com/redis/redis/issues/11898 - [x] (probably a separate PR): improve schemas: add `required` to `object`s - https://github.com/redis/redis/issues/11899 Co-authored-by: Ozan Tezcan Co-authored-by: Hanna Fadida Co-authored-by: Oran Agra Co-authored-by: Shaya Potter --- .github/workflows/ci.yml | 1 + .github/workflows/daily.yml | 43 ++- .github/workflows/reply-schemas-linter.yml | 22 ++ src/Makefile | 14 +- src/blocked.c | 7 + src/commands/acl-cat.json | 18 + src/commands/acl-deluser.json | 4 + src/commands/acl-dryrun.json | 12 + src/commands/acl-genpass.json | 4 + src/commands/acl-getuser.json | 59 ++- src/commands/acl-help.json | 9 +- src/commands/acl-list.json | 9 +- src/commands/acl-load.json | 5 +- src/commands/acl-log.json | 48 +++ src/commands/acl-save.json | 5 +- src/commands/acl-setuser.json | 3 + src/commands/acl-users.json | 9 +- src/commands/acl-whoami.json | 6 +- src/commands/append.json | 4 + src/commands/asking.json | 5 +- src/commands/auth.json | 3 + src/commands/bgrewriteaof.json | 6 +- src/commands/bgsave.json | 12 +- src/commands/bitcount.json | 7 +- src/commands/bitfield.json | 17 +- src/commands/bitfield_ro.json | 9 +- src/commands/bitop.json | 7 +- src/commands/bitpos.json | 15 +- src/commands/blmove.json | 12 + src/commands/blmpop.json | 28 ++ src/commands/blpop.json | 24 ++ src/commands/brpop.json | 25 +- src/commands/brpoplpush.json | 12 + src/commands/bzmpop.json | 40 ++ src/commands/bzpopmax.json | 28 ++ src/commands/bzpopmin.json | 28 ++ src/commands/client-caching.json | 3 + src/commands/client-getname.json | 14 +- src/commands/client-getredir.json | 19 +- src/commands/client-help.json | 9 +- src/commands/client-id.json | 6 +- src/commands/client-info.json | 6 +- src/commands/client-kill.json | 15 +- src/commands/client-list.json | 4 + src/commands/client-no-evict.json | 5 +- src/commands/client-no-touch.json | 3 + src/commands/client-pause.json | 5 +- src/commands/client-reply.json | 4 + src/commands/client-setname.json | 5 +- src/commands/client-tracking.json | 6 +- src/commands/client-trackinginfo.json | 62 +++- src/commands/client-unblock.json | 12 + src/commands/client-unpause.json | 5 +- src/commands/cluster-addslots.json | 5 +- src/commands/cluster-addslotsrange.json | 5 +- src/commands/cluster-bumpepoch.json | 16 +- .../cluster-count-failure-reports.json | 7 +- src/commands/cluster-countkeysinslot.json | 7 +- src/commands/cluster-delslots.json | 5 +- src/commands/cluster-delslotsrange.json | 5 +- src/commands/cluster-failover.json | 5 +- src/commands/cluster-flushslots.json | 5 +- src/commands/cluster-forget.json | 5 +- src/commands/cluster-getkeysinslot.json | 10 +- src/commands/cluster-help.json | 9 +- src/commands/cluster-info.json | 6 +- src/commands/cluster-keyslot.json | 7 +- src/commands/cluster-links.json | 45 ++- src/commands/cluster-meet.json | 5 +- src/commands/cluster-myid.json | 6 +- src/commands/cluster-myshardid.json | 6 +- src/commands/cluster-nodes.json | 6 +- src/commands/cluster-replicas.json | 10 +- src/commands/cluster-replicate.json | 5 +- src/commands/cluster-reset.json | 5 +- src/commands/cluster-saveconfig.json | 5 +- src/commands/cluster-set-config-epoch.json | 5 +- src/commands/cluster-setslot.json | 5 +- src/commands/cluster-shards.json | 73 +++- src/commands/cluster-slaves.json | 10 +- src/commands/cluster-slots.json | 105 +++++- src/commands/command-count.json | 6 +- src/commands/command-docs.json | 181 +++++++++ src/commands/command-getkeys.json | 8 + src/commands/command-getkeysandflags.json | 24 ++ src/commands/command-help.json | 9 +- src/commands/command-info.json | 179 ++++++++- src/commands/command-list.json | 10 +- src/commands/config-get.json | 6 + src/commands/config-help.json | 9 +- src/commands/config-resetstat.json | 5 +- src/commands/config-rewrite.json | 5 +- src/commands/config-set.json | 3 + src/commands/copy.json | 14 +- src/commands/dbsize.json | 6 +- src/commands/decr.json | 4 + src/commands/decrby.json | 4 + src/commands/del.json | 5 + src/commands/discard.json | 5 +- src/commands/dump.json | 12 + src/commands/echo.json | 4 + src/commands/eval.json | 5 +- src/commands/eval_ro.json | 5 +- src/commands/evalsha.json | 5 +- src/commands/evalsha_ro.json | 5 +- src/commands/exec.json | 14 +- src/commands/exists.json | 4 + src/commands/expire.json | 12 + src/commands/expireat.json | 12 + src/commands/expiretime.json | 17 + src/commands/failover.json | 3 + src/commands/fcall.json | 5 +- src/commands/fcall_ro.json | 5 +- src/commands/flushall.json | 3 + src/commands/flushdb.json | 3 + src/commands/function-delete.json | 5 +- src/commands/function-dump.json | 6 +- src/commands/function-flush.json | 5 +- src/commands/function-help.json | 9 +- src/commands/function-kill.json | 5 +- src/commands/function-list.json | 53 +++ src/commands/function-load.json | 6 +- src/commands/function-restore.json | 5 +- src/commands/function-stats.json | 60 ++- src/commands/geoadd.json | 6 +- src/commands/geodist.json | 15 +- src/commands/geohash.json | 9 +- src/commands/geopos.json | 29 +- src/commands/georadius.json | 61 ++- src/commands/georadius_ro.json | 57 ++- src/commands/georadiusbymember.json | 61 ++- src/commands/georadiusbymember_ro.json | 57 ++- src/commands/geosearch.json | 57 ++- src/commands/geosearchstore.json | 6 +- src/commands/get.json | 12 + src/commands/getbit.json | 11 + src/commands/getdel.json | 12 + src/commands/getex.json | 12 + src/commands/getrange.json | 4 + src/commands/getset.json | 12 + src/commands/hdel.json | 4 + src/commands/hello.json | 48 +++ src/commands/hexists.json | 12 + src/commands/hget.json | 12 + src/commands/hgetall.json | 7 + src/commands/hincrby.json | 4 + src/commands/hincrbyfloat.json | 4 + src/commands/hkeys.json | 8 + src/commands/hlen.json | 4 + src/commands/hmget.json | 15 + src/commands/hmset.json | 3 + src/commands/hrandfield.json | 38 ++ src/commands/hscan.json | 21 +- src/commands/hset.json | 3 + src/commands/hsetnx.json | 12 + src/commands/hstrlen.json | 5 + src/commands/hvals.json | 7 + src/commands/incr.json | 6 +- src/commands/incrby.json | 4 + src/commands/incrbyfloat.json | 4 + src/commands/info.json | 3 + src/commands/keys.json | 9 +- src/commands/lastsave.json | 6 +- src/commands/latency-doctor.json | 6 +- src/commands/latency-graph.json | 6 +- src/commands/latency-help.json | 9 +- src/commands/latency-histogram.json | 24 ++ src/commands/latency-history.json | 21 ++ src/commands/latency-latest.json | 29 +- src/commands/latency-reset.json | 4 + src/commands/lcs.json | 55 +++ src/commands/lindex.json | 12 + src/commands/linsert.json | 17 + src/commands/llen.json | 5 + src/commands/lmove.json | 4 + src/commands/lmpop.json | 28 ++ src/commands/lolwut.json | 4 + src/commands/lpop.json | 20 + src/commands/lpos.json | 20 + src/commands/lpush.json | 4 + src/commands/lpushx.json | 5 + src/commands/lrange.json | 9 +- src/commands/lrem.json | 5 + src/commands/lset.json | 3 + src/commands/ltrim.json | 3 + src/commands/memory-doctor.json | 6 +- src/commands/memory-help.json | 9 +- src/commands/memory-malloc-stats.json | 6 +- src/commands/memory-purge.json | 5 +- src/commands/memory-stats.json | 107 +++++- src/commands/memory-usage.json | 12 + src/commands/mget.json | 15 + src/commands/migrate.json | 12 + src/commands/module-help.json | 9 +- src/commands/module-list.json | 31 +- src/commands/module-load.json | 3 + src/commands/module-loadex.json | 3 + src/commands/module-unload.json | 3 + src/commands/move.json | 14 +- src/commands/mset.json | 3 + src/commands/msetnx.json | 12 + src/commands/multi.json | 5 +- src/commands/object-encoding.json | 14 +- src/commands/object-freq.json | 6 +- src/commands/object-help.json | 9 +- src/commands/object-idletime.json | 6 +- src/commands/object-refcount.json | 6 +- src/commands/persist.json | 12 + src/commands/pexpire.json | 12 + src/commands/pexpireat.json | 12 + src/commands/pexpiretime.json | 17 + src/commands/pfadd.json | 14 +- src/commands/pfcount.json | 6 +- src/commands/pfmerge.json | 5 +- src/commands/pfselftest.json | 5 +- src/commands/ping.json | 12 + src/commands/psetex.json | 3 + src/commands/pttl.json | 17 + src/commands/publish.json | 7 +- src/commands/pubsub-channels.json | 10 +- src/commands/pubsub-help.json | 9 +- src/commands/pubsub-numpat.json | 7 +- src/commands/pubsub-numsub.json | 6 +- src/commands/pubsub-shardchannels.json | 10 +- src/commands/pubsub-shardnumsub.json | 6 +- src/commands/quit.json | 5 +- src/commands/randomkey.json | 14 +- src/commands/readonly.json | 5 +- src/commands/readwrite.json | 5 +- src/commands/rename.json | 5 +- src/commands/renamenx.json | 14 +- src/commands/replconf.json | 5 +- src/commands/replicaof.json | 7 +- src/commands/reset.json | 5 +- src/commands/restore-asking.json | 5 +- src/commands/restore.json | 3 + src/commands/role.json | 115 +++++- src/commands/rpop.json | 19 + src/commands/rpoplpush.json | 12 + src/commands/rpush.json | 5 + src/commands/rpushx.json | 5 + src/commands/sadd.json | 4 + src/commands/save.json | 5 +- src/commands/scan.json | 21 +- src/commands/scard.json | 5 + src/commands/script-debug.json | 5 +- src/commands/script-exists.json | 18 +- src/commands/script-flush.json | 5 +- src/commands/script-help.json | 9 +- src/commands/script-kill.json | 5 +- src/commands/script-load.json | 6 +- src/commands/sdiff.json | 8 + src/commands/sdiffstore.json | 5 + src/commands/select.json | 3 + src/commands/sentinel-ckquorum.json | 5 + src/commands/sentinel-config.json | 66 ++++ src/commands/sentinel-failover.json | 4 + src/commands/sentinel-flushconfig.json | 6 +- .../sentinel-get-master-addr-by-name.json | 16 + src/commands/sentinel-help.json | 9 +- .../sentinel-is-master-down-by-addr.json | 27 ++ src/commands/sentinel-master.json | 7 + src/commands/sentinel-monitor.json | 3 + src/commands/sentinel-remove.json | 3 + src/commands/sentinel-replicas.json | 10 + src/commands/sentinel-set.json | 3 + src/commands/set.json | 20 + src/commands/setbit.json | 11 + src/commands/setex.json | 3 + src/commands/setnx.json | 12 + src/commands/setrange.json | 5 + src/commands/shutdown.json | 6 +- src/commands/sinter.json | 8 + src/commands/sintercard.json | 5 + src/commands/sinterstore.json | 5 + src/commands/sismember.json | 12 + src/commands/slaveof.json | 7 +- src/commands/slowlog-get.json | 41 ++ src/commands/slowlog-help.json | 9 +- src/commands/slowlog-len.json | 7 +- src/commands/slowlog-reset.json | 5 +- src/commands/smembers.json | 8 + src/commands/smismember.json | 17 + src/commands/smove.json | 12 + src/commands/sort.json | 26 +- src/commands/sort_ro.json | 9 +- src/commands/spop.json | 20 + src/commands/spublish.json | 7 +- src/commands/srandmember.json | 27 +- src/commands/srem.json | 5 + src/commands/sscan.json | 21 +- src/commands/strlen.json | 5 + src/commands/substr.json | 4 + src/commands/sunion.json | 8 + src/commands/sunionstore.json | 5 + src/commands/swapdb.json | 5 +- src/commands/time.json | 12 +- src/commands/touch.json | 7 +- src/commands/ttl.json | 17 + src/commands/type.json | 12 + src/commands/unlink.json | 7 +- src/commands/unwatch.json | 5 +- src/commands/wait.json | 5 + src/commands/watch.json | 3 + src/commands/xack.json | 7 +- src/commands/xadd.json | 15 +- src/commands/xautoclaim.json | 77 ++++ src/commands/xclaim.json | 40 +- src/commands/xdel.json | 7 +- src/commands/xgroup-create.json | 5 +- src/commands/xgroup-createconsumer.json | 13 +- src/commands/xgroup-delconsumer.json | 7 +- src/commands/xgroup-destroy.json | 13 +- src/commands/xgroup-help.json | 9 +- src/commands/xgroup-setid.json | 5 +- src/commands/xinfo-consumers.json | 25 +- src/commands/xinfo-groups.json | 42 +++ src/commands/xinfo-help.json | 9 +- src/commands/xinfo-stream.json | 285 ++++++++++++++ src/commands/xlen.json | 7 +- src/commands/xpending.json | 73 ++++ src/commands/xrange.json | 24 ++ src/commands/xread.json | 39 +- src/commands/xreadgroup.json | 44 ++- src/commands/xrevrange.json | 25 +- src/commands/xsetid.json | 5 +- src/commands/xtrim.json | 7 +- src/commands/zadd.json | 20 + src/commands/zcard.json | 4 + src/commands/zcount.json | 4 + src/commands/zdiff.json | 30 ++ src/commands/zdiffstore.json | 4 + src/commands/zincrby.json | 4 + src/commands/zinter.json | 30 ++ src/commands/zintercard.json | 5 + src/commands/zinterstore.json | 5 + src/commands/zlexcount.json | 5 + src/commands/zmpop.json | 39 ++ src/commands/zmscore.json | 16 + src/commands/zpopmax.json | 39 ++ src/commands/zpopmin.json | 39 ++ src/commands/zrandmember.json | 38 ++ src/commands/zrange.json | 32 ++ src/commands/zrangebylex.json | 8 + src/commands/zrangebyscore.json | 34 ++ src/commands/zrangestore.json | 4 + src/commands/zrank.json | 28 +- src/commands/zrem.json | 5 + src/commands/zremrangebylex.json | 4 + src/commands/zremrangebyrank.json | 4 + src/commands/zremrangebyscore.json | 4 + src/commands/zrevrange.json | 32 ++ src/commands/zrevrangebylex.json | 8 + src/commands/zrevrangebyscore.json | 34 ++ src/commands/zrevrank.json | 26 ++ src/commands/zscan.json | 21 +- src/commands/zscore.json | 12 + src/commands/zunion.json | 30 ++ src/commands/zunionstore.json | 4 + src/config.c | 6 + src/logreqres.c | 318 ++++++++++++++++ src/networking.c | 20 + src/replication.c | 6 +- src/sentinel.c | 10 +- src/server.c | 69 ++-- src/server.h | 95 +++-- tests/cluster/tests/00-base.tcl | 8 + .../tests/16-transactions-on-replica.tcl | 6 + tests/instances.tcl | 13 + tests/integration/corrupt-dump.tcl | 2 +- tests/integration/rdb.tcl | 1 + tests/integration/redis-benchmark.tcl | 2 +- tests/modules/blockonkeys.c | 1 + tests/sentinel/tests/07-down-conditions.tcl | 2 + tests/support/redis.tcl | 58 ++- tests/support/response_transformers.tcl | 105 ++++++ tests/support/server.tcl | 14 + tests/test_helper.tcl | 12 +- tests/unit/acl.tcl | 4 + tests/unit/aofrw.tcl | 4 +- tests/unit/client-eviction.tcl | 2 +- tests/unit/cluster/misc.tcl | 11 + tests/unit/geo.tcl | 14 + tests/unit/introspection.tcl | 41 +- tests/unit/moduleapi/basics.tcl | 17 +- tests/unit/moduleapi/blockedclient.tcl | 6 + tests/unit/moduleapi/cmdintrospection.tcl | 3 + tests/unit/moduleapi/reply.tcl | 7 + tests/unit/networking.tcl | 9 +- tests/unit/obuf-limits.tcl | 2 +- tests/unit/other.tcl | 24 ++ tests/unit/protocol.tcl | 9 +- tests/unit/pubsub.tcl | 46 ++- tests/unit/scripting.tcl | 12 +- tests/unit/tracking.tcl | 28 +- tests/unit/type/incr.tcl | 4 + tests/unit/type/list.tcl | 6 +- tests/unit/type/string.tcl | 7 + tests/unit/type/zset.tcl | 26 +- utils/generate-command-code.py | 118 ++++-- utils/reply_schema_linter.js | 31 ++ utils/req-res-log-validator.py | 349 ++++++++++++++++++ utils/req-res-validator/requirements.txt | 2 + 403 files changed, 6762 insertions(+), 314 deletions(-) create mode 100644 .github/workflows/reply-schemas-linter.yml create mode 100644 src/logreqres.c create mode 100644 tests/support/response_transformers.tcl create mode 100644 utils/reply_schema_linter.js create mode 100755 utils/req-res-log-validator.py create mode 100644 utils/req-res-validator/requirements.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6a61b0e02..d2edf2f4b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,3 +75,4 @@ jobs: run: | yum -y install gcc make make REDIS_CFLAGS='-Werror' + diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index 5ed0e7a949..d792ae3ab4 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -11,7 +11,7 @@ on: inputs: skipjobs: description: 'jobs to skip (delete the ones you wanna keep, do not leave empty)' - default: 'valgrind,sanitizer,tls,freebsd,macos,alpine,32bit,iothreads,ubuntu,centos,malloc,specific' + default: 'valgrind,sanitizer,tls,freebsd,macos,alpine,32bit,iothreads,ubuntu,centos,malloc,specific,reply-schema' skiptests: description: 'tests to skip (delete the ones you wanna keep, do not leave empty)' default: 'redis,modules,sentinel,cluster,unittest' @@ -898,3 +898,44 @@ jobs: - name: cluster tests if: true && !contains(github.event.inputs.skiptests, 'cluster') run: ./runtest-cluster ${{github.event.inputs.cluster_test_args}} + + reply-schemas-validator: + runs-on: ubuntu-latest + timeout-minutes: 14400 + if: | + (github.event_name == 'workflow_dispatch' || (github.event_name != 'workflow_dispatch' && github.repository == 'redis/redis')) && + !contains(github.event.inputs.skipjobs, 'reply-schema') + steps: + - name: prep + if: github.event_name == 'workflow_dispatch' + run: | + echo "GITHUB_REPOSITORY=${{github.event.inputs.use_repo}}" >> $GITHUB_ENV + echo "GITHUB_HEAD_REF=${{github.event.inputs.use_git_ref}}" >> $GITHUB_ENV + echo "skipping: ${{github.event.inputs.skipjobs}} and ${{github.event.inputs.skiptests}}" + - uses: actions/checkout@v3 + with: + repository: ${{ env.GITHUB_REPOSITORY }} + ref: ${{ env.GITHUB_HEAD_REF }} + - name: make + run: make REDIS_CFLAGS='-Werror -DLOG_REQ_RES' + - name: testprep + run: sudo apt-get install tcl8.6 tclx + - name: test + if: true && !contains(github.event.inputs.skiptests, 'redis') + run: ./runtest --log-req-res --dont-clean --force-resp3 --tags -slow --verbose --dump-logs ${{github.event.inputs.test_args}} + - name: module api test + if: true && !contains(github.event.inputs.skiptests, 'modules') + run: ./runtest-moduleapi --log-req-res --dont-clean --force-resp3 --dont-pre-clean --verbose --dump-logs ${{github.event.inputs.test_args}} + - name: sentinel tests + if: true && !contains(github.event.inputs.skiptests, 'sentinel') + run: ./runtest-sentinel --log-req-res --dont-clean --force-resp3 ${{github.event.inputs.cluster_test_args}} + - name: cluster tests + if: true && !contains(github.event.inputs.skiptests, 'cluster') + run: ./runtest-cluster --log-req-res --dont-clean --force-resp3 ${{github.event.inputs.cluster_test_args}} + - name: Install Python dependencies + uses: py-actions/py-dependency-install@v4 + with: + path: "./utils/req-res-validator/requirements.txt" + - name: validator + run: ./utils/req-res-log-validator.py --verbose --fail-missing-reply-schemas --fail-commands-not-all-hit + diff --git a/.github/workflows/reply-schemas-linter.yml b/.github/workflows/reply-schemas-linter.yml new file mode 100644 index 0000000000..13fc8ab88d --- /dev/null +++ b/.github/workflows/reply-schemas-linter.yml @@ -0,0 +1,22 @@ +name: Reply-schemas linter + +on: + push: + paths: + - 'src/commands/*.json' + pull_request: + paths: + - 'src/commands/*.json' + +jobs: + reply-schemas-linter: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup nodejs + uses: actions/setup-node@v3 + - name: Install packages + run: npm install ajv + - name: linter + run: node ./utils/reply_schema_linter.js + diff --git a/src/Makefile b/src/Makefile index 3600d33d69..13e1424423 100644 --- a/src/Makefile +++ b/src/Makefile @@ -330,9 +330,17 @@ QUIET_LINK = @printf ' %b %b\n' $(LINKCOLOR)LINK$(ENDCOLOR) $(BINCOLOR)$@$(EN QUIET_INSTALL = @printf ' %b %b\n' $(LINKCOLOR)INSTALL$(ENDCOLOR) $(BINCOLOR)$@$(ENDCOLOR) 1>&2; endif +ifneq (, $(findstring LOG_REQ_RES, $(REDIS_CFLAGS))) + COMMANDS_FILENAME=commands_with_reply_schema + GEN_COMMANDS_FLAGS=--with-reply-schema +else + COMMANDS_FILENAME=commands + GEN_COMMANDS_FLAGS= +endif + REDIS_SERVER_NAME=redis-server$(PROG_SUFFIX) REDIS_SENTINEL_NAME=redis-sentinel$(PROG_SUFFIX) -REDIS_SERVER_OBJ=adlist.o quicklist.o ae.o anet.o dict.o server.o sds.o zmalloc.o lzf_c.o lzf_d.o pqsort.o zipmap.o sha1.o ziplist.o release.o networking.o util.o object.o db.o replication.o rdb.o t_string.o t_list.o t_set.o t_zset.o t_hash.o config.o aof.o pubsub.o multi.o debug.o sort.o intset.o syncio.o cluster.o crc16.o endianconv.o slowlog.o eval.o bio.o rio.o rand.o memtest.o syscheck.o crcspeed.o crc64.o bitops.o sentinel.o notify.o setproctitle.o blocked.o hyperloglog.o latency.o sparkline.o redis-check-rdb.o redis-check-aof.o geo.o lazyfree.o module.o evict.o expire.o geohash.o geohash_helper.o childinfo.o defrag.o siphash.o rax.o t_stream.o listpack.o localtime.o lolwut.o lolwut5.o lolwut6.o acl.o tracking.o socket.o tls.o sha256.o timeout.o setcpuaffinity.o monotonic.o mt19937-64.o resp_parser.o call_reply.o script_lua.o script.o functions.o function_lua.o commands.o strl.o connection.o unix.o +REDIS_SERVER_OBJ=adlist.o quicklist.o ae.o anet.o dict.o server.o sds.o zmalloc.o lzf_c.o lzf_d.o pqsort.o zipmap.o sha1.o ziplist.o release.o networking.o util.o object.o db.o replication.o rdb.o t_string.o t_list.o t_set.o t_zset.o t_hash.o config.o aof.o pubsub.o multi.o debug.o sort.o intset.o syncio.o cluster.o crc16.o endianconv.o slowlog.o eval.o bio.o rio.o rand.o memtest.o syscheck.o crcspeed.o crc64.o bitops.o sentinel.o notify.o setproctitle.o blocked.o hyperloglog.o latency.o sparkline.o redis-check-rdb.o redis-check-aof.o geo.o lazyfree.o module.o evict.o expire.o geohash.o geohash_helper.o childinfo.o defrag.o siphash.o rax.o t_stream.o listpack.o localtime.o lolwut.o lolwut5.o lolwut6.o acl.o tracking.o socket.o tls.o sha256.o timeout.o setcpuaffinity.o monotonic.o mt19937-64.o resp_parser.o call_reply.o script_lua.o script.o functions.o function_lua.o $(COMMANDS_FILENAME).o strl.o connection.o unix.o logreqres.o REDIS_CLI_NAME=redis-cli$(PROG_SUFFIX) REDIS_CLI_OBJ=anet.o adlist.o dict.o redis-cli.o zmalloc.o release.o ae.o redisassert.o crcspeed.o crc64.o siphash.o crc16.o monotonic.o cli_common.o mt19937-64.o strl.o REDIS_BENCHMARK_NAME=redis-benchmark$(PROG_SUFFIX) @@ -425,8 +433,8 @@ DEP = $(REDIS_SERVER_OBJ:%.o=%.d) $(REDIS_CLI_OBJ:%.o=%.d) $(REDIS_BENCHMARK_OBJ # The file commands.c is checked in and doesn't normally need to be rebuilt. It # is built only if python is available and its prereqs are modified. ifneq (,$(PYTHON)) -commands.c: commands/*.json ../utils/generate-command-code.py - $(QUIET_GEN)$(PYTHON) ../utils/generate-command-code.py +$(COMMANDS_FILENAME).c: commands/*.json ../utils/generate-command-code.py + $(QUIET_GEN)$(PYTHON) ../utils/generate-command-code.py $(GEN_COMMANDS_FLAGS) endif clean: diff --git a/src/blocked.c b/src/blocked.c index 5909d25222..753442c2b0 100644 --- a/src/blocked.c +++ b/src/blocked.c @@ -195,6 +195,11 @@ void unblockClient(client *c) { * or in case a shutdown operation was canceled and we are still in the processCommand sequence */ if (!(c->flags & CLIENT_PENDING_COMMAND) && c->bstate.btype != BLOCKED_SHUTDOWN) { freeClientOriginalArgv(c); + /* Clients that are not blocked on keys are not reprocessed so we must + * call reqresAppendResponse here (for clients blocked on key, + * unblockClientOnKey is called, which eventually calls processCommand, + * which calls reqresAppendResponse) */ + reqresAppendResponse(c); resetClient(c); } @@ -612,6 +617,8 @@ static void unblockClientOnKey(client *c, robj *key) { c->bstate.btype == BLOCKED_LIST || c->bstate.btype == BLOCKED_ZSET); + /* We need to unblock the client before calling processCommandAndResetClient + * because it checks the CLIENT_BLOCKED flag */ unblockClient(c); /* In case this client was blocked on keys during command * we need to re process the command again */ diff --git a/src/commands/acl-cat.json b/src/commands/acl-cat.json index a132cbcf41..635e2b88df 100644 --- a/src/commands/acl-cat.json +++ b/src/commands/acl-cat.json @@ -13,6 +13,24 @@ "STALE", "SENTINEL" ], + "reply_schema": { + "anyOf": [ + { + "type": "array", + "description": "In case `categoryname` was not given, a list of existing ACL categories", + "items": { + "type": "string" + } + }, + { + "type": "array", + "description": "In case `categoryname` was given, list of commands that fall under the provided ACL category", + "items": { + "type": "string" + } + } + ] + }, "arguments": [ { "name": "categoryname", diff --git a/src/commands/acl-deluser.json b/src/commands/acl-deluser.json index 3c61557d49..9568d6d4ba 100644 --- a/src/commands/acl-deluser.json +++ b/src/commands/acl-deluser.json @@ -14,6 +14,10 @@ "STALE", "SENTINEL" ], + "reply_schema": { + "type": "integer", + "description": "The number of users that were deleted" + }, "arguments": [ { "name": "username", diff --git a/src/commands/acl-dryrun.json b/src/commands/acl-dryrun.json index 544858c3a0..f8d009d4d2 100644 --- a/src/commands/acl-dryrun.json +++ b/src/commands/acl-dryrun.json @@ -15,6 +15,18 @@ "STALE", "SENTINEL" ], + "reply_schema": { + "anyOf": [ + { + "const": "OK", + "description": "The given user may successfully execute the given command." + }, + { + "type": "string", + "description": "The description of the problem, in case the user is not allowed to run the given command." + } + ] + }, "arguments": [ { "name": "username", diff --git a/src/commands/acl-genpass.json b/src/commands/acl-genpass.json index 9de0313ecb..8af04875ac 100644 --- a/src/commands/acl-genpass.json +++ b/src/commands/acl-genpass.json @@ -13,6 +13,10 @@ "STALE", "SENTINEL" ], + "reply_schema": { + "type": "string", + "description": "Pseudorandom data. By default it contains 64 bytes, representing 256 bits of data. If `bits` was given, the output string length is the number of specified bits (rounded to the next multiple of 4) divided by 4." + }, "arguments": [ { "name": "bits", diff --git a/src/commands/acl-getuser.json b/src/commands/acl-getuser.json index b87c7f6d49..b09b6abebb 100644 --- a/src/commands/acl-getuser.json +++ b/src/commands/acl-getuser.json @@ -29,6 +29,63 @@ "name": "username", "type": "string" } - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "a set of ACL rule definitions for the user", + "type": "object", + "additionalProperties": false, + "properties": { + "flags": { + "type": "array", + "items": { + "type": "string" + } + }, + "passwords": { + "type": "array", + "items": { + "type": "string" + } + }, + "commands": { + "description": "root selector's commands", + "type": "string" + }, + "keys": { + "description": "root selector's keys", + "type": "string" + }, + "channels": { + "description": "root selector's channels", + "type": "string" + }, + "selectors": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "commands": { + "type": "string" + }, + "keys": { + "type": "string" + }, + "channels": { + "type": "string" + } + } + } + } + } + }, + { + "description": "If user does not exist", + "type": "null" + } + ] + } } } diff --git a/src/commands/acl-help.json b/src/commands/acl-help.json index 1cec00a538..f6afea3084 100644 --- a/src/commands/acl-help.json +++ b/src/commands/acl-help.json @@ -11,6 +11,13 @@ "LOADING", "STALE", "SENTINEL" - ] + ], + "reply_schema": { + "type": "array", + "description": "A list of subcommands and their description", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/acl-list.json b/src/commands/acl-list.json index f7a740d9de..cb4de7e9f7 100644 --- a/src/commands/acl-list.json +++ b/src/commands/acl-list.json @@ -13,6 +13,13 @@ "LOADING", "STALE", "SENTINEL" - ] + ], + "reply_schema": { + "type": "array", + "description": "A list of currently active ACL rules", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/acl-load.json b/src/commands/acl-load.json index a4f138e48c..b9d377c2d8 100644 --- a/src/commands/acl-load.json +++ b/src/commands/acl-load.json @@ -13,6 +13,9 @@ "LOADING", "STALE", "SENTINEL" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/acl-log.json b/src/commands/acl-log.json index 0e88ed68b9..eb3a59b0e9 100644 --- a/src/commands/acl-log.json +++ b/src/commands/acl-log.json @@ -20,6 +20,54 @@ "STALE", "SENTINEL" ], + "reply_schema": { + "oneOf": [ + { + "description": "In case `RESET` was not given, a list of recent ACL security events.", + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "count": { + "type": "integer" + }, + "reason": { + "type": "string" + }, + "context": { + "type": "string" + }, + "object": { + "type": "string" + }, + "username": { + "type": "string" + }, + "age-seconds": { + "type": "number" + }, + "client-info": { + "type": "string" + }, + "entry-id": { + "type": "integer" + }, + "timestamp-created": { + "type": "integer" + }, + "timestamp-last-updated": { + "type": "integer" + } + } + } + }, + { + "const": "OK", + "description": "In case `RESET` was given, OK indicates ACL log was cleared." + } + ] + }, "arguments": [ { "name": "operation", diff --git a/src/commands/acl-save.json b/src/commands/acl-save.json index 0c6ee8a1d4..4e2942f5d3 100644 --- a/src/commands/acl-save.json +++ b/src/commands/acl-save.json @@ -13,6 +13,9 @@ "LOADING", "STALE", "SENTINEL" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/acl-setuser.json b/src/commands/acl-setuser.json index 7f1f308dfd..875840d42d 100644 --- a/src/commands/acl-setuser.json +++ b/src/commands/acl-setuser.json @@ -24,6 +24,9 @@ "STALE", "SENTINEL" ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "username", diff --git a/src/commands/acl-users.json b/src/commands/acl-users.json index 5d00edbbf9..13e8eba95a 100644 --- a/src/commands/acl-users.json +++ b/src/commands/acl-users.json @@ -13,6 +13,13 @@ "LOADING", "STALE", "SENTINEL" - ] + ], + "reply_schema": { + "type": "array", + "description": "List of existing ACL users", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/acl-whoami.json b/src/commands/acl-whoami.json index 7c3cc9ace9..b0477b363a 100644 --- a/src/commands/acl-whoami.json +++ b/src/commands/acl-whoami.json @@ -12,6 +12,10 @@ "LOADING", "STALE", "SENTINEL" - ] + ], + "reply_schema": { + "type": "string", + "description": "The username of the current connection." + } } } diff --git a/src/commands/append.json b/src/commands/append.json index 77d170541e..0af5e7bb1d 100644 --- a/src/commands/append.json +++ b/src/commands/append.json @@ -34,6 +34,10 @@ } } ], + "reply_schema": { + "type": "integer", + "description": "The the length of the string after the append operation." + }, "arguments": [ { "name": "key", diff --git a/src/commands/asking.json b/src/commands/asking.json index a825804d1b..432c847092 100644 --- a/src/commands/asking.json +++ b/src/commands/asking.json @@ -11,6 +11,9 @@ ], "acl_categories": [ "CONNECTION" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/auth.json b/src/commands/auth.json index ff5e4b2851..aaad29ad20 100644 --- a/src/commands/auth.json +++ b/src/commands/auth.json @@ -24,6 +24,9 @@ "acl_categories": [ "CONNECTION" ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "username", diff --git a/src/commands/bgrewriteaof.json b/src/commands/bgrewriteaof.json index 27d64dd932..e733512948 100644 --- a/src/commands/bgrewriteaof.json +++ b/src/commands/bgrewriteaof.json @@ -10,6 +10,10 @@ "NO_ASYNC_LOADING", "ADMIN", "NOSCRIPT" - ] + ], + "reply_schema": { + "description": "A simple string reply indicating that the rewriting started or is about to start ASAP", + "type": "string" + } } } diff --git a/src/commands/bgsave.json b/src/commands/bgsave.json index 4b645db4b0..28aa14a02a 100644 --- a/src/commands/bgsave.json +++ b/src/commands/bgsave.json @@ -25,6 +25,16 @@ "optional": true, "since": "3.2.2" } - ] + ], + "reply_schema": { + "oneOf": [ + { + "const": "Background saving started" + }, + { + "const": "Background saving scheduled" + } + ] + } } } diff --git a/src/commands/bitcount.json b/src/commands/bitcount.json index da34eec31f..7906415b6a 100644 --- a/src/commands/bitcount.json +++ b/src/commands/bitcount.json @@ -77,6 +77,11 @@ } ] } - ] + ], + "reply_schema": { + "description": "The number of bits set to 1.", + "type": "integer", + "minimum": 0 + } } } diff --git a/src/commands/bitfield.json b/src/commands/bitfield.json index 1f85382940..9f867ab556 100644 --- a/src/commands/bitfield.json +++ b/src/commands/bitfield.json @@ -139,6 +139,21 @@ } ] } - ] + ], + "reply_schema": { + "type": "array", + "items": { + "oneOf": [ + { + "description": "The result of the subcommand at the same position", + "type": "integer" + }, + { + "description": "In case OVERFLOW FAIL was given and overflows or underflows detected", + "type": "null" + } + ] + } + } } } diff --git a/src/commands/bitfield_ro.json b/src/commands/bitfield_ro.json index 92031564b5..ccf1aae231 100644 --- a/src/commands/bitfield_ro.json +++ b/src/commands/bitfield_ro.json @@ -57,6 +57,13 @@ } ] } - ] + ], + "reply_schema": { + "type": "array", + "items": { + "description": "The result of the subcommand at the same position", + "type": "integer" + } + } } } diff --git a/src/commands/bitop.json b/src/commands/bitop.json index 0ddcaa9746..7c959683ec 100644 --- a/src/commands/bitop.json +++ b/src/commands/bitop.json @@ -89,6 +89,11 @@ "key_spec_index": 1, "multiple": true } - ] + ], + "reply_schema": { + "description": "the size of the string stored in the destination key, that is equal to the size of the longest input string", + "type": "integer", + "minimum": 0 + } } } diff --git a/src/commands/bitpos.json b/src/commands/bitpos.json index 24e357dcc6..439b61246d 100644 --- a/src/commands/bitpos.json +++ b/src/commands/bitpos.json @@ -88,6 +88,19 @@ } ] } - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "the position of the first bit set to 1 or 0 according to the request", + "type": "integer", + "minimum": 0 + }, + { + "description": "In case the `bit` argument is 1 and the string is empty or composed of just zero bytes", + "const": -1 + } + ] + } } } diff --git a/src/commands/blmove.json b/src/commands/blmove.json index 62036147bc..cabcbbc7f1 100644 --- a/src/commands/blmove.json +++ b/src/commands/blmove.json @@ -54,6 +54,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "The popped element.", + "type": "string" + }, + { + "description": "Operation timed-out", + "type": "null" + } + ] + }, "arguments": [ { "name": "source", diff --git a/src/commands/blmpop.json b/src/commands/blmpop.json index 29d381ad86..419eb3e6f0 100644 --- a/src/commands/blmpop.json +++ b/src/commands/blmpop.json @@ -35,6 +35,34 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "Operation timed-out", + "type": "null" + }, + { + "description": "The key from which elements were popped and the popped elements", + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "List key from which elements were popped.", + "type": "string" + }, + { + "description": "Array of popped elements.", + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ] + } + ] + }, "arguments": [ { "name": "timeout", diff --git a/src/commands/blpop.json b/src/commands/blpop.json index 6871654812..63625309b7 100644 --- a/src/commands/blpop.json +++ b/src/commands/blpop.json @@ -41,6 +41,30 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "type": "null", + "description": "No element could be popped and timeout expired" + }, + { + "description": "The key from which the element was popped and the value of the popped element", + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "List key from which the element was popped.", + "type": "string" + }, + { + "description": "Value of the popped element.", + "type": "string" + } + ] + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/brpop.json b/src/commands/brpop.json index 8f65202dd3..faea8b8c08 100644 --- a/src/commands/brpop.json +++ b/src/commands/brpop.json @@ -52,6 +52,29 @@ "name": "timeout", "type": "double" } - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "No element could be popped and the timeout expired.", + "type": "null" + }, + { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "The name of the key where an element was popped ", + "type": "string" + }, + { + "description": "The value of the popped element", + "type": "string" + } + ] + } + ] + } } } diff --git a/src/commands/brpoplpush.json b/src/commands/brpoplpush.json index 7f8d11aba9..e959d0fe15 100644 --- a/src/commands/brpoplpush.json +++ b/src/commands/brpoplpush.json @@ -65,6 +65,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "type": "string", + "description": "The element being popped from source and pushed to destination." + }, + { + "type": "null", + "description": "Timeout is reached." + } + ] + }, "arguments": [ { "name": "source", diff --git a/src/commands/bzmpop.json b/src/commands/bzmpop.json index 87a1cd8b3b..30d502dc30 100644 --- a/src/commands/bzmpop.json +++ b/src/commands/bzmpop.json @@ -35,6 +35,46 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "Timeout reached and no elements were popped.", + "type": "null" + }, + { + "description": "The keyname and the popped members.", + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "Keyname", + "type": "string" + }, + { + "description": "Popped members and their scores.", + "type": "array", + "uniqueItems": true, + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "Member", + "type": "string" + }, + { + "description": "Score", + "type": "number" + } + ] + } + } + ] + } + ] + }, "arguments": [ { "name": "timeout", diff --git a/src/commands/bzpopmax.json b/src/commands/bzpopmax.json index 5ca53aa2a0..f1f6861796 100644 --- a/src/commands/bzpopmax.json +++ b/src/commands/bzpopmax.json @@ -42,6 +42,34 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "Timeout reached and no elements were popped.", + "type": "null" + }, + { + "description": "The keyname, popped member, and its score.", + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": [ + { + "description": "Keyname", + "type": "string" + }, + { + "description": "Member", + "type": "string" + }, + { + "description": "Score", + "type": "number" + } + ] + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/bzpopmin.json b/src/commands/bzpopmin.json index 742a2310ce..312f409e52 100644 --- a/src/commands/bzpopmin.json +++ b/src/commands/bzpopmin.json @@ -42,6 +42,34 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "Timeout reached and no elements were popped.", + "type": "null" + }, + { + "description": "The keyname, popped member, and its score.", + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": [ + { + "description": "Keyname", + "type": "string" + }, + { + "description": "Member", + "type": "string" + }, + { + "description": "Score", + "type": "number" + } + ] + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/client-caching.json b/src/commands/client-caching.json index b8beaa8b65..22172e094d 100644 --- a/src/commands/client-caching.json +++ b/src/commands/client-caching.json @@ -16,6 +16,9 @@ "acl_categories": [ "CONNECTION" ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "mode", diff --git a/src/commands/client-getname.json b/src/commands/client-getname.json index 515e0ed674..c8cb219461 100644 --- a/src/commands/client-getname.json +++ b/src/commands/client-getname.json @@ -15,6 +15,18 @@ ], "acl_categories": [ "CONNECTION" - ] + ], + "reply_schema": { + "oneOf": [ + { + "type": "string", + "description": "The connection name of the current connection" + }, + { + "type": "null", + "description": "Connection name was not set" + } + ] + } } } diff --git a/src/commands/client-getredir.json b/src/commands/client-getredir.json index 8d5b23997c..5cbc27b87a 100644 --- a/src/commands/client-getredir.json +++ b/src/commands/client-getredir.json @@ -15,6 +15,23 @@ ], "acl_categories": [ "CONNECTION" - ] + ], + "reply_schema": { + "oneOf": [ + { + "const": 0, + "description": "Not redirecting notifications to any client." + }, + { + "const": -1, + "description": "Client tracking is not enabled." + }, + { + "type": "integer", + "description": "ID of the client we are redirecting the notifications to.", + "minimum": 1 + } + ] + } } } diff --git a/src/commands/client-help.json b/src/commands/client-help.json index fee49f9b84..ab18a6e6c5 100644 --- a/src/commands/client-help.json +++ b/src/commands/client-help.json @@ -14,6 +14,13 @@ ], "acl_categories": [ "CONNECTION" - ] + ], + "reply_schema": { + "type": "array", + "description": "Helpful text about subcommands.", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/client-id.json b/src/commands/client-id.json index 792da7dbbb..771a3d057f 100644 --- a/src/commands/client-id.json +++ b/src/commands/client-id.json @@ -15,6 +15,10 @@ ], "acl_categories": [ "CONNECTION" - ] + ], + "reply_schema": { + "type": "integer", + "description": "The id of the client" + } } } diff --git a/src/commands/client-info.json b/src/commands/client-info.json index 06fa094bb4..19c0a3cfd4 100644 --- a/src/commands/client-info.json +++ b/src/commands/client-info.json @@ -18,6 +18,10 @@ ], "command_tips": [ "NONDETERMINISTIC_OUTPUT" - ] + ], + "reply_schema": { + "description": "a unique string, as described at the CLIENT LIST page, for the current client", + "type": "string" + } } } diff --git a/src/commands/client-kill.json b/src/commands/client-kill.json index 0d48f858ce..5791eea40a 100644 --- a/src/commands/client-kill.json +++ b/src/commands/client-kill.json @@ -141,6 +141,19 @@ } ] } - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "when called in 3 argument format", + "const": "OK" + }, + { + "description": "when called in filter/value format, the number of clients killed", + "type": "integer", + "minimum": 0 + } + ] + } } } diff --git a/src/commands/client-list.json b/src/commands/client-list.json index e34a3cd63d..5c822e6c37 100644 --- a/src/commands/client-list.json +++ b/src/commands/client-list.json @@ -46,6 +46,10 @@ "command_tips": [ "NONDETERMINISTIC_OUTPUT" ], + "reply_schema": { + "type": "string", + "description": "Information and statistics about client connections" + }, "arguments": [ { "token": "TYPE", diff --git a/src/commands/client-no-evict.json b/src/commands/client-no-evict.json index fc0ad71c94..9cfb066281 100644 --- a/src/commands/client-no-evict.json +++ b/src/commands/client-no-evict.json @@ -34,6 +34,9 @@ } ] } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/client-no-touch.json b/src/commands/client-no-touch.json index f59320f211..03c2ad0dc2 100644 --- a/src/commands/client-no-touch.json +++ b/src/commands/client-no-touch.json @@ -15,6 +15,9 @@ "acl_categories": [ "CONNECTION" ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "enabled", diff --git a/src/commands/client-pause.json b/src/commands/client-pause.json index 57c88951cf..2be85424cb 100644 --- a/src/commands/client-pause.json +++ b/src/commands/client-pause.json @@ -46,6 +46,9 @@ } ] } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/client-reply.json b/src/commands/client-reply.json index 5aa919cca7..51e93c81d7 100644 --- a/src/commands/client-reply.json +++ b/src/commands/client-reply.json @@ -16,6 +16,10 @@ "acl_categories": [ "CONNECTION" ], + "reply_schema": { + "const": "OK", + "description": "When called with either OFF or SKIP subcommands, no reply is made. When called with ON, reply is OK." + }, "arguments": [ { "name": "action", diff --git a/src/commands/client-setname.json b/src/commands/client-setname.json index cc9199fea5..c53bf1651f 100644 --- a/src/commands/client-setname.json +++ b/src/commands/client-setname.json @@ -21,6 +21,9 @@ "name": "connection-name", "type": "string" } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/client-tracking.json b/src/commands/client-tracking.json index 40811f155f..48ed0f7ea0 100644 --- a/src/commands/client-tracking.json +++ b/src/commands/client-tracking.json @@ -71,6 +71,10 @@ "type": "pure-token", "optional": true } - ] + ], + "reply_schema": { + "description": "if the client was successfully put into or taken out of tracking mode", + "const": "OK" + } } } diff --git a/src/commands/client-trackinginfo.json b/src/commands/client-trackinginfo.json index 124c442812..6873c75123 100644 --- a/src/commands/client-trackinginfo.json +++ b/src/commands/client-trackinginfo.json @@ -15,6 +15,66 @@ ], "acl_categories": [ "CONNECTION" - ] + ], + "reply_schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "flags": { + "type": "array", + "items": { + "oneOf": [ + { + "const": "off", + "description": "The connection isn't using server assisted client side caching." + }, + { + "const": "on", + "description": "Server assisted client side caching is enabled for the connection." + }, + { + "const": "bcast", + "description": "The client uses broadcasting mode." + }, + { + "const": "optin", + "description": "The client does not cache keys by default." + }, + { + "const": "optout", + "description": "The client caches keys by default." + }, + { + "const": "caching-yes", + "description": "The next command will cache keys (exists only together with optin)." + }, + { + "const": "caching-no", + "description": "The next command won't cache keys (exists only together with optout)." + }, + { + "const": "noloop", + "description": "The client isn't notified about keys modified by itself." + }, + { + "const": "broken_redirect", + "description": "The client ID used for redirection isn't valid anymore." + } + ] + } + }, + "redirect": { + "type": "integer", + "description": "The client ID used for notifications redirection, or -1 when none." + }, + "prefixes": { + "type": "array", + "description": "List of key prefixes for which notifications are sent to the client.", + "items": { + "type": "string" + } + } + } + } } } diff --git a/src/commands/client-unblock.json b/src/commands/client-unblock.json index 4b37d2fbd3..c96b78d249 100644 --- a/src/commands/client-unblock.json +++ b/src/commands/client-unblock.json @@ -17,6 +17,18 @@ "acl_categories": [ "CONNECTION" ], + "reply_schema": { + "oneOf": [ + { + "const": 0, + "description": "if the client was unblocked successfully" + }, + { + "const": 1, + "description": "if the client wasn't unblocked" + } + ] + }, "arguments": [ { "name": "client-id", diff --git a/src/commands/client-unpause.json b/src/commands/client-unpause.json index 661baa0fd5..186b9cc4a5 100644 --- a/src/commands/client-unpause.json +++ b/src/commands/client-unpause.json @@ -16,6 +16,9 @@ ], "acl_categories": [ "CONNECTION" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/cluster-addslots.json b/src/commands/cluster-addslots.json index 0a2d0a82f6..db06e1ddb0 100644 --- a/src/commands/cluster-addslots.json +++ b/src/commands/cluster-addslots.json @@ -18,6 +18,9 @@ "type": "integer", "multiple": true } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/cluster-addslotsrange.json b/src/commands/cluster-addslotsrange.json index e0bc8f228e..769392bca5 100644 --- a/src/commands/cluster-addslotsrange.json +++ b/src/commands/cluster-addslotsrange.json @@ -28,6 +28,9 @@ } ] } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/cluster-bumpepoch.json b/src/commands/cluster-bumpepoch.json index 66dc28f0a7..ce4af47f3c 100644 --- a/src/commands/cluster-bumpepoch.json +++ b/src/commands/cluster-bumpepoch.json @@ -14,6 +14,20 @@ ], "command_tips": [ "NONDETERMINISTIC_OUTPUT" - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "if the epoch was incremented", + "type": "string", + "pattern": "^BUMPED [0-9]*$" + }, + { + "description": "if the node already has the greatest config epoch in the cluster", + "type": "string", + "pattern": "^STILL [0-9]*$" + } + ] + } } } diff --git a/src/commands/cluster-count-failure-reports.json b/src/commands/cluster-count-failure-reports.json index bf25bc2648..7964d2be63 100644 --- a/src/commands/cluster-count-failure-reports.json +++ b/src/commands/cluster-count-failure-reports.json @@ -19,6 +19,11 @@ "name": "node-id", "type": "string" } - ] + ], + "reply_schema": { + "description": "the number of active failure reports for the node", + "type": "integer", + "minimum": 0 + } } } diff --git a/src/commands/cluster-countkeysinslot.json b/src/commands/cluster-countkeysinslot.json index eefae6e160..c4e7d7d4b8 100644 --- a/src/commands/cluster-countkeysinslot.json +++ b/src/commands/cluster-countkeysinslot.json @@ -15,6 +15,11 @@ "name": "slot", "type": "integer" } - ] + ], + "reply_schema": { + "description": "The number of keys in the specified hash slot", + "type": "integer", + "minimum": 0 + } } } diff --git a/src/commands/cluster-delslots.json b/src/commands/cluster-delslots.json index 89c147b35e..cc96b214ff 100644 --- a/src/commands/cluster-delslots.json +++ b/src/commands/cluster-delslots.json @@ -18,6 +18,9 @@ "type": "integer", "multiple": true } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/cluster-delslotsrange.json b/src/commands/cluster-delslotsrange.json index 68a620d69c..2ecc81701b 100644 --- a/src/commands/cluster-delslotsrange.json +++ b/src/commands/cluster-delslotsrange.json @@ -28,6 +28,9 @@ } ] } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/cluster-failover.json b/src/commands/cluster-failover.json index e7daf241cb..f58fd562a7 100644 --- a/src/commands/cluster-failover.json +++ b/src/commands/cluster-failover.json @@ -30,6 +30,9 @@ } ] } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/cluster-flushslots.json b/src/commands/cluster-flushslots.json index 214aa396c9..7834a4f356 100644 --- a/src/commands/cluster-flushslots.json +++ b/src/commands/cluster-flushslots.json @@ -11,6 +11,9 @@ "NO_ASYNC_LOADING", "ADMIN", "STALE" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/cluster-forget.json b/src/commands/cluster-forget.json index 6668eab21e..1c4e74aa33 100644 --- a/src/commands/cluster-forget.json +++ b/src/commands/cluster-forget.json @@ -17,6 +17,9 @@ "name": "node-id", "type": "string" } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/cluster-getkeysinslot.json b/src/commands/cluster-getkeysinslot.json index 06c1b03c8d..6aa90234e3 100644 --- a/src/commands/cluster-getkeysinslot.json +++ b/src/commands/cluster-getkeysinslot.json @@ -22,6 +22,14 @@ "name": "count", "type": "integer" } - ] + ], + "reply_schema": { + "description": "an array with up to count elements", + "type": "array", + "items": { + "description": "key name", + "type": "string" + } + } } } diff --git a/src/commands/cluster-help.json b/src/commands/cluster-help.json index d0ddf11f28..59a8362d78 100644 --- a/src/commands/cluster-help.json +++ b/src/commands/cluster-help.json @@ -10,6 +10,13 @@ "command_flags": [ "LOADING", "STALE" - ] + ], + "reply_schema": { + "type": "array", + "description": "Helpful text about subcommands.", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/cluster-info.json b/src/commands/cluster-info.json index 08250f15b8..cf8b611b43 100644 --- a/src/commands/cluster-info.json +++ b/src/commands/cluster-info.json @@ -12,6 +12,10 @@ ], "command_tips": [ "NONDETERMINISTIC_OUTPUT" - ] + ], + "reply_schema": { + "description": "A map between named fields and values in the form of : lines separated by newlines composed by the two bytes CRLF", + "type": "string" + } } } diff --git a/src/commands/cluster-keyslot.json b/src/commands/cluster-keyslot.json index 10645477fa..19f1ad65ea 100644 --- a/src/commands/cluster-keyslot.json +++ b/src/commands/cluster-keyslot.json @@ -15,6 +15,11 @@ "name": "key", "type": "string" } - ] + ], + "reply_schema": { + "description": "The hash slot number for the specified key", + "type": "integer", + "minimum": 0 + } } } diff --git a/src/commands/cluster-links.json b/src/commands/cluster-links.json index 4d50247645..3681c7c55c 100644 --- a/src/commands/cluster-links.json +++ b/src/commands/cluster-links.json @@ -12,6 +12,49 @@ ], "command_tips": [ "NONDETERMINISTIC_OUTPUT" - ] + ], + "reply_schema": { + "description": "an array of cluster links and their attributes", + "type": "array", + "items": { + "type": "object", + "properties": { + "direction": { + "description": "This link is established by the local node _to_ the peer, or accepted by the local node _from_ the peer.", + "oneOf": [ + { + "description": "connection initiated from peer", + "const": "from" + }, + { + "description": "connection initiated to peer", + "const": "to" + } + ] + }, + "node": { + "description": "the node id of the peer", + "type": "string" + }, + "create-time": { + "description": "unix time creation time of the link. (In the case of a _to_ link, this is the time when the TCP link is created by the local node, not the time when it is actually established.)", + "type": "integer" + }, + "events": { + "description": "events currently registered for the link. r means readable event, w means writable event", + "type": "string" + }, + "send-buffer-allocated": { + "description": "allocated size of the link's send buffer, which is used to buffer outgoing messages toward the peer", + "type": "integer" + }, + "send-buffer-used": { + "description": "size of the portion of the link's send buffer that is currently holding data(messages)", + "type": "integer" + } + }, + "additionalProperties": false + } + } } } diff --git a/src/commands/cluster-meet.json b/src/commands/cluster-meet.json index 63d2cc2019..04d374ae29 100644 --- a/src/commands/cluster-meet.json +++ b/src/commands/cluster-meet.json @@ -33,6 +33,9 @@ "optional": true, "since": "4.0.0" } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/cluster-myid.json b/src/commands/cluster-myid.json index dc4f8a7736..f2c13b581e 100644 --- a/src/commands/cluster-myid.json +++ b/src/commands/cluster-myid.json @@ -9,6 +9,10 @@ "function": "clusterCommand", "command_flags": [ "STALE" - ] + ], + "reply_schema": { + "description": "the node id", + "type": "string" + } } } diff --git a/src/commands/cluster-myshardid.json b/src/commands/cluster-myshardid.json index ffd26eec66..70ac1ddc31 100644 --- a/src/commands/cluster-myshardid.json +++ b/src/commands/cluster-myshardid.json @@ -13,6 +13,10 @@ ], "command_tips": [ "NONDETERMINISTIC_OUTPUT" - ] + ], + "reply_schema": { + "description": "the node's shard id", + "type": "string" + } } } diff --git a/src/commands/cluster-nodes.json b/src/commands/cluster-nodes.json index 9452139853..6eb7f53854 100644 --- a/src/commands/cluster-nodes.json +++ b/src/commands/cluster-nodes.json @@ -12,6 +12,10 @@ ], "command_tips": [ "NONDETERMINISTIC_OUTPUT" - ] + ], + "reply_schema": { + "description": "the serialized cluster configuration", + "type": "string" + } } } diff --git a/src/commands/cluster-replicas.json b/src/commands/cluster-replicas.json index e86322bc16..a7c5a4604d 100644 --- a/src/commands/cluster-replicas.json +++ b/src/commands/cluster-replicas.json @@ -19,6 +19,14 @@ "name": "node-id", "type": "string" } - ] + ], + "reply_schema": { + "description": "a list of replica nodes replicating from the specified master node provided in the same format used by CLUSTER NODES", + "type": "array", + "items": { + "type": "string", + "description": "the serialized cluster configuration" + } + } } } diff --git a/src/commands/cluster-replicate.json b/src/commands/cluster-replicate.json index beda5e7881..d49be4fb2f 100644 --- a/src/commands/cluster-replicate.json +++ b/src/commands/cluster-replicate.json @@ -17,6 +17,9 @@ "name": "node-id", "type": "string" } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/cluster-reset.json b/src/commands/cluster-reset.json index 90c810838e..cd49900c58 100644 --- a/src/commands/cluster-reset.json +++ b/src/commands/cluster-reset.json @@ -30,6 +30,9 @@ } ] } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/cluster-saveconfig.json b/src/commands/cluster-saveconfig.json index 991286d09c..09b73db5ff 100644 --- a/src/commands/cluster-saveconfig.json +++ b/src/commands/cluster-saveconfig.json @@ -11,6 +11,9 @@ "NO_ASYNC_LOADING", "ADMIN", "STALE" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/cluster-set-config-epoch.json b/src/commands/cluster-set-config-epoch.json index 5f07f63c2a..c92cc62a74 100644 --- a/src/commands/cluster-set-config-epoch.json +++ b/src/commands/cluster-set-config-epoch.json @@ -17,6 +17,9 @@ "name": "config-epoch", "type": "integer" } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/cluster-setslot.json b/src/commands/cluster-setslot.json index df3b0448c7..9ddff81365 100644 --- a/src/commands/cluster-setslot.json +++ b/src/commands/cluster-setslot.json @@ -46,6 +46,9 @@ } ] } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/cluster-shards.json b/src/commands/cluster-shards.json index 925ce8bf30..ac45d70ebc 100644 --- a/src/commands/cluster-shards.json +++ b/src/commands/cluster-shards.json @@ -13,6 +13,77 @@ ], "command_tips": [ "NONDETERMINISTIC_OUTPUT" - ] + ], + "reply_schema": { + "description": "a nested list of a map of hash ranges and shard nodes describing individual shards", + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "slots": { + "description": "an even number element array specifying the start and end slot numbers for slot ranges owned by this shard", + "type": "array", + "items": { + "type": "string" + } + }, + "nodes": { + "description": "nodes that handle these slot ranges", + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "tls-port": { + "type": "integer" + }, + "ip": { + "type": "string" + }, + "endpoint": { + "type": "string" + }, + "hostname": { + "type": "string" + }, + "role": { + "oneOf": [ + { + "const": "master" + }, + { + "const": "replica" + } + ] + }, + "replication-offset": { + "type": "integer" + }, + "health": { + "oneOf": [ + { + "const": "fail" + }, + { + "const": "loading" + }, + { + "const": "online" + } + ] + } + } + } + } + } + } + } } } diff --git a/src/commands/cluster-slaves.json b/src/commands/cluster-slaves.json index 0ea77a8765..e7a0eb3be7 100644 --- a/src/commands/cluster-slaves.json +++ b/src/commands/cluster-slaves.json @@ -24,6 +24,14 @@ "name": "node-id", "type": "string" } - ] + ], + "reply_schema": { + "description": "a list of replica nodes replicating from the specified master node provided in the same format used by CLUSTER NODES", + "type": "array", + "items": { + "type": "string", + "description": "the serialized cluster configuration" + } + } } } diff --git a/src/commands/cluster-slots.json b/src/commands/cluster-slots.json index e8782420ec..3f76af518d 100644 --- a/src/commands/cluster-slots.json +++ b/src/commands/cluster-slots.json @@ -27,6 +27,109 @@ ], "command_tips": [ "NONDETERMINISTIC_OUTPUT" - ] + ], + "reply_schema": { + "description": "nested list of slot ranges with networking information", + "type": "array", + "items": { + "type": "array", + "minItems": 3, + "maxItems": 4294967295, + "items": [ + { + "description": "start slot number", + "type": "integer" + }, + { + "description": "end slot number", + "type": "integer" + }, + { + "type": "array", + "description": "Master node for the slot range", + "minItems": 4, + "maxItems": 4, + "items": [ + { + "description": "endpoint description", + "oneOf": [ + { + "description": "hostname or ip", + "type": "string" + }, + { + "description": "unknown type", + "type": "null" + } + ] + }, + { + "description": "port", + "type": "integer" + }, + { + "description": "node name", + "type": "string" + }, + { + "description": "array of node descriptions", + "type": "object", + "additionalProperties": false, + "properties": { + "hostname": { + "type": "string" + }, + "ip": { + "type": "string" + } + } + } + ] + } + ], + "additionalItems": { + "type": "array", + "description": "Replica node for the slot range", + "minItems": 4, + "maxItems": 4, + "items": [ + { + "description": "endpoint description", + "oneOf": [ + { + "description": "hostname or ip", + "type": "string" + }, + { + "description": "unknown type", + "type": "null" + } + ] + }, + { + "description": "port", + "type": "integer" + }, + { + "description": "node name", + "type": "string" + }, + { + "description": "array of node descriptions", + "type": "object", + "additionalProperties": false, + "properties": { + "hostname": { + "type": "string" + }, + "ip": { + "type": "string" + } + } + } + ] + } + } + } } } diff --git a/src/commands/command-count.json b/src/commands/command-count.json index f2081ca465..ecc29e4afb 100644 --- a/src/commands/command-count.json +++ b/src/commands/command-count.json @@ -14,6 +14,10 @@ ], "acl_categories": [ "CONNECTION" - ] + ], + "reply_schema": { + "description": "Number of total commands in this Redis server.", + "type": "integer" + } } } diff --git a/src/commands/command-docs.json b/src/commands/command-docs.json index 7dc81d61c2..73ac9fd3e9 100644 --- a/src/commands/command-docs.json +++ b/src/commands/command-docs.json @@ -18,6 +18,187 @@ "command_tips": [ "NONDETERMINISTIC_OUTPUT_ORDER" ], + "reply_schema": { + "description": "A map where each key is a command name, and each value is the documentary information", + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^.*$": { + "type": "object", + "additionalProperties": false, + "properties": { + "summary": { + "description": "short command description", + "type": "string" + }, + "since": { + "description": "the Redis version that added the command (or for module commands, the module version).", + "type": "string" + }, + "group": { + "description": "the functional group to which the command belongs", + "oneOf": [ + { + "const": "bitmap" + }, + { + "const": "cluster" + }, + { + "const": "connection" + }, + { + "const": "generic" + }, + { + "const": "geo" + }, + { + "const": "hash" + }, + { + "const": "hyperloglog" + }, + { + "const": "list" + }, + { + "const": "module" + }, + { + "const": "pubsub" + }, + { + "const": "scripting" + }, + { + "const": "sentinel" + }, + { + "const": "server" + }, + { + "const": "set" + }, + { + "const": "sorted-set" + }, + { + "const": "stream" + }, + { + "const": "string" + }, + { + "const": "transactions" + } + ] + }, + "complexity": { + "description": "a short explanation about the command's time complexity.", + "type": "string" + }, + "module": { + "type": "string" + }, + "doc_flags": { + "description": "an array of documentation flags", + "type": "array", + "items": { + "oneOf": [ + { + "description": "the command is deprecated.", + "const": "deprecated" + }, + { + "description": "a system command that isn't meant to be called by users.", + "const": "syscmd" + } + ] + } + }, + "deprecated_since": { + "description": "the Redis version that deprecated the command (or for module commands, the module version)", + "type": "string" + }, + "replaced_by": { + "description": "the alternative for a deprecated command.", + "type": "string" + }, + "history": { + "description": "an array of historical notes describing changes to the command's behavior or arguments.", + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "string", + "description": "The Redis version that the entry applies to." + }, + { + "type": "string", + "description": "The description of the change." + } + ] + } + }, + "arguments": { + "description": "an array of maps that describe the command's arguments.", + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "display_text": { + "type": "string" + }, + "key_spec_index": { + "type": "integer" + }, + "token": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "since": { + "type": "string" + }, + "deprecated_since": { + "type": "string" + }, + "flags": { + "type": "array", + "items": { + "type": "string" + } + }, + "arguments": { + "type": "array" + } + } + } + }, + "reply_schema": { + "description": "command reply schema", + "type": "object" + }, + "subcommands": { + "description": "A map where each key is a subcommand, and each value is the documentary information", + "$ref": "#" + } + } + } + } + }, "arguments": [ { "name": "command-name", diff --git a/src/commands/command-getkeys.json b/src/commands/command-getkeys.json index 20bf7519bc..92c95252d6 100644 --- a/src/commands/command-getkeys.json +++ b/src/commands/command-getkeys.json @@ -15,6 +15,14 @@ "acl_categories": [ "CONNECTION" ], + "reply_schema": { + "description": "List of keys from the given Redis command.", + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, "arguments": [ { "name": "command", diff --git a/src/commands/command-getkeysandflags.json b/src/commands/command-getkeysandflags.json index ce96dd4fbe..05668a241c 100644 --- a/src/commands/command-getkeysandflags.json +++ b/src/commands/command-getkeysandflags.json @@ -15,6 +15,30 @@ "acl_categories": [ "CONNECTION" ], + "reply_schema": { + "description": "List of keys from the given Redis command and their usage flags.", + "type": "array", + "uniqueItems": true, + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "Key name", + "type": "string" + }, + { + "description": "Set of key flags", + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ] + } + }, "arguments": [ { "name": "command", diff --git a/src/commands/command-help.json b/src/commands/command-help.json index d5ad719f0e..22d31563a0 100644 --- a/src/commands/command-help.json +++ b/src/commands/command-help.json @@ -14,6 +14,13 @@ ], "acl_categories": [ "CONNECTION" - ] + ], + "reply_schema": { + "type": "array", + "description": "Helpful text about subcommands.", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/command-info.json b/src/commands/command-info.json index 52ab40084b..84e36dfaf9 100644 --- a/src/commands/command-info.json +++ b/src/commands/command-info.json @@ -31,6 +31,183 @@ "optional": true, "multiple": true } - ] + ], + "reply_schema": { + "type": "array", + "items": { + "oneOf": [ + { + "description": "command does not exist", + "type": "null" + }, + { + "description": "command info array output", + "type": "array", + "minItems": 10, + "maxItems": 10, + "items": [ + { + "description": "command name", + "type": "string" + }, + { + "description": "command arity", + "type": "integer" + }, + { + "description": "command flags", + "type": "array", + "items": { + "description": "command flag", + "type": "string" + } + }, + { + "description": "command first key index", + "type": "integer" + }, + { + "description": "command last key index", + "type": "integer" + }, + { + "description": "command key step index", + "type": "integer" + }, + { + "description": "command categories", + "type": "array", + "items": { + "description": "command category", + "type": "string" + } + }, + { + "description": "command tips", + "type": "array", + "items": { + "description": "command tip", + "type": "string" + } + }, + { + "description": "command key specs", + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "notes": { + "type": "string" + }, + "flags": { + "type": "array", + "items": { + "type": "string" + } + }, + "begin_search": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "string" + }, + "spec": { + "anyOf": [ + { + "description": "unknown type, empty map", + "type": "object", + "additionalProperties": false + }, + { + "description": "index type", + "type": "object", + "additionalProperties": false, + "properties": { + "index": { + "type": "integer" + } + } + }, + { + "description": "keyword type", + "type": "object", + "additionalProperties": false, + "properties": { + "keyword": { + "type": "string" + }, + "startfrom": { + "type": "integer" + } + } + } + ] + } + } + }, + "find_keys": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "string" + }, + "spec": { + "anyOf": [ + { + "description": "unknown type", + "type": "object", + "additionalProperties": false + }, + { + "description": "range type", + "type": "object", + "additionalProperties": false, + "properties": { + "lastkey": { + "type": "integer" + }, + "keystep": { + "type": "integer" + }, + "limit": { + "type": "integer" + } + } + }, + { + "description": "keynum type", + "type": "object", + "additionalProperties": false, + "properties": { + "keynumidx": { + "type": "integer" + }, + "firstkey": { + "type": "integer" + }, + "keystep": { + "type": "integer" + } + } + } + ] + } + } + } + } + } + }, + { + "type": "array", + "description": "subcommands" + } + ] + } + ] + } + } } } diff --git a/src/commands/command-list.json b/src/commands/command-list.json index 9ef624f074..5d6be80718 100644 --- a/src/commands/command-list.json +++ b/src/commands/command-list.json @@ -42,6 +42,14 @@ } ] } - ] + ], + "reply_schema": { + "type": "array", + "items": { + "description": "command name", + "type": "string" + }, + "uniqueItems": true + } } } diff --git a/src/commands/config-get.json b/src/commands/config-get.json index 1ea3876867..e21c64b780 100644 --- a/src/commands/config-get.json +++ b/src/commands/config-get.json @@ -19,6 +19,12 @@ "LOADING", "STALE" ], + "reply_schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, "arguments": [ { "name": "parameter", diff --git a/src/commands/config-help.json b/src/commands/config-help.json index 537dd6bba6..cd90bbb594 100644 --- a/src/commands/config-help.json +++ b/src/commands/config-help.json @@ -10,6 +10,13 @@ "command_flags": [ "LOADING", "STALE" - ] + ], + "reply_schema": { + "type": "array", + "description": "Helpful text about subcommands.", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/config-resetstat.json b/src/commands/config-resetstat.json index 0180402abe..353d46bd28 100644 --- a/src/commands/config-resetstat.json +++ b/src/commands/config-resetstat.json @@ -12,6 +12,9 @@ "NOSCRIPT", "LOADING", "STALE" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/config-rewrite.json b/src/commands/config-rewrite.json index 4e31dd82db..3e37bf49a2 100644 --- a/src/commands/config-rewrite.json +++ b/src/commands/config-rewrite.json @@ -12,6 +12,9 @@ "NOSCRIPT", "LOADING", "STALE" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/config-set.json b/src/commands/config-set.json index 3a337f5c6e..6e95fe0ef4 100644 --- a/src/commands/config-set.json +++ b/src/commands/config-set.json @@ -23,6 +23,9 @@ "REQUEST_POLICY:ALL_NODES", "RESPONSE_POLICY:ALL_SUCCEEDED" ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "data", diff --git a/src/commands/copy.json b/src/commands/copy.json index 0ffb949976..450b7397a6 100644 --- a/src/commands/copy.json +++ b/src/commands/copy.json @@ -74,6 +74,18 @@ "type": "pure-token", "optional": true } - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "source was copied", + "const": 1 + }, + { + "description": "source was not copied", + "const": 0 + } + ] + } } } diff --git a/src/commands/dbsize.json b/src/commands/dbsize.json index 4d65574dba..989e37d9fe 100644 --- a/src/commands/dbsize.json +++ b/src/commands/dbsize.json @@ -16,6 +16,10 @@ "command_tips": [ "REQUEST_POLICY:ALL_SHARDS", "RESPONSE_POLICY:AGG_SUM" - ] + ], + "reply_schema": { + "type": "integer", + "description": "The number of keys in the currently-selected database." + } } } diff --git a/src/commands/decr.json b/src/commands/decr.json index 4a5128d278..1bae4e0989 100644 --- a/src/commands/decr.json +++ b/src/commands/decr.json @@ -35,6 +35,10 @@ } } ], + "reply_schema": { + "type": "integer", + "description": "The value of the key after decrementing it." + }, "arguments": [ { "name": "key", diff --git a/src/commands/decrby.json b/src/commands/decrby.json index 19f376b8e7..de5724c3ac 100644 --- a/src/commands/decrby.json +++ b/src/commands/decrby.json @@ -35,6 +35,10 @@ } } ], + "reply_schema": { + "type": "integer", + "description": "The value of the key after decrementing it." + }, "arguments": [ { "name": "key", diff --git a/src/commands/del.json b/src/commands/del.json index bc500a9afa..6d2ff7209c 100644 --- a/src/commands/del.json +++ b/src/commands/del.json @@ -36,6 +36,11 @@ } } ], + "reply_schema": { + "description": "the number of keys that were removed", + "type": "integer", + "minimum": 0 + }, "arguments": [ { "name": "key", diff --git a/src/commands/discard.json b/src/commands/discard.json index 56589a84f3..6ef0dd809c 100644 --- a/src/commands/discard.json +++ b/src/commands/discard.json @@ -15,6 +15,9 @@ ], "acl_categories": [ "TRANSACTION" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/dump.json b/src/commands/dump.json index 2e9453cddd..c0f0aa0ea8 100644 --- a/src/commands/dump.json +++ b/src/commands/dump.json @@ -35,6 +35,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "The serialized value.", + "type": "string" + }, + { + "description": "Key does not exist.", + "type": "null" + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/echo.json b/src/commands/echo.json index f38d10bc52..abb3d26f33 100644 --- a/src/commands/echo.json +++ b/src/commands/echo.json @@ -14,6 +14,10 @@ "acl_categories": [ "CONNECTION" ], + "reply_schema": { + "description": "The given string", + "type": "string" + }, "arguments": [ { "name": "message", diff --git a/src/commands/eval.json b/src/commands/eval.json index 50fc022a2a..4f25b2fe30 100644 --- a/src/commands/eval.json +++ b/src/commands/eval.json @@ -61,6 +61,9 @@ "optional": true, "multiple": true } - ] + ], + "reply_schema": { + "description": "Return value depends on the script that is executed" + } } } diff --git a/src/commands/eval_ro.json b/src/commands/eval_ro.json index 5c74b04379..f9aa54942b 100644 --- a/src/commands/eval_ro.json +++ b/src/commands/eval_ro.json @@ -60,6 +60,9 @@ "optional":true, "multiple": true } - ] + ], + "reply_schema": { + "description": "Return value depends on the script that is executed" + } } } diff --git a/src/commands/evalsha.json b/src/commands/evalsha.json index 9b68b87f1a..66e4379c6b 100644 --- a/src/commands/evalsha.json +++ b/src/commands/evalsha.json @@ -60,6 +60,9 @@ "optional": true, "multiple": true } - ] + ], + "reply_schema": { + "description": "Return value depends on the script that is executed" + } } } diff --git a/src/commands/evalsha_ro.json b/src/commands/evalsha_ro.json index 2628613fb1..8dca0964b7 100644 --- a/src/commands/evalsha_ro.json +++ b/src/commands/evalsha_ro.json @@ -59,6 +59,9 @@ "optional":true, "multiple": true } - ] + ], + "reply_schema": { + "description": "Return value depends on the script that is executed" + } } } diff --git a/src/commands/exec.json b/src/commands/exec.json index 80856ef99f..b5ec6f0ab8 100644 --- a/src/commands/exec.json +++ b/src/commands/exec.json @@ -14,6 +14,18 @@ ], "acl_categories": [ "TRANSACTION" - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "Each element being the reply to each of the commands in the atomic transaction.", + "type": "array" + }, + { + "description": "The transaction was aborted because a `WATCH`ed key was touched", + "type": "null" + } + ] + } } } diff --git a/src/commands/exists.json b/src/commands/exists.json index b313633524..e8793cf2b6 100644 --- a/src/commands/exists.json +++ b/src/commands/exists.json @@ -42,6 +42,10 @@ } } ], + "reply_schema": { + "description": "Number of keys that exist from those specified as arguments.", + "type": "integer" + }, "arguments": [ { "name": "key", diff --git a/src/commands/expire.json b/src/commands/expire.json index 712830d3e9..f0236f1d5a 100644 --- a/src/commands/expire.json +++ b/src/commands/expire.json @@ -39,6 +39,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "The timeout was not set. e.g. key doesn't exist, or operation skipped due to the provided arguments.", + "const": 0 + }, + { + "description": "The timeout was set.", + "const": 1 + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/expireat.json b/src/commands/expireat.json index 43d9b748e3..a6e22754b9 100644 --- a/src/commands/expireat.json +++ b/src/commands/expireat.json @@ -39,6 +39,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "const": 1, + "description": "The timeout was set." + }, + { + "const": 0, + "description": "The timeout was not set. e.g. key doesn't exist, or operation skipped due to the provided arguments." + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/expiretime.json b/src/commands/expiretime.json index 9393c1226e..90d8525ad1 100644 --- a/src/commands/expiretime.json +++ b/src/commands/expiretime.json @@ -33,6 +33,23 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "type": "integer", + "description": "Expiration Unix timestamp in seconds.", + "minimum": 0 + }, + { + "const": -1, + "description": "The key exists but has no associated expiration time." + }, + { + "const": -2, + "description": "The key does not exist." + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/failover.json b/src/commands/failover.json index dd2e2951ff..d208bfcf1b 100644 --- a/src/commands/failover.json +++ b/src/commands/failover.json @@ -11,6 +11,9 @@ "NOSCRIPT", "STALE" ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "target", diff --git a/src/commands/fcall.json b/src/commands/fcall.json index 9e7a905ecd..5817ed7c9c 100644 --- a/src/commands/fcall.json +++ b/src/commands/fcall.json @@ -61,6 +61,9 @@ "optional": true, "multiple": true } - ] + ], + "reply_schema": { + "description": "Return value depends on the function that is executed" + } } } diff --git a/src/commands/fcall_ro.json b/src/commands/fcall_ro.json index 6ba2736c07..def485ffa7 100644 --- a/src/commands/fcall_ro.json +++ b/src/commands/fcall_ro.json @@ -60,6 +60,9 @@ "optional": true, "multiple": true } - ] + ], + "reply_schema": { + "description": "Return value depends on the function that is executed" + } } } diff --git a/src/commands/flushall.json b/src/commands/flushall.json index 4add012bad..dc5c7ca39b 100644 --- a/src/commands/flushall.json +++ b/src/commands/flushall.json @@ -27,6 +27,9 @@ "REQUEST_POLICY:ALL_SHARDS", "RESPONSE_POLICY:ALL_SUCCEEDED" ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "flush-type", diff --git a/src/commands/flushdb.json b/src/commands/flushdb.json index 858939f07e..1590221eb8 100644 --- a/src/commands/flushdb.json +++ b/src/commands/flushdb.json @@ -27,6 +27,9 @@ "REQUEST_POLICY:ALL_SHARDS", "RESPONSE_POLICY:ALL_SUCCEEDED" ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "flush-type", diff --git a/src/commands/function-delete.json b/src/commands/function-delete.json index 01dc78ba4a..c56a0fbe63 100644 --- a/src/commands/function-delete.json +++ b/src/commands/function-delete.json @@ -23,6 +23,9 @@ "name": "library-name", "type": "string" } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/function-dump.json b/src/commands/function-dump.json index de402f5898..b535b28ad8 100644 --- a/src/commands/function-dump.json +++ b/src/commands/function-dump.json @@ -12,6 +12,10 @@ ], "acl_categories": [ "SCRIPTING" - ] + ], + "reply_schema": { + "description": "the serialized payload", + "type": "string" + } } } diff --git a/src/commands/function-flush.json b/src/commands/function-flush.json index a2ab58e4fd..5380748786 100644 --- a/src/commands/function-flush.json +++ b/src/commands/function-flush.json @@ -36,6 +36,9 @@ } ] } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/function-help.json b/src/commands/function-help.json index b8213cb610..37a3826df0 100644 --- a/src/commands/function-help.json +++ b/src/commands/function-help.json @@ -13,6 +13,13 @@ ], "acl_categories": [ "SCRIPTING" - ] + ], + "reply_schema": { + "type": "array", + "description": "Helpful text about subcommands.", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/function-kill.json b/src/commands/function-kill.json index 87432f996c..2f9ae833e4 100644 --- a/src/commands/function-kill.json +++ b/src/commands/function-kill.json @@ -17,6 +17,9 @@ "command_tips": [ "REQUEST_POLICY:ALL_SHARDS", "RESPONSE_POLICY:ONE_SUCCEEDED" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/function-list.json b/src/commands/function-list.json index 6513b80cb4..89c5162e78 100644 --- a/src/commands/function-list.json +++ b/src/commands/function-list.json @@ -16,6 +16,59 @@ "acl_categories": [ "SCRIPTING" ], + "reply_schema": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "library_name": { + "description": " the name of the library", + "type": "string" + }, + "engine": { + "description": "the engine of the library", + "type": "string" + }, + "functions": { + "description": "the list of functions in the library", + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "description": "the name of the function", + "type": "string" + }, + "description": { + "description": "the function's description", + "oneOf": [ + { + "type": "null" + }, + { + "type": "string" + } + ] + }, + "flags": { + "description": "an array of function flags", + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "library_code": { + "description": "the library's source code (when given the WITHCODE modifier)", + "type": "string" + } + } + } + }, "arguments": [ { "name": "library-name-pattern", diff --git a/src/commands/function-load.json b/src/commands/function-load.json index d047212793..28f5b2084e 100644 --- a/src/commands/function-load.json +++ b/src/commands/function-load.json @@ -30,6 +30,10 @@ "name": "function-code", "type": "string" } - ] + ], + "reply_schema": { + "description": "The library name that was loaded", + "type": "string" + } } } diff --git a/src/commands/function-restore.json b/src/commands/function-restore.json index ede016895f..900602577c 100644 --- a/src/commands/function-restore.json +++ b/src/commands/function-restore.json @@ -46,6 +46,9 @@ } ] } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/function-stats.json b/src/commands/function-stats.json index 0d055b65b1..8a64b92460 100644 --- a/src/commands/function-stats.json +++ b/src/commands/function-stats.json @@ -18,6 +18,64 @@ "NONDETERMINISTIC_OUTPUT", "REQUEST_POLICY:ALL_SHARDS", "RESPONSE_POLICY:SPECIAL" - ] + ], + "reply_schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "running_script": { + "description": "information about the running script.", + "oneOf": [ + { + "description": "If there's no in-flight function", + "type": "null" + }, + { + "description": "a map with the information about the running script", + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "description": "the name of the function.", + "type": "string" + }, + "command": { + "description": "the command and arguments used for invoking the function.", + "type": "array", + "items": { + "type": "string" + } + }, + "duration_ms": { + "description": "the function's runtime duration in milliseconds.", + "type": "integer" + } + } + } + ] + }, + "engines": { + "description": "A map when each entry in the map represent a single engine.", + "type": "object", + "patternProperties": { + "^.*$": { + "description": "Engine map contains statistics about the engine", + "type": "object", + "additionalProperties": false, + "properties": { + "libraries_count": { + "description": "number of libraries", + "type": "integer" + }, + "functions_count": { + "description": "number of functions", + "type": "integer" + } + } + } + } + } + } + } } } diff --git a/src/commands/geoadd.json b/src/commands/geoadd.json index d33836cf45..bd9c40ebce 100644 --- a/src/commands/geoadd.json +++ b/src/commands/geoadd.json @@ -89,6 +89,10 @@ } ] } - ] + ], + "reply_schema": { + "description": "When used without optional arguments, the number of elements added to the sorted set (excluding score updates). If the CH option is specified, the number of elements that were changed (added or updated).", + "type": "integer" + } } } diff --git a/src/commands/geodist.json b/src/commands/geodist.json index 87782b243a..61f281a1b3 100644 --- a/src/commands/geodist.json +++ b/src/commands/geodist.json @@ -73,6 +73,19 @@ } ] } - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "one or both of elements are missing", + "type": "null" + }, + { + "description": "distance as a double (represented as a string) in the specified units", + "type": "string", + "pattern": "^[0-9]*(.[0-9]*)?$" + } + ] + } } } diff --git a/src/commands/geohash.json b/src/commands/geohash.json index 040c631d1e..0db62f3152 100644 --- a/src/commands/geohash.json +++ b/src/commands/geohash.json @@ -44,6 +44,13 @@ "multiple": true, "optional": true } - ] + ], + "reply_schema": { + "description": "An array where each element is the Geohash corresponding to each member name passed as argument to the command.", + "type": "array", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/geopos.json b/src/commands/geopos.json index 242f6a887f..64635fa7fd 100644 --- a/src/commands/geopos.json +++ b/src/commands/geopos.json @@ -44,6 +44,33 @@ "multiple": true, "optional": true } - ] + ], + "reply_schema": { + "description": "An array where each element is a two elements array representing longitude and latitude (x,y) of each member name passed as argument to the command", + "type": "array", + "items": { + "oneOf": [ + { + "description": "Element does not exist", + "type": "null" + }, + { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "Latitude (x)", + "type": "number" + }, + { + "description": "Longitude (y)", + "type": "number" + } + ] + } + ] + } + } } } diff --git a/src/commands/georadius.json b/src/commands/georadius.json index cb14d51b43..3c475784b7 100644 --- a/src/commands/georadius.json +++ b/src/commands/georadius.json @@ -201,6 +201,65 @@ "key_spec_index": 2, "optional": true } - ] + ], + "reply_schema": { + "description": "Array of matched members information", + "anyOf": [ + { + "description": "If no WITH* option is specified, array of matched members names", + "type": "array", + "items": { + "description": "name", + "type": "string" + } + }, + { + "type": "array", + "items": { + "type": "array", + "minItems": 1, + "maxItems": 4, + "items": [ + { + "description": "Matched member name", + "type": "string" + } + ], + "additionalItems": { + "oneOf": [ + { + "description": "If WITHDIST option is specified, the distance from the center as a floating point number, in the same unit specified in the radius", + "type": "string" + }, + { + "description": "If WITHHASH option is specified, the geohash integer", + "type": "integer" + }, + { + "description": "If WITHCOORD option is specified, the coordinates as a two items x,y array (longitude,latitude)", + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "latitude (x)", + "type": "number" + }, + { + "description": "longitude (y)", + "type": "number" + } + ] + } + ] + } + } + }, + { + "description": "number of items stored in key", + "type": "integer" + } + ] + } } } diff --git a/src/commands/georadius_ro.json b/src/commands/georadius_ro.json index 4696f78b8d..5b0b2a5e08 100644 --- a/src/commands/georadius_ro.json +++ b/src/commands/georadius_ro.json @@ -141,6 +141,61 @@ } ] } - ] + ], + "reply_schema": { + "description": "Array of matched members information", + "anyOf": [ + { + "description": "If no WITH* option is specified, array of matched members names", + "type": "array", + "items": { + "description": "name", + "type": "string" + } + }, + { + "type": "array", + "items": { + "type": "array", + "minItems": 1, + "maxItems": 4, + "items": [ + { + "description": "Matched member name", + "type": "string" + } + ], + "additionalItems": { + "oneOf": [ + { + "description": "If WITHDIST option is specified, the distance from the center as a floating point number, in the same unit specified in the radius", + "type": "string" + }, + { + "description": "If WITHHASH option is specified, the geohash integer", + "type": "integer" + }, + { + "description": "If WITHCOORD option is specified, the coordinates as a two items x,y array (longitude,latitude)", + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "latitude (x)", + "type": "number" + }, + { + "description": "longitude (y)", + "type": "number" + } + ] + } + ] + } + } + } + ] + } } } diff --git a/src/commands/georadiusbymember.json b/src/commands/georadiusbymember.json index de8aa5b16d..f2db041d1c 100644 --- a/src/commands/georadiusbymember.json +++ b/src/commands/georadiusbymember.json @@ -192,6 +192,65 @@ "key_spec_index": 2, "optional": true } - ] + ], + "reply_schema": { + "description": "Array of matched members information", + "anyOf": [ + { + "description": "If no WITH* option is specified, array of matched members names", + "type": "array", + "items": { + "description": "name", + "type": "string" + } + }, + { + "type": "array", + "items": { + "type": "array", + "minItems": 1, + "maxItems": 4, + "items": [ + { + "description": "Matched member name", + "type": "string" + } + ], + "additionalItems": { + "oneOf": [ + { + "description": "If WITHDIST option is specified, the distance from the center as a floating point number, in the same unit specified in the radius", + "type": "string" + }, + { + "description": "If WITHHASH option is specified, the geohash integer", + "type": "integer" + }, + { + "description": "If WITHCOORD option is specified, the coordinates as a two items x,y array (longitude,latitude)", + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "latitude (x)", + "type": "number" + }, + { + "description": "longitude (y)", + "type": "number" + } + ] + } + ] + } + } + }, + { + "description": "number of items stored in key", + "type": "integer" + } + ] + } } } diff --git a/src/commands/georadiusbymember_ro.json b/src/commands/georadiusbymember_ro.json index 062a72ab26..fa35d529ba 100644 --- a/src/commands/georadiusbymember_ro.json +++ b/src/commands/georadiusbymember_ro.json @@ -130,6 +130,61 @@ } ] } - ] + ], + "reply_schema": { + "description": "Array of matched members information", + "anyOf": [ + { + "description": "If no WITH* option is specified, array of matched members names", + "type": "array", + "items": { + "description": "name", + "type": "string" + } + }, + { + "type": "array", + "items": { + "type": "array", + "minItems": 1, + "maxItems": 4, + "items": [ + { + "description": "Matched member name", + "type": "string" + } + ], + "additionalItems": { + "oneOf": [ + { + "description": "If WITHDIST option is specified, the distance from the center as a floating point number, in the same unit specified in the radius", + "type": "string" + }, + { + "description": "If WITHHASH option is specified, the geohash integer", + "type": "integer" + }, + { + "description": "If WITHCOORD option is specified, the coordinates as a two items x,y array (longitude,latitude)", + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "latitude (x)", + "type": "number" + }, + { + "description": "longitude (y)", + "type": "number" + } + ] + } + ] + } + } + } + ] + } } } diff --git a/src/commands/geosearch.json b/src/commands/geosearch.json index 64f5df355e..88c6bf2875 100644 --- a/src/commands/geosearch.json +++ b/src/commands/geosearch.json @@ -207,6 +207,61 @@ "type": "pure-token", "optional": true } - ] + ], + "reply_schema": { + "description": "Array of matched members information", + "anyOf": [ + { + "description": "If no WITH* option is specified, array of matched members names", + "type": "array", + "items": { + "description": "name", + "type": "string" + } + }, + { + "type": "array", + "items": { + "type": "array", + "minItems": 1, + "maxItems": 4, + "items": [ + { + "description": "Matched member name", + "type": "string" + } + ], + "additionalItems": { + "oneOf": [ + { + "description": "If WITHDIST option is specified, the distance from the center as a floating point number, in the same unit specified in the radius", + "type": "string" + }, + { + "description": "If WITHHASH option is specified, the geohash integer", + "type": "integer" + }, + { + "description": "If WITHCOORD option is specified, the coordinates as a two items x,y array (longitude,latitude)", + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "latitude (x)", + "type": "number" + }, + { + "description": "longitude (y)", + "type": "number" + } + ] + } + ] + } + } + } + ] + } } } diff --git a/src/commands/geosearchstore.json b/src/commands/geosearchstore.json index cd3815166e..abb7ba1526 100644 --- a/src/commands/geosearchstore.json +++ b/src/commands/geosearchstore.json @@ -219,6 +219,10 @@ "type": "pure-token", "optional": true } - ] + ], + "reply_schema": { + "description": "the number of elements in the resulting set", + "type": "integer" + } } } diff --git a/src/commands/get.json b/src/commands/get.json index 342e100e96..0de9b164ca 100644 --- a/src/commands/get.json +++ b/src/commands/get.json @@ -33,6 +33,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "The value of the key.", + "type": "string" + }, + { + "description": "Key does not exist.", + "type": "null" + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/getbit.json b/src/commands/getbit.json index 759784e794..773445f639 100644 --- a/src/commands/getbit.json +++ b/src/commands/getbit.json @@ -33,6 +33,17 @@ } } ], + "reply_schema": { + "description": "The bit value stored at offset.", + "oneOf": [ + { + "const": 0 + }, + { + "const": 1 + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/getdel.json b/src/commands/getdel.json index 1d2f56e301..17e1664bb6 100644 --- a/src/commands/getdel.json +++ b/src/commands/getdel.json @@ -34,6 +34,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "The value of the key.", + "type": "string" + }, + { + "description": "The key does not exist.", + "type": "null" + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/getex.json b/src/commands/getex.json index 8557bcdd9a..cb70eac8f6 100644 --- a/src/commands/getex.json +++ b/src/commands/getex.json @@ -35,6 +35,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "The value of the key.", + "type": "string" + }, + { + "description": "Key does not exist.", + "type": "null" + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/getrange.json b/src/commands/getrange.json index 03eb58e381..f548365351 100644 --- a/src/commands/getrange.json +++ b/src/commands/getrange.json @@ -32,6 +32,10 @@ } } ], + "reply_schema": { + "type": "string", + "description": "The substring of the string value stored at key, determined by the offsets start and end (both are inclusive)." + }, "arguments": [ { "name": "key", diff --git a/src/commands/getset.json b/src/commands/getset.json index 2f6b8911cd..78629ff913 100644 --- a/src/commands/getset.json +++ b/src/commands/getset.json @@ -40,6 +40,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "The old value stored at the key.", + "type": "string" + }, + { + "description": "The key does not exist.", + "type": "null" + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/hdel.json b/src/commands/hdel.json index df70430d87..b5d63476b8 100644 --- a/src/commands/hdel.json +++ b/src/commands/hdel.json @@ -39,6 +39,10 @@ } } ], + "reply_schema": { + "type": "integer", + "description": "The number of fields that were removed from the hash." + }, "arguments": [ { "name": "key", diff --git a/src/commands/hello.json b/src/commands/hello.json index 675edff5ba..7f5d6f202c 100644 --- a/src/commands/hello.json +++ b/src/commands/hello.json @@ -24,6 +24,54 @@ "acl_categories": [ "CONNECTION" ], + "reply_schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "server": { + "type": "string" + }, + "version": { + "type": "string" + }, + "proto": { + "const": 3 + }, + "id": { + "type": "integer" + }, + "mode": { + "type": "string" + }, + "role": { + "type": "string" + }, + "modules": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "ver": { + "type": "integer" + }, + "path": { + "type": "string" + }, + "args": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, "arguments": [ { "name": "arguments", diff --git a/src/commands/hexists.json b/src/commands/hexists.json index 0518e62092..1fc1b65655 100644 --- a/src/commands/hexists.json +++ b/src/commands/hexists.json @@ -32,6 +32,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "The hash does not contain the field, or key does not exist.", + "const": 0 + }, + { + "description": "The hash contains the field.", + "const": 1 + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/hget.json b/src/commands/hget.json index 12e1fbfbc8..101a38ab42 100644 --- a/src/commands/hget.json +++ b/src/commands/hget.json @@ -33,6 +33,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "The value associated with the field.", + "type": "string" + }, + { + "description": "If the field is not present in the hash or key does not exist.", + "type": "null" + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/hgetall.json b/src/commands/hgetall.json index 9fef006468..03373ae770 100644 --- a/src/commands/hgetall.json +++ b/src/commands/hgetall.json @@ -35,6 +35,13 @@ } } ], + "reply_schema": { + "type": "object", + "description": "Map of fields and their values stored in the hash, or an empty list when key does not exist. In RESP2 this is returned as a flat array.", + "additionalProperties": { + "type": "string" + } + }, "arguments": [ { "name": "key", diff --git a/src/commands/hincrby.json b/src/commands/hincrby.json index 124e365f12..bd9b0ea4cb 100644 --- a/src/commands/hincrby.json +++ b/src/commands/hincrby.json @@ -35,6 +35,10 @@ } } ], + "reply_schema": { + "type": "integer", + "description": "The value of the field after the increment operation." + }, "arguments": [ { "name": "key", diff --git a/src/commands/hincrbyfloat.json b/src/commands/hincrbyfloat.json index b4c81d1811..bd34db5e1c 100644 --- a/src/commands/hincrbyfloat.json +++ b/src/commands/hincrbyfloat.json @@ -35,6 +35,10 @@ } } ], + "reply_schema": { + "type": "string", + "description": "The value of the field after the increment operation." + }, "arguments": [ { "name": "key", diff --git a/src/commands/hkeys.json b/src/commands/hkeys.json index 243566aa96..c6a4d1fcf4 100644 --- a/src/commands/hkeys.json +++ b/src/commands/hkeys.json @@ -35,6 +35,14 @@ } } ], + "reply_schema": { + "type": "array", + "description": "List of fields in the hash, or an empty list when the key does not exist.", + "uniqueItems": true, + "items": { + "type": "string" + } + }, "arguments": [ { "name": "key", diff --git a/src/commands/hlen.json b/src/commands/hlen.json index 8320651aea..1b476ab74a 100644 --- a/src/commands/hlen.json +++ b/src/commands/hlen.json @@ -32,6 +32,10 @@ } } ], + "reply_schema": { + "type": "integer", + "description": "Number of the fields in the hash, or 0 when the key does not exist." + }, "arguments": [ { "name": "key", diff --git a/src/commands/hmget.json b/src/commands/hmget.json index d7e7c8e45b..7a31598a73 100644 --- a/src/commands/hmget.json +++ b/src/commands/hmget.json @@ -33,6 +33,21 @@ } } ], + "reply_schema": { + "description": "List of values associated with the given fields, in the same order as they are requested.", + "type": "array", + "minItems": 1, + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, "arguments": [ { "name": "key", diff --git a/src/commands/hmset.json b/src/commands/hmset.json index 1fda1b2b4d..c498b90865 100644 --- a/src/commands/hmset.json +++ b/src/commands/hmset.json @@ -39,6 +39,9 @@ } } ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "key", diff --git a/src/commands/hrandfield.json b/src/commands/hrandfield.json index ef2ff41996..c821d4508c 100644 --- a/src/commands/hrandfield.json +++ b/src/commands/hrandfield.json @@ -35,6 +35,44 @@ } } ], + "reply_schema": { + "anyOf": [ + { + "description": "Key doesn't exist", + "type": "null" + }, + { + "description": "A single random field. Returned in case `COUNT` was not used.", + "type": "string" + }, + { + "description": "A list of fields. Returned in case `COUNT` was used.", + "type": "array", + "items": { + "type": "string" + } + }, + { + "description": "Fields and their values. Returned in case `COUNT` and `WITHVALUES` were used. In RESP2 this is returned as a flat array.", + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "Field", + "type": "string" + }, + { + "description": "Value", + "type": "string" + } + ] + } + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/hscan.json b/src/commands/hscan.json index 67b3bf552e..478bda4475 100644 --- a/src/commands/hscan.json +++ b/src/commands/hscan.json @@ -57,6 +57,25 @@ "type": "integer", "optional": true } - ] + ], + "reply_schema": { + "description": "cursor and scan response in array form", + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "cursor", + "type": "string" + }, + { + "description": "list of key/value pairs from the hash where each even element is the key, and each odd element is the value", + "type": "array", + "items": { + "type": "string" + } + } + ] + } } } diff --git a/src/commands/hset.json b/src/commands/hset.json index 1b665c3698..27762afead 100644 --- a/src/commands/hset.json +++ b/src/commands/hset.json @@ -40,6 +40,9 @@ } } ], + "reply_schema": { + "type": "integer" + }, "arguments": [ { "name": "key", diff --git a/src/commands/hsetnx.json b/src/commands/hsetnx.json index abd0ccafb5..53cd9c9d06 100644 --- a/src/commands/hsetnx.json +++ b/src/commands/hsetnx.json @@ -34,6 +34,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "The field is a new field in the hash and value was set.", + "const": 0 + }, + { + "description": "The field already exists in the hash and no operation was performed.", + "const": 1 + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/hstrlen.json b/src/commands/hstrlen.json index 4ba4df7ad8..5d2c6b1a9f 100644 --- a/src/commands/hstrlen.json +++ b/src/commands/hstrlen.json @@ -32,6 +32,11 @@ } } ], + "reply_schema": { + "type": "integer", + "description": "String length of the value associated with the field, or zero when the field is not present in the hash or key does not exist at all.", + "minimum": 0 + }, "arguments": [ { "name": "key", diff --git a/src/commands/hvals.json b/src/commands/hvals.json index 829f63dea6..6118bcff4b 100644 --- a/src/commands/hvals.json +++ b/src/commands/hvals.json @@ -35,6 +35,13 @@ } } ], + "reply_schema": { + "type": "array", + "description": "List of values in the hash, or an empty list when the key does not exist.", + "items": { + "type": "string" + } + }, "arguments": [ { "name": "key", diff --git a/src/commands/incr.json b/src/commands/incr.json index 09a4960560..d6c1bd2cd1 100644 --- a/src/commands/incr.json +++ b/src/commands/incr.json @@ -41,6 +41,10 @@ "type": "key", "key_spec_index": 0 } - ] + ], + "reply_schema": { + "description": "The value of key after the increment", + "type": "integer" + } } } diff --git a/src/commands/incrby.json b/src/commands/incrby.json index 27418114ad..0febdd56e2 100644 --- a/src/commands/incrby.json +++ b/src/commands/incrby.json @@ -35,6 +35,10 @@ } } ], + "reply_schema": { + "type": "integer", + "description": "The value of the key after incrementing it." + }, "arguments": [ { "name": "key", diff --git a/src/commands/incrbyfloat.json b/src/commands/incrbyfloat.json index f28b91be67..0f24400243 100644 --- a/src/commands/incrbyfloat.json +++ b/src/commands/incrbyfloat.json @@ -35,6 +35,10 @@ } } ], + "reply_schema": { + "type": "string", + "description": "The value of the key after incrementing it." + }, "arguments": [ { "name": "key", diff --git a/src/commands/info.json b/src/commands/info.json index 612294d34b..b44e0c01fd 100644 --- a/src/commands/info.json +++ b/src/commands/info.json @@ -25,6 +25,9 @@ "REQUEST_POLICY:ALL_SHARDS", "RESPONSE_POLICY:SPECIAL" ], + "reply_schema": { + "type": "string" + }, "arguments": [ { "name": "section", diff --git a/src/commands/keys.json b/src/commands/keys.json index 546241f95c..cedf3ce7fe 100644 --- a/src/commands/keys.json +++ b/src/commands/keys.json @@ -22,6 +22,13 @@ "name": "pattern", "type": "pattern" } - ] + ], + "reply_schema": { + "description": "list of keys matching pattern", + "type": "array", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/lastsave.json b/src/commands/lastsave.json index 9290584587..8988e75d7e 100644 --- a/src/commands/lastsave.json +++ b/src/commands/lastsave.json @@ -17,6 +17,10 @@ "acl_categories": [ "ADMIN", "DANGEROUS" - ] + ], + "reply_schema": { + "type": "integer", + "description": "UNIX TIME of the last DB save executed with success." + } } } diff --git a/src/commands/latency-doctor.json b/src/commands/latency-doctor.json index 129b32358e..8d3a98b3c7 100644 --- a/src/commands/latency-doctor.json +++ b/src/commands/latency-doctor.json @@ -17,6 +17,10 @@ "NONDETERMINISTIC_OUTPUT", "REQUEST_POLICY:ALL_NODES", "RESPONSE_POLICY:SPECIAL" - ] + ], + "reply_schema": { + "type": "string", + "description": "A human readable latency analysis report." + } } } diff --git a/src/commands/latency-graph.json b/src/commands/latency-graph.json index 0644c1cb00..d634da7463 100644 --- a/src/commands/latency-graph.json +++ b/src/commands/latency-graph.json @@ -23,6 +23,10 @@ "name": "event", "type": "string" } - ] + ], + "reply_schema": { + "type": "string", + "description": "Latency graph" + } } } diff --git a/src/commands/latency-help.json b/src/commands/latency-help.json index 682beee2ad..e91679eb7a 100644 --- a/src/commands/latency-help.json +++ b/src/commands/latency-help.json @@ -10,6 +10,13 @@ "command_flags": [ "LOADING", "STALE" - ] + ], + "reply_schema": { + "type": "array", + "description": "Helpful text about subcommands.", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/latency-histogram.json b/src/commands/latency-histogram.json index dc14d47f83..d9821ea850 100644 --- a/src/commands/latency-histogram.json +++ b/src/commands/latency-histogram.json @@ -18,6 +18,30 @@ "REQUEST_POLICY:ALL_NODES", "RESPONSE_POLICY:SPECIAL" ], + "reply_schema": { + "type": "object", + "description": "A map where each key is a command name, and each value is a map with the total calls, and an inner map of the histogram time buckets.", + "patternProperties": { + "^.*$": { + "type": "object", + "additionalProperties": false, + "properties": { + "calls": { + "description": "The total calls for the command.", + "type": "integer", + "minimum": 0 + }, + "histogram_usec": { + "description": "Histogram map, bucket id to latency", + "type": "object", + "additionalProperties": { + "type": "integer" + } + } + } + } + } + }, "arguments": [ { "name": "COMMAND", diff --git a/src/commands/latency-history.json b/src/commands/latency-history.json index 6d9267064c..11fa8857b5 100644 --- a/src/commands/latency-history.json +++ b/src/commands/latency-history.json @@ -18,6 +18,27 @@ "REQUEST_POLICY:ALL_NODES", "RESPONSE_POLICY:SPECIAL" ], + "reply_schema": { + "type": "array", + "description": "An array where each element is a two elements array representing the timestamp and the latency of the event.", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "timestamp of the event", + "type": "integer", + "minimum": 0 + }, + { + "description": "latency of the event", + "type": "integer", + "minimum": 0 + } + ] + } + }, "arguments": [ { "name": "event", diff --git a/src/commands/latency-latest.json b/src/commands/latency-latest.json index f513689c56..7e4d423179 100644 --- a/src/commands/latency-latest.json +++ b/src/commands/latency-latest.json @@ -17,6 +17,33 @@ "NONDETERMINISTIC_OUTPUT", "REQUEST_POLICY:ALL_NODES", "RESPONSE_POLICY:SPECIAL" - ] + ], + "reply_schema": { + "type": "array", + "description": "An array where each element is a four elements array representing the event's name, timestamp, latest and all-time latency measurements.", + "items": { + "type": "array", + "minItems": 4, + "maxItems": 4, + "items": [ + { + "type": "string", + "description": "Event name." + }, + { + "type": "integer", + "description": "Timestamp." + }, + { + "type": "integer", + "description": "Latest latency in milliseconds." + }, + { + "type": "integer", + "description": "Max latency in milliseconds." + } + ] + } + } } } diff --git a/src/commands/latency-reset.json b/src/commands/latency-reset.json index 30295cc057..354e3fe740 100644 --- a/src/commands/latency-reset.json +++ b/src/commands/latency-reset.json @@ -17,6 +17,10 @@ "REQUEST_POLICY:ALL_NODES", "RESPONSE_POLICY:ALL_SUCCEEDED" ], + "reply_schema": { + "type": "integer", + "description": "Number of event time series that were reset." + }, "arguments": [ { "name": "event", diff --git a/src/commands/lcs.json b/src/commands/lcs.json index 8d53e425e9..fe7840b34e 100644 --- a/src/commands/lcs.json +++ b/src/commands/lcs.json @@ -32,6 +32,61 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "type": "string", + "description": "The longest common subsequence." + }, + { + "type": "integer", + "description": "The length of the longest common subsequence when 'LEN' is given." + }, + { + "type": "object", + "description": "Array with the LCS length and all the ranges in both the strings when 'IDX' is given. In RESP2 this is returned as a flat array", + "additionalProperties": false, + "properties": { + "matches": { + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 3, + "items": [ + { + "type": "array", + "description": "Matched range in the first string.", + "minItems": 2, + "maxItems": 2, + "items": { + "type": "integer" + } + }, + { + "type": "array", + "description": "Matched range in the second string.", + "minItems": 2, + "maxItems": 2, + "items": { + "type": "integer" + } + } + ], + "additionalItems": { + "type": "integer", + "description": "The length of the match when 'WITHMATCHLEN' is given." + } + } + }, + "len": { + "type": "integer", + "description": "Length of the longest common subsequence." + } + } + } + ] + }, "arguments": [ { "name": "key1", diff --git a/src/commands/lindex.json b/src/commands/lindex.json index b2397241f1..89b14ea9fc 100644 --- a/src/commands/lindex.json +++ b/src/commands/lindex.json @@ -32,6 +32,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "type": "null", + "description": "Index is out of range" + }, + { + "description": "The requested element", + "type": "string" + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/linsert.json b/src/commands/linsert.json index 71046fa583..d31a4de8d3 100644 --- a/src/commands/linsert.json +++ b/src/commands/linsert.json @@ -33,6 +33,23 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "List length after a successful insert operation.", + "type": "integer", + "minimum": 1 + }, + { + "description": "in case key doesn't exist.", + "const": 0 + }, + { + "description": "when the pivot wasn't found.", + "const": -1 + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/llen.json b/src/commands/llen.json index 720b23778c..1452e22d7f 100644 --- a/src/commands/llen.json +++ b/src/commands/llen.json @@ -32,6 +32,11 @@ } } ], + "reply_schema": { + "description": "List length.", + "type": "integer", + "minimum": 0 + }, "arguments": [ { "name": "key", diff --git a/src/commands/lmove.json b/src/commands/lmove.json index 060cc7a321..82c305c12f 100644 --- a/src/commands/lmove.json +++ b/src/commands/lmove.json @@ -52,6 +52,10 @@ } } ], + "reply_schema": { + "description": "The element being popped and pushed.", + "type": "string" + }, "arguments": [ { "name": "source", diff --git a/src/commands/lmpop.json b/src/commands/lmpop.json index 0821e4c689..e788b5136b 100644 --- a/src/commands/lmpop.json +++ b/src/commands/lmpop.json @@ -34,6 +34,34 @@ } } ], + "reply_schema": { + "anyOf": [ + { + "description": "If no element could be popped.", + "type": "null" + }, + { + "description": "List key from which elements were popped.", + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "Name of the key from which elements were popped.", + "type": "string" + }, + { + "description": "Array of popped elements.", + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ] + } + ] + }, "arguments": [ { "name": "numkeys", diff --git a/src/commands/lolwut.json b/src/commands/lolwut.json index cf0a98504e..b093404b59 100644 --- a/src/commands/lolwut.json +++ b/src/commands/lolwut.json @@ -9,6 +9,10 @@ "READONLY", "FAST" ], + "reply_schema": { + "type": "string", + "description": "String containing the generative computer art, and a text with the Redis version." + }, "arguments": [ { "token": "VERSION", diff --git a/src/commands/lpop.json b/src/commands/lpop.json index cd3de0c7ad..a7b93c2766 100644 --- a/src/commands/lpop.json +++ b/src/commands/lpop.json @@ -40,6 +40,26 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "Key does not exist.", + "type": "null" + }, + { + "description": "In case `count` argument was not given, the value of the first element.", + "type": "string" + }, + { + "description": "In case `count` argument was given, a list of popped elements", + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "arguments": [ { "name": "key", diff --git a/src/commands/lpos.json b/src/commands/lpos.json index 3aea3191fc..8e3a9fa02f 100644 --- a/src/commands/lpos.json +++ b/src/commands/lpos.json @@ -32,6 +32,26 @@ } } ], + "reply_schema": { + "anyOf": [ + { + "description": "In case there is no matching element", + "type": "null" + }, + { + "description": "An integer representing the matching element", + "type": "integer" + }, + { + "description": "If the COUNT option is given, an array of integers representing the matching elements (empty if there are no matches)", + "type": "array", + "uniqueItems": true, + "items": { + "type": "integer" + } + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/lpush.json b/src/commands/lpush.json index 23a9c36b43..6fc8152982 100644 --- a/src/commands/lpush.json +++ b/src/commands/lpush.json @@ -40,6 +40,10 @@ } } ], + "reply_schema": { + "description": "Length of the list after the push operations.", + "type": "integer" + }, "arguments": [ { "name": "key", diff --git a/src/commands/lpushx.json b/src/commands/lpushx.json index b1629b6298..d41f50b773 100644 --- a/src/commands/lpushx.json +++ b/src/commands/lpushx.json @@ -40,6 +40,11 @@ } } ], + "reply_schema": { + "type": "integer", + "description": "the length of the list after the push operation", + "minimum": 0 + }, "arguments": [ { "name": "key", diff --git a/src/commands/lrange.json b/src/commands/lrange.json index 8fa9352bb1..1c0868caa5 100644 --- a/src/commands/lrange.json +++ b/src/commands/lrange.json @@ -46,6 +46,13 @@ "name": "stop", "type": "integer" } - ] + ], + "reply_schema": { + "description": "List of elements in the specified range", + "type": "array", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/lrem.json b/src/commands/lrem.json index c845cc04a6..84baeea0bb 100644 --- a/src/commands/lrem.json +++ b/src/commands/lrem.json @@ -32,6 +32,11 @@ } } ], + "reply_schema": { + "description": "The number of removed elements.", + "type": "integer", + "minimum": 0 + }, "arguments": [ { "name": "key", diff --git a/src/commands/lset.json b/src/commands/lset.json index 9a9e4fd5e9..441db073c2 100644 --- a/src/commands/lset.json +++ b/src/commands/lset.json @@ -33,6 +33,9 @@ } } ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "key", diff --git a/src/commands/ltrim.json b/src/commands/ltrim.json index f177d8f66c..c041f49ca7 100644 --- a/src/commands/ltrim.json +++ b/src/commands/ltrim.json @@ -32,6 +32,9 @@ } } ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "key", diff --git a/src/commands/memory-doctor.json b/src/commands/memory-doctor.json index b6691dfa0e..5df7456f59 100644 --- a/src/commands/memory-doctor.json +++ b/src/commands/memory-doctor.json @@ -11,6 +11,10 @@ "NONDETERMINISTIC_OUTPUT", "REQUEST_POLICY:ALL_SHARDS", "RESPONSE_POLICY:SPECIAL" - ] + ], + "reply_schema": { + "description": "memory problems report", + "type": "string" + } } } diff --git a/src/commands/memory-help.json b/src/commands/memory-help.json index a1cda71f84..34ee382c59 100644 --- a/src/commands/memory-help.json +++ b/src/commands/memory-help.json @@ -10,6 +10,13 @@ "command_flags": [ "LOADING", "STALE" - ] + ], + "reply_schema": { + "type": "array", + "description": "Helpful text about subcommands.", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/memory-malloc-stats.json b/src/commands/memory-malloc-stats.json index 5106781fe8..a44959c881 100644 --- a/src/commands/memory-malloc-stats.json +++ b/src/commands/memory-malloc-stats.json @@ -11,6 +11,10 @@ "NONDETERMINISTIC_OUTPUT", "REQUEST_POLICY:ALL_SHARDS", "RESPONSE_POLICY:SPECIAL" - ] + ], + "reply_schema": { + "type": "string", + "description": "The memory allocator's internal statistics report." + } } } diff --git a/src/commands/memory-purge.json b/src/commands/memory-purge.json index b862534d16..09c7d124c8 100644 --- a/src/commands/memory-purge.json +++ b/src/commands/memory-purge.json @@ -10,6 +10,9 @@ "command_tips": [ "REQUEST_POLICY:ALL_SHARDS", "RESPONSE_POLICY:ALL_SUCCEEDED" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/memory-stats.json b/src/commands/memory-stats.json index 76e6baa3e2..b50a9eb31c 100644 --- a/src/commands/memory-stats.json +++ b/src/commands/memory-stats.json @@ -11,6 +11,111 @@ "NONDETERMINISTIC_OUTPUT", "REQUEST_POLICY:ALL_SHARDS", "RESPONSE_POLICY:SPECIAL" - ] + ], + "reply_schema": { + "description": "memory usage details", + "type": "object", + "additionalProperties": false, + "properties": { + "peak.allocated": { + "type": "integer" + }, + "total.allocated": { + "type": "integer" + }, + "startup.allocated": { + "type": "integer" + }, + "replication.backlog": { + "type": "integer" + }, + "clients.slaves": { + "type": "integer" + }, + "clients.normal": { + "type": "integer" + }, + "cluster.links": { + "type": "integer" + }, + "aof.buffer": { + "type": "integer" + }, + "lua.caches": { + "type": "integer" + }, + "functions.caches": { + "type": "integer" + }, + "overhead.total": { + "type": "integer" + }, + "keys.count": { + "type": "integer" + }, + "keys.bytes-per-key": { + "type": "integer" + }, + "dataset.bytes": { + "type": "integer" + }, + "dataset.percentage": { + "type": "number" + }, + "peak.percentage": { + "type": "number" + }, + "allocator.allocated": { + "type": "integer" + }, + "allocator.active": { + "type": "integer" + }, + "allocator.resident": { + "type": "integer" + }, + "allocator-fragmentation.ratio": { + "type": "number" + }, + "allocator-fragmentation.bytes": { + "type": "integer" + }, + "allocator-rss.ratio": { + "type": "number" + }, + "allocator-rss.bytes": { + "type": "integer" + }, + "rss-overhead.ratio": { + "type": "number" + }, + "rss-overhead.bytes": { + "type": "integer" + }, + "fragmentation": { + "type": "number" + }, + "fragmentation.bytes": { + "type": "integer" + } + }, + "patternProperties": { + "^db.": { + "type": "object", + "properties": { + "overhead.hashtable.main": { + "type": "integer" + }, + "overhead.hashtable.expires": { + "type": "integer" + }, + "overhead.hashtable.slot-to-keys": { + "type": "integer" + } + }, + "additionalProperties": false + } + } + } } } diff --git a/src/commands/memory-usage.json b/src/commands/memory-usage.json index fa6b7c7e84..ff977d9be3 100644 --- a/src/commands/memory-usage.json +++ b/src/commands/memory-usage.json @@ -29,6 +29,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "Number of bytes that a key and its value require to be stored in RAM.", + "type": "integer" + }, + { + "description": "Key does not exist.", + "type": "null" + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/mget.json b/src/commands/mget.json index fdff809980..a5bd8c30e7 100644 --- a/src/commands/mget.json +++ b/src/commands/mget.json @@ -36,6 +36,21 @@ } } ], + "reply_schema": { + "description": "List of values at the specified keys.", + "type": "array", + "minItems": 1, + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, "arguments": [ { "name": "key", diff --git a/src/commands/migrate.json b/src/commands/migrate.json index 64bf78bba9..83a1dd1490 100644 --- a/src/commands/migrate.json +++ b/src/commands/migrate.json @@ -77,6 +77,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "const": "OK", + "description": "Success." + }, + { + "const": "NOKEY", + "description": "No keys were found in the source instance." + } + ] + }, "arguments": [ { "name": "host", diff --git a/src/commands/module-help.json b/src/commands/module-help.json index b8db8aee3b..10b59dc2d0 100644 --- a/src/commands/module-help.json +++ b/src/commands/module-help.json @@ -10,6 +10,13 @@ "command_flags": [ "LOADING", "STALE" - ] + ], + "reply_schema": { + "type": "array", + "description": "Helpful text about subcommands.", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/module-list.json b/src/commands/module-list.json index ed6e7d19be..65a8c9f3b7 100644 --- a/src/commands/module-list.json +++ b/src/commands/module-list.json @@ -13,6 +13,35 @@ ], "command_tips": [ "NONDETERMINISTIC_OUTPUT_ORDER" - ] + ], + "reply_schema": { + "type": "array", + "description": "Returns information about the modules loaded to the server.", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "Name of the module." + }, + "ver": { + "type": "integer", + "description": "Version of the module." + }, + "path": { + "type": "string", + "description": "Module path." + }, + "args": { + "type": "array", + "description": "Module arguments.", + "items": { + "type": "string" + } + } + } + } + } } } diff --git a/src/commands/module-load.json b/src/commands/module-load.json index 84e6d35967..1ce5faf10e 100644 --- a/src/commands/module-load.json +++ b/src/commands/module-load.json @@ -13,6 +13,9 @@ "NOSCRIPT", "PROTECTED" ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "path", diff --git a/src/commands/module-loadex.json b/src/commands/module-loadex.json index 9419aa010e..7aa2788315 100644 --- a/src/commands/module-loadex.json +++ b/src/commands/module-loadex.json @@ -13,6 +13,9 @@ "NOSCRIPT", "PROTECTED" ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "path", diff --git a/src/commands/module-unload.json b/src/commands/module-unload.json index 8820ba3aa7..3cd85b3780 100644 --- a/src/commands/module-unload.json +++ b/src/commands/module-unload.json @@ -13,6 +13,9 @@ "NOSCRIPT", "PROTECTED" ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "name", diff --git a/src/commands/move.json b/src/commands/move.json index 0c7c71e058..cd588ff596 100644 --- a/src/commands/move.json +++ b/src/commands/move.json @@ -44,6 +44,18 @@ "name": "db", "type": "integer" } - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "key was moved", + "const": 1 + }, + { + "description": "key wasn't moved", + "const": 0 + } + ] + } } } diff --git a/src/commands/mset.json b/src/commands/mset.json index ebf3c5216e..deff39ec8b 100644 --- a/src/commands/mset.json +++ b/src/commands/mset.json @@ -37,6 +37,9 @@ } } ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "data", diff --git a/src/commands/msetnx.json b/src/commands/msetnx.json index 84ec000630..90a6449d11 100644 --- a/src/commands/msetnx.json +++ b/src/commands/msetnx.json @@ -37,6 +37,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "No key was set (at least one key already existed).", + "const": 0 + }, + { + "description": "All the keys were set.", + "const": 1 + } + ] + }, "arguments": [ { "name": "data", diff --git a/src/commands/multi.json b/src/commands/multi.json index f1299a6f4d..dd94bce26b 100644 --- a/src/commands/multi.json +++ b/src/commands/multi.json @@ -15,6 +15,9 @@ ], "acl_categories": [ "TRANSACTION" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/object-encoding.json b/src/commands/object-encoding.json index 2d39a07ef0..902a2f39ef 100644 --- a/src/commands/object-encoding.json +++ b/src/commands/object-encoding.json @@ -41,6 +41,18 @@ "type": "key", "key_spec_index": 0 } - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "key doesn't exist", + "type": "null" + }, + { + "description": "encoding of the object", + "type": "string" + } + ] + } } } diff --git a/src/commands/object-freq.json b/src/commands/object-freq.json index d184f2e7e8..79c58fb632 100644 --- a/src/commands/object-freq.json +++ b/src/commands/object-freq.json @@ -41,6 +41,10 @@ "type": "key", "key_spec_index": 0 } - ] + ], + "reply_schema": { + "description": "the counter's value", + "type": "integer" + } } } diff --git a/src/commands/object-help.json b/src/commands/object-help.json index 22864bafa6..bf1fac643a 100644 --- a/src/commands/object-help.json +++ b/src/commands/object-help.json @@ -13,6 +13,13 @@ ], "acl_categories": [ "KEYSPACE" - ] + ], + "reply_schema": { + "type": "array", + "description": "Helpful text about subcommands.", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/object-idletime.json b/src/commands/object-idletime.json index 162d6f514e..8e124df6f7 100644 --- a/src/commands/object-idletime.json +++ b/src/commands/object-idletime.json @@ -41,6 +41,10 @@ "type": "key", "key_spec_index": 0 } - ] + ], + "reply_schema": { + "description": "the idle time in seconds", + "type": "integer" + } } } diff --git a/src/commands/object-refcount.json b/src/commands/object-refcount.json index 0f36f50924..82e2a54ea0 100644 --- a/src/commands/object-refcount.json +++ b/src/commands/object-refcount.json @@ -41,6 +41,10 @@ "type": "key", "key_spec_index": 0 } - ] + ], + "reply_schema": { + "description": "the number of references", + "type": "integer" + } } } diff --git a/src/commands/persist.json b/src/commands/persist.json index f08df4c1a9..2ca99642bf 100644 --- a/src/commands/persist.json +++ b/src/commands/persist.json @@ -33,6 +33,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "const": 0, + "description": "Key does not exist or does not have an associated timeout." + }, + { + "const": 1, + "description": "The timeout has been removed." + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/pexpire.json b/src/commands/pexpire.json index 9638b8eaa8..03416a1eb3 100644 --- a/src/commands/pexpire.json +++ b/src/commands/pexpire.json @@ -39,6 +39,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "const": 0, + "description": "The timeout was not set. e.g. key doesn't exist, or operation skipped due to the provided arguments." + }, + { + "const": 1, + "description": "The timeout was set." + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/pexpireat.json b/src/commands/pexpireat.json index c08782c261..ac09ba2e01 100644 --- a/src/commands/pexpireat.json +++ b/src/commands/pexpireat.json @@ -39,6 +39,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "const": 1, + "description": "The timeout was set." + }, + { + "const": 0, + "description": "The timeout was not set. e.g. key doesn't exist, or operation skipped due to the provided arguments." + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/pexpiretime.json b/src/commands/pexpiretime.json index 6ba9212786..9295a6a17a 100644 --- a/src/commands/pexpiretime.json +++ b/src/commands/pexpiretime.json @@ -33,6 +33,23 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "type": "integer", + "description": "Expiration Unix timestamp in milliseconds.", + "minimum": 0 + }, + { + "const": -1, + "description": "The key exists but has no associated expiration time." + }, + { + "const": -2, + "description": "The key does not exist." + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/pfadd.json b/src/commands/pfadd.json index 8076a7631e..f457d0f557 100644 --- a/src/commands/pfadd.json +++ b/src/commands/pfadd.json @@ -46,6 +46,18 @@ "optional": true, "multiple": true } - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "if at least 1 HyperLogLog internal register was altered", + "const": 1 + }, + { + "description": "if no HyperLogLog internal register were altered", + "const": 0 + } + ] + } } } diff --git a/src/commands/pfcount.json b/src/commands/pfcount.json index 4d89e1d380..92f84d8952 100644 --- a/src/commands/pfcount.json +++ b/src/commands/pfcount.json @@ -41,6 +41,10 @@ "key_spec_index": 0, "multiple": true } - ] + ], + "reply_schema": { + "description": "The approximated number of unique elements observed via PFADD", + "type": "integer" + } } } diff --git a/src/commands/pfmerge.json b/src/commands/pfmerge.json index 648a8b43a1..9f1f417fff 100644 --- a/src/commands/pfmerge.json +++ b/src/commands/pfmerge.json @@ -65,6 +65,9 @@ "optional": true, "multiple": true } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/pfselftest.json b/src/commands/pfselftest.json index b75ce03699..f9c6a1002b 100644 --- a/src/commands/pfselftest.json +++ b/src/commands/pfselftest.json @@ -14,6 +14,9 @@ ], "acl_categories": [ "HYPERLOGLOG" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/ping.json b/src/commands/ping.json index e7db2c3324..13db35d438 100644 --- a/src/commands/ping.json +++ b/src/commands/ping.json @@ -17,6 +17,18 @@ "REQUEST_POLICY:ALL_SHARDS", "RESPONSE_POLICY:ALL_SUCCEEDED" ], + "reply_schema": { + "anyOf": [ + { + "const": "PONG", + "description": "Default reply." + }, + { + "type": "string", + "description": "Relay of given `message`." + } + ] + }, "arguments": [ { "name": "message", diff --git a/src/commands/psetex.json b/src/commands/psetex.json index ce957f82e6..427f23279a 100644 --- a/src/commands/psetex.json +++ b/src/commands/psetex.json @@ -38,6 +38,9 @@ } } ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "key", diff --git a/src/commands/pttl.json b/src/commands/pttl.json index 1d37b9a497..8632489017 100644 --- a/src/commands/pttl.json +++ b/src/commands/pttl.json @@ -42,6 +42,23 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "TTL in milliseconds.", + "type": "integer", + "minimum": 0 + }, + { + "description": "The key exists but has no associated expire.", + "const": -1 + }, + { + "description": "The key does not exist.", + "const": -2 + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/publish.json b/src/commands/publish.json index 3c9b12f4d5..05313766b0 100644 --- a/src/commands/publish.json +++ b/src/commands/publish.json @@ -23,6 +23,11 @@ "name": "message", "type": "string" } - ] + ], + "reply_schema": { + "description": "the number of clients that received the message. Note that in a Redis Cluster, only clients that are connected to the same node as the publishing client are included in the count", + "type": "integer", + "minimum": 0 + } } } diff --git a/src/commands/pubsub-channels.json b/src/commands/pubsub-channels.json index 0522504b1c..173271271b 100644 --- a/src/commands/pubsub-channels.json +++ b/src/commands/pubsub-channels.json @@ -18,6 +18,14 @@ "type": "pattern", "optional": true } - ] + ], + "reply_schema": { + "description": "a list of active channels, optionally matching the specified pattern", + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + } } } diff --git a/src/commands/pubsub-help.json b/src/commands/pubsub-help.json index e0c2a61234..09c04f3a45 100644 --- a/src/commands/pubsub-help.json +++ b/src/commands/pubsub-help.json @@ -10,6 +10,13 @@ "command_flags": [ "LOADING", "STALE" - ] + ], + "reply_schema": { + "type": "array", + "description": "Helpful text about subcommands.", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/pubsub-numpat.json b/src/commands/pubsub-numpat.json index 382ec1b70d..b593a4e6dd 100644 --- a/src/commands/pubsub-numpat.json +++ b/src/commands/pubsub-numpat.json @@ -11,6 +11,11 @@ "PUBSUB", "LOADING", "STALE" - ] + ], + "reply_schema": { + "description": "the number of patterns all the clients are subscribed to", + "type": "integer", + "minimum": 0 + } } } diff --git a/src/commands/pubsub-numsub.json b/src/commands/pubsub-numsub.json index fae05c8fd2..1df663be23 100644 --- a/src/commands/pubsub-numsub.json +++ b/src/commands/pubsub-numsub.json @@ -19,6 +19,10 @@ "optional": true, "multiple": true } - ] + ], + "reply_schema": { + "description": "the number of subscribers per channel, each even element (including 0th) is channel name, each odd element is the number of subscribers", + "type": "array" + } } } diff --git a/src/commands/pubsub-shardchannels.json b/src/commands/pubsub-shardchannels.json index 90b907d302..cd196105b5 100644 --- a/src/commands/pubsub-shardchannels.json +++ b/src/commands/pubsub-shardchannels.json @@ -18,6 +18,14 @@ "type": "pattern", "optional": true } - ] + ], + "reply_schema": { + "description": "a list of active channels, optionally matching the specified pattern", + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } } } diff --git a/src/commands/pubsub-shardnumsub.json b/src/commands/pubsub-shardnumsub.json index 89187696a4..536568a2ab 100644 --- a/src/commands/pubsub-shardnumsub.json +++ b/src/commands/pubsub-shardnumsub.json @@ -19,6 +19,10 @@ "optional": true, "multiple": true } - ] + ], + "reply_schema": { + "description": "the number of subscribers per shard channel, each even element (including 0th) is channel name, each odd element is the number of subscribers", + "type": "array" + } } } diff --git a/src/commands/quit.json b/src/commands/quit.json index cdb80336d0..feb371955f 100644 --- a/src/commands/quit.json +++ b/src/commands/quit.json @@ -21,6 +21,9 @@ ], "acl_categories": [ "CONNECTION" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/randomkey.json b/src/commands/randomkey.json index ceaa3c8868..18f838b05d 100644 --- a/src/commands/randomkey.json +++ b/src/commands/randomkey.json @@ -16,6 +16,18 @@ "command_tips": [ "REQUEST_POLICY:ALL_SHARDS", "NONDETERMINISTIC_OUTPUT" - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "when the database is empty", + "type": "null" + }, + { + "description": "random key in db", + "type": "string" + } + ] + } } } diff --git a/src/commands/readonly.json b/src/commands/readonly.json index 1bbc220eda..4dcd50af8b 100644 --- a/src/commands/readonly.json +++ b/src/commands/readonly.json @@ -13,6 +13,9 @@ ], "acl_categories": [ "CONNECTION" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/readwrite.json b/src/commands/readwrite.json index 81e505ffe8..e72d5cac53 100644 --- a/src/commands/readwrite.json +++ b/src/commands/readwrite.json @@ -13,6 +13,9 @@ ], "acl_categories": [ "CONNECTION" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/rename.json b/src/commands/rename.json index 561bf22b64..046d40ae34 100644 --- a/src/commands/rename.json +++ b/src/commands/rename.json @@ -64,6 +64,9 @@ "type": "key", "key_spec_index": 1 } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/renamenx.json b/src/commands/renamenx.json index afa4f658b1..a6c039182f 100644 --- a/src/commands/renamenx.json +++ b/src/commands/renamenx.json @@ -69,6 +69,18 @@ "type": "key", "key_spec_index": 1 } - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "key was renamed to newkey", + "const": 1 + }, + { + "description": "new key already exists", + "const": 0 + } + ] + } } } diff --git a/src/commands/replconf.json b/src/commands/replconf.json index 630b62136e..d5c43d5254 100644 --- a/src/commands/replconf.json +++ b/src/commands/replconf.json @@ -15,6 +15,9 @@ "LOADING", "STALE", "ALLOW_BUSY" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/replicaof.json b/src/commands/replicaof.json index 6299ea3ff5..7f1d08754d 100644 --- a/src/commands/replicaof.json +++ b/src/commands/replicaof.json @@ -21,6 +21,11 @@ "name": "port", "type": "integer" } - ] + ], + "reply_schema": { + "description": "replicaOf status", + "type": "string", + "pattern": "OK*" + } } } diff --git a/src/commands/reset.json b/src/commands/reset.json index 40041cd8c7..81a20a3a8b 100644 --- a/src/commands/reset.json +++ b/src/commands/reset.json @@ -16,6 +16,9 @@ ], "acl_categories": [ "CONNECTION" - ] + ], + "reply_schema": { + "const": "RESET" + } } } diff --git a/src/commands/restore-asking.json b/src/commands/restore-asking.json index f4602f9715..b260478f27 100644 --- a/src/commands/restore-asking.json +++ b/src/commands/restore-asking.json @@ -94,6 +94,9 @@ "optional": true, "since": "5.0.0" } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/restore.json b/src/commands/restore.json index d6cebf578e..7e40d1c4f8 100644 --- a/src/commands/restore.json +++ b/src/commands/restore.json @@ -48,6 +48,9 @@ } } ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "key", diff --git a/src/commands/role.json b/src/commands/role.json index 4d470e3508..498a5de525 100644 --- a/src/commands/role.json +++ b/src/commands/role.json @@ -16,6 +16,119 @@ "acl_categories": [ "ADMIN", "DANGEROUS" - ] + ], + "reply_schema": { + "oneOf": [ + { + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": [ + { + "const": "master" + }, + { + "description": "current replication master offset", + "type": "integer" + }, + { + "description": "connected replicas", + "type": "array", + "items": { + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": [ + { + "description": "replica ip", + "type": "string" + }, + { + "description": "replica port", + "type": "string" + }, + { + "description": "last acknowledged replication offset", + "type": "string" + } + ] + } + } + ] + }, + { + "type": "array", + "minItems": 5, + "maxItems": 5, + "items": [ + { + "const": "slave" + }, + { + "description": "ip of master", + "type": "string" + }, + { + "description": "port number of master", + "type": "integer" + }, + { + "description": "state of the replication from the point of view of the master", + "oneOf": [ + { + "description": "the instance is in handshake with its master", + "const": "handshake" + }, + { + "description": "the instance in not active", + "const": "none" + }, + { + "description": "the instance needs to connect to its master", + "const": "connect" + }, + { + "description": "the master-replica connection is in progress", + "const": "connecting" + }, + { + "description": "the master and replica are trying to perform the synchronization", + "const": "sync" + }, + { + "description": "the replica is online", + "const": "connected" + }, + { + "description": "instance state is unknown", + "const": "unknown" + } + ] + }, + { + "description": "the amount of data received from the replica so far in terms of master replication offset", + "type": "integer" + } + ] + }, + { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "const": "sentinel" + }, + { + "description": "list of master names monitored by this sentinel instance", + "type": "array", + "items": { + "type": "string" + } + } + ] + } + ] + } } } diff --git a/src/commands/rpop.json b/src/commands/rpop.json index 10518a8271..c9b55b1257 100644 --- a/src/commands/rpop.json +++ b/src/commands/rpop.json @@ -40,6 +40,25 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "type": "null", + "description": "Key does not exist." + }, + { + "type": "string", + "description": "When 'COUNT' was not given, the value of the last element." + }, + { + "type": "array", + "description": "When 'COUNT' was given, list of popped elements.", + "items": { + "type": "string" + } + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/rpoplpush.json b/src/commands/rpoplpush.json index ea3c7749da..ddb0537e70 100644 --- a/src/commands/rpoplpush.json +++ b/src/commands/rpoplpush.json @@ -57,6 +57,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "type": "string", + "description": "The element being popped and pushed." + }, + { + "type": "null", + "description": "Source list is empty." + } + ] + }, "arguments": [ { "name": "source", diff --git a/src/commands/rpush.json b/src/commands/rpush.json index 03c1f862a5..7b6a4a6341 100644 --- a/src/commands/rpush.json +++ b/src/commands/rpush.json @@ -40,6 +40,11 @@ } } ], + "reply_schema": { + "description": "Length of the list after the push operations.", + "type": "integer", + "minimum": 1 + }, "arguments": [ { "name": "key", diff --git a/src/commands/rpushx.json b/src/commands/rpushx.json index 9d8c14eedd..19294dd02a 100644 --- a/src/commands/rpushx.json +++ b/src/commands/rpushx.json @@ -40,6 +40,11 @@ } } ], + "reply_schema": { + "type": "integer", + "description": "Length of the list after the push operation.", + "minimum": 0 + }, "arguments": [ { "name": "key", diff --git a/src/commands/sadd.json b/src/commands/sadd.json index 841eb1ffa7..89c8246251 100644 --- a/src/commands/sadd.json +++ b/src/commands/sadd.json @@ -40,6 +40,10 @@ } } ], + "reply_schema": { + "description": "Number of elements that were added to the set, not including all the elements already present in the set.", + "type": "integer" + }, "arguments": [ { "name": "key", diff --git a/src/commands/save.json b/src/commands/save.json index 7cf5cd2462..1885128bf3 100644 --- a/src/commands/save.json +++ b/src/commands/save.json @@ -11,6 +11,9 @@ "ADMIN", "NOSCRIPT", "NO_MULTI" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/scan.json b/src/commands/scan.json index e0856d0f03..bdf27a5755 100644 --- a/src/commands/scan.json +++ b/src/commands/scan.json @@ -47,6 +47,25 @@ "optional": true, "since": "6.0.0" } - ] + ], + "reply_schema": { + "description": "cursor and scan response in array form", + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "cursor", + "type": "string" + }, + { + "description": "list of keys", + "type": "array", + "items": { + "type": "string" + } + } + ] + } } } diff --git a/src/commands/scard.json b/src/commands/scard.json index a1f1f8ef2f..0b7a832de1 100644 --- a/src/commands/scard.json +++ b/src/commands/scard.json @@ -32,6 +32,11 @@ } } ], + "reply_schema": { + "description": "The cardinality (number of elements) of the set, or 0 if key does not exist.", + "type": "integer", + "minimum": 0 + }, "arguments": [ { "name": "key", diff --git a/src/commands/script-debug.json b/src/commands/script-debug.json index a69ddcac1b..25899b94ad 100644 --- a/src/commands/script-debug.json +++ b/src/commands/script-debug.json @@ -35,6 +35,9 @@ } ] } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/script-exists.json b/src/commands/script-exists.json index e4070f2391..6296159077 100644 --- a/src/commands/script-exists.json +++ b/src/commands/script-exists.json @@ -23,6 +23,22 @@ "type": "string", "multiple": true } - ] + ], + "reply_schema": { + "description": "An array of integers that correspond to the specified SHA1 digest arguments.", + "type": "array", + "items": { + "oneOf": [ + { + "description": "sha1 hash exists in script cache", + "const": 1 + }, + { + "description": "sha1 hash does not exist in script cache", + "const": 0 + } + ] + } + } } } diff --git a/src/commands/script-flush.json b/src/commands/script-flush.json index f4d3408330..63cfb1e286 100644 --- a/src/commands/script-flush.json +++ b/src/commands/script-flush.json @@ -42,6 +42,9 @@ } ] } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/script-help.json b/src/commands/script-help.json index 7b3bc89ecb..d6c6853fdf 100644 --- a/src/commands/script-help.json +++ b/src/commands/script-help.json @@ -13,6 +13,13 @@ ], "acl_categories": [ "SCRIPTING" - ] + ], + "reply_schema": { + "type": "array", + "description": "Helpful text about subcommands.", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/script-kill.json b/src/commands/script-kill.json index 970ccd4075..c10ff0a0d9 100644 --- a/src/commands/script-kill.json +++ b/src/commands/script-kill.json @@ -17,6 +17,9 @@ "command_tips": [ "REQUEST_POLICY:ALL_SHARDS", "RESPONSE_POLICY:ONE_SUCCEEDED" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/script-load.json b/src/commands/script-load.json index b0b4e67e56..a369ee459f 100644 --- a/src/commands/script-load.json +++ b/src/commands/script-load.json @@ -23,6 +23,10 @@ "name": "script", "type": "string" } - ] + ], + "reply_schema": { + "description": "The SHA1 digest of the script added into the script cache", + "type": "string" + } } } diff --git a/src/commands/sdiff.json b/src/commands/sdiff.json index 6f5fd0a81c..ce7846c46b 100644 --- a/src/commands/sdiff.json +++ b/src/commands/sdiff.json @@ -35,6 +35,14 @@ } } ], + "reply_schema": { + "type": "array", + "description": "List with the members of the resulting set.", + "uniqueItems": true, + "items": { + "type": "string" + } + }, "arguments": [ { "name": "key", diff --git a/src/commands/sdiffstore.json b/src/commands/sdiffstore.json index c78cd89992..8ba88e627c 100644 --- a/src/commands/sdiffstore.json +++ b/src/commands/sdiffstore.json @@ -51,6 +51,11 @@ } } ], + "reply_schema": { + "description": "Number of the elements in the resulting set.", + "type": "integer", + "minimum": 0 + }, "arguments": [ { "name": "destination", diff --git a/src/commands/select.json b/src/commands/select.json index 4375cac646..0f68cde804 100644 --- a/src/commands/select.json +++ b/src/commands/select.json @@ -14,6 +14,9 @@ "acl_categories": [ "CONNECTION" ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "index", diff --git a/src/commands/sentinel-ckquorum.json b/src/commands/sentinel-ckquorum.json index 6180614cc4..df0a0032e4 100644 --- a/src/commands/sentinel-ckquorum.json +++ b/src/commands/sentinel-ckquorum.json @@ -11,6 +11,11 @@ "SENTINEL", "ONLY_SENTINEL" ], + "reply_schema": { + "type": "string", + "description": "Returns OK if the current Sentinel configuration is able to reach the quorum needed to failover a master, and the majority needed to authorize the failover.", + "pattern": "OK" + }, "arguments": [ { "name": "master-name", diff --git a/src/commands/sentinel-config.json b/src/commands/sentinel-config.json index f68d17ce85..2369ec1fe5 100644 --- a/src/commands/sentinel-config.json +++ b/src/commands/sentinel-config.json @@ -12,6 +12,72 @@ "SENTINEL", "ONLY_SENTINEL" ], + "reply_schema": { + "oneOf": [ + { + "type": "object", + "description": "When 'SENTINEL-CONFIG GET' is called, returns a map.", + "properties": { + "resolve-hostnames": { + "oneOf": [ + { + "const": "yes" + }, + { + "const": "no" + } + ] + }, + "announce-hostnames": { + "oneOf": [ + { + "const": "yes" + }, + { + "const": "no" + } + ] + }, + "announce-ip": { + "type": "string" + }, + "announce-port": { + "type": "integer" + }, + "sentinel-user": { + "type": "string" + }, + "sentinel-pass": { + "type": "string" + }, + "loglevel": { + "oneOf": [ + { + "const": "debug" + }, + { + "const": "verbose" + }, + { + "const": "notice" + }, + { + "const": "warning" + }, + { + "const": "unknown" + } + ] + } + }, + "additionalProperties": false + }, + { + "const": "OK", + "description": "When 'SENTINEL-CONFIG SET' is called, returns OK on success." + } + ] + }, "arguments": [ { "name":"action", diff --git a/src/commands/sentinel-failover.json b/src/commands/sentinel-failover.json index f6640168a6..87f9c4aca2 100644 --- a/src/commands/sentinel-failover.json +++ b/src/commands/sentinel-failover.json @@ -11,6 +11,10 @@ "SENTINEL", "ONLY_SENTINEL" ], + "reply_schema": { + "const": "OK", + "description": "Force a fail over as if the master was not reachable, and without asking for agreement to other Sentinels." + }, "arguments": [ { "name": "master-name", diff --git a/src/commands/sentinel-flushconfig.json b/src/commands/sentinel-flushconfig.json index 7d48cd482d..117109f066 100644 --- a/src/commands/sentinel-flushconfig.json +++ b/src/commands/sentinel-flushconfig.json @@ -11,6 +11,10 @@ "ADMIN", "SENTINEL", "ONLY_SENTINEL" - ] + ], + "reply_schema": { + "const": "OK", + "description": "Force Sentinel to rewrite its configuration on disk, including the current Sentinel state." + } } } diff --git a/src/commands/sentinel-get-master-addr-by-name.json b/src/commands/sentinel-get-master-addr-by-name.json index e0fde851cd..3dc3078672 100644 --- a/src/commands/sentinel-get-master-addr-by-name.json +++ b/src/commands/sentinel-get-master-addr-by-name.json @@ -12,6 +12,22 @@ "SENTINEL", "ONLY_SENTINEL" ], + "reply_schema": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "string", + "description": "IP addr or hostname." + }, + { + "type": "string", + "description": "Port.", + "pattern": "[0-9]+" + } + ] + }, "arguments": [ { "name": "master-name", diff --git a/src/commands/sentinel-help.json b/src/commands/sentinel-help.json index 4c20313eb9..5e3e9a712b 100644 --- a/src/commands/sentinel-help.json +++ b/src/commands/sentinel-help.json @@ -12,6 +12,13 @@ "STALE", "SENTINEL", "ONLY_SENTINEL" - ] + ], + "reply_schema": { + "type": "array", + "description": "Helpful text about subcommands.", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/sentinel-is-master-down-by-addr.json b/src/commands/sentinel-is-master-down-by-addr.json index 456ad183ae..b0ca319f2f 100644 --- a/src/commands/sentinel-is-master-down-by-addr.json +++ b/src/commands/sentinel-is-master-down-by-addr.json @@ -12,6 +12,33 @@ "SENTINEL", "ONLY_SENTINEL" ], + "reply_schema": { + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": [ + { + "oneOf": [ + { + "const": 0, + "description": "Master is up." + }, + { + "const": 1, + "description": "Master is down." + } + ] + }, + { + "type": "string", + "description": "Sentinel address." + }, + { + "type": "integer", + "description": "Port." + } + ] + }, "arguments": [ { "name": "ip", diff --git a/src/commands/sentinel-master.json b/src/commands/sentinel-master.json index ec10f43fda..46d6d950a8 100644 --- a/src/commands/sentinel-master.json +++ b/src/commands/sentinel-master.json @@ -12,6 +12,13 @@ "SENTINEL", "ONLY_SENTINEL" ], + "reply_schema": { + "type": "object", + "description": "The state and info of the specified master.", + "additionalProperties": { + "type": "string" + } + }, "arguments": [ { "name": "master-name", diff --git a/src/commands/sentinel-monitor.json b/src/commands/sentinel-monitor.json index 2c01df9009..2ea9aff58d 100644 --- a/src/commands/sentinel-monitor.json +++ b/src/commands/sentinel-monitor.json @@ -12,6 +12,9 @@ "SENTINEL", "ONLY_SENTINEL" ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "name", diff --git a/src/commands/sentinel-remove.json b/src/commands/sentinel-remove.json index 2e655e7f44..d79f60e6c4 100644 --- a/src/commands/sentinel-remove.json +++ b/src/commands/sentinel-remove.json @@ -12,6 +12,9 @@ "SENTINEL", "ONLY_SENTINEL" ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "master-name", diff --git a/src/commands/sentinel-replicas.json b/src/commands/sentinel-replicas.json index dc175a7ecd..454fcfb913 100644 --- a/src/commands/sentinel-replicas.json +++ b/src/commands/sentinel-replicas.json @@ -12,6 +12,16 @@ "SENTINEL", "ONLY_SENTINEL" ], + "reply_schema": { + "type": "array", + "description": "List of replicas for this master, and their state.", + "items": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, "arguments": [ { "name": "master-name", diff --git a/src/commands/sentinel-set.json b/src/commands/sentinel-set.json index 49feefcede..10dcc57351 100644 --- a/src/commands/sentinel-set.json +++ b/src/commands/sentinel-set.json @@ -12,6 +12,9 @@ "SENTINEL", "ONLY_SENTINEL" ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "master-name", diff --git a/src/commands/set.json b/src/commands/set.json index 688d534d71..3c6c06ff4a 100644 --- a/src/commands/set.json +++ b/src/commands/set.json @@ -55,6 +55,26 @@ } } ], + "reply_schema": { + "anyOf":[ + { + "description": "`GET` not given: Operation was aborted (conflict with one of the `XX`/`NX` options).", + "type": "null" + }, + { + "description": "`GET` not given: The key was set.", + "const": "OK" + }, + { + "description": "`GET` given: The key didn't exist before the `SET`", + "type": "null" + }, + { + "description": "`GET` given: The previous value of the key", + "type": "string" + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/setbit.json b/src/commands/setbit.json index 723dfd8f4d..bf18430785 100644 --- a/src/commands/setbit.json +++ b/src/commands/setbit.json @@ -34,6 +34,17 @@ } } ], + "reply_schema": { + "description": "The original bit value stored at offset.", + "oneOf": [ + { + "const": 0 + }, + { + "const": 1 + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/setex.json b/src/commands/setex.json index b99f59f7e3..a45561a211 100644 --- a/src/commands/setex.json +++ b/src/commands/setex.json @@ -38,6 +38,9 @@ } } ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "key", diff --git a/src/commands/setnx.json b/src/commands/setnx.json index 5150d8fd26..d026272bb5 100644 --- a/src/commands/setnx.json +++ b/src/commands/setnx.json @@ -39,6 +39,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "The key was set.", + "const": 0 + }, + { + "description": "The key was not set.", + "const": 1 + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/setrange.json b/src/commands/setrange.json index d1336719af..f9d61dfa39 100644 --- a/src/commands/setrange.json +++ b/src/commands/setrange.json @@ -33,6 +33,11 @@ } } ], + "reply_schema": { + "description": "Length of the string after it was modified by the command.", + "type": "integer", + "minimum": 0 + }, "arguments": [ { "name": "key", diff --git a/src/commands/shutdown.json b/src/commands/shutdown.json index 001cc25520..033a7012ed 100644 --- a/src/commands/shutdown.json +++ b/src/commands/shutdown.json @@ -60,6 +60,10 @@ "optional": true, "since": "7.0.0" } - ] + ], + "reply_schema": { + "description": "OK if ABORT was specified and shutdown was aborted. On successful shutdown, nothing is returned since the server quits and the connection is closed. On failure, an error is returned.", + "const": "OK" + } } } diff --git a/src/commands/sinter.json b/src/commands/sinter.json index 63f9e8676b..f0cd95de00 100644 --- a/src/commands/sinter.json +++ b/src/commands/sinter.json @@ -35,6 +35,14 @@ } } ], + "reply_schema": { + "type": "array", + "description": "List with the members of the resulting set.", + "uniqueItems": true, + "items": { + "type": "string" + } + }, "arguments": [ { "name": "key", diff --git a/src/commands/sintercard.json b/src/commands/sintercard.json index 8047f7a69d..16769850d4 100644 --- a/src/commands/sintercard.json +++ b/src/commands/sintercard.json @@ -33,6 +33,11 @@ } } ], + "reply_schema": { + "description": "Number of the elements in the resulting intersection.", + "type": "integer", + "minimum": 0 + }, "arguments": [ { "name": "numkeys", diff --git a/src/commands/sinterstore.json b/src/commands/sinterstore.json index 85e462e3e1..60a8db52ae 100644 --- a/src/commands/sinterstore.json +++ b/src/commands/sinterstore.json @@ -51,6 +51,11 @@ } } ], + "reply_schema": { + "description": "Number of the elements in the result set.", + "type": "integer", + "minimum": 0 + }, "arguments": [ { "name": "destination", diff --git a/src/commands/sismember.json b/src/commands/sismember.json index 7a814b82b8..cb81682ccc 100644 --- a/src/commands/sismember.json +++ b/src/commands/sismember.json @@ -32,6 +32,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "const": 0, + "description": "The element is not a member of the set, or the key does not exist." + }, + { + "const": 1, + "description": "The element is a member of the set." + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/slaveof.json b/src/commands/slaveof.json index 70560f1b66..f9266fabeb 100644 --- a/src/commands/slaveof.json +++ b/src/commands/slaveof.json @@ -26,6 +26,11 @@ "name": "port", "type": "integer" } - ] + ], + "reply_schema": { + "description": "slaveOf status", + "type": "string", + "pattern": "OK*" + } } } diff --git a/src/commands/slowlog-get.json b/src/commands/slowlog-get.json index 11212643ef..e4652e895e 100644 --- a/src/commands/slowlog-get.json +++ b/src/commands/slowlog-get.json @@ -22,6 +22,47 @@ "REQUEST_POLICY:ALL_NODES", "NONDETERMINISTIC_OUTPUT" ], + "reply_schema": { + "type": "array", + "description": "Entries from the slow log in chronological order.", + "uniqueItems": true, + "items": { + "type": "array", + "minItems": 6, + "maxItems": 6, + "items": [ + { + "type": "integer", + "description": "Slow log entry ID." + }, + { + "type": "integer", + "description": "The unix timestamp at which the logged command was processed.", + "minimum": 0 + }, + { + "type": "integer", + "description": "The amount of time needed for its execution, in microseconds.", + "minimum": 0 + }, + { + "type": "array", + "description": "The arguments of the command.", + "items": { + "type": "string" + } + }, + { + "type": "string", + "description": "Client IP address and port." + }, + { + "type": "string", + "description": "Client name if set via the CLIENT SETNAME command." + } + ] + } + }, "arguments": [ { "name": "count", diff --git a/src/commands/slowlog-help.json b/src/commands/slowlog-help.json index cf2707d38b..dde8fd4598 100644 --- a/src/commands/slowlog-help.json +++ b/src/commands/slowlog-help.json @@ -10,6 +10,13 @@ "command_flags": [ "LOADING", "STALE" - ] + ], + "reply_schema": { + "type": "array", + "description": "Helpful text about subcommands.", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/slowlog-len.json b/src/commands/slowlog-len.json index 9a8969b0dd..f8c7798cf8 100644 --- a/src/commands/slowlog-len.json +++ b/src/commands/slowlog-len.json @@ -16,6 +16,11 @@ "REQUEST_POLICY:ALL_NODES", "RESPONSE_POLICY:AGG_SUM", "NONDETERMINISTIC_OUTPUT" - ] + ], + "reply_schema": { + "type": "integer", + "description": "Number of entries in the slow log.", + "minimum": 0 + } } } diff --git a/src/commands/slowlog-reset.json b/src/commands/slowlog-reset.json index 36c024156c..c4006e3712 100644 --- a/src/commands/slowlog-reset.json +++ b/src/commands/slowlog-reset.json @@ -15,6 +15,9 @@ "command_tips": [ "REQUEST_POLICY:ALL_NODES", "RESPONSE_POLICY:ALL_SUCCEEDED" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/smembers.json b/src/commands/smembers.json index b5d4ff29d4..ff5969f51b 100644 --- a/src/commands/smembers.json +++ b/src/commands/smembers.json @@ -35,6 +35,14 @@ } } ], + "reply_schema": { + "type": "array", + "description": "All elements of the set.", + "uniqueItems": true, + "items": { + "type": "string" + } + }, "arguments": [ { "name": "key", diff --git a/src/commands/smismember.json b/src/commands/smismember.json index cb4dd2e672..d787879008 100644 --- a/src/commands/smismember.json +++ b/src/commands/smismember.json @@ -33,6 +33,23 @@ } } ], + "reply_schema": { + "type": "array", + "description": "List representing the membership of the given elements, in the same order as they are requested.", + "minItems": 1, + "items": { + "oneOf": [ + { + "const": 0, + "description": "Not a member of the set or the key does not exist." + }, + { + "const": 1, + "description": "A member of the set." + } + ] + } + }, "arguments": [ { "name": "key", diff --git a/src/commands/smove.json b/src/commands/smove.json index de5756de91..df282b2bb2 100644 --- a/src/commands/smove.json +++ b/src/commands/smove.json @@ -52,6 +52,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "const": 1, + "description": "Element is moved." + }, + { + "const": 0, + "description": "The element is not a member of source and no operation was performed." + } + ] + }, "arguments": [ { "name": "source", diff --git a/src/commands/sort.json b/src/commands/sort.json index 1c332461bf..6e1b2626ef 100644 --- a/src/commands/sort.json +++ b/src/commands/sort.json @@ -133,6 +133,30 @@ "key_spec_index": 2, "optional": true } - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "when the store option is specified the command returns the number of sorted elements in the destination list", + "type": "integer", + "minimum": 0 + }, + { + "description": "when not passing the store option the command returns a list of sorted elements", + "type": "array", + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "description": "GET option is specified, but no object was found ", + "type": "null" + } + ] + } + } + ] + } } } diff --git a/src/commands/sort_ro.json b/src/commands/sort_ro.json index 0b00ba8ad2..27af54c992 100644 --- a/src/commands/sort_ro.json +++ b/src/commands/sort_ro.json @@ -112,6 +112,13 @@ "type": "pure-token", "optional": true } - ] + ], + "reply_schema": { + "description": "a list of sorted elements", + "type": "array", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/spop.json b/src/commands/spop.json index c93e426e1c..a116c8473f 100644 --- a/src/commands/spop.json +++ b/src/commands/spop.json @@ -43,6 +43,26 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "type": "null", + "description": "The key does not exist." + }, + { + "type": "string", + "description": "The removed member when 'COUNT' is not given." + }, + { + "type": "array", + "description": "List to the removed members when 'COUNT' is given.", + "uniqueItems": true, + "items": { + "type": "string" + } + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/spublish.json b/src/commands/spublish.json index 6ed748f959..16c948cc6f 100644 --- a/src/commands/spublish.json +++ b/src/commands/spublish.json @@ -41,6 +41,11 @@ } } } - ] + ], + "reply_schema": { + "description": "the number of clients that received the message. Note that in a Redis Cluster, only clients that are connected to the same node as the publishing client are included in the count", + "type": "integer", + "minimum": 0 + } } } diff --git a/src/commands/srandmember.json b/src/commands/srandmember.json index 67efc87cad..4ba2b75012 100644 --- a/src/commands/srandmember.json +++ b/src/commands/srandmember.json @@ -53,6 +53,31 @@ "optional": true, "since": "2.6.0" } - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "In case `count` is not given and key doesn't exist", + "type": "null" + }, + { + "description": "In case `count` is not given, randomly selected element", + "type": "string" + }, + { + "description": "In case `count` is given, an array of elements", + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + }, + { + "description": "In case `count` is given and key doesn't exist", + "type": "array", + "maxItems": 0 + } + ] + } } } diff --git a/src/commands/srem.json b/src/commands/srem.json index 82433a4a89..ec9ab41dbd 100644 --- a/src/commands/srem.json +++ b/src/commands/srem.json @@ -39,6 +39,11 @@ } } ], + "reply_schema": { + "description": "Number of members that were removed from the set, not including non existing members.", + "type": "integer", + "minimum": 0 + }, "arguments": [ { "name": "key", diff --git a/src/commands/sscan.json b/src/commands/sscan.json index 875ad0cd16..f0d89e2244 100644 --- a/src/commands/sscan.json +++ b/src/commands/sscan.json @@ -57,6 +57,25 @@ "type": "integer", "optional": true } - ] + ], + "reply_schema": { + "description": "cursor and scan response in array form", + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "cursor", + "type": "string" + }, + { + "description": "list of set members", + "type": "array", + "items": { + "type": "string" + } + } + ] + } } } diff --git a/src/commands/strlen.json b/src/commands/strlen.json index a5e2d6ffb8..3cdce48078 100644 --- a/src/commands/strlen.json +++ b/src/commands/strlen.json @@ -32,6 +32,11 @@ } } ], + "reply_schema": { + "description": "The length of the string value stored at key, or 0 when key does not exist.", + "type": "integer", + "minimum": 0 + }, "arguments": [ { "name": "key", diff --git a/src/commands/substr.json b/src/commands/substr.json index 9f3f2bf03b..c1134ce663 100644 --- a/src/commands/substr.json +++ b/src/commands/substr.json @@ -37,6 +37,10 @@ } } ], + "reply_schema": { + "type": "string", + "description": "The substring of the string value stored at key, determined by the offsets start and end (both are inclusive)." + }, "arguments": [ { "name": "key", diff --git a/src/commands/sunion.json b/src/commands/sunion.json index 9bdccacd45..3873a6a39e 100644 --- a/src/commands/sunion.json +++ b/src/commands/sunion.json @@ -35,6 +35,14 @@ } } ], + "reply_schema": { + "type": "array", + "description": "List with the members of the resulting set.", + "uniqueItems": true, + "items": { + "type": "string" + } + }, "arguments": [ { "name": "key", diff --git a/src/commands/sunionstore.json b/src/commands/sunionstore.json index f4ef0b3b24..b703904ac3 100644 --- a/src/commands/sunionstore.json +++ b/src/commands/sunionstore.json @@ -51,6 +51,11 @@ } } ], + "reply_schema": { + "type": "integer", + "description": "Number of the elements in the resulting set.", + "minimum": 0 + }, "arguments": [ { "name": "destination", diff --git a/src/commands/swapdb.json b/src/commands/swapdb.json index 6ea2baeaac..7ed001871f 100644 --- a/src/commands/swapdb.json +++ b/src/commands/swapdb.json @@ -23,6 +23,9 @@ "name": "index2", "type": "integer" } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/time.json b/src/commands/time.json index b5711a3ba2..540190f55d 100644 --- a/src/commands/time.json +++ b/src/commands/time.json @@ -13,6 +13,16 @@ ], "command_tips": [ "NONDETERMINISTIC_OUTPUT" - ] + ], + "reply_schema": { + "type": "array", + "description": "Array containing two elements: Unix time in seconds and microseconds.", + "minItems": 2, + "maxItems": 2, + "items": { + "type": "string", + "pattern": "[0-9]+" + } + } } } diff --git a/src/commands/touch.json b/src/commands/touch.json index ef4c1c9262..b2a2894c91 100644 --- a/src/commands/touch.json +++ b/src/commands/touch.json @@ -43,6 +43,11 @@ "key_spec_index": 0, "multiple": true } - ] + ], + "reply_schema": { + "description": "the number of touched keys", + "type": "integer", + "minimum": 0 + } } } diff --git a/src/commands/ttl.json b/src/commands/ttl.json index 36297eeb90..f4d9578254 100644 --- a/src/commands/ttl.json +++ b/src/commands/ttl.json @@ -42,6 +42,23 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "TTL in seconds.", + "type": "integer", + "minimum": 0 + }, + { + "description": "The key exists but has no associated expire.", + "const": -1 + }, + { + "description": "The key does not exist.", + "const": -2 + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/type.json b/src/commands/type.json index df8e453529..b4a4e766fa 100644 --- a/src/commands/type.json +++ b/src/commands/type.json @@ -32,6 +32,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "Key doesn't exist", + "type": "null" + }, + { + "description": "Type of the key", + "type": "string" + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/unlink.json b/src/commands/unlink.json index 511e728a28..559f1e96bf 100644 --- a/src/commands/unlink.json +++ b/src/commands/unlink.json @@ -44,6 +44,11 @@ "key_spec_index": 0, "multiple": true } - ] + ], + "reply_schema": { + "description": "the number of keys that were unlinked", + "type": "integer", + "minimum": 0 + } } } diff --git a/src/commands/unwatch.json b/src/commands/unwatch.json index 820ea5b93e..256411f1da 100644 --- a/src/commands/unwatch.json +++ b/src/commands/unwatch.json @@ -15,6 +15,9 @@ ], "acl_categories": [ "TRANSACTION" - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/wait.json b/src/commands/wait.json index 4930932d93..110c00af63 100644 --- a/src/commands/wait.json +++ b/src/commands/wait.json @@ -16,6 +16,11 @@ "REQUEST_POLICY:ALL_SHARDS", "RESPONSE_POLICY:AGG_MIN" ], + "reply_schema": { + "type": "integer", + "description": "The number of replicas reached by all the writes performed in the context of the current connection.", + "minimum": 0 + }, "arguments": [ { "name": "numreplicas", diff --git a/src/commands/watch.json b/src/commands/watch.json index 0a9e3703e7..3f16f73602 100644 --- a/src/commands/watch.json +++ b/src/commands/watch.json @@ -35,6 +35,9 @@ } } ], + "reply_schema": { + "const": "OK" + }, "arguments": [ { "name": "key", diff --git a/src/commands/xack.json b/src/commands/xack.json index b9d0aa4ddb..f7791f2706 100644 --- a/src/commands/xack.json +++ b/src/commands/xack.json @@ -48,6 +48,11 @@ "type": "string", "multiple": true } - ] + ], + "reply_schema": { + "description": "The command returns the number of messages successfully acknowledged. Certain message IDs may no longer be part of the PEL (for example because they have already been acknowledged), and XACK will not count them as successfully acknowledged.", + "type": "integer", + "minimum": 0 + } } } diff --git a/src/commands/xadd.json b/src/commands/xadd.json index f3d99f8412..72f864d92a 100644 --- a/src/commands/xadd.json +++ b/src/commands/xadd.json @@ -143,6 +143,19 @@ } ] } - ] + ], + "reply_schema": { + "oneOf":[ + { + "description": "The ID of the added entry. The ID is the one auto-generated if * is passed as ID argument, otherwise the command just returns the same ID specified by the user during insertion.", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "description": "The NOMKSTREAM option is given and the key doesn't exist.", + "type": "null" + } + ] + } } } diff --git a/src/commands/xautoclaim.json b/src/commands/xautoclaim.json index 726bf38fec..f23386e4cc 100644 --- a/src/commands/xautoclaim.json +++ b/src/commands/xautoclaim.json @@ -42,6 +42,83 @@ } } ], + "reply_schema": { + "anyOf": [ + { + "description": "Claimed stream entries (with data, if `JUSTID` was not given).", + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": [ + { + "description": "Cursor for next call.", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "type": "array", + "uniqueItems": true, + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "Entry ID", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "description": "Data", + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, + { + "description": "Entry IDs which no longer exist in the stream, and were deleted from the PEL in which they were found.", + "type": "array", + "items": { + "type": "string", + "pattern": "[0-9]+-[0-9]+" + } + } + ] + }, + { + "description": "Claimed stream entries (without data, if `JUSTID` was given).", + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": [ + { + "description": "Cursor for next call.", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "[0-9]+-[0-9]+" + } + }, + { + "description": "Entry IDs which no longer exist in the stream, and were deleted from the PEL in which they were found.", + "type": "array", + "items": { + "type": "string", + "pattern": "[0-9]+-[0-9]+" + } + } + ] + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/xclaim.json b/src/commands/xclaim.json index 132a60c2e5..950c215b92 100644 --- a/src/commands/xclaim.json +++ b/src/commands/xclaim.json @@ -95,6 +95,44 @@ "type": "string", "optional": true } - ] + ], + "reply_schema": { + "description": "Stream entries with IDs matching the specified range.", + "anyOf": [ + { + "description": "If JUSTID option is specified, return just an array of IDs of messages successfully claimed", + "type": "array", + "items": { + "description": "Entry ID", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + } + }, + { + "description": "array of stream entries that contains each entry as an array of 2 elements, the Entry ID and the entry data itself", + "type": "array", + "uniqueItems": true, + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "Entry ID", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "description": "Data", + "type": "array", + "items": { + "type": "string" + } + } + ] + } + } + ] + } } } diff --git a/src/commands/xdel.json b/src/commands/xdel.json index 061ea80c05..5854f50849 100644 --- a/src/commands/xdel.json +++ b/src/commands/xdel.json @@ -44,6 +44,11 @@ "type": "string", "multiple": true } - ] + ], + "reply_schema": { + "description": "The number of entries actually deleted", + "type": "integer", + "minimum": 0 + } } } diff --git a/src/commands/xgroup-create.json b/src/commands/xgroup-create.json index f099d90ce4..16a8b99eb4 100644 --- a/src/commands/xgroup-create.json +++ b/src/commands/xgroup-create.json @@ -77,6 +77,9 @@ "type": "integer", "optional": true } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/xgroup-createconsumer.json b/src/commands/xgroup-createconsumer.json index c338d4af6e..764b264477 100644 --- a/src/commands/xgroup-createconsumer.json +++ b/src/commands/xgroup-createconsumer.json @@ -48,6 +48,17 @@ "name": "consumer", "type": "string" } - ] + ], + "reply_schema": { + "description": "The number of created consumers (0 or 1)", + "oneOf": [ + { + "const": 1 + }, + { + "const": 0 + } + ] + } } } diff --git a/src/commands/xgroup-delconsumer.json b/src/commands/xgroup-delconsumer.json index 29d62912dc..c68f0b5674 100644 --- a/src/commands/xgroup-delconsumer.json +++ b/src/commands/xgroup-delconsumer.json @@ -47,6 +47,11 @@ "name": "consumer", "type": "string" } - ] + ], + "reply_schema": { + "description": "The number of pending messages that were yet associated with such a consumer", + "type": "integer", + "minimum": 0 + } } } diff --git a/src/commands/xgroup-destroy.json b/src/commands/xgroup-destroy.json index 4a78f4c14d..60a481a807 100644 --- a/src/commands/xgroup-destroy.json +++ b/src/commands/xgroup-destroy.json @@ -43,6 +43,17 @@ "name": "group", "type": "string" } - ] + ], + "reply_schema": { + "description": "The number of destroyed consumer groups (0 or 1)", + "oneOf": [ + { + "const": 1 + }, + { + "const": 0 + } + ] + } } } diff --git a/src/commands/xgroup-help.json b/src/commands/xgroup-help.json index 4c5a2b9572..d4e9d4ad3c 100644 --- a/src/commands/xgroup-help.json +++ b/src/commands/xgroup-help.json @@ -13,6 +13,13 @@ ], "acl_categories": [ "STREAM" - ] + ], + "reply_schema": { + "type": "array", + "description": "Helpful text about subcommands.", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/xgroup-setid.json b/src/commands/xgroup-setid.json index e7c41b3abd..89bbc2a6b6 100644 --- a/src/commands/xgroup-setid.json +++ b/src/commands/xgroup-setid.json @@ -71,6 +71,9 @@ "type": "integer", "optional": true } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/xinfo-consumers.json b/src/commands/xinfo-consumers.json index 15fe5e1a27..2acb0bf052 100644 --- a/src/commands/xinfo-consumers.json +++ b/src/commands/xinfo-consumers.json @@ -46,6 +46,29 @@ "name": "group", "type": "string" } - ] + ], + "reply_schema": { + "description": "Array list of consumers", + "type": "array", + "uniqueItems": true, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "pending": { + "type": "integer" + }, + "idle": { + "type": "integer" + }, + "inactive": { + "type": "integer" + } + } + } + } } } diff --git a/src/commands/xinfo-groups.json b/src/commands/xinfo-groups.json index e9b61ba069..4196773f38 100644 --- a/src/commands/xinfo-groups.json +++ b/src/commands/xinfo-groups.json @@ -39,6 +39,48 @@ } } ], + "reply_schema": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "consumers": { + "type": "integer" + }, + "pending": { + "type": "integer" + }, + "last-delivered-id": { + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + "entries-read": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "integer" + } + ] + }, + "lag": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "integer" + } + ] + } + } + } + }, "arguments": [ { "name": "key", diff --git a/src/commands/xinfo-help.json b/src/commands/xinfo-help.json index e11468353c..3441ace145 100644 --- a/src/commands/xinfo-help.json +++ b/src/commands/xinfo-help.json @@ -13,6 +13,13 @@ ], "acl_categories": [ "STREAM" - ] + ], + "reply_schema": { + "type": "array", + "description": "Helpful text about subcommands.", + "items": { + "type": "string" + } + } } } diff --git a/src/commands/xinfo-stream.json b/src/commands/xinfo-stream.json index 233afdde36..25941f33ba 100644 --- a/src/commands/xinfo-stream.json +++ b/src/commands/xinfo-stream.json @@ -43,6 +43,291 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "Summary form, in case `FULL` was not given.", + "type": "object", + "additionalProperties": false, + "properties": { + "length": { + "description": "the number of entries in the stream (see `XLEN`)", + "type": "integer" + }, + "radix-tree-keys": { + "description": "the number of keys in the underlying radix data structure", + "type": "integer" + }, + "radix-tree-nodes": { + "description": "the number of nodes in the underlying radix data structure", + "type": "integer" + }, + "last-generated-id": { + "description": "the ID of the least-recently entry that was added to the stream", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + "max-deleted-entry-id": { + "description": "the maximal entry ID that was deleted from the stream", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + "recorded-first-entry-id": { + "description": "cached copy of the first entry ID", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + "entries-added": { + "description": "the count of all entries added to the stream during its lifetime", + "type": "integer" + }, + "groups": { + "description": "the number of consumer groups defined for the stream", + "type": "integer" + }, + "first-entry": { + "description": "the first entry of the stream", + "oneOf": [ + { + "type": "null" + }, + { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "entry ID", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "description": "data", + "type": "array", + "items": { + "type": "string" + } + } + ] + } + ] + }, + "last-entry": { + "description": "the last entry of the stream", + "oneOf": [ + { + "type": "null" + }, + { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "entry ID", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "description": "data", + "type": "array", + "items": { + "type": "string" + } + } + ] + } + ] + } + } + }, + { + "description": "Extended form, in case `FULL` was given.", + "type": "object", + "additionalProperties": false, + "properties": { + "length": { + "description": "the number of entries in the stream (see `XLEN`)", + "type": "integer" + }, + "radix-tree-keys": { + "description": "the number of keys in the underlying radix data structure", + "type": "integer" + }, + "radix-tree-nodes": { + "description": "the number of nodes in the underlying radix data structure", + "type": "integer" + }, + "last-generated-id": { + "description": "the ID of the least-recently entry that was added to the stream", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + "max-deleted-entry-id": { + "description": "the maximal entry ID that was deleted from the stream", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + "recorded-first-entry-id": { + "description": "cached copy of the first entry ID", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + "entries-added": { + "description": "the count of all entries added to the stream during its lifetime", + "type": "integer" + }, + "entries": { + "description": "all the entries of the stream", + "type": "array", + "uniqueItems": true, + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "entry ID", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "description": "data", + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, + "groups": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "description": "group name", + "type": "string" + }, + "last-delivered-id": { + "description": "last entry ID that was delivered to a consumer", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + "entries-read": { + "description": "total number of entries ever read by consumers in the group", + "oneOf": [ + { + "type": "null" + }, + { + "type": "integer" + } + ] + }, + "lag": { + "description": "number of entries left to be consumed from the stream", + "oneOf": [ + { + "type": "null" + }, + { + "type": "integer" + } + ] + }, + "pel-count": { + "description": "total number of unacknowledged entries", + "type": "integer" + }, + "pending": { + "description": "data about all of the unacknowledged entries", + "type": "array", + "items": { + "type": "array", + "minItems": 4, + "maxItems": 4, + "items": [ + { + "description": "Entry ID", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "description": "Consumer name", + "type": "string" + }, + { + "description": "Delivery timestamp", + "type": "integer" + }, + { + "description": "Delivery count", + "type": "integer" + } + ] + } + }, + "consumers": { + "description": "data about all of the consumers of the group", + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "active-time": { + "type": "integer", + "description": "Last time this consumer was active (successful reading/claiming).", + "minimum": 0 + }, + "name": { + "description": "consumer name", + "type": "string" + }, + "seen-time": { + "description": "timestamp of the last interaction attempt of the consumer", + "type": "integer" + }, + "pel-count": { + "description": "number of unacknowledged entries that belong to the consumer", + "type": "integer" + }, + "pending": { + "description": "data about the unacknowledged entries", + "type": "array", + "items": { + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": [ + { + "description": "Entry ID", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "description": "Delivery timestamp", + "type": "integer" + }, + { + "description": "Delivery count", + "type": "integer" + } + ] + } + } + } + } + } + } + } + } + } + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/xlen.json b/src/commands/xlen.json index 9adc261f7f..b3a82f774d 100644 --- a/src/commands/xlen.json +++ b/src/commands/xlen.json @@ -38,6 +38,11 @@ "type": "key", "key_spec_index": 0 } - ] + ], + "reply_schema": { + "description": "The number of entries of the stream at key", + "type": "integer", + "minimum": 0 + } } } diff --git a/src/commands/xpending.json b/src/commands/xpending.json index cd8ee8dd2d..04725e1e41 100644 --- a/src/commands/xpending.json +++ b/src/commands/xpending.json @@ -41,6 +41,79 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "Extended form, in case `start` was given.", + "type": "array", + "items": { + "type": "array", + "minItems": 4, + "maxItems": 4, + "items": [ + { + "description": "Entry ID", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "description": "Consumer name", + "type": "string" + }, + { + "description": "Idle time", + "type": "integer" + }, + { + "description": "Delivery count", + "type": "integer" + } + ] + } + }, + { + "description": "Summary form, in case `start` was not given.", + "type": "array", + "minItems": 4, + "maxItems": 4, + "items": [ + { + "description": "Total number of pending messages", + "type": "integer" + }, + { + "description": "Minimal pending entry ID", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "description": "Maximal pending entry ID", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "description": "Consumers with pending messages", + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "Consumer name", + "type": "string" + }, + { + "description": "Number of pending messages", + "type": "string" + } + ] + } + } + ] + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/xrange.json b/src/commands/xrange.json index 9a3ddeac0d..325a4564a2 100644 --- a/src/commands/xrange.json +++ b/src/commands/xrange.json @@ -38,6 +38,30 @@ } } ], + "reply_schema": { + "description": "Stream entries with IDs matching the specified range.", + "type": "array", + "uniqueItems": true, + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "Entry ID", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "description": "Data", + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, "arguments": [ { "name": "key", diff --git a/src/commands/xread.json b/src/commands/xread.json index 8f66d7ee5b..e6978794ae 100644 --- a/src/commands/xread.json +++ b/src/commands/xread.json @@ -67,6 +67,43 @@ } ] } - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "A map of key-value elements when each element composed of key name and the entries reported for that key", + "type": "object", + "patternProperties": { + "^.*$": { + "description": "The entries reported for that key", + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "entry id", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "description": "array of field-value pairs", + "type": "array", + "items": { + "type": "string" + } + } + ] + } + } + } + }, + { + "description": "If BLOCK option is given, and a timeout occurs, or there is no stream we can serve", + "type": "null" + } + ] + } } } diff --git a/src/commands/xreadgroup.json b/src/commands/xreadgroup.json index e160f3d000..96d7a89ba6 100644 --- a/src/commands/xreadgroup.json +++ b/src/commands/xreadgroup.json @@ -87,6 +87,48 @@ } ] } - ] + ], + "reply_schema": { + "oneOf": [ + { + "description": "If BLOCK option is specified and the timeout expired", + "type": "null" + }, + { + "description": "A map of key-value elements when each element composed of key name and the entries reported for that key", + "type": "object", + "additionalProperties": { + "description": "The entries reported for that key", + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "Stream id", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "oneOf": [ + { + "description": "Array of field-value pairs", + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + } + ] + } + } + } + ] + } } } diff --git a/src/commands/xrevrange.json b/src/commands/xrevrange.json index 65d81db813..41798a16ca 100644 --- a/src/commands/xrevrange.json +++ b/src/commands/xrevrange.json @@ -58,6 +58,29 @@ "type": "integer", "optional": true } - ] + ], + "reply_schema": { + "description": "An array of the entries with IDs matching the specified range", + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "Stream id", + "type": "string", + "pattern": "[0-9]+-[0-9]+" + }, + { + "description": "Array of field-value pairs", + "type": "array", + "items": { + "type": "string" + } + } + ] + } + } } } diff --git a/src/commands/xsetid.json b/src/commands/xsetid.json index b94b9601e9..b69d80c883 100644 --- a/src/commands/xsetid.json +++ b/src/commands/xsetid.json @@ -64,6 +64,9 @@ "optional": true, "since": "7.0.0" } - ] + ], + "reply_schema": { + "const": "OK" + } } } diff --git a/src/commands/xtrim.json b/src/commands/xtrim.json index 03c48ebb5b..6abd4903bf 100644 --- a/src/commands/xtrim.json +++ b/src/commands/xtrim.json @@ -98,6 +98,11 @@ } ] } - ] + ], + "reply_schema": { + "description": "The number of entries deleted from the stream.", + "type": "integer", + "minimum": 0 + } } } diff --git a/src/commands/zadd.json b/src/commands/zadd.json index 43791deb48..e2cbd98c82 100644 --- a/src/commands/zadd.json +++ b/src/commands/zadd.json @@ -48,6 +48,26 @@ } } ], + "reply_schema": { + "anyOf":[ + { + "description": "Operation was aborted (conflict with one of the `XX`/`NX`/`LT`/`GT` options).", + "type": "null" + }, + { + "description": "The number of new members (when the `CH` option is not used)", + "type": "integer" + }, + { + "description": "The number of new or updated members (when the `CH` option is used)", + "type": "integer" + }, + { + "description": "The updated score of the member (when the `INCR` option is used)", + "type": "number" + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/zcard.json b/src/commands/zcard.json index 84022a7f5e..f80a010072 100644 --- a/src/commands/zcard.json +++ b/src/commands/zcard.json @@ -32,6 +32,10 @@ } } ], + "reply_schema": { + "description": "The cardinality (number of elements) of the sorted set, or 0 if key does not exist", + "type": "integer" + }, "arguments": [ { "name": "key", diff --git a/src/commands/zcount.json b/src/commands/zcount.json index 6572d4a511..9ad8fdb243 100644 --- a/src/commands/zcount.json +++ b/src/commands/zcount.json @@ -33,6 +33,10 @@ } } ], + "reply_schema": { + "description": "The number of elements in the specified score range", + "type": "integer" + }, "arguments": [ { "name": "key", diff --git a/src/commands/zdiff.json b/src/commands/zdiff.json index 3eee28983d..a361e249da 100644 --- a/src/commands/zdiff.json +++ b/src/commands/zdiff.json @@ -33,6 +33,36 @@ } } ], + "reply_schema": { + "anyOf": [ + { + "description": "A list of members. Returned in case `WITHSCORES` was not used.", + "type": "array", + "items": { + "type": "string" + } + }, + { + "description": "Members and their scores. Returned in case `WITHSCORES` was used. In RESP2 this is returned as a flat array", + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "Member", + "type": "string" + }, + { + "description": "Score", + "type": "number" + } + ] + } + } + ] + }, "arguments": [ { "name": "numkeys", diff --git a/src/commands/zdiffstore.json b/src/commands/zdiffstore.json index 7fc0102fd8..26f205edc3 100644 --- a/src/commands/zdiffstore.json +++ b/src/commands/zdiffstore.json @@ -52,6 +52,10 @@ } } ], + "reply_schema": { + "description": "Number of elements in the resulting sorted set at `destination`", + "type": "integer" + }, "arguments": [ { "name": "destination", diff --git a/src/commands/zincrby.json b/src/commands/zincrby.json index 2ebafe0a15..a3283a3b64 100644 --- a/src/commands/zincrby.json +++ b/src/commands/zincrby.json @@ -35,6 +35,10 @@ } } ], + "reply_schema": { + "description": "The new score of `member`", + "type": "number" + }, "arguments": [ { "name": "key", diff --git a/src/commands/zinter.json b/src/commands/zinter.json index b05dc8d3a1..d0dd046edd 100644 --- a/src/commands/zinter.json +++ b/src/commands/zinter.json @@ -33,6 +33,36 @@ } } ], + "reply_schema": { + "anyOf": [ + { + "description": "Result of intersection, containing only the member names. Returned in case `WITHSCORES` was not used.", + "type": "array", + "items": { + "type": "string" + } + }, + { + "description": "Result of intersection, containing members and their scores. Returned in case `WITHSCORES` was used. In RESP2 this is returned as a flat array", + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "Member", + "type": "string" + }, + { + "description": "Score", + "type": "number" + } + ] + } + } + ] + }, "arguments": [ { "name": "numkeys", diff --git a/src/commands/zintercard.json b/src/commands/zintercard.json index 2c2359968d..732bab830d 100644 --- a/src/commands/zintercard.json +++ b/src/commands/zintercard.json @@ -33,6 +33,11 @@ } } ], + "reply_schema": { + "description": "Number of elements in the resulting intersection.", + "type": "integer", + "minimum": 0 + }, "arguments": [ { "name": "numkeys", diff --git a/src/commands/zinterstore.json b/src/commands/zinterstore.json index bd40460ace..32661c5646 100644 --- a/src/commands/zinterstore.json +++ b/src/commands/zinterstore.json @@ -52,6 +52,11 @@ } } ], + "reply_schema": { + "description": "Number of elements in the resulting sorted set.", + "type": "integer", + "minimum": 0 + }, "arguments": [ { "name": "destination", diff --git a/src/commands/zlexcount.json b/src/commands/zlexcount.json index 5dff46e4ba..5366441a63 100644 --- a/src/commands/zlexcount.json +++ b/src/commands/zlexcount.json @@ -33,6 +33,11 @@ } } ], + "reply_schema": { + "description": "Number of elements in the specified score range.", + "type": "integer", + "minimum": 0 + }, "arguments": [ { "name": "key", diff --git a/src/commands/zmpop.json b/src/commands/zmpop.json index 2edeaf2ccc..58e7101700 100644 --- a/src/commands/zmpop.json +++ b/src/commands/zmpop.json @@ -34,6 +34,45 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "description": "No element could be popped.", + "type": "null" + }, + { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "string", + "description": "Name of the key that elements were popped." + }, + { + "type": "array", + "description": "Popped elements.", + "items": { + "type": "array", + "uniqueItems": true, + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "string", + "description": "Name of the member." + }, + { + "type": "number", + "description": "Score." + } + ] + } + } + ] + } + ] + }, "arguments": [ { "name": "numkeys", diff --git a/src/commands/zmscore.json b/src/commands/zmscore.json index 4db291fa32..fa2fba141c 100644 --- a/src/commands/zmscore.json +++ b/src/commands/zmscore.json @@ -33,6 +33,22 @@ } } ], + "reply_schema": { + "type": "array", + "minItems": 1, + "items": { + "oneOf": [ + { + "type": "number", + "description": "The score of the member (a double precision floating point number). In RESP2, this is returned as string." + }, + { + "type": "null", + "description": "Member does not exist in the sorted set." + } + ] + } + }, "arguments": [ { "name": "key", diff --git a/src/commands/zpopmax.json b/src/commands/zpopmax.json index 2e792431a7..33bb85c510 100644 --- a/src/commands/zpopmax.json +++ b/src/commands/zpopmax.json @@ -34,6 +34,45 @@ } } ], + "reply_schema": { + "anyOf": [ + { + "type": "array", + "description": "List of popped elements and scores when 'COUNT' isn't specified.", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "string", + "description": "Popped element." + }, + { + "type": "number", + "description": "Score." + } + ] + }, + { + "type": "array", + "description": "List of popped elements and scores when 'COUNT' is specified.", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "string", + "description": "Popped element." + }, + { + "type": "number", + "description": "Score." + } + ] + } + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/zpopmin.json b/src/commands/zpopmin.json index 9ccce2fe1e..e583eeea02 100644 --- a/src/commands/zpopmin.json +++ b/src/commands/zpopmin.json @@ -34,6 +34,45 @@ } } ], + "reply_schema": { + "anyOf": [ + { + "type": "array", + "description": "List of popped elements and scores when 'COUNT' isn't specified.", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "string", + "description": "Popped element." + }, + { + "type": "number", + "description": "Score." + } + ] + }, + { + "type": "array", + "description": "List of popped elements and scores when 'COUNT' is specified.", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "string", + "description": "Popped element." + }, + { + "type": "number", + "description": "Score." + } + ] + } + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/zrandmember.json b/src/commands/zrandmember.json index e602a154d0..1da1ce68b9 100644 --- a/src/commands/zrandmember.json +++ b/src/commands/zrandmember.json @@ -35,6 +35,44 @@ } } ], + "reply_schema": { + "anyOf": [ + { + "type": "null", + "description": "Key does not exist." + }, + { + "type": "string", + "description": "Randomly selected element when 'COUNT' is not used." + }, + { + "type": "array", + "description": "Randomly selected elements when 'COUNT' is used.", + "items": { + "type": "string" + } + }, + { + "type": "array", + "description": "Randomly selected elements when 'COUNT' and 'WITHSCORES' modifiers are used.", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "string", + "description": "Element." + }, + { + "type": "number", + "description": "Score." + } + ] + } + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/zrange.json b/src/commands/zrange.json index 628be8bfee..24a387160e 100644 --- a/src/commands/zrange.json +++ b/src/commands/zrange.json @@ -38,6 +38,38 @@ } } ], + "reply_schema": { + "anyOf": [ + { + "description": "A list of member elements", + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + }, + { + "description": "Members and their scores. Returned in case `WITHSCORES` was used. In RESP2 this is returned as a flat array", + "type": "array", + "uniqueItems": true, + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "Member", + "type": "string" + }, + { + "description": "Score", + "type": "number" + } + ] + } + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/zrangebylex.json b/src/commands/zrangebylex.json index 1f2c755c9b..d9d8a3a190 100644 --- a/src/commands/zrangebylex.json +++ b/src/commands/zrangebylex.json @@ -37,6 +37,14 @@ } } ], + "reply_schema": { + "type": "array", + "description": "List of elements in the specified score range.", + "uniqueItems": true, + "items": { + "type": "string" + } + }, "arguments": [ { "name": "key", diff --git a/src/commands/zrangebyscore.json b/src/commands/zrangebyscore.json index 44650d32f4..b603db4da9 100644 --- a/src/commands/zrangebyscore.json +++ b/src/commands/zrangebyscore.json @@ -43,6 +43,40 @@ } } ], + "reply_schema": { + "anyOf": [ + { + "type": "array", + "description": "List of the elements in the specified score range, as not WITHSCORES", + "uniqueItems": true, + "items": { + "type": "string", + "description": "Element" + } + }, + { + "type": "array", + "description": "List of the elements and their scores in the specified score range, as WITHSCORES used", + "uniqueItems": true, + "items": { + "type": "array", + "description": "Tuple of element and its score", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "element", + "type": "string" + }, + { + "description": "score", + "type": "number" + } + ] + } + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/zrangestore.json b/src/commands/zrangestore.json index 2e4c85583c..f072553b7c 100644 --- a/src/commands/zrangestore.json +++ b/src/commands/zrangestore.json @@ -51,6 +51,10 @@ } } ], + "reply_schema": { + "type": "integer", + "description": "Number of elements in the resulting sorted set." + }, "arguments": [ { "name": "dst", diff --git a/src/commands/zrank.json b/src/commands/zrank.json index a15d96c595..d08897e27a 100644 --- a/src/commands/zrank.json +++ b/src/commands/zrank.json @@ -39,6 +39,32 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "type": "null", + "description": "Key does not exist or the member does not exist in the sorted set." + }, + { + "type": "integer", + "description": "The rank of the member when 'WITHSCORES' is not used." + }, + { + "type": "array", + "description": "The rank and score of the member when 'WITHSCORES' is used.", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "integer" + }, + { + "type": "number" + } + ] + } + ] + }, "arguments": [ { "name": "key", @@ -57,4 +83,4 @@ } ] } -} \ No newline at end of file +} diff --git a/src/commands/zrem.json b/src/commands/zrem.json index a899400937..f8fceeadb9 100644 --- a/src/commands/zrem.json +++ b/src/commands/zrem.json @@ -39,6 +39,11 @@ } } ], + "reply_schema": { + "description": "The number of members removed from the sorted set, not including non existing members.", + "type": "integer", + "minimum": 0 + }, "arguments": [ { "name": "key", diff --git a/src/commands/zremrangebylex.json b/src/commands/zremrangebylex.json index ad7277723d..34eb999807 100644 --- a/src/commands/zremrangebylex.json +++ b/src/commands/zremrangebylex.json @@ -32,6 +32,10 @@ } } ], + "reply_schema": { + "type": "integer", + "description": "Number of elements removed." + }, "arguments": [ { "name": "key", diff --git a/src/commands/zremrangebyrank.json b/src/commands/zremrangebyrank.json index 62e5055b57..4e7744dff6 100644 --- a/src/commands/zremrangebyrank.json +++ b/src/commands/zremrangebyrank.json @@ -32,6 +32,10 @@ } } ], + "reply_schema": { + "type": "integer", + "description": "Number of elements removed." + }, "arguments": [ { "name": "key", diff --git a/src/commands/zremrangebyscore.json b/src/commands/zremrangebyscore.json index 3f84c33356..d5ca40c426 100644 --- a/src/commands/zremrangebyscore.json +++ b/src/commands/zremrangebyscore.json @@ -32,6 +32,10 @@ } } ], + "reply_schema": { + "type": "integer", + "description": "Number of elements removed." + }, "arguments": [ { "name": "key", diff --git a/src/commands/zrevrange.json b/src/commands/zrevrange.json index 66ddc11467..725abf8d7e 100644 --- a/src/commands/zrevrange.json +++ b/src/commands/zrevrange.json @@ -37,6 +37,38 @@ } } ], + "reply_schema": { + "anyOf": [ + { + "description": "List of member elements.", + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + }, + { + "description": "List of the members and their scores. Returned in case `WITHSCORES` was used.", + "type": "array", + "uniqueItems": true, + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "member", + "type": "string" + }, + { + "description": "score", + "type": "number" + } + ] + } + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/zrevrangebylex.json b/src/commands/zrevrangebylex.json index 07bad6178f..252c96f8c5 100644 --- a/src/commands/zrevrangebylex.json +++ b/src/commands/zrevrangebylex.json @@ -37,6 +37,14 @@ } } ], + "reply_schema": { + "type": "array", + "description": "List of the elements in the specified score range.", + "uniqueItems": true, + "items": { + "type": "string" + } + }, "arguments": [ { "name": "key", diff --git a/src/commands/zrevrangebyscore.json b/src/commands/zrevrangebyscore.json index 9e0bab0966..163faec57c 100644 --- a/src/commands/zrevrangebyscore.json +++ b/src/commands/zrevrangebyscore.json @@ -43,6 +43,40 @@ } } ], + "reply_schema": { + "anyOf": [ + { + "type": "array", + "description": "List of the elements in the specified score range, as not WITHSCORES", + "uniqueItems": true, + "items": { + "type": "string", + "description": "Element" + } + }, + { + "type": "array", + "description": "List of the elements and their scores in the specified score range, as WITHSCORES used", + "uniqueItems": true, + "items": { + "type": "array", + "description": "Tuple of element and its score", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "string", + "description": "element" + }, + { + "type": "number", + "description": "score" + } + ] + } + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/zrevrank.json b/src/commands/zrevrank.json index 7d5aa795bf..0a025fe0d3 100644 --- a/src/commands/zrevrank.json +++ b/src/commands/zrevrank.json @@ -39,6 +39,32 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "type": "null", + "description": "Key does not exist or the member does not exist in the sorted set." + }, + { + "type": "integer", + "description": "The rank of the member when 'WITHSCORES' is not used." + }, + { + "type": "array", + "description": "The rank and score of the member when 'WITHSCORES' is used.", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "integer" + }, + { + "type": "number" + } + ] + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/zscan.json b/src/commands/zscan.json index 71054fb52f..ca4a7dd2a3 100644 --- a/src/commands/zscan.json +++ b/src/commands/zscan.json @@ -57,6 +57,25 @@ "type": "integer", "optional": true } - ] + ], + "reply_schema": { + "description": "cursor and scan response in array form", + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "description": "cursor", + "type": "string" + }, + { + "description": "list of elements of the sorted set, where each even element is the member, and each odd value is its associated score", + "type": "array", + "items": { + "type": "string" + } + } + ] + } } } diff --git a/src/commands/zscore.json b/src/commands/zscore.json index 5ed3575105..7e07c82483 100644 --- a/src/commands/zscore.json +++ b/src/commands/zscore.json @@ -33,6 +33,18 @@ } } ], + "reply_schema": { + "oneOf": [ + { + "type": "number", + "description": "The score of the member (a double precision floating point number). In RESP2, this is returned as string." + }, + { + "type": "null", + "description": "Member does not exist in the sorted set, or key does not exist." + } + ] + }, "arguments": [ { "name": "key", diff --git a/src/commands/zunion.json b/src/commands/zunion.json index cc6c66c093..395a12741c 100644 --- a/src/commands/zunion.json +++ b/src/commands/zunion.json @@ -33,6 +33,36 @@ } } ], + "reply_schema": { + "anyOf": [ + { + "description": "The result of union when 'WITHSCORES' is not used.", + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + }, + { + "description": "The result of union when 'WITHSCORES' is used.", + "type": "array", + "uniqueItems": true, + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "string" + }, + { + "type": "number" + } + ] + } + } + ] + }, "arguments": [ { "name": "numkeys", diff --git a/src/commands/zunionstore.json b/src/commands/zunionstore.json index 257b06d0ef..25273af8c7 100644 --- a/src/commands/zunionstore.json +++ b/src/commands/zunionstore.json @@ -52,6 +52,10 @@ } } ], + "reply_schema": { + "description": "The number of elements in the resulting sorted set.", + "type": "integer" + }, "arguments": [ { "name": "destination", diff --git a/src/config.c b/src/config.c index 97c4028ad4..bd62ca3db2 100644 --- a/src/config.c +++ b/src/config.c @@ -3083,6 +3083,9 @@ standardConfig static_configs[] = { createStringConfig("proc-title-template", NULL, MODIFIABLE_CONFIG, ALLOW_EMPTY_STRING, server.proc_title_template, CONFIG_DEFAULT_PROC_TITLE_TEMPLATE, isValidProcTitleTemplate, updateProcTitleTemplate), createStringConfig("bind-source-addr", NULL, MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.bind_source_addr, NULL, NULL, NULL), createStringConfig("logfile", NULL, IMMUTABLE_CONFIG, ALLOW_EMPTY_STRING, server.logfile, "", NULL, NULL), +#ifdef LOG_REQ_RES + createStringConfig("req-res-logfile", NULL, IMMUTABLE_CONFIG | HIDDEN_CONFIG, EMPTY_STRING_IS_NULL, server.req_res_logfile, NULL, NULL, NULL), +#endif createStringConfig("locale-collate", NULL, MODIFIABLE_CONFIG, ALLOW_EMPTY_STRING, server.locale_collate, "", NULL, updateLocaleCollate), /* SDS Configs */ @@ -3150,6 +3153,9 @@ standardConfig static_configs[] = { createUIntConfig("maxclients", NULL, MODIFIABLE_CONFIG, 1, UINT_MAX, server.maxclients, 10000, INTEGER_CONFIG, NULL, updateMaxclients), createUIntConfig("unixsocketperm", NULL, IMMUTABLE_CONFIG, 0, 0777, server.unixsocketperm, 0, OCTAL_CONFIG, NULL, NULL), createUIntConfig("socket-mark-id", NULL, IMMUTABLE_CONFIG, 0, UINT_MAX, server.socket_mark_id, 0, INTEGER_CONFIG, NULL, NULL), +#ifdef LOG_REQ_RES + createUIntConfig("client-default-resp", NULL, IMMUTABLE_CONFIG | HIDDEN_CONFIG, 2, 3, server.client_default_resp, 2, INTEGER_CONFIG, NULL, NULL), +#endif /* Unsigned Long configs */ createULongConfig("active-defrag-max-scan-fields", NULL, MODIFIABLE_CONFIG, 1, LONG_MAX, server.active_defrag_max_scan_fields, 1000, INTEGER_CONFIG, NULL, NULL), /* Default: keys with more than 1000 fields will be processed separately */ diff --git a/src/logreqres.c b/src/logreqres.c new file mode 100644 index 0000000000..aa54b721df --- /dev/null +++ b/src/logreqres.c @@ -0,0 +1,318 @@ +/* + * Copyright (c) 2021, Redis Ltd. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/* This file implements the interface of logging clients' requests and + * responses into a file. + * This feature needs the LOG_REQ_RES macro to be compiled and is turned + * on by the req-res-logfile config." + * + * Some examples: + * + * PING: + * + * 4 + * ping + * 12 + * __argv_end__ + * +PONG + * + * LRANGE: + * + * 6 + * lrange + * 4 + * list + * 1 + * 0 + * 2 + * -1 + * 12 + * __argv_end__ + * *1 + * $3 + * ele + * + * The request is everything up until the __argv_end__ marker. + * The format is: + * + * + * + * After __argv_end__ the response appears, and the format is + * RESP (2 or 3, depending on what the client has configured) + */ + +#include "server.h" +#include + +#ifdef LOG_REQ_RES + +/* ----- Helpers ----- */ + +static int reqresShouldLog(client *c) { + if (!server.req_res_logfile) + return 0; + + /* Ignore client with streaming non-standard response */ + if (c->flags & (CLIENT_PUBSUB|CLIENT_MONITOR|CLIENT_SLAVE)) + return 0; + + /* We only work on masters (didn't implement reqresAppendResponse to work on shared slave buffers) */ + if (getClientType(c) == CLIENT_TYPE_MASTER) + return 0; + + return 1; +} + +static size_t reqresAppendBuffer(client *c, void *buf, size_t len) { + if (!c->reqres.buf) { + c->reqres.capacity = max(len, 1024); + c->reqres.buf = zmalloc(c->reqres.capacity); + } else if (c->reqres.capacity - c->reqres.used < len) { + c->reqres.capacity += len; + c->reqres.buf = zrealloc(c->reqres.buf, c->reqres.capacity); + } + + memcpy(c->reqres.buf + c->reqres.used, buf, len); + c->reqres.used += len; + return len; +} + +/* Functions for requests */ + +static size_t reqresAppendArg(client *c, char *arg, size_t arg_len) { + char argv_len_buf[LONG_STR_SIZE]; + size_t argv_len_buf_len = ll2string(argv_len_buf,sizeof(argv_len_buf),(long)arg_len); + size_t ret = reqresAppendBuffer(c, argv_len_buf, argv_len_buf_len); + ret += reqresAppendBuffer(c, "\r\n", 2); + ret += reqresAppendBuffer(c, arg, arg_len); + ret += reqresAppendBuffer(c, "\r\n", 2); + return ret; +} + +/* ----- API ----- */ + + +/* Zero out the clientReqResInfo struct inside the client, + * and free the buffer if needed */ +void reqresReset(client *c, int free_buf) { + if (free_buf && c->reqres.buf) + zfree(c->reqres.buf); + memset(&c->reqres, 0, sizeof(c->reqres)); +} + +/* Save the offset of the reply buffer (or the reply list). + * Should be called when adding a reply (but it will only save the offset + * on the very first time it's called, because of c->reqres.offset.saved) + * The idea is: + * 1. When a client is executing a command, we save the reply offset. + * 2. During the execution, the reply offset may grow, as addReply* functions are called. + * 3. When client is done with the command (commandProcessed), reqresAppendResponse + * is called. + * 4. reqresAppendResponse will append the diff between the current offset and the one from step (1) + * 5. When client is reset before the next command, we clear c->reqres.offset.saved and start again + * + * We cannot reply on c->sentlen to keep track because it depends on the network + * (reqresAppendResponse will always write the whole buffer, unlike writeToClient) + * + * Ideally, we would just have this code inside reqresAppendRequest, which is called + * from processCommand, but we cannot save the reply offset inside processCommand + * because of the following pipe-lining scenario: + * set rd [redis_deferring_client] + * set buf "" + * append buf "SET key vale\r\n" + * append buf "BLPOP mylist 0\r\n" + * $rd write $buf + * $rd flush + * + * Let's assume we save the reply offset in processCommand + * When BLPOP is processed the offset is 5 (+OK\r\n from the SET) + * Then beforeSleep is called, the +OK is written to network, and bufpos is 0 + * When the client is finally unblocked, the cached offset is 5, but bufpos is already + * 0, so we would miss the first 5 bytes of the reply. + **/ +void reqresSaveClientReplyOffset(client *c) { + if (!reqresShouldLog(c)) + return; + + if (c->reqres.offset.saved) + return; + + c->reqres.offset.saved = 1; + + c->reqres.offset.bufpos = c->bufpos; + if (listLength(c->reply) && listNodeValue(listLast(c->reply))) { + c->reqres.offset.last_node.index = listLength(c->reply) - 1; + c->reqres.offset.last_node.used = ((clientReplyBlock *)listNodeValue(listLast(c->reply)))->used; + } else { + c->reqres.offset.last_node.index = 0; + c->reqres.offset.last_node.used = 0; + } +} + +size_t reqresAppendRequest(client *c) { + robj **argv = c->argv; + int argc = c->argc; + + serverAssert(argc); + + if (!reqresShouldLog(c)) + return 0; + + /* Ignore commands that have streaming non-standard response */ + sds cmd = argv[0]->ptr; + if (!strcasecmp(cmd,"sync") || + !strcasecmp(cmd,"psync") || + !strcasecmp(cmd,"monitor") || + !strcasecmp(cmd,"subscribe") || + !strcasecmp(cmd,"unsubscribe") || + !strcasecmp(cmd,"ssubscribe") || + !strcasecmp(cmd,"sunsubscribe") || + !strcasecmp(cmd,"psubscribe") || + !strcasecmp(cmd,"punsubscribe") || + !strcasecmp(cmd,"debug") || + !strcasecmp(cmd,"pfdebug") || + !strcasecmp(cmd,"lolwut") || + (!strcasecmp(cmd,"sentinel") && argc > 1 && !strcasecmp(argv[1]->ptr,"debug"))) + { + return 0; + } + + c->reqres.argv_logged = 1; + + size_t ret = 0; + for (int i = 0; i < argc; i++) { + if (sdsEncodedObject(argv[i])) { + ret += reqresAppendArg(c, argv[i]->ptr, sdslen(argv[i]->ptr)); + } else if (argv[i]->encoding == OBJ_ENCODING_INT) { + char buf[LONG_STR_SIZE]; + size_t len = ll2string(buf,sizeof(buf),(long)argv[i]->ptr); + ret += reqresAppendArg(c, buf, len); + } else { + serverPanic("Wrong encoding in reqresAppendRequest()"); + } + } + return ret + reqresAppendArg(c, "__argv_end__", 12); +} + +size_t reqresAppendResponse(client *c) { + size_t ret = 0; + + if (!reqresShouldLog(c)) + return 0; + + if (!c->reqres.argv_logged) /* Example: UNSUBSCRIBE */ + return 0; + + if (!c->reqres.offset.saved) /* Example: module client blocked on keys + CLIENT KILL */ + return 0; + + /* First append the static reply buffer */ + if (c->bufpos > c->reqres.offset.bufpos) { + size_t written = reqresAppendBuffer(c, c->buf + c->reqres.offset.bufpos, c->bufpos - c->reqres.offset.bufpos); + ret += written; + } + + int curr_index = 0; + size_t curr_used = 0; + if (listLength(c->reply)) { + curr_index = listLength(c->reply) - 1; + curr_used = ((clientReplyBlock *)listNodeValue(listLast(c->reply)))->used; + } + + /* Now, append reply bytes from the reply list */ + if (curr_index > c->reqres.offset.last_node.index || + curr_used > c->reqres.offset.last_node.used) + { + int i = 0; + listIter iter; + listNode *curr; + clientReplyBlock *o; + listRewind(c->reply, &iter); + while ((curr = listNext(&iter)) != NULL) { + size_t written; + + /* Skip nodes we had already processed */ + if (i < c->reqres.offset.last_node.index) { + i++; + continue; + } + o = listNodeValue(curr); + if (o->used == 0) { + i++; + continue; + } + if (i == c->reqres.offset.last_node.index) { + /* Write the potentially incomplete node, which had data from + * before the current command started */ + written = reqresAppendBuffer(c, + o->buf + c->reqres.offset.last_node.used, + o->used - c->reqres.offset.last_node.used); + } else { + /* New node */ + written = reqresAppendBuffer(c, o->buf, o->used); + } + ret += written; + i++; + } + } + serverAssert(ret); + + /* Flush both request and response to file */ + FILE *fp = fopen(server.req_res_logfile, "a"); + serverAssert(fp); + fwrite(c->reqres.buf, c->reqres.used, 1, fp); + fclose(fp); + + return ret; +} + +#else /* #ifdef LOG_REQ_RES */ + +/* Just mimic the API without doing anything */ + +void reqresReset(client *c, int free_buf) { + UNUSED(c); + UNUSED(free_buf); +} + +inline void reqresSaveClientReplyOffset(client *c) { + UNUSED(c); +} + +inline size_t reqresAppendRequest(client *c) { + UNUSED(c); + return 0; +} + +inline size_t reqresAppendResponse(client *c) { + UNUSED(c); + return 0; +} + +#endif /* #ifdef LOG_REQ_RES */ diff --git a/src/networking.c b/src/networking.c index 2edb7b72a6..3b4caa4afb 100644 --- a/src/networking.c +++ b/src/networking.c @@ -139,7 +139,12 @@ client *createClient(connection *conn) { uint64_t client_id; atomicGetIncr(server.next_client_id, client_id, 1); c->id = client_id; +#ifdef LOG_REQ_RES + reqresReset(c, 0); + c->resp = server.client_default_resp; +#else c->resp = 2; +#endif c->conn = conn; c->name = NULL; c->bufpos = 0; @@ -390,6 +395,10 @@ void _addReplyToBufferOrList(client *c, const char *s, size_t len) { return; } + /* We call it here because this function may affect the reply + * buffer offset (see function comment) */ + reqresSaveClientReplyOffset(c); + size_t reply_len = _addReplyToBuffer(c,s,len); if (len > reply_len) _addReplyProtoToList(c,s+reply_len,len-reply_len); } @@ -714,6 +723,10 @@ void *addReplyDeferredLen(client *c) { return NULL; } + /* We call it here because this function conceptually affects the reply + * buffer offset (see function comment) */ + reqresSaveClientReplyOffset(c); + trimReplyUnusedTailSpace(c); listAddNodeTail(c->reply,NULL); /* NULL is our placeholder. */ return listLast(c->reply); @@ -1575,6 +1588,9 @@ void freeClient(client *c) { freeClientOriginalArgv(c); if (c->deferred_reply_errors) listRelease(c->deferred_reply_errors); +#ifdef LOG_REQ_RES + reqresReset(c, 1); +#endif /* Unlink the client: this will close the socket, remove the I/O * handlers, and remove references of the client from different @@ -2000,6 +2016,9 @@ void resetClient(client *c) { c->slot = -1; c->duration = 0; c->flags &= ~CLIENT_EXECUTING_COMMAND; +#ifdef LOG_REQ_RES + reqresReset(c, 1); +#endif if (c->deferred_reply_errors) listRelease(c->deferred_reply_errors); @@ -2357,6 +2376,7 @@ void commandProcessed(client *c) { * since we have not applied the command. */ if (c->flags & CLIENT_BLOCKED) return; + reqresAppendResponse(c); resetClient(c); long long prev_offset = c->reploff; diff --git a/src/replication.c b/src/replication.c index 95ce31d64e..33bb8242c4 100644 --- a/src/replication.c +++ b/src/replication.c @@ -3540,16 +3540,18 @@ void processClientsWaitingReplicas(void) { if (last_offset && last_offset >= c->bstate.reploffset && last_numreplicas >= c->bstate.numreplicas) { - unblockClient(c); + /* Reply before unblocking, because unblock client calls reqresAppendResponse */ addReplyLongLong(c,last_numreplicas); + unblockClient(c); } else { int numreplicas = replicationCountAcksByOffset(c->bstate.reploffset); if (numreplicas >= c->bstate.numreplicas) { last_offset = c->bstate.reploffset; last_numreplicas = numreplicas; - unblockClient(c); + /* Reply before unblocking, because unblock client calls reqresAppendResponse */ addReplyLongLong(c,numreplicas); + unblockClient(c); } } } diff --git a/src/sentinel.c b/src/sentinel.c index 54e1eff829..035776781b 100644 --- a/src/sentinel.c +++ b/src/sentinel.c @@ -2776,7 +2776,9 @@ void sentinelInfoReplyCallback(redisAsyncContext *c, void *reply, void *privdata link->pending_commands--; r = reply; - if (r->type == REDIS_REPLY_STRING) + /* INFO reply type is verbatim in resp3. Normally, sentinel will not use + * resp3 but this is required for testing (see logreqres.c). */ + if (r->type == REDIS_REPLY_STRING || r->type == REDIS_REPLY_VERB) sentinelRefreshInstanceInfo(ri,r->str); } @@ -2987,8 +2989,10 @@ void sentinelReceiveHelloMessages(redisAsyncContext *c, void *reply, void *privd ri->link->pc_last_activity = mstime(); /* Sanity check in the reply we expect, so that the code that follows - * can avoid to check for details. */ - if (r->type != REDIS_REPLY_ARRAY || + * can avoid to check for details. + * Note: Reply type is PUSH in resp3. Normally, sentinel will not use + * resp3 but this is required for testing (see logreqres.c). */ + if ((r->type != REDIS_REPLY_ARRAY && r->type != REDIS_REPLY_PUSH) || r->elements != 3 || r->element[0]->type != REDIS_REPLY_STRING || r->element[1]->type != REDIS_REPLY_STRING || diff --git a/src/server.c b/src/server.c index 60522acb03..b42f226161 100644 --- a/src/server.c +++ b/src/server.c @@ -3782,6 +3782,9 @@ int processCommand(client *c) { * this is a reprocessing of this command, so we do not want to perform some of the actions again. */ int client_reprocessing_command = c->cmd ? 1 : 0; + if (!client_reprocessing_command) + reqresAppendRequest(c); + /* Handle possible security attacks. */ if (!strcasecmp(c->argv[0]->ptr,"host:") || !strcasecmp(c->argv[0]->ptr,"post")) { securityWarningCommand(c); @@ -4641,30 +4644,41 @@ void addReplyCommandArgList(client *c, struct redisCommandArg *args, int num_arg } } -/* Must match redisCommandRESP2Type */ -const char *RESP2_TYPE_STR[] = { - "simple-string", - "error", - "integer", - "bulk-string", - "null-bulk-string", - "array", - "null-array", -}; +#ifdef LOG_REQ_RES -/* Must match redisCommandRESP3Type */ -const char *RESP3_TYPE_STR[] = { - "simple-string", - "error", - "integer", - "double", - "bulk-string", - "array", - "map", - "set", - "bool", - "null", -}; +void addReplyJson(client *c, struct jsonObject *rs) { + addReplyMapLen(c, rs->length); + + for (int i = 0; i < rs->length; i++) { + struct jsonObjectElement *curr = &rs->elements[i]; + addReplyBulkCString(c, curr->key); + switch (curr->type) { + case (JSON_TYPE_BOOLEAN): + addReplyBool(c, curr->value.boolean); + break; + case (JSON_TYPE_INTEGER): + addReplyLongLong(c, curr->value.integer); + break; + case (JSON_TYPE_STRING): + addReplyBulkCString(c, curr->value.string); + break; + case (JSON_TYPE_OBJECT): + addReplyJson(c, curr->value.object); + break; + case (JSON_TYPE_ARRAY): + addReplyArrayLen(c, curr->value.array.length); + for (int k = 0; k < curr->value.array.length; k++) { + struct jsonObject *object = curr->value.array.objects[k]; + addReplyJson(c, object); + } + break; + default: + serverPanic("Invalid JSON type %d", curr->type); + } + } +} + +#endif void addReplyCommandHistory(client *c, struct redisCommand *cmd) { addReplySetLen(c, cmd->num_history); @@ -4862,6 +4876,9 @@ void addReplyCommandDocs(client *c, struct redisCommand *cmd) { if (cmd->deprecated_since) maplen++; if (cmd->replaced_by) maplen++; if (cmd->history) maplen++; +#ifdef LOG_REQ_RES + if (cmd->reply_schema) maplen++; +#endif if (cmd->args) maplen++; if (cmd->subcommands_dict) maplen++; addReplyMapLen(c, maplen); @@ -4903,6 +4920,12 @@ void addReplyCommandDocs(client *c, struct redisCommand *cmd) { addReplyBulkCString(c, "history"); addReplyCommandHistory(c, cmd); } +#ifdef LOG_REQ_RES + if (cmd->reply_schema) { + addReplyBulkCString(c, "reply_schema"); + addReplyJson(c, cmd->reply_schema); + } +#endif if (cmd->args) { addReplyBulkCString(c, "arguments"); addReplyCommandArgList(c, cmd->args, cmd->num_args); diff --git a/src/server.h b/src/server.h index 36cd8c760e..056123d0fc 100644 --- a/src/server.h +++ b/src/server.h @@ -1101,6 +1101,31 @@ typedef struct { size_t mem_usage_sum; } clientMemUsageBucket; +#ifdef LOG_REQ_RES +/* Structure used to log client's requests and their + * responses (see logreqres.c) */ +typedef struct { + /* General */ + int argv_logged; /* 1 if the command was logged */ + /* Vars for log buffer */ + unsigned char *buf; /* Buffer holding the data (request and response) */ + size_t used; + size_t capacity; + /* Vars for offsets within the client's reply */ + struct { + /* General */ + int saved; /* 1 if we already saved the offset (first time we call addReply*) */ + /* Offset within the static reply buffer */ + int bufpos; + /* Offset within the reply block list */ + struct { + int index; + size_t used; + } last_node; + } offset; +} clientReqResInfo; +#endif + typedef struct client { uint64_t id; /* Client incremental unique ID. */ uint64_t flags; /* Client flags: CLIENT_* macros. */ @@ -1212,6 +1237,9 @@ typedef struct client { int bufpos; size_t buf_usable_size; /* Usable size of buffer. */ char *buf; +#ifdef LOG_REQ_RES + clientReqResInfo reqres; +#endif } client; /* ACL information */ @@ -1540,6 +1568,11 @@ struct redisServer { client *current_client; /* The client that triggered the command execution (External or AOF). */ client *executing_client; /* The client executing the current command (possibly script or module). */ +#ifdef LOG_REQ_RES + char *req_res_logfile; /* Path of log file for logging all requests and their replies. If NULL, no logging will be performed */ + unsigned int client_default_resp; +#endif + /* Stuff for client mem eviction */ clientMemUsageBucket* client_mem_usage_buckets; @@ -2106,30 +2139,38 @@ typedef struct redisCommandArg { int num_args; } redisCommandArg; -/* Must be synced with RESP2_TYPE_STR and generate-command-code.py */ -typedef enum { - RESP2_SIMPLE_STRING, - RESP2_ERROR, - RESP2_INTEGER, - RESP2_BULK_STRING, - RESP2_NULL_BULK_STRING, - RESP2_ARRAY, - RESP2_NULL_ARRAY, -} redisCommandRESP2Type; +#ifdef LOG_REQ_RES -/* Must be synced with RESP3_TYPE_STR and generate-command-code.py */ +/* Must be synced with generate-command-code.py */ typedef enum { - RESP3_SIMPLE_STRING, - RESP3_ERROR, - RESP3_INTEGER, - RESP3_DOUBLE, - RESP3_BULK_STRING, - RESP3_ARRAY, - RESP3_MAP, - RESP3_SET, - RESP3_BOOL, - RESP3_NULL, -} redisCommandRESP3Type; + JSON_TYPE_STRING, + JSON_TYPE_INTEGER, + JSON_TYPE_BOOLEAN, + JSON_TYPE_OBJECT, + JSON_TYPE_ARRAY, +} jsonType; + +typedef struct jsonObjectElement { + jsonType type; + const char *key; + union { + const char *string; + long long integer; + int boolean; + struct jsonObject *object; + struct { + struct jsonObject **objects; + int length; + } array; + } value; +} jsonObjectElement; + +typedef struct jsonObject { + struct jsonObjectElement *elements; + int length; +} jsonObject; + +#endif /* WARNING! This struct must match RedisModuleCommandHistoryEntry */ typedef struct { @@ -2280,6 +2321,10 @@ struct redisCommand { struct redisCommand *subcommands; /* Array of arguments (may be NULL) */ struct redisCommandArg *args; +#ifdef LOG_REQ_RES + /* Reply schema */ + struct jsonObject *reply_schema; +#endif /* Runtime populated data */ long long microseconds, calls, rejected_calls, failed_calls; @@ -2587,6 +2632,12 @@ client *lookupClientByID(uint64_t id); int authRequired(client *c); void putClientInPendingWriteQueue(client *c); +/* logreqres.c - logging of requests and responses */ +void reqresReset(client *c, int free_buf); +void reqresSaveClientReplyOffset(client *c); +size_t reqresAppendRequest(client *c); +size_t reqresAppendResponse(client *c); + #ifdef __GNUC__ void addReplyErrorFormatEx(client *c, int flags, const char *fmt, ...) __attribute__((format(printf, 3, 4))); diff --git a/tests/cluster/tests/00-base.tcl b/tests/cluster/tests/00-base.tcl index 08ecd5e4a5..0126303ae6 100644 --- a/tests/cluster/tests/00-base.tcl +++ b/tests/cluster/tests/00-base.tcl @@ -74,3 +74,11 @@ test "CLUSTER RESET SOFT test" { R 1 CLUSTER RESET SOFT assert {[get_info_field [R 1 cluster info] cluster_current_epoch] eq $last_epoch_node1} } + +test "Coverage: CLUSTER HELP" { + assert_match "*CLUSTER *" [R 0 CLUSTER HELP] +} + +test "Coverage: ASKING" { + assert_equal {OK} [R 0 ASKING] +} diff --git a/tests/cluster/tests/16-transactions-on-replica.tcl b/tests/cluster/tests/16-transactions-on-replica.tcl index ec5699c989..8bec06ee4a 100644 --- a/tests/cluster/tests/16-transactions-on-replica.tcl +++ b/tests/cluster/tests/16-transactions-on-replica.tcl @@ -20,6 +20,12 @@ test "Can't read from replica without READONLY" { assert {[string range $err 0 4] eq {MOVED}} } +test "Can't read from replica after READWRITE" { + $replica READWRITE + catch {$replica GET a} err + assert {[string range $err 0 4] eq {MOVED}} +} + test "Can read from replica after READONLY" { $replica READONLY assert {[$replica GET a] eq {1}} diff --git a/tests/instances.tcl b/tests/instances.tcl index 4e4091c319..56a51a8725 100644 --- a/tests/instances.tcl +++ b/tests/instances.tcl @@ -105,6 +105,15 @@ proc spawn_instance {type base_port count {conf {}} {base_conf_file ""}} { } else { puts $cfg "port $port" } + + if {$::log_req_res} { + puts $cfg "req-res-logfile stdout.reqres" + } + + if {$::force_resp3} { + puts $cfg "client-default-resp 3" + } + puts $cfg "repl-diskless-sync-delay 0" puts $cfg "dir ./$dirname" puts $cfg "logfile log.txt" @@ -293,6 +302,10 @@ proc parse_options {} { set ::stop_on_failure 1 } elseif {$opt eq {--loop}} { set ::loop 1 + } elseif {$opt eq {--log-req-res}} { + set ::log_req_res 1 + } elseif {$opt eq {--force-resp3}} { + set ::force_resp3 1 } elseif {$opt eq "--help"} { puts "--single Only runs tests specified by pattern." puts "--dont-clean Keep log files on exit." diff --git a/tests/integration/corrupt-dump.tcl b/tests/integration/corrupt-dump.tcl index 35dca23be4..3c9e5ce811 100644 --- a/tests/integration/corrupt-dump.tcl +++ b/tests/integration/corrupt-dump.tcl @@ -827,7 +827,7 @@ test {corrupt payload: fuzzer findings - set with duplicate elements causes sdif assert_equal {0 2 4 6 8 _1 _3 _3 _5 _9} [lsort [r smembers _key]] assert_equal {0 2 4 6 8 _1 _3 _5 _9} [lsort [r sdiff _key]] } -} +} {} {logreqres:skip} ;# This test violates {"uniqueItems": true} } ;# tags diff --git a/tests/integration/rdb.tcl b/tests/integration/rdb.tcl index 1479b500fc..2362ef079a 100644 --- a/tests/integration/rdb.tcl +++ b/tests/integration/rdb.tcl @@ -218,6 +218,7 @@ start_server {} { test {Test RDB load info} { r debug populate 1000 r save + assert {[r lastsave] <= [lindex [r time] 0]} restart_server 0 true false wait_done_loading r assert {[s rdb_last_load_keys_expired] == 0} diff --git a/tests/integration/redis-benchmark.tcl b/tests/integration/redis-benchmark.tcl index 5e8555b1be..8035632c7c 100644 --- a/tests/integration/redis-benchmark.tcl +++ b/tests/integration/redis-benchmark.tcl @@ -25,7 +25,7 @@ proc default_set_get_checks {} { assert_match {} [cmdstat lrange] } -start_server {tags {"benchmark network external:skip"}} { +start_server {tags {"benchmark network external:skip logreqres:skip"}} { start_server {} { set master_host [srv 0 host] set master_port [srv 0 port] diff --git a/tests/modules/blockonkeys.c b/tests/modules/blockonkeys.c index 3011e4170f..8f4353a553 100644 --- a/tests/modules/blockonkeys.c +++ b/tests/modules/blockonkeys.c @@ -89,6 +89,7 @@ int get_fsl(RedisModuleCtx *ctx, RedisModuleString *keyname, int mode, int creat create = 0; /* No need to create, key exists in its basic state */ } else { RedisModule_DeleteKey(key); + *fsl = NULL; } } else { /* Key exists, and has elements in it - no need to create anything */ diff --git a/tests/sentinel/tests/07-down-conditions.tcl b/tests/sentinel/tests/07-down-conditions.tcl index bb24d6dff0..403f81e73a 100644 --- a/tests/sentinel/tests/07-down-conditions.tcl +++ b/tests/sentinel/tests/07-down-conditions.tcl @@ -72,6 +72,7 @@ test "SDOWN is triggered by masters advertising as slaves" { ensure_master_up } +if {!$::log_req_res} { # this test changes 'dir' config to '/' and logreqres.c cannot open protocol dump file under the root directory. test "SDOWN is triggered by misconfigured instance replying with errors" { ensure_master_up set orig_dir [lindex [R 0 config get dir] 1] @@ -90,6 +91,7 @@ test "SDOWN is triggered by misconfigured instance replying with errors" { R 0 bgsave ensure_master_up } +} # We use this test setup to also test command renaming, as a side # effect of the master going down if we send PONG instead of PING diff --git a/tests/support/redis.tcl b/tests/support/redis.tcl index 861e8bc274..53fa9fe915 100644 --- a/tests/support/redis.tcl +++ b/tests/support/redis.tcl @@ -28,6 +28,8 @@ package require Tcl 8.5 package provide redis 0.1 +source [file join [file dirname [info script]] "response_transformers.tcl"] + namespace eval redis {} set ::redis::id 0 array set ::redis::fd {} @@ -41,6 +43,11 @@ array set ::redis::tls {} array set ::redis::callback {} array set ::redis::state {} ;# State in non-blocking reply reading array set ::redis::statestack {} ;# Stack of states, for nested mbulks +array set ::redis::curr_argv {} ;# Remember the current argv, to be used in response_transformers.tcl +array set ::redis::testing_resp3 {} ;# Indicating if the current client is using RESP3 (only if the test is trying to test RESP3 specific behavior. It won't be on in case of force_resp3) + +set ::force_resp3 0 +set ::log_req_res 0 proc redis {{server 127.0.0.1} {port 6379} {defer 0} {tls 0} {tlsoptions {}} {readraw 0}} { if {$tls} { @@ -62,6 +69,8 @@ proc redis {{server 127.0.0.1} {port 6379} {defer 0} {tls 0} {tlsoptions {}} {re set ::redis::deferred($id) $defer set ::redis::readraw($id) $readraw set ::redis::reconnect($id) 0 + set ::redis::curr_argv($id) 0 + set ::redis::testing_resp3($id) 0 set ::redis::tls($id) $tls ::redis::redis_reset_state $id interp alias {} ::redis::redisHandle$id {} ::redis::__dispatch__ $id @@ -123,6 +132,20 @@ proc ::redis::__dispatch__raw__ {id method argv} { set fd $::redis::fd($id) } + # Transform HELLO 2 to HELLO 3 if force_resp3 + # All set the connection var testing_resp3 in case of HELLO 3 + if {[llength $argv] > 0 && [string compare -nocase $method "HELLO"] == 0} { + if {[lindex $argv 0] == 3} { + set ::redis::testing_resp3($id) 1 + } else { + set ::redis::testing_resp3($id) 0 + if {$::force_resp3} { + # If we are in force_resp3 we run HELLO 3 instead of HELLO 2 + lset argv 0 3 + } + } + } + set blocking $::redis::blocking($id) set deferred $::redis::deferred($id) if {$blocking == 0} { @@ -146,6 +169,7 @@ proc ::redis::__dispatch__raw__ {id method argv} { return -code error "I/O error reading reply" } + set ::redis::curr_argv($id) [concat $method $argv] if {!$deferred} { if {$blocking} { ::redis::redis_read_reply $id $fd @@ -200,6 +224,8 @@ proc ::redis::__method__close {id fd} { catch {unset ::redis::state($id)} catch {unset ::redis::statestack($id)} catch {unset ::redis::callback($id)} + catch {unset ::redis::curr_argv($id)} + catch {unset ::redis::testing_resp3($id)} catch {interp alias {} ::redis::redisHandle$id {}} } @@ -253,7 +279,7 @@ proc ::redis::redis_multi_bulk_read {id fd} { set err {} for {set i 0} {$i < $count} {incr i} { if {[catch { - lappend l [redis_read_reply $id $fd] + lappend l [redis_read_reply_logic $id $fd] } e] && $err eq {}} { set err $e } @@ -269,8 +295,8 @@ proc ::redis::redis_read_map {id fd} { set err {} for {set i 0} {$i < $count} {incr i} { if {[catch { - set k [redis_read_reply $id $fd] ; # key - set v [redis_read_reply $id $fd] ; # value + set k [redis_read_reply_logic $id $fd] ; # key + set v [redis_read_reply_logic $id $fd] ; # value dict set d $k $v } e] && $err eq {}} { set err $e @@ -296,13 +322,25 @@ proc ::redis::redis_read_bool fd { return -code error "Bad protocol, '$v' as bool type" } +proc ::redis::redis_read_double {id fd} { + set v [redis_read_line $fd] + # unlike many other DTs, there is a textual difference between double and a string with the same value, + # so we need to transform to double if we are testing RESP3 (i.e. some tests check that a + # double reply is "1.0" and not "1") + if {[should_transform_to_resp2 $id]} { + return $v + } else { + return [expr {double($v)}] + } +} + proc ::redis::redis_read_verbatim_str fd { set v [redis_bulk_read $fd] # strip the first 4 chars ("txt:") return [string range $v 4 end] } -proc ::redis::redis_read_reply {id fd} { +proc ::redis::redis_read_reply_logic {id fd} { if {$::redis::readraw($id)} { return [redis_read_line $fd] } @@ -314,7 +352,7 @@ proc ::redis::redis_read_reply {id fd} { : - ( - + {return [redis_read_line $fd]} - , {return [expr {double([redis_read_line $fd])}]} + , {return [redis_read_double $id $fd]} # {return [redis_read_bool $fd]} = {return [redis_read_verbatim_str $fd]} - {return -code error [redis_read_line $fd]} @@ -340,6 +378,11 @@ proc ::redis::redis_read_reply {id fd} { } } +proc ::redis::redis_read_reply {id fd} { + set response [redis_read_reply_logic $id $fd] + ::response_transformers::transform_response_if_needed $id $::redis::curr_argv($id) $response +} + proc ::redis::redis_reset_state id { set ::redis::state($id) [dict create buf {} mbulk -1 bulk -1 reply {}] set ::redis::statestack($id) {} @@ -416,3 +459,8 @@ proc ::redis::redis_readable {fd id} { } } } + +# when forcing resp3 some tests that rely on resp2 can fail, so we have to translate the resp3 response to resp2 +proc ::redis::should_transform_to_resp2 {id} { + return [expr {$::force_resp3 && !$::redis::testing_resp3($id)}] +} diff --git a/tests/support/response_transformers.tcl b/tests/support/response_transformers.tcl new file mode 100644 index 0000000000..45b3cf8f2e --- /dev/null +++ b/tests/support/response_transformers.tcl @@ -0,0 +1,105 @@ +# Tcl client library - used by the Redis test +# Copyright (C) 2009-2023 Redis Ltd. +# Released under the BSD license like Redis itself +# +# This file contains a bunch of commands whose purpose is to transform +# a RESP3 response to RESP2 +# Why is it needed? +# When writing the reply_schema part in COMMAND DOCS we decided to use +# the existing tests in order to verify the schemas (see logreqres.c) +# The problem was that many tests were relying on the RESP2 structure +# of the response (e.g. HRANDFIELD WITHVALUES in RESP2: {f1 v1 f2 v2} +# vs. RESP3: {{f1 v1} {f2 v2}}). +# Instead of adjusting the tests to expect RESP3 responses (a lot of +# changes in many files) we decided to transform the response to RESP2 +# when running with --force-resp3 + +package require Tcl 8.5 + +namespace eval response_transformers {} + +# Transform a map response into an array of tuples (tuple = array with 2 elements) +# Used for XREAD[GROUP] +proc transfrom_map_to_tupple_array {argv response} { + set tuparray {} + foreach {key val} $response { + set tmp {} + lappend tmp $key + lappend tmp $val + lappend tuparray $tmp + } + return $tuparray +} + +# Transform an array of tuples to a flat array +proc transfrom_tuple_array_to_flat_array {argv response} { + set flatarray {} + foreach pair $response { + lappend flatarray {*}$pair + } + return $flatarray +} + +# With HRANDFIELD, we only need to transform the response if the request had WITHVALUES +# (otherwise the returned response is a flat array in both RESPs) +proc transfrom_hrandfield_command {argv response} { + foreach ele $argv { + if {[string compare -nocase $ele "WITHVALUES"] == 0} { + return [transfrom_tuple_array_to_flat_array $argv $response] + } + } + return $response +} + +# With some zset commands, we only need to transform the response if the request had WITHSCORES +# (otherwise the returned response is a flat array in both RESPs) +proc transfrom_zset_withscores_command {argv response} { + foreach ele $argv { + if {[string compare -nocase $ele "WITHSCORES"] == 0} { + return [transfrom_tuple_array_to_flat_array $argv $response] + } + } + return $response +} + +# With ZPOPMIN/ZPOPMAX, we only need to transform the response if the request had COUNT (3rd arg) +# (otherwise the returned response is a flat array in both RESPs) +proc transfrom_zpopmin_zpopmax {argv response} { + if {[llength $argv] == 3} { + return [transfrom_tuple_array_to_flat_array $argv $response] + } + return $response +} + +set ::trasformer_funcs { + XREAD transfrom_map_to_tupple_array + XREADGROUP transfrom_map_to_tupple_array + HRANDFIELD transfrom_hrandfield_command + ZRANDMEMBER transfrom_zset_withscores_command + ZRANGE transfrom_zset_withscores_command + ZRANGEBYSCORE transfrom_zset_withscores_command + ZRANGEBYLEX transfrom_zset_withscores_command + ZREVRANGE transfrom_zset_withscores_command + ZREVRANGEBYSCORE transfrom_zset_withscores_command + ZREVRANGEBYLEX transfrom_zset_withscores_command + ZUNION transfrom_zset_withscores_command + ZDIFF transfrom_zset_withscores_command + ZINTER transfrom_zset_withscores_command + ZPOPMIN transfrom_zpopmin_zpopmax + ZPOPMAX transfrom_zpopmin_zpopmax +} + +proc ::response_transformers::transform_response_if_needed {id argv response} { + if {![::redis::should_transform_to_resp2 $id] || $::redis::readraw($id)} { + return $response + } + + set key [string toupper [lindex $argv 0]] + if {![dict exists $::trasformer_funcs $key]} { + return $response + } + + set transform [dict get $::trasformer_funcs $key] + + return [$transform $argv $response] +} diff --git a/tests/support/server.tcl b/tests/support/server.tcl index a23224bd76..4c596290d8 100644 --- a/tests/support/server.tcl +++ b/tests/support/server.tcl @@ -207,6 +207,12 @@ proc tags_acceptable {tags err_return} { } } + # some units mess with the client output buffer so we can't really use the req-res logging mechanism. + if {$::log_req_res && [lsearch $tags "logreqres:skip"] >= 0} { + set err "Not supported when running in log-req-res mode" + return 0 + } + if {$::external && [lsearch $tags "external:skip"] >= 0} { set err "Not supported on external server" return 0 @@ -511,6 +517,14 @@ proc start_server {options {code undefined}} { dict unset config $directive } + if {$::log_req_res} { + dict set config "req-res-logfile" "stdout.reqres" + } + + if {$::force_resp3} { + dict set config "client-default-resp" "3" + } + # write new configuration to temporary file set config_file [tmpfile redis.conf] create_server_config_file $config_file $config $config_lines diff --git a/tests/test_helper.tcl b/tests/test_helper.tcl index 3a612c8c29..8079955930 100644 --- a/tests/test_helper.tcl +++ b/tests/test_helper.tcl @@ -100,6 +100,7 @@ set ::all_tests { unit/cluster/hostnames unit/cluster/multi-slot-operations unit/cluster/slot-ownership + unit/cluster/links } # Index to the next test to run in the ::all_tests list. set ::next_test 0 @@ -134,6 +135,7 @@ set ::timeout 1200; # 20 minutes without progresses will quit the test. set ::last_progress [clock seconds] set ::active_servers {} ; # Pids of active Redis instances. set ::dont_clean 0 +set ::dont_pre_clean 0 set ::wait_server 0 set ::stop_on_failure 0 set ::dump_logs 0 @@ -144,6 +146,8 @@ set ::cluster_mode 0 set ::ignoreencoding 0 set ::ignoredigest 0 set ::large_memory 0 +set ::log_req_res 0 +set ::force_resp3 0 # Set to 1 when we are running in client mode. The Redis test uses a # server-client model to run tests simultaneously. The server instance @@ -319,7 +323,7 @@ proc cleanup {} { } proc test_server_main {} { - cleanup + if {!$::dont_pre_clean} cleanup set tclsh [info nameofexecutable] # Open a listening socket, trying different ports in order to find a # non busy one. @@ -650,6 +654,10 @@ for {set j 0} {$j < [llength $argv]} {incr j} { lappend ::global_overrides $arg lappend ::global_overrides $arg2 incr j 2 + } elseif {$opt eq {--log-req-res}} { + set ::log_req_res 1 + } elseif {$opt eq {--force-resp3}} { + set ::force_resp3 1 } elseif {$opt eq {--skipfile}} { incr j set fp [open $arg r] @@ -724,6 +732,8 @@ for {set j 0} {$j < [llength $argv]} {incr j} { set ::durable 1 } elseif {$opt eq {--dont-clean}} { set ::dont_clean 1 + } elseif {$opt eq {--dont-pre-clean}} { + set ::dont_pre_clean 1 } elseif {$opt eq {--no-latency}} { set ::no_latency 1 } elseif {$opt eq {--wait-server}} { diff --git a/tests/unit/acl.tcl b/tests/unit/acl.tcl index 59626caafc..13eea86de5 100644 --- a/tests/unit/acl.tcl +++ b/tests/unit/acl.tcl @@ -7,6 +7,10 @@ start_server {tags {"acl external:skip"}} { r ACL setuser newuser } + test {Coverage: ACL USERS} { + r ACL USERS + } {default newuser} + test {Usernames can not contain spaces or null characters} { catch {r ACL setuser "a a"} err set err diff --git a/tests/unit/aofrw.tcl b/tests/unit/aofrw.tcl index 00fc9e3bdc..fe07351a3a 100644 --- a/tests/unit/aofrw.tcl +++ b/tests/unit/aofrw.tcl @@ -1,4 +1,6 @@ -start_server {tags {"aofrw external:skip"}} { +# This unit has the potential to create huge .reqres files, causing log-req-res-validator.py to run for a very long time... +# Since this unit doesn't do anything worth validating, reply_schema-wise, we decided to skip it +start_server {tags {"aofrw external:skip logreqres:skip"}} { # Enable the AOF r config set appendonly yes r config set auto-aof-rewrite-percentage 0 ; # Disable auto-rewrite. diff --git a/tests/unit/client-eviction.tcl b/tests/unit/client-eviction.tcl index db6a224994..76f7bf0f29 100644 --- a/tests/unit/client-eviction.tcl +++ b/tests/unit/client-eviction.tcl @@ -1,4 +1,4 @@ -tags {"external:skip"} { +tags {"external:skip logreqres:skip"} { # Get info about a redis client connection: # name - name of client we want to query diff --git a/tests/unit/cluster/misc.tcl b/tests/unit/cluster/misc.tcl index 35308b81ab..3ff21010a2 100644 --- a/tests/unit/cluster/misc.tcl +++ b/tests/unit/cluster/misc.tcl @@ -12,5 +12,16 @@ start_cluster 2 2 {tags {external:skip cluster}} { assert_error {ASK*} {R 0 GET FOO} R 0 ping } {PONG} + + test "Coverage: Basic cluster commands" { + assert_equal {OK} [R 0 CLUSTER saveconfig] + + set id [R 0 CLUSTER MYID] + assert_equal {0} [R 0 CLUSTER count-failure-reports $id] + assert_match "*shard-id*" [R 0 CLUSTER slaves $id] + + R 0 flushall + assert_equal {OK} [R 0 CLUSTER flushslots] + } } diff --git a/tests/unit/geo.tcl b/tests/unit/geo.tcl index e07a6784c9..85c9485e4f 100644 --- a/tests/unit/geo.tcl +++ b/tests/unit/geo.tcl @@ -222,6 +222,10 @@ start_server {tags {"geo"}} { r georadius nyc -73.9798091 40.7598464 3 km asc } {{central park n/q/r} 4545 {union square}} + test {GEORADIUS_RO simple (sorted)} { + r georadius_ro nyc -73.9798091 40.7598464 3 km asc + } {{central park n/q/r} 4545 {union square}} + test {GEOSEARCH simple (sorted)} { r geosearch nyc fromlonlat -73.9798091 40.7598464 bybox 6 6 km asc } {{central park n/q/r} 4545 {union square} {lic market}} @@ -263,6 +267,12 @@ start_server {tags {"geo"}} { r georadius nyc -73.9798091 40.7598464 10 km COUNT 3 } {{central park n/q/r} 4545 {union square}} + test {GEORADIUS with multiple WITH* tokens} { + assert_match {{{central park n/q/r} 1791875761332224 {-73.97334* 40.76480*}} {4545 1791875796750882 {-73.95641* 40.74809*}}} [r georadius nyc -73.9798091 40.7598464 10 km WITHCOORD WITHHASH COUNT 2] + assert_match {{{central park n/q/r} 1791875761332224 {-73.97334* 40.76480*}} {4545 1791875796750882 {-73.95641* 40.74809*}}} [r georadius nyc -73.9798091 40.7598464 10 km WITHHASH WITHCOORD COUNT 2] + assert_match {{{central park n/q/r} 0.7750 1791875761332224 {-73.97334* 40.76480*}} {4545 2.3651 1791875796750882 {-73.95641* 40.74809*}}} [r georadius nyc -73.9798091 40.7598464 10 km WITHDIST WITHHASH WITHCOORD COUNT 2] + } + test {GEORADIUS with ANY not sorted by default} { r georadius nyc -73.9798091 40.7598464 10 km COUNT 3 ANY } {{wtc one} {union square} {central park n/q/r}} @@ -293,6 +303,10 @@ start_server {tags {"geo"}} { test {GEORADIUSBYMEMBER simple (sorted)} { r georadiusbymember nyc "wtc one" 7 km } {{wtc one} {union square} {central park n/q/r} 4545 {lic market}} + + test {GEORADIUSBYMEMBER_RO simple (sorted)} { + r georadiusbymember_ro nyc "wtc one" 7 km + } {{wtc one} {union square} {central park n/q/r} 4545 {lic market}} test {GEORADIUSBYMEMBER search areas contain satisfied points in oblique direction} { r del k1 diff --git a/tests/unit/introspection.tcl b/tests/unit/introspection.tcl index 4617334e7a..097074047f 100644 --- a/tests/unit/introspection.tcl +++ b/tests/unit/introspection.tcl @@ -7,7 +7,7 @@ start_server {tags {"introspection"}} { test {CLIENT LIST} { r client list - } {id=* addr=*:* laddr=*:* fd=* name=* age=* idle=* flags=N db=* sub=0 psub=0 ssub=0 multi=-1 qbuf=26 qbuf-free=* argv-mem=* multi-mem=0 rbs=* rbp=* obl=0 oll=0 omem=0 tot-mem=* events=r cmd=client|list user=* redir=-1 resp=2*} + } {id=* addr=*:* laddr=*:* fd=* name=* age=* idle=* flags=N db=* sub=0 psub=0 ssub=0 multi=-1 qbuf=26 qbuf-free=* argv-mem=* multi-mem=0 rbs=* rbp=* obl=0 oll=0 omem=0 tot-mem=* events=r cmd=client|list user=* redir=-1 resp=*} test {CLIENT LIST with IDs} { set myid [r client id] @@ -17,7 +17,7 @@ start_server {tags {"introspection"}} { test {CLIENT INFO} { r client info - } {id=* addr=*:* laddr=*:* fd=* name=* age=* idle=* flags=N db=* sub=0 psub=0 ssub=0 multi=-1 qbuf=26 qbuf-free=* argv-mem=* multi-mem=0 rbs=* rbp=* obl=0 oll=0 omem=0 tot-mem=* events=r cmd=client|info user=* redir=-1 resp=2*} + } {id=* addr=*:* laddr=*:* fd=* name=* age=* idle=* flags=N db=* sub=0 psub=0 ssub=0 multi=-1 qbuf=26 qbuf-free=* argv-mem=* multi-mem=0 rbs=* rbp=* obl=0 oll=0 omem=0 tot-mem=* events=r cmd=client|info user=* redir=-1 resp=*} test {CLIENT KILL with illegal arguments} { assert_error "ERR wrong number of arguments for 'client|kill' command" {r client kill} @@ -138,14 +138,22 @@ start_server {tags {"introspection"}} { r migrate [srv 0 host] [srv 0 port] key 9 5000 AUTH2 user password catch {r auth not-real} _ catch {r auth not-real not-a-password} _ - catch {r hello 2 AUTH not-real not-a-password} _ - + assert_match {*"key"*"9"*"5000"*} [$rd read] assert_match {*"key"*"9"*"5000"*"(redacted)"*} [$rd read] assert_match {*"key"*"9"*"5000"*"(redacted)"*"(redacted)"*} [$rd read] assert_match {*"auth"*"(redacted)"*} [$rd read] assert_match {*"auth"*"(redacted)"*"(redacted)"*} [$rd read] - assert_match {*"hello"*"2"*"AUTH"*"(redacted)"*"(redacted)"*} [$rd read] + + foreach resp {3 2} { + if {[lsearch $::denytags "resp3"] >= 0} { + if {$resp == 3} {continue} + } elseif {$::force_resp3} { + if {$resp == 2} {continue} + } + catch {r hello $resp AUTH not-real not-a-password} _ + assert_match "*\"hello\"*\"$resp\"*\"AUTH\"*\"(redacted)\"*\"(redacted)\"*" [$rd read] + } $rd close } {0} {needs:repl} @@ -225,6 +233,27 @@ start_server {tags {"introspection"}} { r client list } {*name= *} + test {Coverage: Basic CLIENT CACHING} { + set rd_redirection [redis_deferring_client] + $rd_redirection client id + set redir_id [$rd_redirection read] + r CLIENT TRACKING on OPTIN REDIRECT $redir_id + r CLIENT CACHING yes + r CLIENT TRACKING off + } {OK} + + test {Coverage: Basic CLIENT REPLY} { + r CLIENT REPLY on + } {OK} + + test {Coverage: Basic CLIENT TRACKINGINFO} { + r CLIENT TRACKINGINFO + } {flags off redirect -1 prefixes {}} + + test {Coverage: Basic CLIENT GETREDIR} { + r CLIENT GETREDIR + } {-1} + test {CLIENT SETNAME does not accept spaces} { catch {r client setname "foo bar"} e set e @@ -325,6 +354,8 @@ start_server {tags {"introspection"}} { logfile dir socket-mark-id + req-res-logfile + client-default-resp } if {!$::tls} { diff --git a/tests/unit/moduleapi/basics.tcl b/tests/unit/moduleapi/basics.tcl index be606ced0b..042e3474a0 100644 --- a/tests/unit/moduleapi/basics.tcl +++ b/tests/unit/moduleapi/basics.tcl @@ -21,12 +21,17 @@ start_server {tags {"modules"}} { } test {test get resp} { - r hello 2 - set reply [r test.getresp] - assert_equal $reply 2 - r hello 3 - set reply [r test.getresp] - assert_equal $reply 3 + foreach resp {3 2} { + if {[lsearch $::denytags "resp3"] >= 0} { + if {$resp == 3} {continue} + } elseif {$::force_resp3} { + if {$resp == 2} {continue} + } + r hello $resp + set reply [r test.getresp] + assert_equal $reply $resp + r hello 2 + } } test "Unload the module - test" { diff --git a/tests/unit/moduleapi/blockedclient.tcl b/tests/unit/moduleapi/blockedclient.tcl index 2cb44788ea..f0faea5c3a 100644 --- a/tests/unit/moduleapi/blockedclient.tcl +++ b/tests/unit/moduleapi/blockedclient.tcl @@ -91,6 +91,11 @@ start_server {tags {"modules"}} { test {RESP version carries through to blocked client} { for {set client_proto 2} {$client_proto <= 3} {incr client_proto} { + if {[lsearch $::denytags "resp3"] >= 0} { + if {$client_proto == 3} {continue} + } elseif {$::force_resp3} { + if {$client_proto == 2} {continue} + } r hello $client_proto r readraw 1 set ret [r do_fake_bg_true] @@ -100,6 +105,7 @@ start_server {tags {"modules"}} { assert_equal $ret "#t" } r readraw 0 + r hello 2 } } diff --git a/tests/unit/moduleapi/cmdintrospection.tcl b/tests/unit/moduleapi/cmdintrospection.tcl index 4d67af1e11..6ba69a1ed7 100644 --- a/tests/unit/moduleapi/cmdintrospection.tcl +++ b/tests/unit/moduleapi/cmdintrospection.tcl @@ -37,6 +37,9 @@ start_server {tags {"modules"}} { dict unset redis_reply group dict unset module_reply group dict unset module_reply module + if {$::log_req_res} { + dict unset redis_reply reply_schema + } assert_equal $redis_reply $module_reply } diff --git a/tests/unit/moduleapi/reply.tcl b/tests/unit/moduleapi/reply.tcl index 356d1a0edf..291253d3cc 100644 --- a/tests/unit/moduleapi/reply.tcl +++ b/tests/unit/moduleapi/reply.tcl @@ -5,6 +5,11 @@ start_server {tags {"modules"}} { # test all with hello 2/3 for {set proto 2} {$proto <= 3} {incr proto} { + if {[lsearch $::denytags "resp3"] >= 0} { + if {$proto == 3} {continue} + } elseif {$::force_resp3} { + if {$proto == 2} {continue} + } r hello $proto test "RESP$proto: RM_ReplyWithString: an string reply" { @@ -120,6 +125,8 @@ start_server {tags {"modules"}} { catch {r rw.error} e assert_match "An error" $e } + + r hello 2 } test "Unload the module - replywith" { diff --git a/tests/unit/networking.tcl b/tests/unit/networking.tcl index 559a88e747..79d6e399d0 100644 --- a/tests/unit/networking.tcl +++ b/tests/unit/networking.tcl @@ -121,7 +121,14 @@ start_server {config "minimal.conf" tags {"external:skip"}} { # Make sure bind parameter is as expected and server handles binding # accordingly. - assert_equal {bind {}} [rediscli_exec 0 config get bind] + # (it seems that rediscli_exec behaves differently in RESP3, possibly + # because CONFIG GET returns a dict instead of a list so redis-cli emits + # it in a single line) + if {$::force_resp3} { + assert_equal {{bind }} [rediscli_exec 0 config get bind] + } else { + assert_equal {bind {}} [rediscli_exec 0 config get bind] + } catch {reconnect 0} err assert_match {*connection refused*} $err diff --git a/tests/unit/obuf-limits.tcl b/tests/unit/obuf-limits.tcl index 7eb6def58a..45efc26b45 100644 --- a/tests/unit/obuf-limits.tcl +++ b/tests/unit/obuf-limits.tcl @@ -1,4 +1,4 @@ -start_server {tags {"obuf-limits external:skip"}} { +start_server {tags {"obuf-limits external:skip logreqres:skip"}} { test {CONFIG SET client-output-buffer-limit} { set oldval [lindex [r config get client-output-buffer-limit] 1] diff --git a/tests/unit/other.tcl b/tests/unit/other.tcl index 2ae09b5b7a..41e5508901 100644 --- a/tests/unit/other.tcl +++ b/tests/unit/other.tcl @@ -6,6 +6,30 @@ start_server {tags {"other"}} { } {ok} } + test {Coverage: HELP commands} { + assert_match "*OBJECT *" [r OBJECT HELP] + assert_match "*MEMORY *" [r MEMORY HELP] + assert_match "*PUBSUB *" [r PUBSUB HELP] + assert_match "*SLOWLOG *" [r SLOWLOG HELP] + assert_match "*CLIENT *" [r CLIENT HELP] + assert_match "*COMMAND *" [r COMMAND HELP] + assert_match "*CONFIG *" [r CONFIG HELP] + assert_match "*FUNCTION *" [r FUNCTION HELP] + assert_match "*MODULE *" [r MODULE HELP] + } + + test {Coverage: MEMORY MALLOC-STATS} { + if {[string match {*jemalloc*} [s mem_allocator]]} { + assert_match "*jemalloc*" [r memory malloc-stats] + } + } + + test {Coverage: MEMORY PURGE} { + if {[string match {*jemalloc*} [s mem_allocator]]} { + assert_equal {OK} [r memory purge] + } + } + test {SAVE - make sure there are all the types as values} { # Wait for a background saving in progress to terminate waitForBgsave r diff --git a/tests/unit/protocol.tcl b/tests/unit/protocol.tcl index 50305bd272..e3b4115a8a 100644 --- a/tests/unit/protocol.tcl +++ b/tests/unit/protocol.tcl @@ -110,16 +110,21 @@ start_server {tags {"protocol network"}} { # raw RESP response tests r readraw 1 + set nullres {*-1} + if {$::force_resp3} { + set nullres {_} + } + test "raw protocol response" { r srandmember nonexisting_key - } {*-1} + } "$nullres" r deferred 1 test "raw protocol response - deferred" { r srandmember nonexisting_key r read - } {*-1} + } "$nullres" test "raw protocol response - multiline" { r sadd ss a diff --git a/tests/unit/pubsub.tcl b/tests/unit/pubsub.tcl index ea8964cf39..fe486edf34 100644 --- a/tests/unit/pubsub.tcl +++ b/tests/unit/pubsub.tcl @@ -5,21 +5,41 @@ start_server {tags {"pubsub network"}} { set db 9 } - test "Pub/Sub PING" { + foreach resp {2 3} { set rd1 [redis_deferring_client] - subscribe $rd1 somechannel - # While subscribed to non-zero channels PING works in Pub/Sub mode. - $rd1 ping - $rd1 ping "foo" - set reply1 [$rd1 read] - set reply2 [$rd1 read] - unsubscribe $rd1 somechannel - # Now we are unsubscribed, PING should just return PONG. - $rd1 ping - set reply3 [$rd1 read] + if {[lsearch $::denytags "resp3"] >= 0} { + if {$resp == 3} {continue} + } elseif {$::force_resp3} { + if {$resp == 2} {continue} + } + + $rd1 hello $resp + $rd1 read + + test "Pub/Sub PING on RESP$resp" { + subscribe $rd1 somechannel + # While subscribed to non-zero channels PING works in Pub/Sub mode. + $rd1 ping + $rd1 ping "foo" + # In RESP3, the SUBSCRIBEd client can issue any command and get a reply, so the PINGs are standard + # In RESP2, only a handful of commands are allowed after a client is SUBSCRIBED (PING is one of them). + # For some reason, the reply in that case is an array with two elements: "pong" and argv[1] or an empty string + # God knows why. Done in commit 2264b981 + if {$resp == 3} { + assert_equal {PONG} [$rd1 read] + assert_equal {foo} [$rd1 read] + } else { + assert_equal {pong {}} [$rd1 read] + assert_equal {pong foo} [$rd1 read] + } + unsubscribe $rd1 somechannel + # Now we are unsubscribed, PING should just return PONG. + $rd1 ping + assert_equal {PONG} [$rd1 read] + + } $rd1 close - list $reply1 $reply2 $reply3 - } {{pong {}} {pong foo} PONG} + } test "PUBLISH/SUBSCRIBE basics" { set rd1 [redis_deferring_client] diff --git a/tests/unit/scripting.tcl b/tests/unit/scripting.tcl index e3b1a620b5..02459354a3 100644 --- a/tests/unit/scripting.tcl +++ b/tests/unit/scripting.tcl @@ -119,6 +119,10 @@ start_server {tags {"scripting"}} { r evalsha fd758d1589d044dd850a6f05d52f2eefd27f033f 1 mykey } {myval} + test {EVALSHA_RO - Can we call a SHA1 if already defined?} { + r evalsha_ro fd758d1589d044dd850a6f05d52f2eefd27f033f 1 mykey + } {myval} + test {EVALSHA - Can we call a SHA1 in uppercase?} { r evalsha FD758D1589D044DD850A6F05D52F2EEFD27F033F 1 mykey } {myval} @@ -703,6 +707,7 @@ start_server {tags {"scripting"}} { assert_equal $res $expected_list } {} {resp3} + if {!$::log_req_res} { # this test creates a huge nested array which python can't handle (RecursionError: maximum recursion depth exceeded in comparison) test {Script return recursive object} { r readraw 1 set res [run_script {local a = {}; local b = {a}; a[1] = b; return a} 0] @@ -718,6 +723,7 @@ start_server {tags {"scripting"}} { # make sure the connection is still valid assert_equal [r ping] {PONG} } + } test {Script check unpack with massive arguments} { run_script { @@ -1257,9 +1263,10 @@ start_server {tags {"scripting needs:debug"}} { for {set client_proto 2} {$client_proto <= 3} {incr client_proto} { if {[lsearch $::denytags "resp3"] >= 0} { if {$client_proto == 3} {continue} - } else { - r hello $client_proto + } elseif {$::force_resp3} { + if {$client_proto == 2} {continue} } + r hello $client_proto set extra "RESP$i/$client_proto" r readraw 1 @@ -1367,6 +1374,7 @@ start_server {tags {"scripting needs:debug"}} { } r readraw 0 + r hello 2 } } diff --git a/tests/unit/tracking.tcl b/tests/unit/tracking.tcl index 13ca8f2782..21036352f9 100644 --- a/tests/unit/tracking.tcl +++ b/tests/unit/tracking.tcl @@ -1,4 +1,5 @@ -start_server {tags {"tracking network"}} { +# logreqres:skip because it seems many of these tests rely heavily on RESP2 +start_server {tags {"tracking network logreqres:skip"}} { # Create a deferred client we'll use to redirect invalidation # messages to. set rd_redirection [redis_deferring_client] @@ -783,3 +784,28 @@ start_server {tags {"tracking network"}} { $rd_redirection close $rd close } + +# Just some extra covergae for --log-req-res, because we do not +# run the full tracking unit in that mode +start_server {tags {"tracking network"}} { + test {Coverage: Basic CLIENT CACHING} { + set rd_redirection [redis_deferring_client] + $rd_redirection client id + set redir_id [$rd_redirection read] + assert_equal {OK} [r CLIENT TRACKING on OPTIN REDIRECT $redir_id] + assert_equal {OK} [r CLIENT CACHING yes] + r CLIENT TRACKING off + } {OK} + + test {Coverage: Basic CLIENT REPLY} { + r CLIENT REPLY on + } {OK} + + test {Coverage: Basic CLIENT TRACKINGINFO} { + r CLIENT TRACKINGINFO + } {flags off redirect -1 prefixes {}} + + test {Coverage: Basic CLIENT GETREDIR} { + r CLIENT GETREDIR + } {-1} +} diff --git a/tests/unit/type/incr.tcl b/tests/unit/type/incr.tcl index 7732921d96..a64f357ae7 100644 --- a/tests/unit/type/incr.tcl +++ b/tests/unit/type/incr.tcl @@ -9,6 +9,10 @@ start_server {tags {"incr"}} { r incr novar } {2} + test {DECR against key created by incr} { + r decr novar + } {1} + test {INCR against key originally set with SET} { r set novar 100 r incr novar diff --git a/tests/unit/type/list.tcl b/tests/unit/type/list.tcl index dee8482bff..dad5e595a0 100644 --- a/tests/unit/type/list.tcl +++ b/tests/unit/type/list.tcl @@ -558,9 +558,10 @@ foreach {type large} [array get largevalue] { foreach resp {3 2} { if {[lsearch $::denytags "resp3"] >= 0} { if {$resp == 3} {continue} - } else { - r hello $resp + } elseif {$::force_resp3} { + if {$resp == 2} {continue} } + r hello $resp # Make sure we can distinguish between an empty array and a null response r readraw 1 @@ -589,6 +590,7 @@ foreach {type large} [array get largevalue] { } r readraw 0 + r hello 2 } test {Variadic RPUSH/LPUSH} { diff --git a/tests/unit/type/string.tcl b/tests/unit/type/string.tcl index 3734d1f505..a9fa894dcc 100644 --- a/tests/unit/type/string.tcl +++ b/tests/unit/type/string.tcl @@ -459,6 +459,13 @@ start_server {tags {"string"}} { assert_equal [string range $bin $_start $_end] [r getrange bin $start $end] } } + + test "Coverage: SUBSTR" { + r set key abcde + assert_equal "a" [r substr key 0 0] + assert_equal "abcd" [r substr key 0 3] + assert_equal "bcde" [r substr key -4 -1] + } if {[string match {*jemalloc*} [s mem_allocator]]} { test {trim on SET with big value} { diff --git a/tests/unit/type/zset.tcl b/tests/unit/type/zset.tcl index 7a9721905f..a52a77f24f 100644 --- a/tests/unit/type/zset.tcl +++ b/tests/unit/type/zset.tcl @@ -431,6 +431,10 @@ start_server {tags {"zset"}} { } test "ZRANK/ZREVRANK basics - $encoding" { + set nullres {$-1} + if {$::force_resp3} { + set nullres {_} + } r del zranktmp r zadd zranktmp 10 x r zadd zranktmp 20 y @@ -442,11 +446,15 @@ start_server {tags {"zset"}} { assert_equal 1 [r zrevrank zranktmp y] assert_equal 0 [r zrevrank zranktmp z] r readraw 1 - assert_equal {$-1} [r zrank zranktmp foo] - assert_equal {$-1} [r zrevrank zranktmp foo] + assert_equal $nullres [r zrank zranktmp foo] + assert_equal $nullres [r zrevrank zranktmp foo] r readraw 0 # withscores + set nullres {*-1} + if {$::force_resp3} { + set nullres {_} + } assert_equal {0 10} [r zrank zranktmp x withscore] assert_equal {1 20} [r zrank zranktmp y withscore] assert_equal {2 30} [r zrank zranktmp z withscore] @@ -454,8 +462,8 @@ start_server {tags {"zset"}} { assert_equal {1 20} [r zrevrank zranktmp y withscore] assert_equal {0 30} [r zrevrank zranktmp z withscore] r readraw 1 - assert_equal {*-1} [r zrank zranktmp foo withscore] - assert_equal {*-1} [r zrevrank zranktmp foo withscore] + assert_equal $nullres [r zrank zranktmp foo withscore] + assert_equal $nullres [r zrevrank zranktmp foo withscore] r readraw 0 } @@ -1243,11 +1251,12 @@ start_server {tags {"zset"}} { if {[lsearch $::denytags "resp3"] >= 0} { if {$resp == 3} {continue} - } else { - r hello $resp - $rd hello $resp - $rd read + } elseif {$::force_resp3} { + if {$resp == 2} {continue} } + r hello $resp + $rd hello $resp + $rd read test "ZPOPMIN/ZPOPMAX readraw in RESP$resp" { r del zset{t} @@ -1401,6 +1410,7 @@ start_server {tags {"zset"}} { } $rd close + r hello 2 } test {ZINTERSTORE regression with two sets, intset+hashtable} { diff --git a/utils/generate-command-code.py b/utils/generate-command-code.py index 24ecaef3e4..b5847c4696 100755 --- a/utils/generate-command-code.py +++ b/utils/generate-command-code.py @@ -2,6 +2,7 @@ import glob import json import os +import argparse ARG_TYPES = { "string": "ARG_TYPE_STRING", @@ -35,29 +36,6 @@ GROUPS = { "bitmap": "COMMAND_GROUP_BITMAP", } -RESP2_TYPES = { - "simple-string": "RESP2_SIMPLE_STRING", - "error": "RESP2_ERROR", - "integer": "RESP2_INTEGER", - "bulk-string": "RESP2_BULK_STRING", - "null-bulk-string": "RESP2_NULL_BULK_STRING", - "array": "RESP2_ARRAY", - "null-array": "RESP2_NULL_ARRAY", -} - -RESP3_TYPES = { - "simple-string": "RESP3_SIMPLE_STRING", - "error": "RESP3_ERROR", - "integer": "RESP3_INTEGER", - "double": "RESP3_DOUBLE", - "bulk-string": "RESP3_BULK_STRING", - "array": "RESP3_ARRAY", - "map": "RESP3_MAP", - "set": "RESP3_SET", - "bool": "RESP3_BOOL", - "null": "RESP3_NULL", -} - def get_optional_desc_string(desc, field, force_uppercase=False): v = desc.get(field, None) @@ -194,7 +172,6 @@ class Argument(object): self.type = self.desc["type"] self.key_spec_index = self.desc.get("key_spec_index", None) self.subargs = [] - self.subargs_name = None if self.type in ["oneof", "block"]: self.display = None for subdesc in self.desc["arguments"]: @@ -264,6 +241,75 @@ class Argument(object): f.write("};\n\n") +def to_c_name(str): + return str.replace(":", "").replace(".", "_").replace("$", "_")\ + .replace("^", "_").replace("*", "_").replace("-", "_") + + +class ReplySchema(object): + def __init__(self, name, desc): + self.name = to_c_name(name) + self.schema = {} + if desc.get("type") == "object": + if desc.get("properties") and desc.get("additionalProperties") is None: + print(f"{self.name}: Any object that has properties should have the additionalProperties field") + exit(1) + elif desc.get("type") == "array": + if desc.get("items") and isinstance(desc["items"], list) and any([desc.get(k) is None for k in ["minItems", "maxItems"]]): + print(f"{self.name}: Any array that has items should have the minItems and maxItems fields") + exit(1) + for k, v in desc.items(): + if isinstance(v, dict): + self.schema[k] = ReplySchema("%s_%s" % (self.name, k), v) + elif isinstance(v, list): + self.schema[k] = [] + for i, subdesc in enumerate(v): + self.schema[k].append(ReplySchema("%s_%s_%i" % (self.name, k,i), subdesc)) + else: + self.schema[k] = v + + def write(self, f): + def struct_code(name, k, v): + if isinstance(v, ReplySchema): + t = "JSON_TYPE_OBJECT" + vstr = ".value.object=&%s" % name + elif isinstance(v, list): + t = "JSON_TYPE_ARRAY" + vstr = ".value.array={.objects=%s,.length=%d}" % (name, len(v)) + elif isinstance(v, bool): + t = "JSON_TYPE_BOOLEAN" + vstr = ".value.boolean=%d" % int(v) + elif isinstance(v, str): + t = "JSON_TYPE_STRING" + vstr = ".value.string=\"%s\"" % v + elif isinstance(v, int): + t = "JSON_TYPE_INTEGER" + vstr = ".value.integer=%d" % v + + return "%s,\"%s\",%s" % (t, k, vstr) + + for k, v in self.schema.items(): + if isinstance(v, ReplySchema): + v.write(f) + elif isinstance(v, list): + for i, schema in enumerate(v): + schema.write(f) + name = to_c_name("%s_%s" % (self.name, k)) + f.write("/* %s array reply schema */\n" % name) + f.write("struct jsonObject *%s[] = {\n" % name) + for i, schema in enumerate(v): + f.write("&%s,\n" % schema.name) + f.write("};\n\n") + + f.write("/* %s reply schema */\n" % self.name) + f.write("struct jsonObjectElement %s_elements[] = {\n" % self.name) + for k, v in self.schema.items(): + name = to_c_name("%s_%s" % (self.name, k)) + f.write("{%s},\n" % struct_code(name, k, v)) + f.write("};\n\n") + f.write("struct jsonObject %s = {%s_elements,.length=%d};\n\n" % (self.name, self.name, len(self.schema))) + + class Command(object): def __init__(self, name, desc): self.name = name.upper() @@ -273,9 +319,11 @@ class Command(object): self.subcommands = [] self.args = [] for arg_desc in self.desc.get("arguments", []): - arg = Argument(self.fullname(), arg_desc) - self.args.append(arg) + self.args.append(Argument(self.fullname(), arg_desc)) verify_no_dup_names(self.fullname(), self.args) + self.reply_schema = None + if "reply_schema" in self.desc: + self.reply_schema = ReplySchema(self.reply_schema_name(), self.desc["reply_schema"]) def fullname(self): return self.name.replace("-", "_").replace(":", "") @@ -296,6 +344,9 @@ class Command(object): def arg_table_name(self): return "%s_Args" % (self.fullname().replace(" ", "_")) + def reply_schema_name(self): + return "%s_ReplySchema" % (self.fullname().replace(" ", "_")) + def struct_name(self): return "%s_Command" % (self.fullname().replace(" ", "_")) @@ -377,6 +428,9 @@ class Command(object): if self.args: s += ".args=%s," % self.arg_table_name() + if self.reply_schema and args.with_reply_schema: + s += ".reply_schema=&%s," % self.reply_schema_name() + return s[:-1] def write_internal_structs(self, f): @@ -423,6 +477,9 @@ class Command(object): f.write("{0}\n") f.write("};\n\n") + if self.reply_schema and args.with_reply_schema: + self.reply_schema.write(f) + class Subcommand(Command): def __init__(self, name, desc): @@ -447,6 +504,10 @@ def create_command(name, desc): # Figure out where the sources are srcdir = os.path.abspath(os.path.dirname(os.path.abspath(__file__)) + "/../src") +parser = argparse.ArgumentParser() +parser.add_argument('--with-reply-schema', action='store_true') +args = parser.parse_args() + # Create all command objects print("Processing json files...") for filename in glob.glob('%s/commands/*.json' % srcdir): @@ -481,8 +542,9 @@ if check_command_error_counter != 0: print("Error: There are errors in the commands check, please check the above logs.") exit(1) -print("Generating commands.c...") -with open("%s/commands.c" % srcdir, "w") as f: +commands_filename = "commands_with_reply_schema" if args.with_reply_schema else "commands" +print(f"Generating {commands_filename}.c...") +with open(f"{srcdir}/{commands_filename}.c", "w") as f: f.write("/* Automatically generated by %s, do not edit. */\n\n" % os.path.basename(__file__)) f.write("#include \"server.h\"\n") f.write( diff --git a/utils/reply_schema_linter.js b/utils/reply_schema_linter.js new file mode 100644 index 0000000000..e2358d4b93 --- /dev/null +++ b/utils/reply_schema_linter.js @@ -0,0 +1,31 @@ +function validate_schema(command_schema) { + var error_status = false + const Ajv = require("ajv/dist/2019") + const ajv = new Ajv({strict: true, strictTuples: false}) + let json = require('../src/commands/'+ command_schema); + for (var item in json) { + const schema = json[item].reply_schema + if (schema == undefined) + continue; + try { + ajv.compile(schema) + } catch (error) { + console.error(command_schema + " : " + error.toString()) + error_status = true + } + } + return error_status +} + +const schema_directory_path = './src/commands' +const path = require('path') +var fs = require('fs'); +var files = fs.readdirSync(schema_directory_path); +jsonFiles = files.filter(el => path.extname(el) === '.json') +var error_status = false +jsonFiles.forEach(function(file){ + if (validate_schema(file)) + error_status = true +}) +if (error_status) + process.exit(1) diff --git a/utils/req-res-log-validator.py b/utils/req-res-log-validator.py new file mode 100755 index 0000000000..e2b9d4f8d4 --- /dev/null +++ b/utils/req-res-log-validator.py @@ -0,0 +1,349 @@ +#!/usr/bin/env python3 +import os +import glob +import json +import sys + +import jsonschema +import subprocess +import redis +import time +import argparse +import multiprocessing +import collections +import io +import signal +import traceback +from datetime import timedelta +from functools import partial +try: + from jsonschema import Draft201909Validator as schema_validator +except ImportError: + from jsonschema import Draft7Validator as schema_validator + +""" +The purpose of this file is to validate the reply_schema values of COMMAND DOCS. +Basically, this is what it does: +1. Goes over req-res files, generated by redis-servers, spawned by the testsuite (see logreqres.c) +2. For each request-response pair, it validates the response against the request's reply_schema (obtained from COMMAND DOCS) + +This script spins up a redis-server and a redis-cli in order to obtain COMMAND DOCS. + +In order to use this file you must run the redis testsuite with the following flags: +./runtest --dont-clean --force-resp3 --log-req-res + +And then: +./utils/req-res-log-validator.py + +The script will fail only if: +1. One or more of the replies doesn't comply with its schema. +2. One or more of the commands in COMMANDS DOCS doesn't have the reply_schema field (with --fail-missing-reply-schemas) +3. The testsuite didn't execute all of the commands (with --fail-commands-not-all-hit) + +Future validations: +1. Fail the script if one or more of the branches of the reply schema (e.g. oneOf, anyOf) was not hit. +""" + +IGNORED_COMMANDS = [ + "sync", + "psync", + "monitor", + "subscribe", + "unsubscribe", + "ssubscribe", + "sunsubscribe", + "psubscribe", + "punsubscribe", + "debug", + "pfdebug", + "lolwut", +] + + +class Request(object): + """ + This class represents a Redis request (AKA command, argv) + """ + def __init__(self, f, docs, line_counter): + """ + Read lines from `f` (generated by logreqres.c) and populates the argv array + """ + self.command = None + self.schema = None + self.argv = [] + + while True: + line = f.readline() + line_counter[0] += 1 + if not line: + break + length = int(line) + arg = str(f.read(length)) + f.read(2) # read \r\n + line_counter[0] += 1 + if arg == "__argv_end__": + break + self.argv.append(arg) + + if not self.argv: + return + + self.command = self.argv[0].lower() + doc = docs.get(self.command, {}) + if not doc and len(self.argv) > 1: + self.command = f"{self.argv[0].lower()}|{self.argv[1].lower()}" + doc = docs.get(self.command, {}) + + if not doc: + self.command = None + return + + self.schema = doc.get("reply_schema") + + def __str__(self): + return json.dumps(self.argv) + + +class Response(object): + """ + This class represents a Redis response in RESP3 + """ + def __init__(self, f, line_counter): + """ + Read lines from `f` (generated by logreqres.c) and build the JSON representing the response in RESP3 + """ + self.error = False + self.queued = False + self.json = None + + line = f.readline()[:-2] + line_counter[0] += 1 + if line[0] == '+': + self.json = line[1:] + if self.json == "QUEUED": + self.queued = True + elif line[0] == '-': + self.json = line[1:] + self.error = True + elif line[0] == '$': + self.json = str(f.read(int(line[1:]))) + f.read(2) # read \r\n + line_counter[0] += 1 + elif line[0] == ':': + self.json = int(line[1:]) + elif line[0] == ',': + self.json = float(line[1:]) + elif line[0] == '_': + self.json = None + elif line[0] == '#': + self.json = line[1] == 't' + elif line[0] == '!': + self.json = str(f.read(int(line[1:]))) + f.read(2) # read \r\n + line_counter[0] += 1 + self.error = True + elif line[0] == '=': + self.json = str(f.read(int(line[1:])))[4:] # skip "txt:" or "mkd:" + f.read(2) # read \r\n + line_counter[0] += 1 + self.json.count("\r\n") + elif line[0] == '(': + self.json = line[1:] # big-number is actually a string + elif line[0] in ['*', '~', '>']: # unfortunately JSON doesn't tell the difference between a list and a set + self.json = [] + count = int(line[1:]) + for i in range(count): + ele = Response(f, line_counter) + self.json.append(ele.json) + elif line[0] in ['%', '|']: + self.json = {} + count = int(line[1:]) + for i in range(count): + field = Response(f, line_counter) + # Redis allows fields to be non-strings but JSON doesn't. + # Luckily, for any kind of response we can validate, the fields are + # always strings (example: XINFO STREAM) + # The reason we can't always convert to string is because of DEBUG PROTOCOL MAP + # which anyway doesn't have a schema + if isinstance(field.json, str): + field = field.json + value = Response(f, line_counter) + self.json[field] = value.json + if line[0] == '|': + # We don't care abou the attributes, read the real response + real_res = Response(f, line_counter) + self.__dict__.update(real_res.__dict__) + + + def __str__(self): + return json.dumps(self.json) + + +def process_file(docs, path): + """ + This function processes a single filegenerated by logreqres.c + """ + line_counter = [0] # A list with one integer: to force python to pass it by reference + command_counter = dict() + + print(f"Processing {path} ...") + + # Convert file to StringIO in order to minimize IO operations + with open(path, "r", newline="\r\n", encoding="latin-1") as f: + content = f.read() + + with io.StringIO(content) as fakefile: + while True: + try: + req = Request(fakefile, docs, line_counter) + if not req.argv: + # EOF + break + res = Response(fakefile, line_counter) + except json.decoder.JSONDecodeError as err: + print(f"JSON decoder error while processing {path}:{line_counter[0]}: {err}") + print(traceback.format_exc()) + raise + except Exception as err: + print(f"General error while processing {path}:{line_counter[0]}: {err}") + print(traceback.format_exc()) + raise + + if not req.command: + # Unknown command + continue + + command_counter[req.command] = command_counter.get(req.command, 0) + 1 + + if res.error or res.queued: + continue + + try: + jsonschema.validate(instance=res.json, schema=req.schema, cls=schema_validator) + except (jsonschema.ValidationError, jsonschema.exceptions.SchemaError) as err: + print(f"JSON schema validation error on {path}: {err}") + print(f"argv: {req.argv}") + try: + print(f"Response: {res}") + except UnicodeDecodeError as err: + print("Response: (unprintable)") + print(f"Schema: {json.dumps(req.schema, indent=2)}") + print(traceback.format_exc()) + raise + + return command_counter + + +def fetch_schemas(cli, port, args, docs): + redis_proc = subprocess.Popen(args, stdout=subprocess.PIPE) + + while True: + try: + print('Connecting to Redis...') + r = redis.Redis(port=port) + r.ping() + break + except Exception as e: + time.sleep(0.1) + pass + print('Connected') + + cli_proc = subprocess.Popen([cli, '-p', str(port), '--json', 'command', 'docs'], stdout=subprocess.PIPE) + stdout, stderr = cli_proc.communicate() + docs_response = json.loads(stdout) + + for name, doc in docs_response.items(): + if "subcommands" in doc: + for subname, subdoc in doc["subcommands"].items(): + docs[subname] = subdoc + else: + docs[name] = doc + + redis_proc.terminate() + redis_proc.wait() + + +if __name__ == '__main__': + # Figure out where the sources are + srcdir = os.path.abspath(os.path.dirname(os.path.abspath(__file__)) + "/../src") + testdir = os.path.abspath(os.path.dirname(os.path.abspath(__file__)) + "/../tests") + + parser = argparse.ArgumentParser() + parser.add_argument('--server', type=str, default='%s/redis-server' % srcdir) + parser.add_argument('--port', type=int, default=6534) + parser.add_argument('--cli', type=str, default='%s/redis-cli' % srcdir) + parser.add_argument('--module', type=str, action='append', default=[]) + parser.add_argument('--verbose', action='store_true') + parser.add_argument('--fail-commands-not-all-hit', action='store_true') + parser.add_argument('--fail-missing-reply-schemas', action='store_true') + args = parser.parse_args() + + docs = dict() + + # Fetch schemas from a Redis instance + print('Starting Redis server') + redis_args = [args.server, '--port', str(args.port)] + for module in args.module: + redis_args += ['--loadmodule', 'tests/modules/%s.so' % module] + + fetch_schemas(args.cli, args.port, redis_args, docs) + + missing_schema = [k for k, v in docs.items() + if "reply_schema" not in v and k not in IGNORED_COMMANDS] + if missing_schema: + print("WARNING! The following commands are missing a reply_schema:") + for k in sorted(missing_schema): + print(f" {k}") + if args.fail_missing_reply_schemas: + print("ERROR! at least one command does not have a reply_schema") + sys.exit(1) + + # Fetch schemas from a sentinel + print('Starting Redis sentinel') + + # Sentinel needs a config file to start + config_file = "tmpsentinel.conf" + open(config_file, 'a').close() + + sentinel_args = [args.server, config_file, '--port', str(args.port), "--sentinel"] + fetch_schemas(args.cli, args.port, sentinel_args, docs) + os.unlink(config_file) + + start = time.time() + + # Obtain all the files toprocesses + paths = [] + for path in glob.glob('%s/tmp/*/*.reqres' % testdir): + paths.append(path) + + for path in glob.glob('%s/cluster/tmp/*/*.reqres' % testdir): + paths.append(path) + + for path in glob.glob('%s/sentinel/tmp/*/*.reqres' % testdir): + paths.append(path) + + counter = collections.Counter() + # Spin several processes to handle the files in parallel + with multiprocessing.Pool(multiprocessing.cpu_count()) as pool: + func = partial(process_file, docs) + # pool.map blocks until all the files have been processed + for result in pool.map(func, paths): + counter.update(result) + command_counter = dict(counter) + + elapsed = time.time() - start + print(f"Done. ({timedelta(seconds=elapsed)})") + print("Hits per command:") + for k, v in sorted(command_counter.items()): + print(f" {k}: {v}") + # We don't care about SENTINEL commands + not_hit = set(filter(lambda x: not x.startswith("sentinel"), + set(docs.keys()) - set(command_counter.keys()) - set(IGNORED_COMMANDS))) + if not_hit: + if args.verbose: + print("WARNING! The following commands were not hit at all:") + for k in sorted(not_hit): + print(f" {k}") + if args.fail_commands_not_all_hit: + print("ERROR! at least one command was not hit by the tests") + sys.exit(1) + diff --git a/utils/req-res-validator/requirements.txt b/utils/req-res-validator/requirements.txt new file mode 100644 index 0000000000..0e3024b86e --- /dev/null +++ b/utils/req-res-validator/requirements.txt @@ -0,0 +1,2 @@ +jsonschema==4.17.3 +redis==4.5.1 \ No newline at end of file