diff --git a/00-RELEASENOTES b/00-RELEASENOTES index 78dbc5ad6..b6ebeb5ed 100644 --- a/00-RELEASENOTES +++ b/00-RELEASENOTES @@ -11,6 +11,23 @@ CRITICAL: There is a critical bug affecting MOST USERS. Upgrade ASAP. SECURITY: There are security fixes in the release. -------------------------------------------------------------------------------- + +================================================================================ +Redis 7.0.2 Released Sunday Jun 12 12:00:00 IST 2022 +================================================================================ + +Upgrade urgency: MODERATE, specifically if you're using a previous release of +Redis 7.0, contains fixes for bugs in previous 7.0 releases. + +Bug Fixes +========= + +* Fixed SET and BITFIELD commands being wrongly marked movablekeys (#10837) + Regression in 7.0 possibly resulting in excessive roundtrip from cluster clients. +* Fix crash when /proc/sys/vm/overcommit_memory is inaccessible (#10848) + Regression in 7.0.1 resulting in crash on startup on some configurations. + + ================================================================================ Redis 7.0.1 Released Wed Jun 8 12:00:00 IST 2022 ================================================================================ @@ -79,8 +96,8 @@ Bug Fixes * Replica fail and retry the PSYNC if the master is unresponsive (#10726) * Fix ZRANGESTORE crash when zset_max_listpack_entries is 0 (#10767) -Fixes for issues in previous release candidates of Redis 7.0 ------------------------------------------------------------- +Fixes for issues in previous releases of Redis 7.0 +-------------------------------------------------- * CONFIG REWRITE could cause a config change to be dropped for aliased configs (#10811) * CONFIG REWRITE would omit rename-command and include lines (#10761) diff --git a/src/module.c b/src/module.c index 64ec8f38c..a1f6a66f5 100644 --- a/src/module.c +++ b/src/module.c @@ -1154,15 +1154,10 @@ RedisModuleCommand *moduleCreateCommandProxy(struct RedisModule *module, sds dec 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); + populateCommandLegacyRangeSpec(cp->rediscmd); cp->rediscmd->microseconds = 0; cp->rediscmd->calls = 0; cp->rediscmd->rejected_calls = 0; @@ -1697,10 +1692,9 @@ int RM_SetCommandInfo(RedisModuleCommand *command, const RedisModuleCommandInfo } } - /* Update the legacy (first,last,step) spec used by the COMMAND command, + /* Update the legacy (first,last,step) spec and "movablekeys" flag used by the COMMAND command, * by trying to "glue" consecutive range key specs. */ populateCommandLegacyRangeSpec(cmd); - populateCommandMovableKeys(cmd); } if (info->args) { diff --git a/src/server.c b/src/server.c index dfb627694..96ceb0122 100644 --- a/src/server.c +++ b/src/server.c @@ -372,7 +372,7 @@ int dictExpandAllowed(size_t moreMem, double usedRatio) { * belonging to the same cluster slot. See the Slot to Key API in cluster.c. */ size_t dictEntryMetadataSize(dict *d) { UNUSED(d); - /* NOTICE: this also affect overhead_ht_slot_to_keys in getMemoryOverheadData. + /* NOTICE: this also affects overhead_ht_slot_to_keys in getMemoryOverheadData. * If we ever add non-cluster related data here, that code must be modified too. */ return server.cluster_enabled ? sizeof(clusterDictEntryMetadata) : 0; } @@ -774,7 +774,7 @@ int clientsCronResizeOutputBuffer(client *c, mstime_t now_ms) { * * This is how it works. We have an array of CLIENTS_PEAK_MEM_USAGE_SLOTS slots * where we track, for each, the biggest client output and input buffers we - * saw in that slot. Every slot correspond to one of the latest seconds, since + * saw in that slot. Every slot corresponds to one of the latest seconds, since * the array is indexed by doing UNIXTIME % CLIENTS_PEAK_MEM_USAGE_SLOTS. * * When we want to know what was recently the peak memory usage, we just scan @@ -2643,9 +2643,12 @@ void InitServerLast() { * 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"). + * first key is "RW ACCESS DELETE", second key is "RW INSERT"). * - * This functions uses very basic heuristics and is "best effort": + * Additionally set the CMD_MOVABLE_KEYS flag for commands that may have key + * names in their arguments, but the legacy range spec doesn't cover all of them. + * + * This function 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. @@ -2667,15 +2670,26 @@ void InitServerLast() { void populateCommandLegacyRangeSpec(struct redisCommand *c) { memset(&c->legacy_range_key_spec, 0, sizeof(c->legacy_range_key_spec)); - if (c->key_specs_num == 0) + /* Set the movablekeys flag if we have a GETKEYS flag for modules. + * Note that for native redis commands, we always have keyspecs, + * with enough information to rely on for movablekeys. */ + if (c->flags & CMD_MODULE_GETKEYS) + c->flags |= CMD_MOVABLE_KEYS; + + /* no key-specs, no keys, exit. */ + 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 */ + /* Quick win, exactly one range spec. */ c->legacy_range_key_spec = c->key_specs[0]; + /* If it has the incomplete flag, set the movablekeys flag on the command. */ + if (c->key_specs[0].flags & CMD_KEY_INCOMPLETE) + c->flags |= CMD_MOVABLE_KEYS; return; } @@ -2684,11 +2698,23 @@ void populateCommandLegacyRangeSpec(struct redisCommand *c) { 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) + { + /* Found an incompatible (non range) spec, skip it, and set the movablekeys flag. */ + c->flags |= CMD_MOVABLE_KEYS; 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; + } + if (c->key_specs[i].fk.range.keystep != 1 || + (prev_lastkey && prev_lastkey != c->key_specs[i].bs.index.pos-1)) + { + /* Found a range spec that's not plain (step of 1) or not consecutive to the previous one. + * Skip it, and we set the movablekeys flag. */ + c->flags |= CMD_MOVABLE_KEYS; + continue; + } + if (c->key_specs[i].flags & CMD_KEY_INCOMPLETE) { + /* The spec we're using is incomplete, we can use it, but we also have to set the movablekeys flag. */ + c->flags |= CMD_MOVABLE_KEYS; + } 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; @@ -2696,10 +2722,14 @@ void populateCommandLegacyRangeSpec(struct redisCommand *c) { 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); + prev_lastkey = lastkey; } - if (firstkey == INT_MAX) + if (firstkey == INT_MAX) { + /* Couldn't find range specs, the legacy range spec will remain empty, and we set the movablekeys flag. */ + c->flags |= CMD_MOVABLE_KEYS; return; + } serverAssert(firstkey != 0); serverAssert(lastkey != 0); @@ -2787,11 +2817,9 @@ void populateCommandStructure(struct redisCommand *c) { c->num_tips++; c->num_args = populateArgsStructure(c->args); + /* Handle the legacy range spec and the "movablekeys" flag (must be done after populating all key specs). */ populateCommandLegacyRangeSpec(c); - /* Handle the "movablekeys" flag (must be done after populating all key specs). */ - populateCommandMovableKeys(c); - /* Assign the ID used for ACL. */ c->id = ACLGetCommandID(c->fullname); @@ -2812,7 +2840,7 @@ void populateCommandStructure(struct redisCommand *c) { extern struct redisCommand redisCommandTable[]; -/* Populates the Redis Command Table dict from from the static table in commands.c +/* Populates the Redis Command Table dict from the static table in commands.c * which is auto generated from the json files in the commands folder. */ void populateCommandTable(void) { int j; @@ -3498,31 +3526,6 @@ void afterCommand(client *c) { } } -/* 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_GETKEYS)) { - /* 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; - } - } - } - - if (movablekeys) - cmd->flags |= CMD_MOVABLE_KEYS; -} - /* Check if c->cmd exists, fills `err` with details in case it doesn't. * Return 1 if exists. */ int commandCheckExistence(client *c, sds *err) { @@ -6028,7 +6031,7 @@ static int THPDisable(void) { } void linuxMemoryWarnings(void) { - sds err_msg; + sds err_msg = NULL; if (checkOvercommit(&err_msg) < 0) { serverLog(LL_WARNING,"WARNING %s", err_msg); sdsfree(err_msg); @@ -6939,7 +6942,7 @@ int main(int argc, char **argv) { serverLog(LL_WARNING,"Server initialized"); #ifdef __linux__ linuxMemoryWarnings(); - sds err_msg; + sds err_msg = NULL; if (checkXenClocksource(&err_msg) < 0) { serverLog(LL_WARNING, "WARNING %s", err_msg); sdsfree(err_msg); diff --git a/src/server.h b/src/server.h index 6dfa8a067..abaa5f046 100644 --- a/src/server.h +++ b/src/server.h @@ -215,7 +215,8 @@ extern int configOOMScoreAdjValuesDefaults[CONFIG_OOM_COUNT]; #define CMD_MODULE_NO_CLUSTER (1ULL<<22) /* Deny on Redis Cluster. */ #define CMD_NO_ASYNC_LOADING (1ULL<<23) #define CMD_NO_MULTI (1ULL<<24) -#define CMD_MOVABLE_KEYS (1ULL<<25) /* populated by populateCommandMovableKeys */ +#define CMD_MOVABLE_KEYS (1ULL<<25) /* The legacy range spec doesn't cover all keys. + * Populated by populateCommandLegacyRangeSpec. */ #define CMD_ALLOW_BUSY ((1ULL<<26)) #define CMD_MODULE_GETCHANNELS (1ULL<<27) /* Use the modules getchannels interface. */ @@ -3550,7 +3551,6 @@ void mixDigest(unsigned char *digest, const void *ptr, size_t len); void xorDigest(unsigned char *digest, const void *ptr, size_t len); sds catSubCommandFullname(const char *parent_name, const char *sub_name); void commandAddSubcommand(struct redisCommand *parent, struct redisCommand *subcommand, const char *declared_name); -void populateCommandMovableKeys(struct redisCommand *cmd); void debugDelay(int usec); void killIOThreads(void); void killThreads(void); diff --git a/src/syscheck.c b/src/syscheck.c index 9f338b118..58dc78f1b 100644 --- a/src/syscheck.c +++ b/src/syscheck.c @@ -143,7 +143,7 @@ int checkOvercommit(sds *error_msg) { FILE *fp = fopen("/proc/sys/vm/overcommit_memory","r"); char buf[64]; - if (!fp) return -1; + if (!fp) return 0; if (fgets(buf,64,fp) == NULL) { fclose(fp); return 0; @@ -152,7 +152,7 @@ int checkOvercommit(sds *error_msg) { if (atoi(buf)) { *error_msg = sdsnew( - "WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. " + "overcommit_memory is set to 0! Background save may fail under low memory condition. " "To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the " "command 'sysctl vm.overcommit_memory=1' for this to take effect."); return -1; @@ -351,7 +351,7 @@ check checks[] = { int syscheck(void) { check *cur_check = checks; int ret = 1; - sds err_msg; + sds err_msg = NULL; while (cur_check->check_fn) { int res = cur_check->check_fn(&err_msg); printf("[%s]...", cur_check->name); diff --git a/src/t_stream.c b/src/t_stream.c index 4383dcd5a..617976c9c 100644 --- a/src/t_stream.c +++ b/src/t_stream.c @@ -2144,7 +2144,7 @@ void xrevrangeCommand(client *c) { xrangeGenericCommand(c,1); } -/* XLEN */ +/* XLEN key*/ void xlenCommand(client *c) { robj *o; if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL @@ -2156,10 +2156,10 @@ void xlenCommand(client *c) { /* XREAD [BLOCK ] [COUNT ] STREAMS key_1 key_2 ... key_N * ID_1 ID_2 ... ID_N * - * This function also implements the XREAD-GROUP command, which is like XREAD + * This function also implements the XREADGROUP command, which is like XREAD * but accepting the [GROUP group-name consumer-name] additional option. * This is useful because while XREAD is a read command and can be called - * on slaves, XREAD-GROUP is not. */ + * on slaves, XREADGROUP is not. */ #define XREAD_BLOCKED_DEFAULT_COUNT 1000 void xreadCommand(client *c) { long long timeout = -1; /* -1 means, no BLOCK argument given. */ @@ -2566,8 +2566,8 @@ void streamDelConsumer(streamCG *cg, streamConsumer *consumer) { * Consumer groups commands * ----------------------------------------------------------------------- */ -/* XGROUP CREATE [MKSTREAM] [ENTRIESADDED count] - * XGROUP SETID [ENTRIESADDED count] +/* XGROUP CREATE [MKSTREAM] [ENTRIESREAD entries_read] + * XGROUP SETID [ENTRIESREAD entries_read] * XGROUP DESTROY * XGROUP CREATECONSUMER * XGROUP DELCONSUMER */ @@ -2805,7 +2805,6 @@ void xsetidCommand(client *c) { } /* XACK ... - * * Acknowledge a message as processed. In practical terms we just check the * pending entries list (PEL) of the group, and delete the PEL entry both from * the group and the consumer (pending messages are referenced in both places). @@ -3050,7 +3049,7 @@ void xpendingCommand(client *c) { * [IDLE ] [TIME ] [RETRYCOUNT ] * [FORCE] [JUSTID] * - * Gets ownership of one or multiple messages in the Pending Entries List + * Changes ownership of one or multiple messages in the Pending Entries List * of a given stream consumer group. * * If the message ID (among the specified ones) exists, and its idle @@ -3316,7 +3315,7 @@ cleanup: /* XAUTOCLAIM [COUNT ] [JUSTID] * - * Gets ownership of one or multiple messages in the Pending Entries List + * Changes ownership of one or multiple messages in the Pending Entries List * of a given stream consumer group. * * For each PEL entry, if its idle time greater or equal to , diff --git a/src/version.h b/src/version.h index 84d0dd25c..eff0a6015 100644 --- a/src/version.h +++ b/src/version.h @@ -1,2 +1,2 @@ -#define REDIS_VERSION "7.0.1" -#define REDIS_VERSION_NUM 0x00070001 +#define REDIS_VERSION "7.0.2" +#define REDIS_VERSION_NUM 0x00070002 diff --git a/tests/integration/logging.tcl b/tests/integration/logging.tcl index ef74ef498..8617ed2fc 100644 --- a/tests/integration/logging.tcl +++ b/tests/integration/logging.tcl @@ -8,9 +8,12 @@ if {$system_name eq {darwin}} { set backtrace_supported 1 } elseif {$system_name eq {linux}} { # Avoid the test on libmusl, which does not support backtrace - set ldd [exec ldd src/redis-server] - if {![string match {*libc.*musl*} $ldd]} { - set backtrace_supported 1 + # and on static binaries (ldd exit code 1) where we can't detect libmusl + catch { + set ldd [exec ldd src/redis-server] + if {![string match {*libc.*musl*} $ldd]} { + set backtrace_supported 1 + } } } diff --git a/tests/modules/keyspecs.c b/tests/modules/keyspecs.c index 32a6bebaa..d2ae9fd6c 100644 --- a/tests/modules/keyspecs.c +++ b/tests/modules/keyspecs.c @@ -18,6 +18,13 @@ int createKspecNone(RedisModuleCtx *ctx) { return REDISMODULE_OK; } +int createKspecNoneWithGetkeys(RedisModuleCtx *ctx) { + /* A command without keyspecs; only the legacy (first,last,step) triple (MSET like spec), but also has a getkeys callback */ + if (RedisModule_CreateCommand(ctx,"kspec.nonewithgetkeys",kspec_impl,"getkeys-api",1,-1,2) == REDISMODULE_ERR) + return REDISMODULE_ERR; + return REDISMODULE_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. */ @@ -51,6 +58,39 @@ int createKspecTwoRanges(RedisModuleCtx *ctx) { return REDISMODULE_OK; } +int createKspecTwoRangesWithGap(RedisModuleCtx *ctx) { + /* Test that two position/range-based key specs are combined to produce the + * legacy (first,last,step) values representing just one key. */ + if (RedisModule_CreateCommand(ctx,"kspec.tworangeswithgap",kspec_impl,"",0,0,0) == REDISMODULE_ERR) + return REDISMODULE_ERR; + + RedisModuleCommand *command = RedisModule_GetCommand(ctx,"kspec.tworangeswithgap"); + 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 = 3, + /* 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) @@ -177,7 +217,9 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) return REDISMODULE_ERR; if (createKspecNone(ctx) == REDISMODULE_ERR) return REDISMODULE_ERR; + if (createKspecNoneWithGetkeys(ctx) == REDISMODULE_ERR) return REDISMODULE_ERR; if (createKspecTwoRanges(ctx) == REDISMODULE_ERR) return REDISMODULE_ERR; + if (createKspecTwoRangesWithGap(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; diff --git a/tests/unit/introspection-2.tcl b/tests/unit/introspection-2.tcl index 46dac50b7..dab8008e8 100644 --- a/tests/unit/introspection-2.tcl +++ b/tests/unit/introspection-2.tcl @@ -176,4 +176,19 @@ start_server {tags {"introspection"}} { assert_equal {{}} [r command info get|key] assert_equal {{}} [r command info config|get|key] } + + foreach cmd {SET GET MSET BITFIELD LMOVE LPOP BLPOP PING MEMORY MEMORY|USAGE RENAME GEORADIUS_RO} { + test "$cmd command will not be marked with movablekeys" { + set info [lindex [r command info $cmd] 0] + assert_no_match {*movablekeys*} [lindex $info 2] + } + } + + foreach cmd {ZUNIONSTORE XREAD EVAL SORT SORT_RO MIGRATE GEORADIUS} { + test "$cmd command is marked with movablekeys" { + set info [lindex [r command info $cmd] 0] + assert_match {*movablekeys*} [lindex $info 2] + } + } + } diff --git a/tests/unit/moduleapi/keyspecs.tcl b/tests/unit/moduleapi/keyspecs.tcl index 60d3fe5d3..ef5b92334 100644 --- a/tests/unit/moduleapi/keyspecs.tcl +++ b/tests/unit/moduleapi/keyspecs.tcl @@ -31,6 +31,20 @@ start_server {tags {"modules"}} { assert_equal [r command getkeys kspec.tworanges foo bar baz quux] {foo bar} } + test "Module key specs: Two ranges with gap" { + set reply [lindex [r command info kspec.tworangeswithgap] 0] + # 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 {RO access} 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 index spec {index 3}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} + assert_equal [r command getkeys kspec.tworangeswithgap foo bar baz quux] {foo baz} + } + 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 @@ -79,7 +93,7 @@ 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.keyword kspec.none kspec.tworanges} + assert_equal [lsort $reply] {kspec.complex1 kspec.complex2 kspec.keyword kspec.none kspec.nonewithgetkeys kspec.tworanges kspec.tworangeswithgap} assert_equal [r command getkeys kspec.complex2 foo bar 2 baz quux banana STORE dst dummy MOREKEYS hey ho] {dst foo bar baz quux hey ho} } @@ -108,6 +122,20 @@ start_server {tags {"modules"}} { assert_equal "This user has no permissions to access the 'write' key" [r ACL DRYRUN testuser kspec.tworanges write rw] } + foreach cmd {kspec.none kspec.tworanges} { + test "$cmd command will not be marked with movablekeys" { + set info [lindex [r command info $cmd] 0] + assert_no_match {*movablekeys*} [lindex $info 2] + } + } + + foreach cmd {kspec.keyword kspec.complex1 kspec.complex2 kspec.nonewithgetkeys} { + test "$cmd command is marked with movablekeys" { + set info [lindex [r command info $cmd] 0] + assert_match {*movablekeys*} [lindex $info 2] + } + } + test "Unload the module - keyspecs" { assert_equal {OK} [r module unload keyspecs] } diff --git a/utils/generate-module-api-doc.rb b/utils/generate-module-api-doc.rb index 1f7b7c2e5..d4282cbfa 100755 --- a/utils/generate-module-api-doc.rb +++ b/utils/generate-module-api-doc.rb @@ -20,7 +20,7 @@ def markdown(s) # Add backquotes around RedisModule functions and type where missing. l = l.gsub(/(?