Unverified Commit 03fcc211 authored by guybe7's avatar guybe7 Committed by GitHub
Browse files

A better approach for COMMAND INFO for movablekeys commands (#8324)

Fix #7297

The problem:

Today, there is no way for a client library or app to know the key name indexes for commands such as
ZUNIONSTORE/EVAL and others with "numkeys", since COMMAND INFO returns no useful info for them.

For cluster-aware redis clients, this requires to 'patch' the client library code specifically for each of these commands or to
resolve each execution of these commands with COMMAND GETKEYS.

The solution:

Introducing key specs other than the legacy "range" (first,last,step)

The 8th element of the command info array, if exists, holds an array of key specs. The array may be empty, which indicates
the command doesn't take any key arguments or may contain one or more key-specs, each one may leads to the discovery
of 0 or more key arguments.

A client library that doesn't support this key-spec feature will keep using the first,last,step and movablekeys flag which will
obviously remain unchanged.

A client that supports this key-specs feature needs only to look at the key-specs array. If it finds an unrecognized spec, it
must resort to using COMMAND GETKEYS if it wishes to get all key name arguments, but if all it needs is one key in order
to know which cluster node to use, then maybe another spec (if the command has several) can supply that, and there's no
need to use GETKEYS.

Each spec is an array of arguments, first one is the spec name, the second is an array of flags, and the third is an array
containing details about the spec (specific meaning for each spec type)
The initial flags we support are "read" and "write" indicating if the keys that this key-spec finds are used for read or for write.
clients should ignore any unfamiliar flags.

In order to easily find the positions of keys in a given array of args we introduce keys specs. There are two logical steps of
key specs:
1. `start_search`: Given an array of args, indicate where we should start searching for keys
2. `find_keys`: Given the output of start_search and an array of args, indicate all possible indices of keys.

### start_search step specs
- `index`: specify an argument index explicitly
  - `index`: 0 based index (1 means the first command argument)
- `keyword`: specify a string to match in `argv`. We should start searching for keys just after the keyword appears.
  - `keyword`: the string to search for
  - `start_search`: an index from which to start the keyword search (can be negative, which means to search from the end)

Examples:
- `SET` has start_search of type `index` with value `1`
- `XREAD` has start_search of type `keyword` with value `[“STREAMS”,1]`
- `MIGRATE` has start_search of type `keyword` with value `[“KEYS”,-2]`

### find_keys step specs
- `range`: specify `[count, step, limit]`.
  - `lastkey`: index of the last key. relative to the index returned from begin_search. -1 indicating till the last argument, -2 one before the last
  - `step`: how many args should we skip after finding a key, in order to find the next one
  - `limit`: if count is -1, we use limit to stop the search by a factor. 0 and 1 mean no limit. 2 means ½ of the remaining args, 3 means ⅓, and so on.
- “keynum”: specify `[keynum_index, first_key_index, step]`.
  - `keynum_index`: is relative to the return of the `start_search` spec.
  - `first_key_index`: is relative to `keynum_index`.
  - `step`: how many args should we skip after finding a key, in order to find the next one

Examples:
- `SET` has `range` of `[0,1,0]`
- `MSET` has `range` of `[-1,2,0]`
- `XREAD` has `range` of `[-1,1,2]`
- `ZUNION` has `start_search` of type `index` with value `1` and `find_keys` of type `keynum` with value `[0,1,1]`
- `AI.DAGRUN` has `start_search` of type `keyword` with value `[“LOAD“,1]` and `find_keys` of type `keynum` with value
  `[0,1,1]` (see https://oss.redislabs.com/redisai/master/commands/#aidagrun)

Note: this solution is not perfect as the module writers can come up with anything, but at least we will be able to find the key
args of the vast majority of commands.
If one of the above specs can’t describe the key positions, the module writer can always fall back to the `getkeys-api` option.

Some keys cannot be found easily (`KEYS` in `MIGRATE`: Imagine the argument for `AUTH` is the string “KEYS” - we will
start searching in the wrong index). 
The guarantee is that the specs may be incomplete (`incomplete` will be specified in the spec to denote that) but we never
report false information (assuming the command syntax is correct).
For `MIGRATE` we start searching from the end - `startfrom=-1` - and if one of the keys is actually called "keys" we will
report only a subset of all keys - hence the `incomplete` flag.
Some `incomplete` specs can be completely empty (i.e. UNKNOWN begin_search) which should tell the client that
COMMAND GETKEYS (or any other way to get the keys) must be used (Example: For `SORT` there is no way to describe
the STORE keyword spec, as the word "store" can appear anywhere in the command).

We will expose these key specs in the `COMMAND` command so that clients can learn, on startup, where the keys are for
all commands instead of holding hardcoded tables or use `COMMAND GETKEYS` in runtime.

Comments:
1. Redis doesn't internally use the new specs, they are only used for COMMAND output.
2. In order to support the current COMMAND INFO format (reply array indices 4, 5, 6) we created a synthetic range, called
   legacy_range, that, if possible, is built according to the new specs.
3. Redis currently uses only getkeys_proc or the legacy_range to get the keys indices (in COMMAND GETKEYS for
   example).

"incomplete" specs:
the command we have issues with are MIGRATE, STRALGO, and SORT
for MIGRATE, because the token KEYS, if exists, must be the last token, we can search in reverse. it one of the keys is
actually the string "keys" will return just a subset of the keys (hence, it's "incomplete")
for SORT and STRALGO we can use this heuristic (the keys can be anywhere in the command) and therefore we added a
key spec that is both "incomplete" and of "unknown type"

if a client encounters an "incomplete" spec it means that it must find a different way (either COMMAND GETKEYS or have
its own parser) to retrieve the keys.
please note that all commands, apart from the three mentioned above, have "complete" key specs
parent b5a879e1
......@@ -34,6 +34,7 @@ $TCLSH tests/test_helper.tcl \
--single unit/moduleapi/getkeys \
--single unit/moduleapi/test_lazyfree \
--single unit/moduleapi/defrag \
--single unit/moduleapi/keyspecs \
--single unit/moduleapi/hash \
--single unit/moduleapi/zset \
--single unit/moduleapi/list \
......
......@@ -1225,7 +1225,7 @@ int ACLCheckCommandPerm(client *c, int *keyidxptr) {
/* Check if the user can execute commands explicitly touching the keys
* mentioned in the command arguments. */
if (!(c->user->flags & USER_FLAG_ALLKEYS) &&
(c->cmd->getkeys_proc || c->cmd->firstkey))
(c->cmd->getkeys_proc || c->cmd->key_specs_num))
{
getKeysResult result = GETKEYS_RESULT_INIT;
int numkeys = getKeysFromCommand(c->cmd,c->argv,c->argc,&result);
......
......@@ -1575,23 +1575,30 @@ int *getKeysPrepareResult(getKeysResult *result, int numkeys) {
}
/* The base case is to use the keys position as given in the command table
* (firstkey, lastkey, step). */
int getKeysUsingCommandTable(struct redisCommand *cmd,robj **argv, int argc, getKeysResult *result) {
int j, i = 0, last, *keys;
* (firstkey, lastkey, step).
* This function works only on command with the legacy_range_key_spec,
* all other commands should be handled by getkeys_proc. */
int getKeysUsingLegacyRangeSpec(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result) {
int j, i = 0, last, first, step, *keys;
UNUSED(argv);
if (cmd->firstkey == 0) {
if (cmd->legacy_range_key_spec.begin_search_type == KSPEC_BS_INVALID) {
result->numkeys = 0;
return 0;
}
last = cmd->lastkey;
first = cmd->legacy_range_key_spec.bs.index.pos;
last = cmd->legacy_range_key_spec.fk.range.lastkey;
if (last >= 0)
last += first;
step = cmd->legacy_range_key_spec.fk.range.keystep;
if (last < 0) last = argc+last;
int count = ((last - cmd->firstkey)+1);
int count = ((last - first)+1);
keys = getKeysPrepareResult(result, count);
for (j = cmd->firstkey; j <= last; j += cmd->keystep) {
for (j = first; j <= last; j += step) {
if (j >= argc) {
/* Modules commands, and standard commands with a not fixed number
* of arguments (negative arity parameter) do not have dispatch
......@@ -1629,7 +1636,7 @@ int getKeysFromCommand(struct redisCommand *cmd, robj **argv, int argc, getKeysR
} else if (!(cmd->flags & CMD_MODULE) && cmd->getkeys_proc) {
return cmd->getkeys_proc(cmd,argv,argc,result);
} else {
return getKeysUsingCommandTable(cmd,argv,argc,result);
return getKeysUsingLegacyRangeSpec(cmd,argv,argc,result);
}
}
......
......@@ -808,6 +808,26 @@ int64_t commandFlagsFromString(char *s) {
return flags;
}
/* Helper for RM_CreateCommand(). Turns a string representing keys spec
* flags into the keys spec flags used by the Redis core.
*
* It returns the set of flags, or -1 if unknown flags are found. */
int64_t commandKeySpecsFlagsFromString(const char *s) {
int count, j;
int64_t flags = 0;
sds *tokens = sdssplitlen(s,strlen(s)," ",1,&count);
for (j = 0; j < count; j++) {
char *t = tokens[j];
if (!strcasecmp(t,"write")) flags |= CMD_KEY_WRITE;
else if (!strcasecmp(t,"read")) flags |= CMD_KEY_READ;
else if (!strcasecmp(t,"incomplete")) flags |= CMD_KEY_INCOMPLETE;
else break;
}
sdsfreesplitres(tokens,count);
if (j != count) return -1; /* Some token not processed correctly. */
return flags;
}
/* Register a new command in the Redis server, that will be handled by
* calling the function pointer 'cmdfunc' using the RedisModule calling
* convention. The function returns REDISMODULE_ERR if the specified command
......@@ -882,6 +902,12 @@ int64_t commandFlagsFromString(char *s) {
* 0 for commands with no keys.
*
* This information is used by ACL, Cluster and the 'COMMAND' command.
*
* NOTE: The scheme described above serves a limited purpose and can
* only be used to find keys that exist at constant indices.
* For non-trivial key arguments, you may pass 0,0,0 and use
* RedisModule_AddCommandKeySpec (see documentation).
*
*/
int RM_CreateCommand(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc cmdfunc, const char *strflags, int firstkey, int lastkey, int keystep) {
int64_t flags = strflags ? commandFlagsFromString((char*)strflags) : 0;
......@@ -915,9 +941,26 @@ int RM_CreateCommand(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc c
cp->rediscmd->arity = -1;
cp->rediscmd->flags = flags | CMD_MODULE;
cp->rediscmd->getkeys_proc = (redisGetKeysProc*)(unsigned long)cp;
cp->rediscmd->firstkey = firstkey;
cp->rediscmd->lastkey = lastkey;
cp->rediscmd->keystep = keystep;
cp->rediscmd->key_specs_max = STATIC_KEY_SPECS_NUM;
cp->rediscmd->key_specs = cp->rediscmd->key_specs_static;
if (firstkey != 0) {
cp->rediscmd->key_specs_num = 1;
cp->rediscmd->key_specs[0].flags = 0;
cp->rediscmd->key_specs[0].begin_search_type = KSPEC_BS_INDEX;
cp->rediscmd->key_specs[0].bs.index.pos = firstkey;
cp->rediscmd->key_specs[0].find_keys_type = KSPEC_FK_RANGE;
cp->rediscmd->key_specs[0].fk.range.lastkey = lastkey < 0 ? lastkey : (lastkey-firstkey);
cp->rediscmd->key_specs[0].fk.range.keystep = keystep;
cp->rediscmd->key_specs[0].fk.range.limit = 0;
/* Copy the default range to legacy_range_key_spec */
cp->rediscmd->legacy_range_key_spec = cp->rediscmd->key_specs[0];
} else {
cp->rediscmd->key_specs_num = 0;
cp->rediscmd->legacy_range_key_spec.begin_search_type = KSPEC_BS_INVALID;
cp->rediscmd->legacy_range_key_spec.find_keys_type = KSPEC_FK_INVALID;
}
populateCommandMovableKeys(cp->rediscmd);
cp->rediscmd->microseconds = 0;
cp->rediscmd->calls = 0;
cp->rediscmd->rejected_calls = 0;
......@@ -928,6 +971,224 @@ int RM_CreateCommand(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc c
return REDISMODULE_OK;
}
void extendKeySpecsIfNeeded(struct redisCommand *cmd) {
/* We extend even if key_specs_num == key_specs_max because
* this function is called prior to adding a new spec */
if (cmd->key_specs_num < cmd->key_specs_max)
return;
cmd->key_specs_max++;
if (cmd->key_specs == cmd->key_specs_static) {
cmd->key_specs = zmalloc(sizeof(keySpec) * cmd->key_specs_max);
memcpy(cmd->key_specs, cmd->key_specs_static, sizeof(keySpec) * cmd->key_specs_num);
} else {
cmd->key_specs = zrealloc(cmd->key_specs, sizeof(keySpec) * cmd->key_specs_max);
}
}
int moduleAddCommandKeySpec(RedisModuleCtx *ctx, const char *name, const char *specflags, int *index) {
int64_t flags = specflags ? commandKeySpecsFlagsFromString(specflags) : 0;
if (flags == -1)
return REDISMODULE_ERR;
struct redisCommand *cmd = lookupCommandByCString(name);
if (cmd == NULL)
return REDISMODULE_ERR;
if (!(cmd->flags & CMD_MODULE))
return REDISMODULE_ERR;
RedisModuleCommandProxy *cp = (RedisModuleCommandProxy*)(unsigned long)cmd->getkeys_proc;
if (cp->module != ctx->module)
return REDISMODULE_ERR;
extendKeySpecsIfNeeded(cmd);
*index = cmd->key_specs_num;
cmd->key_specs[cmd->key_specs_num].begin_search_type = KSPEC_BS_INVALID;
cmd->key_specs[cmd->key_specs_num].find_keys_type = KSPEC_FK_INVALID;
cmd->key_specs[cmd->key_specs_num].flags = flags;
cmd->key_specs_num++;
return REDISMODULE_OK;
}
int moduleSetCommandKeySpecBeginSearch(RedisModuleCtx *ctx, const char *name, int index, keySpec *spec) {
struct redisCommand *cmd = lookupCommandByCString(name);
if (cmd == NULL)
return REDISMODULE_ERR;
if (!(cmd->flags & CMD_MODULE))
return REDISMODULE_ERR;
RedisModuleCommandProxy *cp = (RedisModuleCommandProxy*)(unsigned long)cmd->getkeys_proc;
if (cp->module != ctx->module)
return REDISMODULE_ERR;
if (index >= cmd->key_specs_num)
return REDISMODULE_ERR;
cmd->key_specs[index].begin_search_type = spec->begin_search_type;
cmd->key_specs[index].bs = spec->bs;
return REDISMODULE_OK;
}
int moduleSetCommandKeySpecFindKeys(RedisModuleCtx *ctx, const char *name, int index, keySpec *spec) {
struct redisCommand *cmd = lookupCommandByCString(name);
if (cmd == NULL)
return REDISMODULE_ERR;
if (!(cmd->flags & CMD_MODULE))
return REDISMODULE_ERR;
RedisModuleCommandProxy *cp = (RedisModuleCommandProxy*)(unsigned long)cmd->getkeys_proc;
if (cp->module != ctx->module)
return REDISMODULE_ERR;
if (index >= cmd->key_specs_num)
return REDISMODULE_ERR;
cmd->key_specs[index].find_keys_type = spec->find_keys_type;
cmd->key_specs[index].fk = spec->fk;
/* Refresh legacy range */
populateCommandLegacyRangeSpec(cmd);
/* Refresh movablekeys flag */
populateCommandMovableKeys(cmd);
return REDISMODULE_OK;
}
/* Key specs is a scheme that tries to describe the location
* of key arguments better than the old [first,last,step] scheme
* which is limited and doesn't fit many commands.
*
* This information is used by ACL, Cluster and the 'COMMAND' command.
*
* There are two steps to retrieve the key arguments:
*
* - begin_search (BS): in which index should we start seacrhing for keys?
* - find_keys (FK): relative to the output of BS, how can we will which args are keys?
*
* There are two types of BS:
*
* - index: key args start at a constant index
* - keyword: key args start just after a specific keyword
*
* There are two kinds of FK:
*
* - range: keys end at a specific index (or relative to the last argument)
* - keynum: there's an arg that contains the number of key args somewhere before the keys themselves
*
* This function adds a new key spec to a command, returning a unique id in `spec_id`.
* The caller must then call one of the RedisModule_SetCommandKeySpecBeginSearch* APIs
* followed by one of the RedisModule_SetCommandKeySpecFindKeys* APIs.
*
* It should be called just after RedisModule_CreateCommand.
*
* Example:
*
* if (RedisModule_CreateCommand(ctx,"kspec.smove",kspec_legacy,"",0,0,0) == REDISMODULE_ERR)
* return REDISMODULE_ERR;
*
* if (RedisModule_AddCommandKeySpec(ctx,"kspec.smove","read write",&spec_id) == REDISMODULE_ERR)
* return REDISMODULE_ERR;
* if (RedisModule_SetCommandKeySpecBeginSearchIndex(ctx,"kspec.smove",spec_id,1) == REDISMODULE_ERR)
* return REDISMODULE_ERR;
* if (RedisModule_SetCommandKeySpecFindKeysRange(ctx,"kspec.smove",spec_id,0,1,0) == REDISMODULE_ERR)
* return REDISMODULE_ERR;
*
* if (RedisModule_AddCommandKeySpec(ctx,"kspec.smove","write",&spec_id) == REDISMODULE_ERR)
* return REDISMODULE_ERR;
* if (RedisModule_SetCommandKeySpecBeginSearchIndex(ctx,"kspec.smove",spec_id,2) == REDISMODULE_ERR)
* return REDISMODULE_ERR;
* if (RedisModule_SetCommandKeySpecFindKeysRange(ctx,"kspec.smove",spec_id,0,1,0) == REDISMODULE_ERR)
* return REDISMODULE_ERR;
*
* Returns REDISMODULE_OK on success
*/
int RM_AddCommandKeySpec(RedisModuleCtx *ctx, const char *name, const char *specflags, int *spec_id) {
return moduleAddCommandKeySpec(ctx, name, specflags, spec_id);
}
/* Set a "index" key arguments spec to a command (begin_search step).
* See RedisModule_AddCommandKeySpec's doc.
*
* - `index`: The index from which we start the search for keys
*
* Returns REDISMODULE_OK */
int RM_SetCommandKeySpecBeginSearchIndex(RedisModuleCtx *ctx, const char *name, int spec_id, int index) {
keySpec spec;
spec.begin_search_type = KSPEC_BS_INDEX;
spec.bs.index.pos = index;
return moduleSetCommandKeySpecBeginSearch(ctx, name, spec_id, &spec);
}
/* Set a "keyword" key arguments spec to a command (begin_search step).
* See RedisModule_AddCommandKeySpec's doc.
*
* - `keyword`: The keyword that indicates the beginning of key args
* - `startfrom`: 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)
*
* Returns REDISMODULE_OK */
int RM_SetCommandKeySpecBeginSearchKeyword(RedisModuleCtx *ctx, const char *name, int spec_id, const char *keyword, int startfrom) {
keySpec spec;
spec.begin_search_type = KSPEC_BS_KEYWORD;
spec.bs.keyword.keyword = keyword;
spec.bs.keyword.startfrom = startfrom;
return moduleSetCommandKeySpecBeginSearch(ctx, name, spec_id, &spec);
}
/* Set a "range" key arguments spec to a command (find_keys step).
* See RedisModule_AddCommandKeySpec's doc.
*
* - `lastkey`: Relative index (to the result of the begin_search step) where the last key is.
* Can be negative, in which case it's not relative. -1 indicating till the last argument,
* -2 one before the last and so on.
* - `keystep`: How many args should we skip after finding a key, in order to find the next one.
* - `limit`: 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.
*
* Returns REDISMODULE_OK */
int RM_SetCommandKeySpecFindKeysRange(RedisModuleCtx *ctx, const char *name, int spec_id, int lastkey, int keystep, int limit) {
keySpec spec;
spec.find_keys_type = KSPEC_FK_RANGE;
spec.fk.range.lastkey = lastkey;
spec.fk.range.keystep = keystep;
spec.fk.range.limit = limit;
return moduleSetCommandKeySpecFindKeys(ctx, name, spec_id, &spec);
}
/* Set a "keynum" key arguments spec to a command (find_keys step).
* See RedisModule_AddCommandKeySpec's doc.
*
* - `keynumidx`: Relative index (to the result of the begin_search step) where the arguments that
* contains the number of keys is.
* - `firstkey`: Relative index (to the result of the begin_search step) where the first key is
* found (Usually it's just after keynumidx, so it should be keynumidx+1)
* - `keystep`: How many args should we skip after finding a key, in order to find the next one.
*
* Returns REDISMODULE_OK */
int RM_SetCommandKeySpecFindKeysKeynum(RedisModuleCtx *ctx, const char *name, int spec_id, int keynumidx, int firstkey, int keystep) {
keySpec spec;
spec.find_keys_type = KSPEC_FK_KEYNUM;
spec.fk.keynum.keynumidx = keynumidx;
spec.fk.keynum.firstkey = firstkey;
spec.fk.keynum.keystep = keystep;
return moduleSetCommandKeySpecFindKeys(ctx, name, spec_id, &spec);
}
/* --------------------------------------------------------------------------
* ## Module information and time measurement
* -------------------------------------------------------------------------- */
......@@ -9033,6 +9294,8 @@ void moduleUnregisterCommands(struct RedisModule *module) {
(void*)(unsigned long)cmd->getkeys_proc;
sds cmdname = cp->rediscmd->name;
if (cp->module == module) {
if (cp->rediscmd->key_specs != cp->rediscmd->key_specs_static)
zfree(cp->rediscmd->key_specs);
dictDelete(server.commands,cmdname);
dictDelete(server.orig_commands,cmdname);
sdsfree(cmdname);
......@@ -9516,7 +9779,7 @@ int *RM_GetCommandKeys(RedisModuleCtx *ctx, RedisModuleString **argv, int argc,
}
/* Bail out if command has no keys */
if (cmd->getkeys_proc == NULL && cmd->firstkey == 0) {
if (cmd->getkeys_proc == NULL && cmd->key_specs_num == 0) {
errno = 0;
return NULL;
}
......@@ -10072,4 +10335,9 @@ void moduleRegisterCoreAPI(void) {
REGISTER_API(DefragShouldStop);
REGISTER_API(DefragCursorSet);
REGISTER_API(DefragCursorGet);
REGISTER_API(AddCommandKeySpec);
REGISTER_API(SetCommandKeySpecBeginSearchIndex);
REGISTER_API(SetCommandKeySpecBeginSearchKeyword);
REGISTER_API(SetCommandKeySpecFindKeysRange);
REGISTER_API(SetCommandKeySpecFindKeysKeynum);
}
......@@ -826,6 +826,11 @@ 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;
REDISMODULE_API int (*RedisModule_AddCommandKeySpec)(RedisModuleCtx *ctx, const char *name, const char *specflags, int *spec_id) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_SetCommandKeySpecBeginSearchIndex)(RedisModuleCtx *ctx, const char *name, int spec_id, int index) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_SetCommandKeySpecBeginSearchKeyword)(RedisModuleCtx *ctx, const char *name, int spec_id, const char *keyword, int startfrom) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_SetCommandKeySpecFindKeysRange)(RedisModuleCtx *ctx, const char *name, int spec_id, int lastkey, int keystep, int limit) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_SetCommandKeySpecFindKeysKeynum)(RedisModuleCtx *ctx, const char *name, int spec_id, int keynumidx, int firstkey, int keystep) REDISMODULE_ATTR;
/* Experimental APIs */
#ifdef REDISMODULE_EXPERIMENTAL_API
......@@ -1132,6 +1137,11 @@ static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int
REDISMODULE_GET_API(IsSubEventSupported);
REDISMODULE_GET_API(GetServerVersion);
REDISMODULE_GET_API(GetTypeMethodVersion);
REDISMODULE_GET_API(AddCommandKeySpec);
REDISMODULE_GET_API(SetCommandKeySpecBeginSearchIndex);
REDISMODULE_GET_API(SetCommandKeySpecBeginSearchKeyword);
REDISMODULE_GET_API(SetCommandKeySpecFindKeysRange);
REDISMODULE_GET_API(SetCommandKeySpecFindKeysKeynum);
#ifdef REDISMODULE_EXPERIMENTAL_API
REDISMODULE_GET_API(GetThreadSafeContext);
......
......@@ -465,21 +465,21 @@ void sentinelConfigGetCommand(client *c);
void sentinelConfigSetCommand(client *c);
struct redisCommand sentinelcmds[] = {
{"ping",pingCommand,1,"fast @connection",0,NULL,0,0,0,0,0},
{"sentinel",sentinelCommand,-2,"admin",0,NULL,0,0,0,0,0},
{"subscribe",subscribeCommand,-2,"pub-sub",0,NULL,0,0,0,0,0},
{"unsubscribe",unsubscribeCommand,-1,"pub-sub",0,NULL,0,0,0,0,0},
{"psubscribe",psubscribeCommand,-2,"pub-sub",0,NULL,0,0,0,0,0},
{"punsubscribe",punsubscribeCommand,-1,"pub-sub",0,NULL,0,0,0,0,0},
{"publish",sentinelPublishCommand,3,"pub-sub fast",0,NULL,0,0,0,0,0},
{"info",sentinelInfoCommand,-1,"random @dangerous",0,NULL,0,0,0,0,0},
{"role",sentinelRoleCommand,1,"fast read-only @dangerous",0,NULL,0,0,0,0,0},
{"client",clientCommand,-2,"admin random @connection",0,NULL,0,0,0,0,0},
{"shutdown",shutdownCommand,-1,"admin",0,NULL,0,0,0,0,0},
{"auth",authCommand,-2,"no-auth fast @connection",0,NULL,0,0,0,0,0},
{"hello",helloCommand,-1,"no-auth fast @connection",0,NULL,0,0,0,0,0},
{"acl",aclCommand,-2,"admin",0,NULL,0,0,0,0,0,0},
{"command",commandCommand,-1, "random @connection", 0,NULL,0,0,0,0,0,0}
{"ping",pingCommand,1,"fast @connection"},
{"sentinel",sentinelCommand,-2,"admin"},
{"subscribe",subscribeCommand,-2,"pub-sub"},
{"unsubscribe",unsubscribeCommand,-1,"pub-sub"},
{"psubscribe",psubscribeCommand,-2,"pub-sub"},
{"punsubscribe",punsubscribeCommand,-1,"pub-sub"},
{"publish",sentinelPublishCommand,3,"pub-sub fast"},
{"info",sentinelInfoCommand,-1,"random @dangerous"},
{"role",sentinelRoleCommand,1,"fast read-only @dangerous"},
{"client",clientCommand,-2,"admin random @connection"},
{"shutdown",shutdownCommand,-1,"admin"},
{"auth",authCommand,-2,"no-auth fast @connection"},
{"hello",helloCommand,-1,"no-auth fast @connection"},
{"acl",aclCommand,-2,"admin"},
{"command",commandCommand,-1, "random @connection"}
};
/* this array is used for sentinel config lookup, which need to be loaded
......@@ -525,7 +525,7 @@ void initSentinel(void) {
/* Translate the command string flags description into an actual
* set of flags. */
if (populateCommandTableParseFlags(cmd,cmd->sflags) == C_ERR)
if (populateSingleCommand(cmd,cmd->sflags) == C_ERR)
serverPanic("Unsupported command flag");
}
......
......@@ -193,848 +193,1196 @@ struct redisServer server; /* Server global state */
* TYPE, EXPIRE*, PEXPIRE*, TTL, PTTL, ...
*/
struct redisCommand redisCommandTable[] = {
{"module",moduleCommand,-2,
"admin no-script",
0,NULL,0,0,0,0,0,0},
"admin no-script"},
{"get",getCommand,2,
"read-only fast @string",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"getex",getexCommand,-2,
"write fast @string",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"getdel",getdelCommand,2,
"write fast @string",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
/* Note that we can't flag set as fast, since it may perform an
* implicit DEL of a large key. */
{"set",setCommand,-3,
"write use-memory @string",
0,NULL,1,1,1,0,0,0},
{{"read write", /* "read" because of the GET token */
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"setnx",setnxCommand,3,
"write use-memory fast @string",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"setex",setexCommand,4,
"write use-memory @string",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"psetex",psetexCommand,4,
"write use-memory @string",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"append",appendCommand,3,
"write use-memory fast @string",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"strlen",strlenCommand,2,
"read-only fast @string",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"del",delCommand,-2,
"write @keyspace",
0,NULL,1,-1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={-1,1,0}}}},
{"unlink",unlinkCommand,-2,
"write fast @keyspace",
0,NULL,1,-1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={-1,1,0}}}},
{"exists",existsCommand,-2,
"read-only fast @keyspace",
0,NULL,1,-1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={-1,1,0}}}},
{"setbit",setbitCommand,4,
"write use-memory @bitmap",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"getbit",getbitCommand,3,
"read-only fast @bitmap",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"bitfield",bitfieldCommand,-2,
"write use-memory @bitmap",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"bitfield_ro",bitfieldroCommand,-2,
"read-only fast @bitmap",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"setrange",setrangeCommand,4,
"write use-memory @string",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"getrange",getrangeCommand,4,
"read-only @string",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"substr",getrangeCommand,4,
"read-only @string",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"incr",incrCommand,2,
"write use-memory fast @string",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"decr",decrCommand,2,
"write use-memory fast @string",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"mget",mgetCommand,-2,
"read-only fast @string",
0,NULL,1,-1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={-1,1,0}}}},
{"rpush",rpushCommand,-3,
"write use-memory fast @list",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"lpush",lpushCommand,-3,
"write use-memory fast @list",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"rpushx",rpushxCommand,-3,
"write use-memory fast @list",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"lpushx",lpushxCommand,-3,
"write use-memory fast @list",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"linsert",linsertCommand,5,
"write use-memory @list",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"rpop",rpopCommand,-2,
"write fast @list",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"lpop",lpopCommand,-2,
"write fast @list",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"lmpop",lmpopCommand,-4,
"write @list",
0,lmpopGetKeys,0,0,0,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_KEYNUM,.fk.keynum={0,1,1}}},
lmpopGetKeys},
{"brpop",brpopCommand,-3,
"write no-script @list @blocking",
0,NULL,1,-2,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={-2,1,0}}}},
{"brpoplpush",brpoplpushCommand,4,
"write use-memory no-script @list @blocking",
0,NULL,1,2,1,0,0,0},
{{"read write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}},
{"write",
KSPEC_BS_INDEX,.bs.index={2},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"blmove",blmoveCommand,6,
"write use-memory no-script @list @blocking",
0,NULL,1,2,1,0,0,0},
{{"read write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}},
{"write",
KSPEC_BS_INDEX,.bs.index={2},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"blpop",blpopCommand,-3,
"write no-script @list @blocking",
0,NULL,1,-2,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={-2,1,0}}}},
{"blmpop",blmpopCommand,-5,
"write @list @blocking",
0,blmpopGetKeys,0,0,0,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={2},
KSPEC_FK_KEYNUM,.fk.keynum={0,1,1}}},
blmpopGetKeys},
{"llen",llenCommand,2,
"read-only fast @list",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"lindex",lindexCommand,3,
"read-only @list",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"lset",lsetCommand,4,
"write use-memory @list",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"lrange",lrangeCommand,4,
"read-only @list",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"ltrim",ltrimCommand,4,
"write @list",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"lpos",lposCommand,-3,
"read-only @list",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"lrem",lremCommand,4,
"write @list",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"rpoplpush",rpoplpushCommand,3,
"write use-memory @list",
0,NULL,1,2,1,0,0,0},
{{"read write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}},
{"write",
KSPEC_BS_INDEX,.bs.index={2},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"lmove",lmoveCommand,5,
"write use-memory @list",
0,NULL,1,2,1,0,0,0},
{{"read write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}},
{"write",
KSPEC_BS_INDEX,.bs.index={2},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"sadd",saddCommand,-3,
"write use-memory fast @set",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"srem",sremCommand,-3,
"write fast @set",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"smove",smoveCommand,4,
"write fast @set",
0,NULL,1,2,1,0,0,0},
{{"read write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}},
{"write",
KSPEC_BS_INDEX,.bs.index={2},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"sismember",sismemberCommand,3,
"read-only fast @set",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"smismember",smismemberCommand,-3,
"read-only fast @set",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"scard",scardCommand,2,
"read-only fast @set",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"spop",spopCommand,-2,
"write random fast @set",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"srandmember",srandmemberCommand,-2,
"read-only random @set",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"sinter",sinterCommand,-2,
"read-only to-sort @set",
0,NULL,1,-1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={-1,1,0}}}},
{"sintercard",sinterCardCommand,-2,
"read-only @set",
0,NULL,1,-1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={-1,1,0}}}},
{"sinterstore",sinterstoreCommand,-3,
"write use-memory @set",
0,NULL,1,-1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}},
{"read",
KSPEC_BS_INDEX,.bs.index={2},
KSPEC_FK_RANGE,.fk.range={-1,1,0}}}},
{"sunion",sunionCommand,-2,
"read-only to-sort @set",
0,NULL,1,-1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={-1,1,0}}}},
{"sunionstore",sunionstoreCommand,-3,
"write use-memory @set",
0,NULL,1,-1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}},
{"read",
KSPEC_BS_INDEX,.bs.index={2},
KSPEC_FK_RANGE,.fk.range={-1,1,0}}}},
{"sdiff",sdiffCommand,-2,
"read-only to-sort @set",
0,NULL,1,-1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={-1,1,0}}}},
{"sdiffstore",sdiffstoreCommand,-3,
"write use-memory @set",
0,NULL,1,-1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}},
{"read",
KSPEC_BS_INDEX,.bs.index={2},
KSPEC_FK_RANGE,.fk.range={-1,1,0}}}},
{"smembers",sinterCommand,2,
"read-only to-sort @set",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"sscan",sscanCommand,-3,
"read-only random @set",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"zadd",zaddCommand,-4,
"write use-memory fast @sortedset",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"zincrby",zincrbyCommand,4,
"write use-memory fast @sortedset",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"zrem",zremCommand,-3,
"write fast @sortedset",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"zremrangebyscore",zremrangebyscoreCommand,4,
"write @sortedset",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"zremrangebyrank",zremrangebyrankCommand,4,
"write @sortedset",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"zremrangebylex",zremrangebylexCommand,4,
"write @sortedset",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"zunionstore",zunionstoreCommand,-4,
"write use-memory @sortedset",
0,zunionInterDiffStoreGetKeys,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}},
{"read",
KSPEC_BS_INDEX,.bs.index={2},
KSPEC_FK_KEYNUM,.fk.keynum={0,1,1}}},
zunionInterDiffStoreGetKeys},
{"zinterstore",zinterstoreCommand,-4,
"write use-memory @sortedset",
0,zunionInterDiffStoreGetKeys,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}},
{"read",
KSPEC_BS_INDEX,.bs.index={2},
KSPEC_FK_KEYNUM,.fk.keynum={0,1,1}}},
zunionInterDiffStoreGetKeys},
{"zdiffstore",zdiffstoreCommand,-4,
"write use-memory @sortedset",
0,zunionInterDiffStoreGetKeys,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}},
{"read",
KSPEC_BS_INDEX,.bs.index={2},
KSPEC_FK_KEYNUM,.fk.keynum={0,1,1}}},
zunionInterDiffStoreGetKeys},
{"zunion",zunionCommand,-3,
"read-only @sortedset",
0,zunionInterDiffGetKeys,0,0,0,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_KEYNUM,.fk.keynum={0,1,1}}},
zunionInterDiffGetKeys},
{"zinter",zinterCommand,-3,
"read-only @sortedset",
0,zunionInterDiffGetKeys,0,0,0,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_KEYNUM,.fk.keynum={0,1,1}}},
zunionInterDiffGetKeys},
{"zintercard",zinterCardCommand,-3,
"read-only @sortedset",
0,zunionInterDiffGetKeys,0,0,0,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_KEYNUM,.fk.keynum={0,1,1}}},
zunionInterDiffGetKeys},
{"zdiff",zdiffCommand,-3,
"read-only @sortedset",
0,zunionInterDiffGetKeys,0,0,0,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_KEYNUM,.fk.keynum={0,1,1}}},
zunionInterDiffGetKeys},
{"zrange",zrangeCommand,-4,
"read-only @sortedset",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"zrangestore",zrangestoreCommand,-5,
"write use-memory @sortedset",
0,NULL,1,2,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}},
{"read",
KSPEC_BS_INDEX,.bs.index={2},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"zrangebyscore",zrangebyscoreCommand,-4,
"read-only @sortedset",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"zrevrangebyscore",zrevrangebyscoreCommand,-4,
"read-only @sortedset",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"zrangebylex",zrangebylexCommand,-4,
"read-only @sortedset",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"zrevrangebylex",zrevrangebylexCommand,-4,
"read-only @sortedset",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"zcount",zcountCommand,4,
"read-only fast @sortedset",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"zlexcount",zlexcountCommand,4,
"read-only fast @sortedset",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"zrevrange",zrevrangeCommand,-4,
"read-only @sortedset",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"zcard",zcardCommand,2,
"read-only fast @sortedset",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"zscore",zscoreCommand,3,
"read-only fast @sortedset",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"zmscore",zmscoreCommand,-3,
"read-only fast @sortedset",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"zrank",zrankCommand,3,
"read-only fast @sortedset",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"zrevrank",zrevrankCommand,3,
"read-only fast @sortedset",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"zscan",zscanCommand,-3,
"read-only random @sortedset",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"zpopmin",zpopminCommand,-2,
"write fast @sortedset",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"zpopmax",zpopmaxCommand,-2,
"write fast @sortedset",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"bzpopmin",bzpopminCommand,-3,
"write no-script fast @sortedset @blocking",
0,NULL,1,-2,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={-2,1,0}}}},
{"bzpopmax",bzpopmaxCommand,-3,
"write no-script fast @sortedset @blocking",
0,NULL,1,-2,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={-2,1,0}}}},
{"zrandmember",zrandmemberCommand,-2,
"read-only random @sortedset",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"hset",hsetCommand,-4,
"write use-memory fast @hash",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"hsetnx",hsetnxCommand,4,
"write use-memory fast @hash",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"hget",hgetCommand,3,
"read-only fast @hash",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"hmset",hsetCommand,-4,
"write use-memory fast @hash",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"hmget",hmgetCommand,-3,
"read-only fast @hash",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"hincrby",hincrbyCommand,4,
"write use-memory fast @hash",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"hincrbyfloat",hincrbyfloatCommand,4,
"write use-memory fast @hash",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"hdel",hdelCommand,-3,
"write fast @hash",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"hlen",hlenCommand,2,
"read-only fast @hash",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"hstrlen",hstrlenCommand,3,
"read-only fast @hash",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"hkeys",hkeysCommand,2,
"read-only to-sort @hash",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"hvals",hvalsCommand,2,
"read-only to-sort @hash",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"hgetall",hgetallCommand,2,
"read-only random @hash",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"hexists",hexistsCommand,3,
"read-only fast @hash",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"hrandfield",hrandfieldCommand,-2,
"read-only random @hash",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"hscan",hscanCommand,-3,
"read-only random @hash",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"incrby",incrbyCommand,3,
"write use-memory fast @string",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"decrby",decrbyCommand,3,
"write use-memory fast @string",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"incrbyfloat",incrbyfloatCommand,3,
"write use-memory fast @string",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"getset",getsetCommand,3,
"write use-memory fast @string",
0,NULL,1,1,1,0,0,0},
{{"read write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"mset",msetCommand,-3,
"write use-memory @string",
0,NULL,1,-1,2,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={-1,2,0}}}},
{"msetnx",msetnxCommand,-3,
"write use-memory @string",
0,NULL,1,-1,2,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={-1,2,0}}}},
{"randomkey",randomkeyCommand,1,
"read-only random @keyspace",
0,NULL,0,0,0,0,0,0},
"read-only random @keyspace"},
{"select",selectCommand,2,
"ok-loading fast ok-stale @connection",
0,NULL,0,0,0,0,0,0},
"ok-loading fast ok-stale @connection"},
{"swapdb",swapdbCommand,3,
"write fast @keyspace @dangerous",
0,NULL,0,0,0,0,0,0},
"write fast @keyspace @dangerous"},
{"move",moveCommand,3,
"write fast @keyspace",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"copy",copyCommand,-3,
"write use-memory @keyspace",
0,NULL,1,2,1,0,0,0},
/* Like for SET, we can't mark rename as a fast command because
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}},
{"write",
KSPEC_BS_INDEX,.bs.index={2},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
/* Like for SET, we can't mark RENAME as a fast command because
* overwriting the target key may result in an implicit slow DEL. */
{"rename",renameCommand,3,
"write @keyspace",
0,NULL,1,2,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={1,1,0}}}},
{"renamenx",renamenxCommand,3,
"write fast @keyspace",
0,NULL,1,2,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={1,1,0}}}},
{"expire",expireCommand,-3,
"write fast @keyspace",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"expireat",expireatCommand,-3,
"write fast @keyspace",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"pexpire",pexpireCommand,-3,
"write fast @keyspace",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"pexpireat",pexpireatCommand,-3,
"write fast @keyspace",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"keys",keysCommand,2,
"read-only to-sort @keyspace @dangerous",
0,NULL,0,0,0,0,0,0},
"read-only to-sort @keyspace @dangerous"},
{"scan",scanCommand,-2,
"read-only random @keyspace",
0,NULL,0,0,0,0,0,0},
"read-only random @keyspace"},
{"dbsize",dbsizeCommand,1,
"read-only fast @keyspace",
0,NULL,0,0,0,0,0,0},
"read-only fast @keyspace"},
{"auth",authCommand,-2,
"no-auth no-script ok-loading ok-stale fast @connection",
0,NULL,0,0,0,0,0,0},
"no-auth no-script ok-loading ok-stale fast @connection"},
/* We don't allow PING during loading since in Redis PING is used as
* failure detection, and a loading server is considered to be
* not available. */
{"ping",pingCommand,-1,
"ok-stale fast @connection",
0,NULL,0,0,0,0,0,0},
"ok-stale fast @connection"},
{"echo",echoCommand,2,
"fast @connection",
0,NULL,0,0,0,0,0,0},
"fast @connection"},
{"save",saveCommand,1,
"admin no-script",
0,NULL,0,0,0,0,0,0},
"admin no-script"},
{"bgsave",bgsaveCommand,-1,
"admin no-script",
0,NULL,0,0,0,0,0,0},
"admin no-script"},
{"bgrewriteaof",bgrewriteaofCommand,1,
"admin no-script",
0,NULL,0,0,0,0,0,0},
"admin no-script"},
{"shutdown",shutdownCommand,-1,
"admin no-script ok-loading ok-stale",
0,NULL,0,0,0,0,0,0},
"admin no-script ok-loading ok-stale"},
{"lastsave",lastsaveCommand,1,
"random fast ok-loading ok-stale @admin @dangerous",
0,NULL,0,0,0,0,0,0},
"random fast ok-loading ok-stale @admin @dangerous"},
{"type",typeCommand,2,
"read-only fast @keyspace",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"multi",multiCommand,1,
"no-script fast ok-loading ok-stale @transaction",
0,NULL,0,0,0,0,0,0},
"no-script fast ok-loading ok-stale @transaction"},
{"exec",execCommand,1,
"no-script no-slowlog ok-loading ok-stale @transaction",
0,NULL,0,0,0,0,0,0},
"no-script no-slowlog ok-loading ok-stale @transaction"},
{"discard",discardCommand,1,
"no-script fast ok-loading ok-stale @transaction",
0,NULL,0,0,0,0,0,0},
"no-script fast ok-loading ok-stale @transaction"},
{"sync",syncCommand,1,
"admin no-script",
0,NULL,0,0,0,0,0,0},
"admin no-script"},
{"psync",syncCommand,-3,
"admin no-script",
0,NULL,0,0,0,0,0,0},
"admin no-script"},
{"replconf",replconfCommand,-1,
"admin no-script ok-loading ok-stale",
0,NULL,0,0,0,0,0,0},
"admin no-script ok-loading ok-stale"},
{"flushdb",flushdbCommand,-1,
"write @keyspace @dangerous",
0,NULL,0,0,0,0,0,0},
"write @keyspace @dangerous"},
{"flushall",flushallCommand,-1,
"write @keyspace @dangerous",
0,NULL,0,0,0,0,0,0},
"write @keyspace @dangerous"},
{"sort",sortCommand,-2,
"write use-memory @list @set @sortedset @dangerous",
0,sortGetKeys,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}},
{"write incomplete", /* We can't use "keyword" here because we may give false information. */
KSPEC_BS_UNKNOWN,{{0}},
KSPEC_FK_UNKNOWN,{{0}}}},
sortGetKeys},
{"sort_ro",sortroCommand,-2,
"read-only @list @set @sortedset @dangerous",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"info",infoCommand,-1,
"ok-loading ok-stale random @dangerous",
0,NULL,0,0,0,0,0,0},
"ok-loading ok-stale random @dangerous"},
{"monitor",monitorCommand,1,
"admin no-script ok-loading ok-stale",
0,NULL,0,0,0,0,0,0},
"admin no-script ok-loading ok-stale"},
{"ttl",ttlCommand,2,
"read-only fast random @keyspace",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"touch",touchCommand,-2,
"read-only fast @keyspace",
0,NULL,1,-1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={-1,1,0}}}},
{"pttl",pttlCommand,2,
"read-only fast random @keyspace",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"expiretime",expiretimeCommand,2,
"read-only fast random @keyspace",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"pexpiretime",pexpiretimeCommand,2,
"read-only fast random @keyspace",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"persist",persistCommand,2,
"write fast @keyspace",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"slaveof",replicaofCommand,3,
"admin no-script ok-stale",
0,NULL,0,0,0,0,0,0},
"admin no-script ok-stale"},
{"replicaof",replicaofCommand,3,
"admin no-script ok-stale",
0,NULL,0,0,0,0,0,0},
"admin no-script ok-stale"},
{"role",roleCommand,1,
"ok-loading ok-stale no-script fast @admin @dangerous",
0,NULL,0,0,0,0,0,0},
"ok-loading ok-stale no-script fast @admin @dangerous"},
{"debug",debugCommand,-2,
"admin no-script ok-loading ok-stale",
0,NULL,0,0,0,0,0,0},
"admin no-script ok-loading ok-stale"},
{"config",configCommand,-2,
"admin ok-loading ok-stale no-script",
0,NULL,0,0,0,0,0,0},
"admin ok-loading ok-stale no-script"},
{"subscribe",subscribeCommand,-2,
"pub-sub no-script ok-loading ok-stale",
0,NULL,0,0,0,0,0,0},
"pub-sub no-script ok-loading ok-stale"},
{"unsubscribe",unsubscribeCommand,-1,
"pub-sub no-script ok-loading ok-stale",
0,NULL,0,0,0,0,0,0},
"pub-sub no-script ok-loading ok-stale"},
{"psubscribe",psubscribeCommand,-2,
"pub-sub no-script ok-loading ok-stale",
0,NULL,0,0,0,0,0,0},
"pub-sub no-script ok-loading ok-stale"},
{"punsubscribe",punsubscribeCommand,-1,
"pub-sub no-script ok-loading ok-stale",
0,NULL,0,0,0,0,0,0},
"pub-sub no-script ok-loading ok-stale"},
{"publish",publishCommand,3,
"pub-sub ok-loading ok-stale fast may-replicate",
0,NULL,0,0,0,0,0,0},
"pub-sub ok-loading ok-stale fast may-replicate"},
{"pubsub",pubsubCommand,-2,
"pub-sub ok-loading ok-stale random",
0,NULL,0,0,0,0,0,0},
"pub-sub ok-loading ok-stale random"},
{"watch",watchCommand,-2,
"no-script fast ok-loading ok-stale @transaction",
0,NULL,1,-1,1,0,0,0},
{{"",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={-1,1,0}}}},
{"unwatch",unwatchCommand,1,
"no-script fast ok-loading ok-stale @transaction",
0,NULL,0,0,0,0,0,0},
"no-script fast ok-loading ok-stale @transaction"},
{"cluster",clusterCommand,-2,
"admin ok-stale random",
0,NULL,0,0,0,0,0,0},
"admin ok-stale random"},
{"restore",restoreCommand,-4,
"write use-memory @keyspace @dangerous",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"restore-asking",restoreCommand,-4,
"write use-memory cluster-asking @keyspace @dangerous",
0,NULL,1,1,1,0,0,0},
"write use-memory cluster-asking @keyspace @dangerous",
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"migrate",migrateCommand,-6,
"write random @keyspace @dangerous",
0,migrateGetKeys,3,3,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={3},
KSPEC_FK_RANGE,.fk.range={0,1,0}},
{"write incomplete",
KSPEC_BS_KEYWORD,.bs.keyword={"KEYS",-2},
KSPEC_FK_RANGE,.fk.range={-1,1,0}}},
migrateGetKeys},
{"asking",askingCommand,1,
"fast @connection",
0,NULL,0,0,0,0,0,0},
"fast @connection"},
{"readonly",readonlyCommand,1,
"fast @connection",
0,NULL,0,0,0,0,0,0},
"fast @connection"},
{"readwrite",readwriteCommand,1,
"fast @connection",
0,NULL,0,0,0,0,0,0},
"fast @connection"},
{"dump",dumpCommand,2,
"read-only random @keyspace",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"object",objectCommand,-2,
"read-only random @keyspace",
0,NULL,2,2,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={2},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"memory",memoryCommand,-2,
"random read-only",
0,memoryGetKeys,0,0,0,0,0,0},
{{"read",
KSPEC_BS_KEYWORD,.bs.keyword={"USAGE",1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}},
memoryGetKeys},
{"client",clientCommand,-2,
"admin no-script random ok-loading ok-stale @connection",
0,NULL,0,0,0,0,0,0},
"admin no-script random ok-loading ok-stale @connection"},
{"hello",helloCommand,-1,
"no-auth no-script fast ok-loading ok-stale @connection",
0,NULL,0,0,0,0,0,0},
"no-auth no-script fast ok-loading ok-stale @connection"},
/* EVAL can modify the dataset, however it is not flagged as a write
* command since we do the check while running commands from Lua.
*
*
* EVAL and EVALSHA also feed monitors before the commands are executed,
* as opposed to after.
*/
{"eval",evalCommand,-3,
"no-script no-monitor may-replicate @scripting",
0,evalGetKeys,0,0,0,0,0,0},
{{"read write", /* We pass both read and write because these flag are worst-case-scenario */
KSPEC_BS_INDEX,.bs.index={2},
KSPEC_FK_KEYNUM,.fk.keynum={0,1,1}}},
evalGetKeys},
{"eval_ro",evalRoCommand,-3,
"no-script no-monitor @scripting",
0,evalGetKeys,0,0,0,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={2},
KSPEC_FK_KEYNUM,.fk.keynum={0,1,1}}},
evalGetKeys},
{"evalsha",evalShaCommand,-3,
"no-script no-monitor may-replicate @scripting",
0,evalGetKeys,0,0,0,0,0,0},
{{"read write", /* We pass both read and write because these flag are worst-case-scenario */
KSPEC_BS_INDEX,.bs.index={2},
KSPEC_FK_KEYNUM,.fk.keynum={0,1,1}}},
evalGetKeys},
{"evalsha_ro",evalShaRoCommand,-3,
"no-script no-monitor @scripting",
0,evalGetKeys,0,0,0,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={2},
KSPEC_FK_KEYNUM,.fk.keynum={0,1,1}}},
evalGetKeys},
{"slowlog",slowlogCommand,-2,
"admin random ok-loading ok-stale",
0,NULL,0,0,0,0,0,0},
"admin random ok-loading ok-stale"},
{"script",scriptCommand,-2,
"no-script may-replicate @scripting",
0,NULL,0,0,0,0,0,0},
"no-script may-replicate @scripting"},
{"time",timeCommand,1,
"random fast ok-loading ok-stale",
0,NULL,0,0,0,0,0,0},
"random fast ok-loading ok-stale"},
{"bitop",bitopCommand,-4,
"write use-memory @bitmap",
0,NULL,2,-1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={2},
KSPEC_FK_RANGE,.fk.range={0,1,0}},
{"read",
KSPEC_BS_INDEX,.bs.index={3},
KSPEC_FK_RANGE,.fk.range={-1,1,0}}}},
{"bitcount",bitcountCommand,-2,
"read-only @bitmap",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"bitpos",bitposCommand,-3,
"read-only @bitmap",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"wait",waitCommand,3,
"no-script @connection",
0,NULL,0,0,0,0,0,0},
"no-script @connection"},
{"command",commandCommand,-1,
"ok-loading ok-stale random @connection",
0,NULL,0,0,0,0,0,0},
"ok-loading ok-stale random @connection"},
{"geoadd",geoaddCommand,-5,
"write use-memory @geo",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
/* GEORADIUS has store options that may write. */
{"georadius",georadiusCommand,-6,
"write use-memory @geo",
0,georadiusGetKeys,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}},
{"write",
KSPEC_BS_KEYWORD,.bs.keyword={"STORE",6},
KSPEC_FK_RANGE,.fk.range={0,1,0}},
{"write",
KSPEC_BS_KEYWORD,.bs.keyword={"STOREDIST",6},
KSPEC_FK_RANGE,.fk.range={0,1,0}}},
georadiusGetKeys},
{"georadius_ro",georadiusroCommand,-6,
"read-only @geo",
0,NULL,1,1,1,0,0,0},
{"georadiusbymember",georadiusbymemberCommand,-5,
"write use-memory @geo",
0,georadiusGetKeys,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"georadiusbymember",georadiusbymemberCommand,-5,"write use-memory @geo",
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}},
{"write",
KSPEC_BS_KEYWORD,.bs.keyword={"STORE",5},
KSPEC_FK_RANGE,.fk.range={0,1,0}},
{"write",
KSPEC_BS_KEYWORD,.bs.keyword={"STOREDIST",5},
KSPEC_FK_RANGE,.fk.range={0,1,0}}},
georadiusGetKeys},
{"georadiusbymember_ro",georadiusbymemberroCommand,-5,
"read-only @geo",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"geohash",geohashCommand,-2,
"read-only @geo",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"geopos",geoposCommand,-2,
"read-only @geo",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"geodist",geodistCommand,-4,
"read-only @geo",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"geosearch",geosearchCommand,-7,
"read-only @geo",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"geosearchstore",geosearchstoreCommand,-8,
"write use-memory @geo",
0,NULL,1,2,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}},
{"read",
KSPEC_BS_INDEX,.bs.index={2},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"pfselftest",pfselftestCommand,1,
"admin @hyperloglog",
0,NULL,0,0,0,0,0,0},
"admin @hyperloglog"},
{"pfadd",pfaddCommand,-2,
"write use-memory fast @hyperloglog",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
/* Technically speaking PFCOUNT may change the key since it changes the
* final bytes in the HyperLogLog representation. However in this case
......@@ -1042,110 +1390,147 @@ struct redisCommand redisCommandTable[] = {
* affair, and the command is semantically read only. */
{"pfcount",pfcountCommand,-2,
"read-only may-replicate @hyperloglog",
0,NULL,1,-1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={-1,1,0}}}},
{"pfmerge",pfmergeCommand,-2,
"write use-memory @hyperloglog",
0,NULL,1,-1,1,0,0,0},
{{"read write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}},
{"read",
KSPEC_BS_INDEX,.bs.index={2},
KSPEC_FK_RANGE,.fk.range={-1,1,0}}}},
/* Unlike PFCOUNT that is considered as a read-only command (although
* it changes a bit), PFDEBUG may change the entire key when converting
* from sparse to dense representation */
{"pfdebug",pfdebugCommand,-3,
"admin write use-memory @hyperloglog",
0,NULL,2,2,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={2},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"xadd",xaddCommand,-5,
"write use-memory fast random @stream",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"xrange",xrangeCommand,-4,
"read-only @stream",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"xrevrange",xrevrangeCommand,-4,
"read-only @stream",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"xlen",xlenCommand,2,
"read-only fast @stream",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"xread",xreadCommand,-4,
"read-only @stream @blocking",
0,xreadGetKeys,0,0,0,0,0,0},
{{"read",
KSPEC_BS_KEYWORD,.bs.keyword={"STREAMS",1},
KSPEC_FK_RANGE,.fk.range={-1,1,2}}},
xreadGetKeys},
{"xreadgroup",xreadCommand,-7,
"write @stream @blocking",
0,xreadGetKeys,0,0,0,0,0,0},
{{"read",
KSPEC_BS_KEYWORD,.bs.keyword={"STREAMS",4},
KSPEC_FK_RANGE,.fk.range={-1,1,2}}},
xreadGetKeys},
{"xgroup",xgroupCommand,-2,
"write use-memory @stream",
0,NULL,2,2,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={2},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"xsetid",xsetidCommand,3,
"write use-memory fast @stream",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"xack",xackCommand,-4,
"write fast random @stream",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"xpending",xpendingCommand,-3,
"read-only random @stream",
0,NULL,1,1,1,0,0,0},
{{"read",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"xclaim",xclaimCommand,-6,
"write random fast @stream",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"xautoclaim",xautoclaimCommand,-6,
"write random fast @stream",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"xinfo",xinfoCommand,-2,
"read-only random @stream",
0,NULL,2,2,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={2},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"xdel",xdelCommand,-3,
"write fast @stream",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"xtrim",xtrimCommand,-4,
"write random @stream",
0,NULL,1,1,1,0,0,0},
{{"write",
KSPEC_BS_INDEX,.bs.index={1},
KSPEC_FK_RANGE,.fk.range={0,1,0}}}},
{"post",securityWarningCommand,-1,
"ok-loading ok-stale read-only",
0,NULL,0,0,0,0,0,0},
"ok-loading ok-stale read-only"},
{"host:",securityWarningCommand,-1,
"ok-loading ok-stale read-only",
0,NULL,0,0,0,0,0,0},
"ok-loading ok-stale read-only"},
{"latency",latencyCommand,-2,
"admin no-script ok-loading ok-stale",
0,NULL,0,0,0,0,0,0},
"admin no-script ok-loading ok-stale"},
{"lolwut",lolwutCommand,-1,
"read-only fast",
0,NULL,0,0,0,0,0,0},
"read-only fast"},
{"acl",aclCommand,-2,
"admin no-script ok-loading ok-stale",
0,NULL,0,0,0,0,0,0},
"admin no-script ok-loading ok-stale"},
{"stralgo",stralgoCommand,-2,
"read-only @string",
0,lcsGetKeys,0,0,0,0,0,0},
{{"read incomplete", /* We can't use "keyword" here because we may give false information. */
KSPEC_BS_UNKNOWN,{{0}},
KSPEC_FK_UNKNOWN,{{0}}}},
lcsGetKeys},
{"reset",resetCommand,1,
"no-script ok-stale ok-loading fast @connection",
0,NULL,0,0,0,0,0,0},
"no-script ok-stale ok-loading fast @connection"},
{"failover",failoverCommand,-1,
"admin no-script ok-stale",
0,NULL,0,0,0,0,0,0}
"admin no-script ok-stale"}
};
/*============================ Utility functions ============================ */
......@@ -3398,10 +3783,85 @@ void InitServerLast() {
server.initial_memory_usage = zmalloc_used_memory();
}
/* The purpose of this function is to try to "glue" consecutive range
* key specs in order to build the legacy (first,last,step) spec
* used by the COMMAND command.
* By far the most common case is just one range spec (e.g. SET)
* but some commands' ranges were split into two or more ranges
* in order to have different flags for different keys (e.g. SMOVE,
* first key is "read write", second key is "write").
*
* This functions uses very basic heuristics and is "best effort":
* 1. Only commands which have only "range" specs are considered.
* 2. Only range specs with keystep of 1 are considered.
* 3. The order of the range specs must be ascending (i.e.
* lastkey of spec[i] == firstkey-1 of spec[i+1]).
*
* This function will succeed on all native Redis commands and may
* fail on module commands, even if it only has "range" specs that
* could actually be "glued", in the following cases:
* 1. The order of "range" specs is not ascending (e.g. the spec for
* the key at index 2 was added before the spec of the key at
* index 1).
* 2. The "range" specs have keystep >1.
*
* If this functions fails it means that the legacy (first,last,step)
* spec used by COMMAND will show 0,0,0. This is not a dire situation
* because anyway the legacy (first,last,step) spec is to be dperecated
* and one should use the new key specs scheme.
*/
void populateCommandLegacyRangeSpec(struct redisCommand *c) {
memset(&c->legacy_range_key_spec, 0, sizeof(c->legacy_range_key_spec));
if (c->key_specs_num == 0)
return;
if (c->key_specs_num == 1 &&
c->key_specs[0].begin_search_type == KSPEC_BS_INDEX &&
c->key_specs[0].find_keys_type == KSPEC_FK_RANGE)
{
/* Quick win */
c->legacy_range_key_spec = c->key_specs[0];
return;
}
int firstkey = INT_MAX, lastkey = 0;
int prev_lastkey = 0;
for (int i = 0; i < c->key_specs_num; i++) {
if (c->key_specs[i].begin_search_type != KSPEC_BS_INDEX ||
c->key_specs[i].find_keys_type != KSPEC_FK_RANGE)
continue;
if (c->key_specs[i].fk.range.keystep != 1)
return;
if (prev_lastkey && prev_lastkey != c->key_specs[i].bs.index.pos-1)
return;
firstkey = min(firstkey, c->key_specs[i].bs.index.pos);
/* Get the absolute index for lastkey (in the "range" spec, lastkey is relative to firstkey) */
int lastkey_abs_index = c->key_specs[i].fk.range.lastkey;
if (lastkey_abs_index >= 0)
lastkey_abs_index += c->key_specs[i].bs.index.pos;
/* For lastkey we use unsigned comparison to handle negative values correctly */
lastkey = max((unsigned)lastkey, (unsigned)lastkey_abs_index);
}
if (firstkey == INT_MAX)
return;
serverAssert(firstkey != 0);
serverAssert(lastkey != 0);
c->legacy_range_key_spec.begin_search_type = KSPEC_BS_INDEX;
c->legacy_range_key_spec.bs.index.pos = firstkey;
c->legacy_range_key_spec.find_keys_type = KSPEC_FK_RANGE;
c->legacy_range_key_spec.fk.range.lastkey = lastkey < 0 ? lastkey : (lastkey-firstkey); /* in the "range" spec, lastkey is relative to firstkey */
c->legacy_range_key_spec.fk.range.keystep = 1;
c->legacy_range_key_spec.fk.range.limit = 0;
}
/* Parse the flags string description 'strflags' and set them to the
* command 'c'. If the flags are all valid C_OK is returned, otherwise
* C_ERR is returned (yet the recognized flags are set in the command). */
int populateCommandTableParseFlags(struct redisCommand *c, char *strflags) {
int populateSingleCommand(struct redisCommand *c, char *strflags) {
int argc;
sds *argv;
......@@ -3460,6 +3920,43 @@ int populateCommandTableParseFlags(struct redisCommand *c, char *strflags) {
if (!(c->flags & CMD_CATEGORY_FAST)) c->flags |= CMD_CATEGORY_SLOW;
sdsfreesplitres(argv,argc);
/* Now handle the key arguments spec flags */
/* Redis commands don't need more args than STATIC_KEY_SPECS_NUM (Number of keys
* specs can be greater than STATIC_KEY_SPECS_NUM only for module commands) */
c->key_specs = c->key_specs_static;
c->key_specs_max = STATIC_KEY_SPECS_NUM;
for (int i = 0; i < STATIC_KEY_SPECS_NUM; i++) {
if (c->key_specs[i].begin_search_type == KSPEC_BS_INVALID)
continue;
/* Split the line into arguments for processing. */
argv = sdssplitargs(c->key_specs[i].sflags,&argc);
if (argv == NULL)
return C_ERR;
for (int j = 0; j < argc; j++) {
char *flag = argv[j];
if (!strcasecmp(flag,"write")) {
c->key_specs[i].flags |= CMD_KEY_WRITE;
} else if (!strcasecmp(flag,"read")) {
c->key_specs[i].flags |= CMD_KEY_READ;
} else if (!strcasecmp(flag,"incomplete")) {
c->key_specs[i].flags |= CMD_KEY_INCOMPLETE;
}
}
c->key_specs_num++;
sdsfreesplitres(argv,argc);
}
populateCommandLegacyRangeSpec(c);
/* Handle the "movablekeys" flag (must be done after populating all key specs). */
populateCommandMovableKeys(c);
return C_OK;
}
......@@ -3475,8 +3972,8 @@ void populateCommandTable(void) {
/* Translate the command string flags description into an actual
* set of flags. */
if (populateCommandTableParseFlags(c,c->sflags) == C_ERR)
serverPanic("Unsupported command flag");
if (populateSingleCommand(c,c->sflags) == C_ERR)
serverPanic("Unsupported command flag or key spec flag");
c->id = ACLGetCommandID(c->name); /* Assign the ID used for ACL. */
retval1 = dictAdd(server.commands, sdsnew(c->name), c);
......@@ -3963,11 +4460,31 @@ void rejectCommandFormat(client *c, const char *fmt, ...) {
}
}
/* Returns 1 for commands that may have key names in their arguments, but have
* no pre-determined key positions. */
static int cmdHasMovableKeys(struct redisCommand *cmd) {
return (cmd->getkeys_proc && !(cmd->flags & CMD_MODULE)) ||
cmd->flags & CMD_MODULE_GETKEYS;
/* Returns 1 for commands that may have key names in their arguments, but the legacy range
* spec doesn't cover all of them. */
void populateCommandMovableKeys(struct redisCommand *cmd) {
int movablekeys = 0;
if (cmd->getkeys_proc && !(cmd->flags & CMD_MODULE)) {
/* Redis command with getkeys proc */
movablekeys = 1;
} else if (cmd->flags & CMD_MODULE_GETKEYS) {
/* Module command with getkeys proc */
movablekeys = 1;
} else {
/* Redis command without getkeys proc, but possibly has
* movable keys because of a keys spec. */
for (int i = 0; i < cmd->key_specs_num; i++) {
if (cmd->key_specs[i].begin_search_type != KSPEC_BS_INDEX ||
cmd->key_specs[i].find_keys_type != KSPEC_FK_RANGE)
{
/* If we have a non-range spec it means we have movable keys */
movablekeys = 1;
break;
}
}
}
cmd->movablekeys = movablekeys;
}
/* If this function gets called we already read a whole
......@@ -4084,7 +4601,7 @@ int processCommand(client *c) {
!(c->flags & CLIENT_MASTER) &&
!(c->flags & CLIENT_LUA &&
server.lua_caller->flags & CLIENT_MASTER) &&
!(!cmdHasMovableKeys(c->cmd) && c->cmd->firstkey == 0 &&
!(!c->cmd->movablekeys && c->cmd->key_specs_num == 0 &&
c->cmd->proc != execCommand))
{
int hashslot;
......@@ -4491,53 +5008,160 @@ void timeCommand(client *c) {
}
/* Helper function for addReplyCommand() to output flags. */
int addReplyCommandFlag(client *c, struct redisCommand *cmd, int f, char *reply) {
if (cmd->flags & f) {
int addReplyCommandFlag(client *c, uint64_t flags, uint64_t f, char *reply) {
if (flags & f) {
addReplyStatus(c, reply);
return 1;
}
return 0;
}
void addReplyFlagsForCommand(client *c, struct redisCommand *cmd) {
int flagcount = 0;
void *flaglen = addReplyDeferredLen(c);
flagcount += addReplyCommandFlag(c,cmd->flags,CMD_WRITE, "write");
flagcount += addReplyCommandFlag(c,cmd->flags,CMD_READONLY, "readonly");
flagcount += addReplyCommandFlag(c,cmd->flags,CMD_DENYOOM, "denyoom");
flagcount += addReplyCommandFlag(c,cmd->flags,CMD_ADMIN, "admin");
flagcount += addReplyCommandFlag(c,cmd->flags,CMD_PUBSUB, "pubsub");
flagcount += addReplyCommandFlag(c,cmd->flags,CMD_NOSCRIPT, "noscript");
flagcount += addReplyCommandFlag(c,cmd->flags,CMD_RANDOM, "random");
flagcount += addReplyCommandFlag(c,cmd->flags,CMD_SORT_FOR_SCRIPT,"sort_for_script");
flagcount += addReplyCommandFlag(c,cmd->flags,CMD_LOADING, "loading");
flagcount += addReplyCommandFlag(c,cmd->flags,CMD_STALE, "stale");
flagcount += addReplyCommandFlag(c,cmd->flags,CMD_SKIP_MONITOR, "skip_monitor");
flagcount += addReplyCommandFlag(c,cmd->flags,CMD_SKIP_SLOWLOG, "skip_slowlog");
flagcount += addReplyCommandFlag(c,cmd->flags,CMD_ASKING, "asking");
flagcount += addReplyCommandFlag(c,cmd->flags,CMD_FAST, "fast");
flagcount += addReplyCommandFlag(c,cmd->flags,CMD_NO_AUTH, "no_auth");
flagcount += addReplyCommandFlag(c,cmd->flags,CMD_MAY_REPLICATE, "may_replicate");
if (cmd->movablekeys) {
addReplyStatus(c, "movablekeys");
flagcount += 1;
}
setDeferredSetLen(c, flaglen, flagcount);
}
void addReplyFlagsForKeyArgs(client *c, uint64_t flags) {
int flagcount = 0;
void *flaglen = addReplyDeferredLen(c);
flagcount += addReplyCommandFlag(c,flags,CMD_KEY_WRITE, "write");
flagcount += addReplyCommandFlag(c,flags,CMD_KEY_READ, "read");
flagcount += addReplyCommandFlag(c,flags,CMD_KEY_INCOMPLETE, "incomplete");
setDeferredSetLen(c, flaglen, flagcount);
}
void addReplyCommandKeyArgs(client *c, struct redisCommand *cmd) {
addReplySetLen(c, cmd->key_specs_num);
for (int i = 0; i < cmd->key_specs_num; i++) {
addReplyMapLen(c, 3);
addReplyBulkCString(c, "flags");
addReplyFlagsForKeyArgs(c,cmd->key_specs[i].flags);
addReplyBulkCString(c, "begin_search");
switch (cmd->key_specs[i].begin_search_type) {
case KSPEC_BS_UNKNOWN:
addReplyMapLen(c, 2);
addReplyBulkCString(c, "type");
addReplyBulkCString(c, "unknown");
addReplyBulkCString(c, "spec");
addReplyMapLen(c, 0);
break;
case KSPEC_BS_INDEX:
addReplyMapLen(c, 2);
addReplyBulkCString(c, "type");
addReplyBulkCString(c, "index");
addReplyBulkCString(c, "spec");
addReplyMapLen(c, 1);
addReplyBulkCString(c, "index");
addReplyLongLong(c, cmd->key_specs[i].bs.index.pos);
break;
case KSPEC_BS_KEYWORD:
addReplyMapLen(c, 2);
addReplyBulkCString(c, "type");
addReplyBulkCString(c, "keyword");
addReplyBulkCString(c, "spec");
addReplyMapLen(c, 2);
addReplyBulkCString(c, "keyword");
addReplyBulkCString(c, cmd->key_specs[i].bs.keyword.keyword);
addReplyBulkCString(c, "startfrom");
addReplyLongLong(c, cmd->key_specs[i].bs.keyword.startfrom);
break;
default:
serverPanic("Invalid begin_search key spec type %d", cmd->key_specs[i].begin_search_type);
}
addReplyBulkCString(c, "find_keys");
switch (cmd->key_specs[i].find_keys_type) {
case KSPEC_FK_UNKNOWN:
addReplyMapLen(c, 2);
addReplyBulkCString(c, "type");
addReplyBulkCString(c, "unknown");
addReplyBulkCString(c, "spec");
addReplyMapLen(c, 0);
break;
case KSPEC_FK_RANGE:
addReplyMapLen(c, 2);
addReplyBulkCString(c, "type");
addReplyBulkCString(c, "range");
addReplyBulkCString(c, "spec");
addReplyMapLen(c, 3);
addReplyBulkCString(c, "lastkey");
addReplyLongLong(c, cmd->key_specs[i].fk.range.lastkey);
addReplyBulkCString(c, "keystep");
addReplyLongLong(c, cmd->key_specs[i].fk.range.keystep);
addReplyBulkCString(c, "limit");
addReplyLongLong(c, cmd->key_specs[i].fk.range.limit);
break;
case KSPEC_FK_KEYNUM:
addReplyMapLen(c, 2);
addReplyBulkCString(c, "type");
addReplyBulkCString(c, "keynum");
addReplyBulkCString(c, "spec");
addReplyMapLen(c, 3);
addReplyBulkCString(c, "keynumidx");
addReplyLongLong(c, cmd->key_specs[i].fk.keynum.keynumidx);
addReplyBulkCString(c, "firstkey");
addReplyLongLong(c, cmd->key_specs[i].fk.keynum.firstkey);
addReplyBulkCString(c, "keystep");
addReplyLongLong(c, cmd->key_specs[i].fk.keynum.keystep);
break;
default:
serverPanic("Invalid begin_search key spec type %d", cmd->key_specs[i].begin_search_type);
}
}
}
/* Output the representation of a Redis command. Used by the COMMAND command. */
void addReplyCommand(client *c, struct redisCommand *cmd) {
if (!cmd) {
addReplyNull(c);
} else {
/* We are adding: command name, arg count, flags, first, last, offset, categories */
addReplyArrayLen(c, 7);
int firstkey = 0, lastkey = 0, keystep = 0;
if (cmd->legacy_range_key_spec.begin_search_type != KSPEC_BS_INVALID) {
firstkey = cmd->legacy_range_key_spec.bs.index.pos;
lastkey = cmd->legacy_range_key_spec.fk.range.lastkey;
if (lastkey >= 0)
lastkey += firstkey;
keystep = cmd->legacy_range_key_spec.fk.range.keystep;
}
/* We are adding: command name, arg count, flags, first, last, offset, categories, key args */
addReplyArrayLen(c, 8);
addReplyBulkCString(c, cmd->name);
addReplyLongLong(c, cmd->arity);
int flagcount = 0;
void *flaglen = addReplyDeferredLen(c);
flagcount += addReplyCommandFlag(c,cmd,CMD_WRITE, "write");
flagcount += addReplyCommandFlag(c,cmd,CMD_READONLY, "readonly");
flagcount += addReplyCommandFlag(c,cmd,CMD_DENYOOM, "denyoom");
flagcount += addReplyCommandFlag(c,cmd,CMD_ADMIN, "admin");
flagcount += addReplyCommandFlag(c,cmd,CMD_PUBSUB, "pubsub");
flagcount += addReplyCommandFlag(c,cmd,CMD_NOSCRIPT, "noscript");
flagcount += addReplyCommandFlag(c,cmd,CMD_RANDOM, "random");
flagcount += addReplyCommandFlag(c,cmd,CMD_SORT_FOR_SCRIPT,"sort_for_script");
flagcount += addReplyCommandFlag(c,cmd,CMD_LOADING, "loading");
flagcount += addReplyCommandFlag(c,cmd,CMD_STALE, "stale");
flagcount += addReplyCommandFlag(c,cmd,CMD_SKIP_MONITOR, "skip_monitor");
flagcount += addReplyCommandFlag(c,cmd,CMD_SKIP_SLOWLOG, "skip_slowlog");
flagcount += addReplyCommandFlag(c,cmd,CMD_ASKING, "asking");
flagcount += addReplyCommandFlag(c,cmd,CMD_FAST, "fast");
flagcount += addReplyCommandFlag(c,cmd,CMD_NO_AUTH, "no_auth");
flagcount += addReplyCommandFlag(c,cmd,CMD_MAY_REPLICATE, "may_replicate");
if (cmdHasMovableKeys(cmd)) {
addReplyStatus(c, "movablekeys");
flagcount += 1;
}
setDeferredSetLen(c, flaglen, flagcount);
addReplyLongLong(c, cmd->firstkey);
addReplyLongLong(c, cmd->lastkey);
addReplyLongLong(c, cmd->keystep);
addReplyFlagsForCommand(c, cmd);
addReplyLongLong(c, firstkey);
addReplyLongLong(c, lastkey);
addReplyLongLong(c, keystep);
addReplyCommandCategories(c,cmd);
addReplyCommandKeyArgs(c,cmd);
}
}
......@@ -4582,7 +5206,7 @@ NULL
if (!cmd) {
addReplyError(c,"Invalid command specified");
return;
} else if (cmd->getkeys_proc == NULL && cmd->firstkey == 0) {
} else if (cmd->getkeys_proc == NULL && cmd->key_specs_num == 0) {
addReplyError(c,"The command has no key arguments");
return;
} else if ((cmd->arity > 0 && cmd->arity != c->argc-2) ||
......
......@@ -84,6 +84,10 @@ typedef long long ustime_t; /* microsecond time type. */
#include "endianconv.h"
#include "crc64.h"
/* min/max */
#define min(a, b) ((a) < (b) ? (a) : (b))
#define max(a, b) ((a) > (b) ? (a) : (b))
/* Error codes */
#define C_OK 0
#define C_ERR -1
......@@ -193,6 +197,12 @@ extern int configOOMScoreAdjValuesDefaults[CONFIG_OOM_COUNT];
#define CMD_NO_AUTH (1ULL<<15) /* "no-auth" flag */
#define CMD_MAY_REPLICATE (1ULL<<16) /* "may-replicate" flag */
/* Key argument flags. Please check the command table defined in the server.c file
* for more information about the meaning of every flag. */
#define CMD_KEY_WRITE (1ULL<<0) /* "write" flag */
#define CMD_KEY_READ (1ULL<<1) /* "read" flag */
#define CMD_KEY_INCOMPLETE (1ULL<<2) /* "incomplete" flag (meaning that the keyspec might not point out to all keys it should cover) */
/* Command flags used by the module system. */
#define CMD_MODULE_GETKEYS (1ULL<<17) /* Use the modules getkeys interface. */
#define CMD_MODULE_NO_CLUSTER (1ULL<<18) /* Deny on Redis Cluster. */
......@@ -1700,27 +1710,120 @@ typedef struct {
} getKeysResult;
#define GETKEYS_RESULT_INIT { {0}, NULL, 0, MAX_KEYS_BUFFER }
/* Key specs definitions.
*
* Brief: This is a scheme that tries to describe the location
* of key arguments better than the old [first,last,step] scheme
* which is limited and doesn't fit many commands.
*
* There are two steps:
* 1. begin_search (BS): in which index should we start seacrhing for keys?
* 2. find_keys (FK): relative to the output of BS, how can we will which args are keys?
*
* There are two types of BS:
* 1. index: key args start at a constant index
* 2. keyword: key args start just after a specific keyword
*
* There are two kinds of FK:
* 1. range: keys end at a specific index (or relative to the last argument)
* 2. keynum: there's an arg that contains the number of key args somewhere before the keys themselves
*/
typedef enum {
KSPEC_BS_INVALID = 0, /* Must be 0 */
KSPEC_BS_UNKNOWN,
KSPEC_BS_INDEX,
KSPEC_BS_KEYWORD
} kspec_bs_type;
typedef enum {
KSPEC_FK_INVALID = 0, /* Must be 0 */
KSPEC_FK_UNKNOWN,
KSPEC_FK_RANGE,
KSPEC_FK_KEYNUM
} kspec_fk_type;
typedef struct {
/* Declarative data */
const char *sflags;
kspec_bs_type 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;
kspec_fk_type find_keys_type;
union {
/* NOTE: Indices in this struct are relative to the result of the begin_search step!
* These are: range.lastkey, keynum.keynumidx, keynum.firstkey */
struct {
/* Index of the last key.
* 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 */
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. */
int keystep;
} keynum;
} fk;
/* Runtime data */
uint64_t flags;
} keySpec;
/* Number of static key specs */
#define STATIC_KEY_SPECS_NUM 4
typedef void redisCommandProc(client *c);
typedef int redisGetKeysProc(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result);
struct redisCommand {
/* Declarative data */
char *name;
redisCommandProc *proc;
int arity;
char *sflags; /* Flags as string representation, one char per flag. */
uint64_t flags; /* The actual flags, obtained from the 'sflags' field. */
keySpec key_specs_static[STATIC_KEY_SPECS_NUM];
/* Use a function to determine keys arguments in a command line.
* Used for Redis Cluster redirect. */
redisGetKeysProc *getkeys_proc;
/* Runtime data */
uint64_t flags; /* The actual flags, obtained from the 'sflags' field. */
/* What keys should be loaded in background when calling this command? */
int firstkey; /* The first argument that's a key (0 = no keys) */
int lastkey; /* The last argument that's a key */
int keystep; /* The step between first and last key */
long long microseconds, calls, rejected_calls, failed_calls;
int id; /* Command ID. This is a progressive ID starting from 0 that
is assigned at runtime, and is used in order to check
ACLs. A connection is able to execute a given command if
the user associated to the connection has this command
bit set in the bitmap of allowed commands. */
keySpec *key_specs;
keySpec legacy_range_key_spec; /* The legacy (first,last,step) key spec is
* still maintained (if applicable) so that
* we can still support the reply format of
* COMMAND INFO and COMMAND GETKEYS */
int key_specs_num;
int key_specs_max;
int movablekeys; /* See populateCommandMovableKeys */
};
struct redisError {
......@@ -1810,6 +1913,9 @@ extern dict *modules;
* Functions prototypes
*----------------------------------------------------------------------------*/
/* Key arguments specs */
void populateCommandLegacyRangeSpec(struct redisCommand *c);
/* Modules */
void moduleInitModulesSystem(void);
void moduleInitModulesSystemLast(void);
......@@ -2791,7 +2897,8 @@ void serverLogHexDump(int level, char *descr, void *value, size_t len);
int memtest_preserving_test(unsigned long *m, size_t bytes, int passes);
void mixDigest(unsigned char *digest, void *ptr, size_t len);
void xorDigest(unsigned char *digest, void *ptr, size_t len);
int populateCommandTableParseFlags(struct redisCommand *c, char *strflags);
int populateSingleCommand(struct redisCommand *c, char *strflags);
void populateCommandMovableKeys(struct redisCommand *cmd);
void debugDelay(int usec);
void killIOThreads(void);
void killThreads(void);
......
......@@ -37,6 +37,7 @@ TEST_MODULES = \
test_lazyfree.so \
timer.so \
defragtest.so \
keyspecs.so \
hash.so \
zset.so \
stream.so \
......
#include "redismodule.h"
#define UNUSED(V) ((void) V)
int kspec_legacy(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");
return REDISMODULE_OK;
}
int kspec_complex2(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);
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;
if (RedisModule_AddCommandKeySpec(ctx,"kspec.legacy","read",&spec_id) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecBeginSearchIndex(ctx,"kspec.legacy",spec_id,1) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecFindKeysRange(ctx,"kspec.legacy",spec_id,0,1,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_AddCommandKeySpec(ctx,"kspec.legacy","write",&spec_id) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecBeginSearchIndex(ctx,"kspec.legacy",spec_id,2) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecFindKeysRange(ctx,"kspec.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;
if (RedisModule_AddCommandKeySpec(ctx,"kspec.complex1","write",&spec_id) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecBeginSearchKeyword(ctx,"kspec.complex1",spec_id,"STORE",2) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecFindKeysRange(ctx,"kspec.complex1",spec_id,0,1,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_AddCommandKeySpec(ctx,"kspec.complex1","read",&spec_id) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecBeginSearchKeyword(ctx,"kspec.complex1",spec_id,"KEYS",2) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecFindKeysKeynum(ctx,"kspec.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;
if (RedisModule_AddCommandKeySpec(ctx,"kspec.complex2","write",&spec_id) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecBeginSearchKeyword(ctx,"kspec.complex2",spec_id,"STORE",5) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecFindKeysRange(ctx,"kspec.complex2",spec_id,0,1,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_AddCommandKeySpec(ctx,"kspec.complex2","read",&spec_id) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecBeginSearchIndex(ctx,"kspec.complex2",spec_id,1) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecFindKeysRange(ctx,"kspec.complex2",spec_id,0,1,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_AddCommandKeySpec(ctx,"kspec.complex2","read",&spec_id) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecBeginSearchIndex(ctx,"kspec.complex2",spec_id,2) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecFindKeysRange(ctx,"kspec.complex2",spec_id,0,1,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_AddCommandKeySpec(ctx,"kspec.complex2","write",&spec_id) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecBeginSearchIndex(ctx,"kspec.complex2",spec_id,3) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecFindKeysKeynum(ctx,"kspec.complex2",spec_id,0,1,1) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_AddCommandKeySpec(ctx,"kspec.complex2","write",&spec_id) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecBeginSearchKeyword(ctx,"kspec.complex2",spec_id,"MOREKEYS",5) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_SetCommandKeySpecFindKeysRange(ctx,"kspec.complex2",spec_id,-1,1,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
return REDISMODULE_OK;
}
set testmodule [file normalize tests/modules/keyspecs.so]
start_server {tags {"modules"}} {
r module load $testmodule
test "Module key specs: Legacy" {
set reply [r command info kspec.legacy]
assert_equal $reply {{kspec.legacy -1 {} 1 2 1 {} {{flags read begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} {flags write begin_search {type index spec {index 2}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}}}}}
}
test "Module key specs: Complex specs, case 1" {
set reply [r command info kspec.complex1]
assert_equal $reply {{kspec.complex1 -1 movablekeys 1 1 1 {} {{flags {} begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} {flags write begin_search {type keyword spec {keyword STORE startfrom 2}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} {flags read 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 [r command info kspec.complex2]
assert_equal $reply {{kspec.complex2 -1 movablekeys 1 2 1 {} {{flags write begin_search {type keyword spec {keyword STORE startfrom 5}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} {flags read begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} {flags read begin_search {type index spec {index 2}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} {flags write begin_search {type index spec {index 3}} find_keys {type keynum spec {keynumidx 0 firstkey 1 keystep 1}}} {flags write begin_search {type keyword spec {keyword MOREKEYS startfrom 5}} find_keys {type range spec {lastkey -1 keystep 1 limit 0}}}}}}
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment