Command info module API (#10108)

Adds RM_SetCommandInfo, allowing modules to provide the following command info:

* summary
* complexity
* since
* history
* hints
* arity
* key specs
* args

This information affects the output of `COMMAND`, `COMMAND INFO` and `COMMAND DOCS`,
Cluster, ACL and is used to filter commands with the wrong number of arguments before
the call reaches the module code.

The recently added API functions for key specs (never released) are removed.

A minimalist example would look like so:
```c
    RedisModuleCommand *mycmd = RedisModule_GetCommand(ctx,"mymodule.mycommand");
    RedisModuleCommandInfo mycmd_info = {
        .version = REDISMODULE_COMMAND_INFO_VERSION,
        .arity = -5,
        .summary = "some description",
    };
    if (RedisModule_SetCommandInfo(mycmd, &mycmd_info) == REDISMODULE_ERR)
        return REDISMODULE_ERR;
````

Notes:
* All the provided information (including strings) is copied, not keeping references to the API input data.
* The version field is actually a static struct that contains the sizes of the the structs used in arrays,
  so we can extend these in the future and old version will still be able to take the part they can support.
This commit is contained in:
Viktor Söderqvist 2022-02-04 20:09:36 +01:00 committed by GitHub
parent d7fcb3c5a1
commit 0a82fe8447
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1306 additions and 392 deletions

View File

@ -44,7 +44,7 @@ $TCLSH tests/test_helper.tcl \
--single unit/moduleapi/aclcheck \
--single unit/moduleapi/subcommands \
--single unit/moduleapi/reply \
--single unit/moduleapi/cmdintrospection \
--single unit/moduleapi/eventloop \
--single unit/moduleapi/timer \
"${@}"

File diff suppressed because it is too large Load Diff

View File

@ -244,6 +244,8 @@ typedef uint64_t RedisModuleTimerID;
* are modified from the user's sperspective, to invalidate WATCH. */
#define REDISMODULE_OPTION_NO_IMPLICIT_SIGNAL_MODIFIED (1<<1)
/* Definitions for RedisModule_SetCommandInfo. */
typedef enum {
REDISMODULE_ARG_TYPE_STRING,
REDISMODULE_ARG_TYPE_INTEGER,
@ -260,6 +262,139 @@ typedef enum {
#define REDISMODULE_CMD_ARG_OPTIONAL (1<<0) /* The argument is optional (like GET in SET command) */
#define REDISMODULE_CMD_ARG_MULTIPLE (1<<1) /* The argument may repeat itself (like key in DEL) */
#define REDISMODULE_CMD_ARG_MULTIPLE_TOKEN (1<<2) /* The argument may repeat itself, and so does its token (like `GET pattern` in SORT) */
#define _REDISMODULE_CMD_ARG_NEXT (1<<3)
typedef enum {
REDISMODULE_KSPEC_BS_INVALID = 0, /* Must be zero. An implicitly value of
* zero is provided when the field is
* absent in a struct literal. */
REDISMODULE_KSPEC_BS_UNKNOWN,
REDISMODULE_KSPEC_BS_INDEX,
REDISMODULE_KSPEC_BS_KEYWORD
} RedisModuleKeySpecBeginSearchType;
typedef enum {
REDISMODULE_KSPEC_FK_OMITTED = 0, /* Used when the field is absent in a
* struct literal. Don't use this value
* explicitly. */
REDISMODULE_KSPEC_FK_UNKNOWN,
REDISMODULE_KSPEC_FK_RANGE,
REDISMODULE_KSPEC_FK_KEYNUM
} RedisModuleKeySpecFindKeysType;
/* Key-spec flags. For details, see the documentation of
* RedisModule_SetCommandInfo and the key-spec flags in server.h. */
#define REDISMODULE_CMD_KEY_RO (1ULL<<0)
#define REDISMODULE_CMD_KEY_RW (1ULL<<1)
#define REDISMODULE_CMD_KEY_OW (1ULL<<2)
#define REDISMODULE_CMD_KEY_RM (1ULL<<3)
#define REDISMODULE_CMD_KEY_ACCESS (1ULL<<4)
#define REDISMODULE_CMD_KEY_UPDATE (1ULL<<5)
#define REDISMODULE_CMD_KEY_INSERT (1ULL<<6)
#define REDISMODULE_CMD_KEY_DELETE (1ULL<<7)
#define REDISMODULE_CMD_KEY_CHANNEL (1ULL<<8)
#define REDISMODULE_CMD_KEY_INCOMPLETE (1ULL<<9)
#define REDISMODULE_CMD_KEY_VARIABLE_FLAGS (1ULL<<10)
typedef struct RedisModuleCommandArg {
const char *name;
RedisModuleCommandArgType type;
int key_spec_index; /* If type is KEY, this is a zero-based index of
* the key_spec in the command. For other types,
* you may specify -1. */
const char *token; /* If type is PURE_TOKEN, this is the token. */
const char *summary;
const char *since;
int flags; /* The REDISMODULE_CMD_ARG_* macros. */
struct RedisModuleCommandArg *subargs;
} RedisModuleCommandArg;
typedef struct {
const char *since;
const char *changes;
} RedisModuleCommandHistoryEntry;
typedef struct {
const char *notes;
uint64_t flags; /* REDISMODULE_CMD_KEY_* macros. */
RedisModuleKeySpecBeginSearchType begin_search_type;
union {
struct {
/* The index from which we start the search for keys */
int pos;
} index;
struct {
/* The keyword that indicates the beginning of key args */
const char *keyword;
/* An index in argv from which to start searching.
* Can be negative, which means start search from the end, in reverse
* (Example: -2 means to start in reverse from the panultimate arg) */
int startfrom;
} keyword;
} bs;
RedisModuleKeySpecFindKeysType find_keys_type;
union {
struct {
/* Index of the last key relative to the result of the begin search
* step. Can be negative, in which case it's not relative. -1
* indicating till the last argument, -2 one before the last and so
* on. */
int lastkey;
/* How many args should we skip after finding a key, in order to
* find the next one. */
int keystep;
/* If lastkey is -1, we use limit to stop the search by a factor. 0
* and 1 mean no limit. 2 means 1/2 of the remaining args, 3 means
* 1/3, and so on. */
int limit;
} range;
struct {
/* Index of the argument containing the number of keys to come
* relative to the result of the begin search step */
int keynumidx;
/* Index of the fist key. (Usually it's just after keynumidx, in
* which case it should be set to keynumidx + 1.) */
int firstkey;
/* How many args should we skip after finding a key, in order to
* find the next one, relative to the result of the begin search
* step. */
int keystep;
} keynum;
} fk;
} RedisModuleCommandKeySpec;
typedef struct {
int version;
size_t sizeof_historyentry;
size_t sizeof_keyspec;
size_t sizeof_arg;
} RedisModuleCommandInfoVersion;
static const RedisModuleCommandInfoVersion RedisModule_CurrentCommandInfoVersion = {
.version = 1,
.sizeof_historyentry = sizeof(RedisModuleCommandHistoryEntry),
.sizeof_keyspec = sizeof(RedisModuleCommandKeySpec),
.sizeof_arg = sizeof(RedisModuleCommandArg)
};
#define REDISMODULE_COMMAND_INFO_VERSION (&RedisModule_CurrentCommandInfoVersion)
typedef struct {
/* Always set version to REDISMODULE_COMMAND_INFO_VERSION */
const RedisModuleCommandInfoVersion *version;
/* Version 1 fields (added in Redis 7.0.0) */
const char *summary; /* Summary of the command */
const char *complexity; /* Complexity description */
const char *since; /* Debut module version of the command */
RedisModuleCommandHistoryEntry *history; /* History */
/* A string of space-separated tips meant for clients/proxies regarding this
* command */
const char *tips;
/* Number of arguments, it is possible to use -N to say >= N */
int arity;
RedisModuleCommandKeySpec *key_specs;
RedisModuleCommandArg *args;
} RedisModuleCommandInfo;
/* Redis ACL key permission flags, which specify which permissions a module
* needs on a key. */
@ -623,7 +758,6 @@ typedef struct RedisModuleScanCursor RedisModuleScanCursor;
typedef struct RedisModuleDefragCtx RedisModuleDefragCtx;
typedef struct RedisModuleUser RedisModuleUser;
typedef struct RedisModuleKeyOptCtx RedisModuleKeyOptCtx;
typedef struct RedisModuleCommandArg RedisModuleCommandArg;
typedef int (*RedisModuleCmdFunc)(RedisModuleCtx *ctx, RedisModuleString **argv, int argc);
typedef void (*RedisModuleDisconnectFunc)(RedisModuleCtx *ctx, RedisModuleBlockedClient *bc);
@ -697,6 +831,7 @@ REDISMODULE_API int (*RedisModule_GetApi)(const char *, void *) REDISMODULE_ATTR
REDISMODULE_API int (*RedisModule_CreateCommand)(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc cmdfunc, const char *strflags, int firstkey, int lastkey, int keystep) REDISMODULE_ATTR;
REDISMODULE_API RedisModuleCommand *(*RedisModule_GetCommand)(RedisModuleCtx *ctx, const char *name) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_CreateSubcommand)(RedisModuleCommand *parent, const char *name, RedisModuleCmdFunc cmdfunc, const char *strflags, int firstkey, int lastkey, int keystep) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_SetCommandInfo)(RedisModuleCommand *command, const RedisModuleCommandInfo *info) REDISMODULE_ATTR;
REDISMODULE_API void (*RedisModule_SetModuleAttribs)(RedisModuleCtx *ctx, const char *name, int ver, int apiver) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_IsModuleNameBusy)(const char *name) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_WrongArity)(RedisModuleCtx *ctx) REDISMODULE_ATTR;
@ -924,14 +1059,6 @@ REDISMODULE_API int (*RedisModule_GetKeyspaceNotificationFlagsAll)() REDISMODULE
REDISMODULE_API int (*RedisModule_IsSubEventSupported)(RedisModuleEvent event, uint64_t subevent) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_GetServerVersion)() REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_GetTypeMethodVersion)() REDISMODULE_ATTR;
#ifdef INCLUDE_UNRELEASED_KEYSPEC_API
REDISMODULE_API int (*RedisModule_AddCommandKeySpec)(RedisModuleCommand *command, const char *specflags, int *spec_id) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_SetCommandKeySpecBeginSearchIndex)(RedisModuleCommand *command, int spec_id, int index) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_SetCommandKeySpecBeginSearchKeyword)(RedisModuleCommand *command, int spec_id, const char *keyword, int startfrom) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_SetCommandKeySpecFindKeysRange)(RedisModuleCommand *command, int spec_id, int lastkey, int keystep, int limit) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_SetCommandKeySpecFindKeysKeynum)(RedisModuleCommand *command, int spec_id, int keynumidx, int firstkey, int keystep) REDISMODULE_ATTR;
#endif
REDISMODULE_API void (*RedisModule_Yield)(RedisModuleCtx *ctx, int flags, const char *busy_reply) REDISMODULE_ATTR;
REDISMODULE_API RedisModuleBlockedClient * (*RedisModule_BlockClient)(RedisModuleCtx *ctx, RedisModuleCmdFunc reply_callback, RedisModuleCmdFunc timeout_callback, void (*free_privdata)(RedisModuleCtx*,void*), long long timeout_ms) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_UnblockClient)(RedisModuleBlockedClient *bc, void *privdata) REDISMODULE_ATTR;
@ -1023,6 +1150,7 @@ static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int
REDISMODULE_GET_API(CreateCommand);
REDISMODULE_GET_API(GetCommand);
REDISMODULE_GET_API(CreateSubcommand);
REDISMODULE_GET_API(SetCommandInfo);
REDISMODULE_GET_API(SetModuleAttribs);
REDISMODULE_GET_API(IsModuleNameBusy);
REDISMODULE_GET_API(WrongArity);
@ -1250,13 +1378,6 @@ static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int
REDISMODULE_GET_API(IsSubEventSupported);
REDISMODULE_GET_API(GetServerVersion);
REDISMODULE_GET_API(GetTypeMethodVersion);
#ifdef INCLUDE_UNRELEASED_KEYSPEC_API
REDISMODULE_GET_API(AddCommandKeySpec);
REDISMODULE_GET_API(SetCommandKeySpecBeginSearchIndex);
REDISMODULE_GET_API(SetCommandKeySpecBeginSearchKeyword);
REDISMODULE_GET_API(SetCommandKeySpecFindKeysRange);
REDISMODULE_GET_API(SetCommandKeySpecFindKeysKeynum);
#endif
REDISMODULE_GET_API(Yield);
REDISMODULE_GET_API(GetThreadSafeContext);
REDISMODULE_GET_API(GetDetachedThreadSafeContext);
@ -1347,7 +1468,6 @@ static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int
/* Things only defined for the modules core, not exported to modules
* including this file. */
#define RedisModuleString robj
#define RedisModuleCommandArg redisCommandArg
#endif /* REDISMODULE_CORE */
#endif /* REDISMODULE_H */

View File

@ -2631,12 +2631,14 @@ void setImplicitACLCategories(struct redisCommand *c) {
c->acl_categories |= ACL_CATEGORY_SLOW;
}
/* Recursively populate the args structure and return the number of args. */
/* Recursively populate the args structure (setting num_args to the number of
* subargs) and return the number of args. */
int populateArgsStructure(struct redisCommandArg *args) {
if (!args)
return 0;
int count = 0;
while (args->name) {
serverAssert(count < INT_MAX);
args->num_args = populateArgsStructure(args->subargs);
count++;
args++;

View File

@ -2308,8 +2308,9 @@ extern dict *modules;
* Functions prototypes
*----------------------------------------------------------------------------*/
/* Key arguments specs */
/* Command metadata */
void populateCommandLegacyRangeSpec(struct redisCommand *c);
int populateArgsStructure(struct redisCommandArg *args);
/* Modules */
void moduleInitModulesSystem(void);

View File

@ -2,11 +2,12 @@
# find the OS
uname_S := $(shell sh -c 'uname -s 2>/dev/null || echo not')
warning_cflags = -W -Wall -Wno-missing-field-initializers
ifeq ($(uname_S),Darwin)
SHOBJ_CFLAGS ?= -W -Wall -dynamic -fno-common -g -ggdb -std=c99 -O2
SHOBJ_CFLAGS ?= $(warning_cflags) -dynamic -fno-common -g -ggdb -std=c99 -O2
SHOBJ_LDFLAGS ?= -bundle -undefined dynamic_lookup
else # Linux, others
SHOBJ_CFLAGS ?= -W -Wall -fno-common -g -ggdb -std=c99 -O2
SHOBJ_CFLAGS ?= $(warning_cflags) -fno-common -g -ggdb -std=c99 -O2
SHOBJ_LDFLAGS ?= -shared
endif
@ -51,6 +52,7 @@ TEST_MODULES = \
list.so \
subcommands.so \
reply.so \
cmdintrospection.so \
eventloop.so
.PHONY: all

View File

@ -0,0 +1,157 @@
#include "redismodule.h"
#define UNUSED(V) ((void) V)
int cmd_xadd(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
UNUSED(argv);
UNUSED(argc);
RedisModule_ReplyWithSimpleString(ctx, "OK");
return REDISMODULE_OK;
}
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
REDISMODULE_NOT_USED(argv);
REDISMODULE_NOT_USED(argc);
if (RedisModule_Init(ctx, "cmdintrospection", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx,"cmdintrospection.xadd",cmd_xadd,"write deny-oom random fast",0,0,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
RedisModuleCommand *xadd = RedisModule_GetCommand(ctx,"cmdintrospection.xadd");
RedisModuleCommandInfo info = {
.version = REDISMODULE_COMMAND_INFO_VERSION,
.arity = -5,
.summary = "Appends a new entry to a stream",
.since = "5.0.0",
.complexity = "O(1) when adding a new entry, O(N) when trimming where N being the number of entries evicted.",
.tips = "nondeterministic_output",
.history = (RedisModuleCommandHistoryEntry[]){
/* NOTE: All versions specified should be the module's versions, not
* Redis'! We use Redis versions in this example for the purpose of
* testing (comparing the output with the output of the vanilla
* XADD). */
{"6.2.0", "Added the `NOMKSTREAM` option, `MINID` trimming strategy and the `LIMIT` option."},
{"7.0.0", "Added support for the `<ms>-*` explicit ID form."},
{0}
},
.key_specs = (RedisModuleCommandKeySpec[]){
{
.notes = "UPDATE instead of INSERT because of the optional trimming feature",
.flags = REDISMODULE_CMD_KEY_RW | REDISMODULE_CMD_KEY_UPDATE,
.begin_search_type = REDISMODULE_KSPEC_BS_INDEX,
.bs.index.pos = 1,
.find_keys_type = REDISMODULE_KSPEC_FK_RANGE,
.fk.range = {0,1,0}
},
{0}
},
.args = (RedisModuleCommandArg[]){
{
.name = "key",
.type = REDISMODULE_ARG_TYPE_KEY,
.key_spec_index = 0
},
{
.name = "nomkstream",
.type = REDISMODULE_ARG_TYPE_PURE_TOKEN,
.token = "NOMKSTREAM",
.since = "6.2.0",
.flags = REDISMODULE_CMD_ARG_OPTIONAL
},
{
.name = "trim",
.type = REDISMODULE_ARG_TYPE_BLOCK,
.flags = REDISMODULE_CMD_ARG_OPTIONAL,
.subargs = (RedisModuleCommandArg[]){
{
.name = "strategy",
.type = REDISMODULE_ARG_TYPE_ONEOF,
.subargs = (RedisModuleCommandArg[]){
{
.name = "maxlen",
.type = REDISMODULE_ARG_TYPE_PURE_TOKEN,
.token = "MAXLEN",
},
{
.name = "minid",
.type = REDISMODULE_ARG_TYPE_PURE_TOKEN,
.token = "MINID",
.since = "6.2.0",
},
{0}
}
},
{
.name = "operator",
.type = REDISMODULE_ARG_TYPE_ONEOF,
.flags = REDISMODULE_CMD_ARG_OPTIONAL,
.subargs = (RedisModuleCommandArg[]){
{
.name = "equal",
.type = REDISMODULE_ARG_TYPE_PURE_TOKEN,
.token = "="
},
{
.name = "approximately",
.type = REDISMODULE_ARG_TYPE_PURE_TOKEN,
.token = "~"
},
{0}
}
},
{
.name = "threshold",
.type = REDISMODULE_ARG_TYPE_STRING,
},
{
.name = "count",
.type = REDISMODULE_ARG_TYPE_INTEGER,
.token = "LIMIT",
.since = "6.2.0",
.flags = REDISMODULE_CMD_ARG_OPTIONAL
},
{0}
}
},
{
.name = "id_or_auto",
.type = REDISMODULE_ARG_TYPE_ONEOF,
.subargs = (RedisModuleCommandArg[]){
{
.name = "auto_id",
.type = REDISMODULE_ARG_TYPE_PURE_TOKEN,
.token = "*"
},
{
.name = "id",
.type = REDISMODULE_ARG_TYPE_STRING,
},
{0}
}
},
{
.name = "field_value",
.type = REDISMODULE_ARG_TYPE_BLOCK,
.flags = REDISMODULE_CMD_ARG_MULTIPLE,
.subargs = (RedisModuleCommandArg[]){
{
.name = "field",
.type = REDISMODULE_ARG_TYPE_STRING,
},
{
.name = "value",
.type = REDISMODULE_ARG_TYPE_STRING,
},
{0}
}
},
{0}
}
};
if (RedisModule_SetCommandInfo(xadd, &info) == REDISMODULE_ERR)
return REDISMODULE_ERR;
return REDISMODULE_OK;
}

View File

@ -1,26 +1,171 @@
#include "redismodule.h"
#ifdef INCLUDE_UNRELEASED_KEYSPEC_API
#define UNUSED(V) ((void) V)
int kspec_legacy(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
/* This function implements all commands in this module. All we care about is
* the COMMAND metadata anyway. */
int kspec_impl(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
UNUSED(argv);
UNUSED(argc);
RedisModule_ReplyWithSimpleString(ctx, "OK");
return REDISMODULE_OK;
}
int kspec_complex1(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
UNUSED(argv);
UNUSED(argc);
RedisModule_ReplyWithSimpleString(ctx, "OK");
int createKspecNone(RedisModuleCtx *ctx) {
/* A command without keyspecs; only the legacy (first,last,step) triple. */
if (RedisModule_CreateCommand(ctx,"kspec.none",kspec_impl,"",1,3,2) == REDISMODULE_ERR)
return REDISMODULE_ERR;
return REDISMODULE_OK;
}
int kspec_complex2(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
UNUSED(argv);
UNUSED(argc);
RedisModule_ReplyWithSimpleString(ctx, "OK");
int createKspecTwoRanges(RedisModuleCtx *ctx) {
/* Test that two position/range-based key specs are combined to produce the
* legacy (first,last,step) values representing both keys. */
if (RedisModule_CreateCommand(ctx,"kspec.tworanges",kspec_impl,"",0,0,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
RedisModuleCommand *command = RedisModule_GetCommand(ctx,"kspec.tworanges");
RedisModuleCommandInfo info = {
.version = REDISMODULE_COMMAND_INFO_VERSION,
.arity = -2,
.key_specs = (RedisModuleCommandKeySpec[]){
{
.flags = REDISMODULE_CMD_KEY_RO | REDISMODULE_CMD_KEY_ACCESS,
.begin_search_type = REDISMODULE_KSPEC_BS_INDEX,
.bs.index.pos = 1,
.find_keys_type = REDISMODULE_KSPEC_FK_RANGE,
.fk.range = {0,1,0}
},
{
.flags = REDISMODULE_CMD_KEY_RW | REDISMODULE_CMD_KEY_UPDATE,
.begin_search_type = REDISMODULE_KSPEC_BS_INDEX,
.bs.index.pos = 2,
/* Omitted find_keys_type is shorthand for RANGE {0,1,0} */
},
{0}
}
};
if (RedisModule_SetCommandInfo(command, &info) == REDISMODULE_ERR)
return REDISMODULE_ERR;
return REDISMODULE_OK;
}
int createKspecKeyword(RedisModuleCtx *ctx) {
/* Only keyword-based specs. The legacy triple is wiped and set to (0,0,0). */
if (RedisModule_CreateCommand(ctx,"kspec.keyword",kspec_impl,"",3,-1,1) == REDISMODULE_ERR)
return REDISMODULE_ERR;
RedisModuleCommand *command = RedisModule_GetCommand(ctx,"kspec.keyword");
RedisModuleCommandInfo info = {
.version = REDISMODULE_COMMAND_INFO_VERSION,
.key_specs = (RedisModuleCommandKeySpec[]){
{
.flags = REDISMODULE_CMD_KEY_RO | REDISMODULE_CMD_KEY_ACCESS,
.begin_search_type = REDISMODULE_KSPEC_BS_KEYWORD,
.bs.keyword.keyword = "KEYS",
.bs.keyword.startfrom = 1,
.find_keys_type = REDISMODULE_KSPEC_FK_RANGE,
.fk.range = {-1,1,0}
},
{0}
}
};
if (RedisModule_SetCommandInfo(command, &info) == REDISMODULE_ERR)
return REDISMODULE_ERR;
return REDISMODULE_OK;
}
int createKspecComplex1(RedisModuleCtx *ctx) {
/* First is a range a single key. The rest are keyword-based specs. */
if (RedisModule_CreateCommand(ctx,"kspec.complex1",kspec_impl,"",1,1,1) == REDISMODULE_ERR)
return REDISMODULE_ERR;
RedisModuleCommand *command = RedisModule_GetCommand(ctx,"kspec.complex1");
RedisModuleCommandInfo info = {
.version = REDISMODULE_COMMAND_INFO_VERSION,
.key_specs = (RedisModuleCommandKeySpec[]){
{
.flags = REDISMODULE_CMD_KEY_RO,
.begin_search_type = REDISMODULE_KSPEC_BS_INDEX,
.bs.index.pos = 1,
},
{
.flags = REDISMODULE_CMD_KEY_RW | REDISMODULE_CMD_KEY_UPDATE,
.begin_search_type = REDISMODULE_KSPEC_BS_KEYWORD,
.bs.keyword.keyword = "STORE",
.bs.keyword.startfrom = 2,
},
{
.flags = REDISMODULE_CMD_KEY_RO | REDISMODULE_CMD_KEY_ACCESS,
.begin_search_type = REDISMODULE_KSPEC_BS_KEYWORD,
.bs.keyword.keyword = "KEYS",
.bs.keyword.startfrom = 2,
.find_keys_type = REDISMODULE_KSPEC_FK_KEYNUM,
.fk.keynum = {0,1,1}
},
{0}
}
};
if (RedisModule_SetCommandInfo(command, &info) == REDISMODULE_ERR)
return REDISMODULE_ERR;
return REDISMODULE_OK;
}
int createKspecComplex2(RedisModuleCtx *ctx) {
/* First is not legacy, more than STATIC_KEYS_SPECS_NUM specs */
if (RedisModule_CreateCommand(ctx,"kspec.complex2",kspec_impl,"",0,0,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
RedisModuleCommand *command = RedisModule_GetCommand(ctx,"kspec.complex2");
RedisModuleCommandInfo info = {
.version = REDISMODULE_COMMAND_INFO_VERSION,
.key_specs = (RedisModuleCommandKeySpec[]){
{
.flags = REDISMODULE_CMD_KEY_RW | REDISMODULE_CMD_KEY_UPDATE,
.begin_search_type = REDISMODULE_KSPEC_BS_KEYWORD,
.bs.keyword.keyword = "STORE",
.bs.keyword.startfrom = 5,
.find_keys_type = REDISMODULE_KSPEC_FK_RANGE,
.fk.range = {0,1,0}
},
{
.flags = REDISMODULE_CMD_KEY_RO | REDISMODULE_CMD_KEY_ACCESS,
.begin_search_type = REDISMODULE_KSPEC_BS_INDEX,
.bs.index.pos = 1,
.find_keys_type = REDISMODULE_KSPEC_FK_RANGE,
.fk.range = {0,1,0}
},
{
.flags = REDISMODULE_CMD_KEY_RO | REDISMODULE_CMD_KEY_ACCESS,
.begin_search_type = REDISMODULE_KSPEC_BS_INDEX,
.bs.index.pos = 2,
.find_keys_type = REDISMODULE_KSPEC_FK_RANGE,
.fk.range = {0,1,0}
},
{
.flags = REDISMODULE_CMD_KEY_RW | REDISMODULE_CMD_KEY_UPDATE,
.begin_search_type = REDISMODULE_KSPEC_BS_INDEX,
.bs.index.pos = 3,
.find_keys_type = REDISMODULE_KSPEC_FK_KEYNUM,
.fk.keynum = {0,1,1}
},
{
.flags = REDISMODULE_CMD_KEY_RW | REDISMODULE_CMD_KEY_UPDATE,
.begin_search_type = REDISMODULE_KSPEC_BS_KEYWORD,
.bs.keyword.keyword = "MOREKEYS",
.bs.keyword.startfrom = 5,
.find_keys_type = REDISMODULE_KSPEC_FK_RANGE,
.fk.range = {-1,1,0}
},
{0}
}
};
if (RedisModule_SetCommandInfo(command, &info) == REDISMODULE_ERR)
return REDISMODULE_ERR;
return REDISMODULE_OK;
}
@ -28,89 +173,13 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
REDISMODULE_NOT_USED(argv);
REDISMODULE_NOT_USED(argc);
int spec_id;
if (RedisModule_Init(ctx, "keyspecs", 1, REDISMODULE_APIVER_1)== REDISMODULE_ERR)
return REDISMODULE_ERR;
/* Test legacy range "gluing" */
if (RedisModule_CreateCommand(ctx,"kspec.legacy",kspec_legacy,"",0,0,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
RedisModuleCommand *legacy = RedisModule_GetCommand(ctx,"kspec.legacy");
if (RedisModule_AddCommandKeySpec(legacy,"RO ACCESS",&spec_id) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecBeginSearchIndex(legacy,spec_id,1) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecFindKeysRange(legacy,spec_id,0,1,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_AddCommandKeySpec(legacy,"RW UPDATE",&spec_id) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecBeginSearchIndex(legacy,spec_id,2) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecFindKeysRange(legacy,spec_id,0,1,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
/* First is legacy, rest are new specs */
if (RedisModule_CreateCommand(ctx,"kspec.complex1",kspec_complex1,"",1,1,1) == REDISMODULE_ERR)
return REDISMODULE_ERR;
RedisModuleCommand *complex1 = RedisModule_GetCommand(ctx,"kspec.complex1");
if (RedisModule_AddCommandKeySpec(complex1,"RW UPDATE",&spec_id) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecBeginSearchKeyword(complex1,spec_id,"STORE",2) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecFindKeysRange(complex1,spec_id,0,1,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_AddCommandKeySpec(complex1,"RO ACCESS",&spec_id) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecBeginSearchKeyword(complex1,spec_id,"KEYS",2) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecFindKeysKeynum(complex1,spec_id,0,1,1) == REDISMODULE_ERR)
return REDISMODULE_ERR;
/* First is not legacy, more than STATIC_KEYS_SPECS_NUM specs */
if (RedisModule_CreateCommand(ctx,"kspec.complex2",kspec_complex2,"",0,0,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
RedisModuleCommand *complex2 = RedisModule_GetCommand(ctx,"kspec.complex2");
if (RedisModule_AddCommandKeySpec(complex2,"RW UPDATE",&spec_id) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecBeginSearchKeyword(complex2,spec_id,"STORE",5) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecFindKeysRange(complex2,spec_id,0,1,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_AddCommandKeySpec(complex2,"RO ACCESS",&spec_id) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecBeginSearchIndex(complex2,spec_id,1) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecFindKeysRange(complex2,spec_id,0,1,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_AddCommandKeySpec(complex2,"RO ACCESS",&spec_id) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecBeginSearchIndex(complex2,spec_id,2) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecFindKeysRange(complex2,spec_id,0,1,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_AddCommandKeySpec(complex2,"RW UPDATE",&spec_id) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecBeginSearchIndex(complex2,spec_id,3) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecFindKeysKeynum(complex2,spec_id,0,1,1) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_AddCommandKeySpec(complex2,"RW UPDATE",&spec_id) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecBeginSearchKeyword(complex2,spec_id,"MOREKEYS",5) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecFindKeysRange(complex2,spec_id,-1,1,0) == REDISMODULE_ERR)
if (RedisModule_Init(ctx, "keyspecs", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (createKspecNone(ctx) == REDISMODULE_ERR) return REDISMODULE_ERR;
if (createKspecTwoRanges(ctx) == REDISMODULE_ERR) return REDISMODULE_ERR;
if (createKspecKeyword(ctx) == REDISMODULE_ERR) return REDISMODULE_ERR;
if (createKspecComplex1(ctx) == REDISMODULE_ERR) return REDISMODULE_ERR;
if (createKspecComplex2(ctx) == REDISMODULE_ERR) return REDISMODULE_ERR;
return REDISMODULE_OK;
}
#endif

View File

@ -29,11 +29,7 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
REDISMODULE_NOT_USED(argv);
REDISMODULE_NOT_USED(argc);
#ifdef INCLUDE_UNRELEASED_KEYSPEC_API
int spec_id;
#endif
if (RedisModule_Init(ctx, "subcommands", 1, REDISMODULE_APIVER_1)== REDISMODULE_ERR)
if (RedisModule_Init(ctx, "subcommands", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx,"subcommands.bitarray",NULL,"",0,0,0) == REDISMODULE_ERR)
@ -43,28 +39,40 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
if (RedisModule_CreateSubcommand(parent,"set",cmd_set,"",0,0,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
RedisModuleCommand *subcmd = RedisModule_GetCommand(ctx,"subcommands.bitarray|set");
#ifdef INCLUDE_UNRELEASED_KEYSPEC_API
if (RedisModule_AddCommandKeySpec(subcmd,"RW UPDATE",&spec_id) == REDISMODULE_ERR)
RedisModuleCommandInfo cmd_set_info = {
.version = REDISMODULE_COMMAND_INFO_VERSION,
.key_specs = (RedisModuleCommandKeySpec[]){
{
.flags = REDISMODULE_CMD_KEY_RW | REDISMODULE_CMD_KEY_UPDATE,
.begin_search_type = REDISMODULE_KSPEC_BS_INDEX,
.bs.index.pos = 1,
.find_keys_type = REDISMODULE_KSPEC_FK_RANGE,
.fk.range = {0,1,0}
},
{0}
}
};
if (RedisModule_SetCommandInfo(subcmd, &cmd_set_info) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecBeginSearchIndex(subcmd,spec_id,1) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecFindKeysRange(subcmd,spec_id,0,1,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
#endif
if (RedisModule_CreateSubcommand(parent,"get",cmd_get,"",0,0,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
subcmd = RedisModule_GetCommand(ctx,"subcommands.bitarray|get");
#ifdef INCLUDE_UNRELEASED_KEYSPEC_API
if (RedisModule_AddCommandKeySpec(subcmd,"RO ACCESS",&spec_id) == REDISMODULE_ERR)
RedisModuleCommandInfo cmd_get_info = {
.version = REDISMODULE_COMMAND_INFO_VERSION,
.key_specs = (RedisModuleCommandKeySpec[]){
{
.flags = REDISMODULE_CMD_KEY_RO | REDISMODULE_CMD_KEY_ACCESS,
.begin_search_type = REDISMODULE_KSPEC_BS_INDEX,
.bs.index.pos = 1,
.find_keys_type = REDISMODULE_KSPEC_FK_RANGE,
.fk.range = {0,1,0}
},
{0}
}
};
if (RedisModule_SetCommandInfo(subcmd, &cmd_get_info) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecBeginSearchIndex(subcmd,spec_id,1) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecFindKeysRange(subcmd,spec_id,0,1,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
#endif
/* Get the name of the command currently running. */
if (RedisModule_CreateCommand(ctx,"subcommands.parent_get_fullname",cmd_get_fullname,"",0,0,0) == REDISMODULE_ERR)

View File

@ -0,0 +1,42 @@
set testmodule [file normalize tests/modules/cmdintrospection.so]
start_server {tags {"modules"}} {
r module load $testmodule
# cmdintrospection.xadd mimics XADD with regards to how
# what COMMAND exposes. There are two differences:
#
# 1. cmdintrospection.xadd (and all module commands) do not have ACL categories
# 2. cmdintrospection.xadd's `group` is "module"
#
# This tests verify that, apart from the above differences, the output of
# COMMAND INFO and COMMAND DOCS are identical for the two commands.
test "Module command introspection via COMMAND INFO" {
set redis_reply [lindex [r command info xadd] 0]
set module_reply [lindex [r command info cmdintrospection.xadd] 0]
for {set i 1} {$i < [llength $redis_reply]} {incr i} {
if {$i == 2} {
# Remove the "module" flag
set mylist [lindex $module_reply $i]
set idx [lsearch $mylist "module"]
set mylist [lreplace $mylist $idx $idx]
lset module_reply $i $mylist
}
if {$i == 6} {
# Skip ACL categories
continue
}
assert_equal [lindex $redis_reply $i] [lindex $module_reply $i]
}
}
test "Module command introspection via COMMAND DOCS" {
set redis_reply [dict create {*}[lindex [r command docs xadd] 1]]
set module_reply [dict create {*}[lindex [r command docs cmdintrospection.xadd] 1]]
# Compare the maps. We need to pop "group" first.
dict unset redis_reply group
dict unset module_reply group
assert_equal $redis_reply $module_reply
}
}

View File

@ -1,12 +1,25 @@
set testmodule [file normalize tests/modules/keyspecs.so]
if 0 { ; # Test suite disabled due to planned API changes
start_server {tags {"modules"}} {
r module load $testmodule
test "Module key specs: Legacy" {
set reply [lindex [r command info kspec.legacy] 0]
# Verify (first, last, step)
test "Module key specs: No spec, only legacy triple" {
set reply [lindex [r command info kspec.none] 0]
# Verify (first, last, step) and not movablekeys
assert_equal [lindex $reply 2] {module}
assert_equal [lindex $reply 3] 1
assert_equal [lindex $reply 4] 3
assert_equal [lindex $reply 5] 2
# Verify key-spec auto-generated from the legacy triple
set keyspecs [lindex $reply 8]
assert_equal [llength $keyspecs] 1
assert_equal [lindex $keyspecs 0] {flags {} begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 2 keystep 2 limit 0}}}
}
test "Module key specs: Two ranges" {
set reply [lindex [r command info kspec.tworanges] 0]
# Verify (first, last, step) and not movablekeys
assert_equal [lindex $reply 2] {module}
assert_equal [lindex $reply 3] 1
assert_equal [lindex $reply 4] 2
assert_equal [lindex $reply 5] 1
@ -16,22 +29,36 @@ start_server {tags {"modules"}} {
assert_equal [lindex $keyspecs 1] {flags {RW update} begin_search {type index spec {index 2}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}}
}
test "Module key specs: Keyword-only spec clears the legacy triple" {
set reply [lindex [r command info kspec.keyword] 0]
# Verify (first, last, step) and movablekeys
assert_equal [lindex $reply 2] {module movablekeys}
assert_equal [lindex $reply 3] 0
assert_equal [lindex $reply 4] 0
assert_equal [lindex $reply 5] 0
# Verify key-specs
set keyspecs [lindex $reply 8]
assert_equal [lindex $keyspecs 0] {flags {RO access} begin_search {type keyword spec {keyword KEYS startfrom 1}} find_keys {type range spec {lastkey -1 keystep 1 limit 0}}}
}
test "Module key specs: Complex specs, case 1" {
set reply [lindex [r command info kspec.complex1] 0]
# Verify (first, last, step)
# Verify (first, last, step) and movablekeys
assert_equal [lindex $reply 2] {module movablekeys}
assert_equal [lindex $reply 3] 1
assert_equal [lindex $reply 4] 1
assert_equal [lindex $reply 5] 1
# Verify key-specs
set keyspecs [lindex $reply 8]
assert_equal [lindex $keyspecs 0] {flags {} begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}}
assert_equal [lindex $keyspecs 0] {flags RO begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}}
assert_equal [lindex $keyspecs 1] {flags {RW update} begin_search {type keyword spec {keyword STORE startfrom 2}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}}
assert_equal [lindex $keyspecs 2] {flags {RO access} begin_search {type keyword spec {keyword KEYS startfrom 2}} find_keys {type keynum spec {keynumidx 0 firstkey 1 keystep 1}}}
}
test "Module key specs: Complex specs, case 2" {
set reply [lindex [r command info kspec.complex2] 0]
# Verify (first, last, step)
# Verify (first, last, step) and movablekeys
assert_equal [lindex $reply 2] {module movablekeys}
assert_equal [lindex $reply 3] 1
assert_equal [lindex $reply 4] 2
assert_equal [lindex $reply 5] 1
@ -47,12 +74,10 @@ start_server {tags {"modules"}} {
test "Module command list filtering" {
;# Note: we piggyback this tcl file to test the general functionality of command list filtering
set reply [r command list filterby module keyspecs]
assert_equal [lsort $reply] {kspec.complex1 kspec.complex2 kspec.legacy}
assert_equal [lsort $reply] {kspec.complex1 kspec.complex2 kspec.keyword kspec.none kspec.tworanges}
}
test "Unload the module - keyspecs" {
assert_equal {OK} [r module unload keyspecs]
}
}
} ; # Test suite disabled

View File

@ -8,13 +8,8 @@ start_server {tags {"modules"}} {
set command_reply [r command info subcommands.bitarray]
set first_cmd [lindex $command_reply 0]
set subcmds_in_command [lsort [lindex $first_cmd 9]]
if 0 { ; # Keyspecs disabled due to planned changes in keyspec API
assert_equal [lindex $subcmds_in_command 0] {subcommands.bitarray|get -2 module 1 1 1 {} {} {{flags {RO access} begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}}} {}}
assert_equal [lindex $subcmds_in_command 1] {subcommands.bitarray|set -2 module 1 1 1 {} {} {{flags {RW update} begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}}} {}}
} else { ; # The same asserts without the key specs
assert_equal [lindex $subcmds_in_command 0] {subcommands.bitarray|get -2 module 0 0 0 {} {} {} {}}
assert_equal [lindex $subcmds_in_command 1] {subcommands.bitarray|set -2 module 0 0 0 {} {} {} {}}
}
assert_equal [lindex $subcmds_in_command 0] {subcommands.bitarray|get -2 module 1 1 1 {} {} {{flags {RO access} begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}}} {}}
assert_equal [lindex $subcmds_in_command 1] {subcommands.bitarray|set -2 module 1 1 1 {} {} {{flags {RW update} begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}}} {}}
# Verify that module subcommands are displayed correctly in COMMAND DOCS
set docs_reply [r command docs subcommands.bitarray]