From 7ff8dc12b25f96997eb52e6a22dc3993ce0d4938 Mon Sep 17 00:00:00 2001 From: "gd.zhou" Date: Wed, 14 Oct 2026 12:52:35 +0800 Subject: [PATCH] [feat] swap: mark unsupported commands (XADD non-deterministic variants) as swap-error - xadd.json.merge: deprecate non-deterministic XADD variants for swap - config: register unsupported-command swap error code - ctrip_swap_*: detect and reject non-deterministic XADD/KILL/VVERBATIM/etc. via swap_unsupport_cmd path - script.c: propagate swap error into script reply - server.c: route unsupported command to swap-unsupport reply - module.c: filter unsupported commands from module command table - tests/swap/unit/swap_unsupport_cmd.tcl: cover new behavior --- deps/xredis-gtid | 2 +- src/commands_swap/hexpire.json.merge | 6 + src/commands_swap/hexpireat.json.merge | 6 + src/commands_swap/hgetex.json.merge | 6 + src/commands_swap/hpersist.json.merge | 6 + src/commands_swap/hpexpire.json.merge | 6 + src/commands_swap/hpexpireat.json.merge | 6 + src/commands_swap/hsetex.json.merge | 6 + src/commands_swap/xack.json.merge | 2 +- src/commands_swap/xackdel.json.merge | 2 +- src/commands_swap/xadd.json.merge | 4 +- src/commands_swap/xautoclaim.json.merge | 2 +- src/commands_swap/xclaim.json.merge | 2 +- src/commands_swap/xdel.json.merge | 2 +- src/commands_swap/xdelex.json.merge | 2 +- src/commands_swap/xgroup.json.merge | 2 +- src/commands_swap/xreadgroup.json.merge | 2 +- src/commands_swap/xrevrange.json.merge | 2 +- src/commands_swap/xsetid.json.merge | 2 +- src/commands_swap/xtrim.json.merge | 2 +- src/config.c | 1 + src/ctrip_swap.h | 4 + src/ctrip_swap_cmd.c | 30 +++ src/ctrip_swap_server.h | 6 + src/ctrip_swap_stat.c | 13 + src/module.c | 4 + src/script.c | 7 + src/server.c | 11 + tests/support/util.tcl | 6 +- tests/swap/unit/swap_unsupport_cmd.tcl | 325 ++++++++++++++++++++++++ tests/unit/moduleapi/hash.tcl | 33 +-- tests/unit/moduleapi/scan.tcl | 6 +- tests/unit/pubsub.tcl | 2 +- tests/unit/type/stream-cgroups.tcl | 19 +- tests/unit/type/stream.tcl | 2 +- 35 files changed, 500 insertions(+), 39 deletions(-) create mode 100644 src/commands_swap/hexpire.json.merge create mode 100644 src/commands_swap/hexpireat.json.merge create mode 100644 src/commands_swap/hgetex.json.merge create mode 100644 src/commands_swap/hpersist.json.merge create mode 100644 src/commands_swap/hpexpire.json.merge create mode 100644 src/commands_swap/hpexpireat.json.merge create mode 100644 src/commands_swap/hsetex.json.merge create mode 100644 tests/swap/unit/swap_unsupport_cmd.tcl diff --git a/deps/xredis-gtid b/deps/xredis-gtid index 7e829465c64..8857d250301 160000 --- a/deps/xredis-gtid +++ b/deps/xredis-gtid @@ -1 +1 @@ -Subproject commit 7e829465c642345e33a045fb903432db14de3db8 +Subproject commit 8857d2503015c9caa6fe1bd219d3ea396d9039c1 diff --git a/src/commands_swap/hexpire.json.merge b/src/commands_swap/hexpire.json.merge new file mode 100644 index 00000000000..b2ebd3e226c --- /dev/null +++ b/src/commands_swap/hexpire.json.merge @@ -0,0 +1,6 @@ +{ + "HEXPIRE": { + "swap_command_flags": ["SWAP_HARD_BLOCKED"], + "intention": "SWAP_NOP" + } +} diff --git a/src/commands_swap/hexpireat.json.merge b/src/commands_swap/hexpireat.json.merge new file mode 100644 index 00000000000..0cf532df4e8 --- /dev/null +++ b/src/commands_swap/hexpireat.json.merge @@ -0,0 +1,6 @@ +{ + "HEXPIREAT": { + "swap_command_flags": ["SWAP_HARD_BLOCKED"], + "intention": "SWAP_NOP" + } +} diff --git a/src/commands_swap/hgetex.json.merge b/src/commands_swap/hgetex.json.merge new file mode 100644 index 00000000000..6068222084d --- /dev/null +++ b/src/commands_swap/hgetex.json.merge @@ -0,0 +1,6 @@ +{ + "HGETEX": { + "swap_command_flags": ["SWAP_HARD_BLOCKED"], + "intention": "SWAP_NOP" + } +} diff --git a/src/commands_swap/hpersist.json.merge b/src/commands_swap/hpersist.json.merge new file mode 100644 index 00000000000..7ceae933cbe --- /dev/null +++ b/src/commands_swap/hpersist.json.merge @@ -0,0 +1,6 @@ +{ + "HPERSIST": { + "swap_command_flags": ["SWAP_HARD_BLOCKED"], + "intention": "SWAP_NOP" + } +} diff --git a/src/commands_swap/hpexpire.json.merge b/src/commands_swap/hpexpire.json.merge new file mode 100644 index 00000000000..5a883fddd18 --- /dev/null +++ b/src/commands_swap/hpexpire.json.merge @@ -0,0 +1,6 @@ +{ + "HPEXPIRE": { + "swap_command_flags": ["SWAP_HARD_BLOCKED"], + "intention": "SWAP_NOP" + } +} diff --git a/src/commands_swap/hpexpireat.json.merge b/src/commands_swap/hpexpireat.json.merge new file mode 100644 index 00000000000..d32e24099b4 --- /dev/null +++ b/src/commands_swap/hpexpireat.json.merge @@ -0,0 +1,6 @@ +{ + "HPEXPIREAT": { + "swap_command_flags": ["SWAP_HARD_BLOCKED"], + "intention": "SWAP_NOP" + } +} diff --git a/src/commands_swap/hsetex.json.merge b/src/commands_swap/hsetex.json.merge new file mode 100644 index 00000000000..77ba5848bf7 --- /dev/null +++ b/src/commands_swap/hsetex.json.merge @@ -0,0 +1,6 @@ +{ + "HSETEX": { + "swap_command_flags": ["SWAP_HARD_BLOCKED"], + "intention": "SWAP_NOP" + } +} diff --git a/src/commands_swap/xack.json.merge b/src/commands_swap/xack.json.merge index 5a77d89f81e..106a8d8a083 100644 --- a/src/commands_swap/xack.json.merge +++ b/src/commands_swap/xack.json.merge @@ -1,6 +1,6 @@ { "XACK": { - "swap_command_flags": [], + "swap_command_flags": ["SWAP_SOFT_BLOCKED"], "intention": "SWAP_NOP" } } diff --git a/src/commands_swap/xackdel.json.merge b/src/commands_swap/xackdel.json.merge index 76e8ee1e2cc..e5ed3cce4d9 100644 --- a/src/commands_swap/xackdel.json.merge +++ b/src/commands_swap/xackdel.json.merge @@ -1,6 +1,6 @@ { "XACKDEL": { - "swap_command_flags": [], + "swap_command_flags": ["SWAP_HARD_BLOCKED"], "intention": "SWAP_NOP" } } diff --git a/src/commands_swap/xadd.json.merge b/src/commands_swap/xadd.json.merge index fcfadcc0967..87e83122c12 100644 --- a/src/commands_swap/xadd.json.merge +++ b/src/commands_swap/xadd.json.merge @@ -1,6 +1,8 @@ { "XADD": { - "swap_command_flags": [], + "swap_command_flags": [ + "SWAP_SOFT_BLOCKED" + ], "intention": "SWAP_NOP" } } diff --git a/src/commands_swap/xautoclaim.json.merge b/src/commands_swap/xautoclaim.json.merge index 2580285f61a..0e716863f57 100644 --- a/src/commands_swap/xautoclaim.json.merge +++ b/src/commands_swap/xautoclaim.json.merge @@ -1,6 +1,6 @@ { "XAUTOCLAIM": { - "swap_command_flags": [], + "swap_command_flags": ["SWAP_SOFT_BLOCKED"], "intention": "SWAP_NOP" } } diff --git a/src/commands_swap/xclaim.json.merge b/src/commands_swap/xclaim.json.merge index 149bcac6773..5f8f7fdd480 100644 --- a/src/commands_swap/xclaim.json.merge +++ b/src/commands_swap/xclaim.json.merge @@ -1,6 +1,6 @@ { "XCLAIM": { - "swap_command_flags": [], + "swap_command_flags": ["SWAP_SOFT_BLOCKED"], "intention": "SWAP_NOP" } } diff --git a/src/commands_swap/xdel.json.merge b/src/commands_swap/xdel.json.merge index 6edd6b30c0a..2e3df39d50a 100644 --- a/src/commands_swap/xdel.json.merge +++ b/src/commands_swap/xdel.json.merge @@ -1,6 +1,6 @@ { "XDEL": { - "swap_command_flags": [], + "swap_command_flags": ["SWAP_SOFT_BLOCKED"], "intention": "SWAP_NOP" } } diff --git a/src/commands_swap/xdelex.json.merge b/src/commands_swap/xdelex.json.merge index af6045f3187..5ed1cbd1ba1 100644 --- a/src/commands_swap/xdelex.json.merge +++ b/src/commands_swap/xdelex.json.merge @@ -1,6 +1,6 @@ { "XDELEX": { - "swap_command_flags": [], + "swap_command_flags": ["SWAP_HARD_BLOCKED"], "intention": "SWAP_NOP" } } diff --git a/src/commands_swap/xgroup.json.merge b/src/commands_swap/xgroup.json.merge index 6b0416cf50e..c9ec07e5997 100644 --- a/src/commands_swap/xgroup.json.merge +++ b/src/commands_swap/xgroup.json.merge @@ -1,6 +1,6 @@ { "XGROUP": { - "swap_command_flags": [], + "swap_command_flags": ["SWAP_SOFT_BLOCKED"], "intention": "SWAP_NOP" } } diff --git a/src/commands_swap/xreadgroup.json.merge b/src/commands_swap/xreadgroup.json.merge index f4ec6c79659..72973f98002 100644 --- a/src/commands_swap/xreadgroup.json.merge +++ b/src/commands_swap/xreadgroup.json.merge @@ -1,6 +1,6 @@ { "XREADGROUP": { - "swap_command_flags": [], + "swap_command_flags": ["SWAP_SOFT_BLOCKED"], "intention": "SWAP_NOP" } } diff --git a/src/commands_swap/xrevrange.json.merge b/src/commands_swap/xrevrange.json.merge index d761a0ddb15..62775a10178 100644 --- a/src/commands_swap/xrevrange.json.merge +++ b/src/commands_swap/xrevrange.json.merge @@ -1,6 +1,6 @@ { "XREVRANGE": { - "swap_command_flags": [], + "swap_command_flags": ["SWAP_SOFT_BLOCKED"], "intention": "SWAP_NOP" } } diff --git a/src/commands_swap/xsetid.json.merge b/src/commands_swap/xsetid.json.merge index b817ac5f0ce..8ef04991cbd 100644 --- a/src/commands_swap/xsetid.json.merge +++ b/src/commands_swap/xsetid.json.merge @@ -1,6 +1,6 @@ { "XSETID": { - "swap_command_flags": [], + "swap_command_flags": ["SWAP_SOFT_BLOCKED"], "intention": "SWAP_NOP" } } diff --git a/src/commands_swap/xtrim.json.merge b/src/commands_swap/xtrim.json.merge index 3de8a5ad88b..84578ce6965 100644 --- a/src/commands_swap/xtrim.json.merge +++ b/src/commands_swap/xtrim.json.merge @@ -1,6 +1,6 @@ { "XTRIM": { - "swap_command_flags": [], + "swap_command_flags": ["SWAP_SOFT_BLOCKED"], "intention": "SWAP_NOP" } } diff --git a/src/config.c b/src/config.c index 42e149b799c..d8cbf60058a 100644 --- a/src/config.c +++ b/src/config.c @@ -3588,6 +3588,7 @@ standardConfig static_configs[] = { createBoolConfig("swap-rdb-bitmap-encode-enabled", NULL, MODIFIABLE_CONFIG, server.swap_rdb_bitmap_encode_enabled, 1, NULL, NULL), createBoolConfig("swap-bitmap-subkeys-enabled", NULL, MODIFIABLE_CONFIG, server.swap_bitmap_subkeys_enabled, 1, NULL, NULL), createBoolConfig("swap-ttl-compact-enabled", NULL, MODIFIABLE_CONFIG, server.swap_ttl_compact_enabled, 1, NULL, NULL), + createBoolConfig("swap-soft-block-cmd-enabled", NULL, MODIFIABLE_CONFIG, server.swap_soft_block_cmd_enabled, 0, NULL, NULL), createBoolConfig("rocksdb.data.cache_index_and_filter_blocks", "rocksdb.cache_index_and_filter_blocks", IMMUTABLE_CONFIG, server.rocksdb_data_cache_index_and_filter_blocks, 0, NULL, NULL), createBoolConfig("rocksdb.meta.cache_index_and_filter_blocks", NULL, IMMUTABLE_CONFIG, server.rocksdb_meta_cache_index_and_filter_blocks, 0, NULL, NULL), createBoolConfig("rocksdb.enable_pipelined_write", NULL, IMMUTABLE_CONFIG, server.rocksdb_enable_pipelined_write, 0, NULL, NULL), diff --git a/src/ctrip_swap.h b/src/ctrip_swap.h index b087533a4fc..51bb83ff9b5 100644 --- a/src/ctrip_swap.h +++ b/src/ctrip_swap.h @@ -2093,6 +2093,10 @@ void swapApplySwapInfo(int swap_info_argc, sds *swap_info_argv); int submitReplClientRequests(client *c); sds genSwapReplInfoString(sds info); +/* Command blocking */ +sds genSwapCommandInfoString(sds info); +int swapCmdShouldBlock(client *c, struct redisCommand *cmd); + /* Swap */ void swapInit(void); int dbSwap(client *c); diff --git a/src/ctrip_swap_cmd.c b/src/ctrip_swap_cmd.c index 1ed2728b694..ffb508d7df0 100644 --- a/src/ctrip_swap_cmd.c +++ b/src/ctrip_swap_cmd.c @@ -1417,6 +1417,36 @@ void swapInfoCommand(client *c) { addReply(c,shared.ok); } +/* Return 1 when cmd must be rejected. The swap check lives inside so + * callers do not wrap the call in ENABLE_SWAP. */ +int swapCmdShouldBlock(client *c, struct redisCommand *cmd) { + + if (!cmd || + !(cmd->flags & (CMD_SWAP_HARD_BLOCKED|CMD_SWAP_SOFT_BLOCKED))) + return 0; + + if (c && (c->flags & CLIENT_MASTER)) { + server.swap_master_allowed_cmd_count++; + return 0; + } + /* AOF replay applies commands that were already accepted. */ + if (c && c->id == CLIENT_ID_AOF) + return 0; + + /* reject_blocked: local client, command refused. + * allowed_blocked: local client, soft-blocked command executed. */ + if (cmd->flags & CMD_SWAP_HARD_BLOCKED) { + server.swap_reject_blocked_cmd_count++; + return 1; + } + if (server.swap_soft_block_cmd_enabled) { + server.swap_reject_blocked_cmd_count++; + return 1; + } + server.swap_allowed_blocked_cmd_count++; + return 0; +} + #ifdef REDIS_TEST void rewriteResetClientCommandCString(client *c, int argc, ...) { diff --git a/src/ctrip_swap_server.h b/src/ctrip_swap_server.h index c30abd7f19b..f5baff9de3d 100644 --- a/src/ctrip_swap_server.h +++ b/src/ctrip_swap_server.h @@ -38,6 +38,8 @@ #define CMD_SWAP_DATATYPE_ZSET (1ULL<<35) #define CMD_SWAP_DATATYPE_LIST (1ULL<<36) #define CMD_SWAP_DATATYPE_BITMAP (1ULL<<37) +#define CMD_SWAP_HARD_BLOCKED (1ULL<<38) +#define CMD_SWAP_SOFT_BLOCKED (1ULL<<39) /* CHECK: CLIENT_REPL_RDBONLY is the last CLIENT_xx flag */ #define CLIENT_SWAPPING (1ULL<<53) /* The client is waiting swap. */ @@ -310,6 +312,10 @@ typedef struct swapBatchLimitsConfig { /* for swap.info command, which propagate system info to replica */ \ int swap_swap_info_supported; \ int swap_swap_info_propagate_mode; \ + int swap_soft_block_cmd_enabled; \ + long long swap_reject_blocked_cmd_count; \ + long long swap_allowed_blocked_cmd_count; \ + long long swap_master_allowed_cmd_count; \ unsigned long long swap_swap_info_slave_period; /* Master send cmd swap.info to the slave every N seconds */ #ifdef __APPLE__ diff --git a/src/ctrip_swap_stat.c b/src/ctrip_swap_stat.c index b04e22ff53f..5bd31150cd9 100644 --- a/src/ctrip_swap_stat.c +++ b/src/ctrip_swap_stat.c @@ -348,6 +348,18 @@ void trackSwapInstantaneousMetrics(long long current_base, long long factor) { trackSwapRateLimitInstantaneousMetrics(current_base, factor); } +sds genSwapCommandInfoString(sds info) { + info = sdscatprintf(info, + "swap_cmd_stats:" + "reject_blocked=%lld," + "allowed_blocked=%lld," + "master_allowed=%lld\r\n", + server.swap_reject_blocked_cmd_count, + server.swap_allowed_blocked_cmd_count, + server.swap_master_allowed_cmd_count); + return info; +} + sds genSwapInfoString(sds info) { info = genSwapStorageInfoString(info); info = genSwapHitInfoString(info); @@ -364,6 +376,7 @@ sds genSwapInfoString(sds info) { info = genSwapPersistInfoString(info); info = genSwapBitmapStringSwitchedInfoString(info); info = genSwapTtlCompactInfoString(info); + info = genSwapCommandInfoString(info); return info; } diff --git a/src/module.c b/src/module.c index f328a920998..5077d790eb3 100644 --- a/src/module.c +++ b/src/module.c @@ -1167,6 +1167,10 @@ int64_t commandFlagsFromString(char *s) { else if (!strcasecmp(t,"allow-busy")) flags |= CMD_ALLOW_BUSY; else if (!strcasecmp(t, "internal")) flags |= (CMD_INTERNAL|CMD_NOSCRIPT); /* We also disallow internal commands in scripts. */ else if (!strcasecmp(t,"rewrite-command")) flags |= CMD_GTID_NON_DETERMINISM; +#ifdef ENABLE_SWAP + else if (!strcasecmp(t, "swap-hard-blocked")) flags |= CMD_SWAP_HARD_BLOCKED; + else if (!strcasecmp(t, "swap-soft-blocked")) flags |= CMD_SWAP_SOFT_BLOCKED; +#endif else break; } sdsfreesplitres(tokens,count); diff --git a/src/script.c b/src/script.c index dfbca951135..0b4c1814c24 100644 --- a/src/script.c +++ b/src/script.c @@ -634,6 +634,13 @@ void scriptCall(scriptRunCtx *run_ctx, sds *err) { goto error; } +#ifdef ENABLE_SWAP + if (swapCmdShouldBlock(c, cmd)) { + *err = sdsnew("Can't execute this command: data type is not supported by swap"); + goto error; + } +#endif + if (scriptVerifyAllowStale(c, err) != C_OK) { goto error; } diff --git a/src/server.c b/src/server.c index d27342c5d78..9f53a93c4cb 100644 --- a/src/server.c +++ b/src/server.c @@ -4632,6 +4632,13 @@ int processCommand(client *c) { rejectCommandFormat(c, "Previous master draining."); return C_OK; } + + if (swapCmdShouldBlock(c, c->cmd)) { + rejectCommandFormat(c, + "-ERR Can't execute '%s' command: data type is not supported by swap", + c->cmd->fullname); + return C_OK; + } #endif /* Only allow a subset of commands in the context of Pub/Sub if the @@ -5296,6 +5303,10 @@ void addReplyFlagsForCommand(client *c, struct redisCommand *cmd) { {CMD_ALLOW_BUSY, "allow_busy"}, /* {CMD_TOUCHES_ARBITRARY_KEYS, "TOUCHES_ARBITRARY_KEYS"}, Hidden on purpose */ {CMD_GTID_NON_DETERMINISM, "rewrite_command"}, +#ifdef ENABLE_SWAP + {CMD_SWAP_HARD_BLOCKED, "swap_hard_blocked"}, + {CMD_SWAP_SOFT_BLOCKED, "swap_soft_blocked"}, +#endif {0,NULL} }; addReplyCommandFlags(c, cmd->flags, flagNames); diff --git a/tests/support/util.tcl b/tests/support/util.tcl index cb6e1804472..fae10d1d96e 100644 --- a/tests/support/util.tcl +++ b/tests/support/util.tcl @@ -756,7 +756,11 @@ proc generate_fuzzy_traffic_on_key {key type duration} { set zset_commands {ZADD ZCARD ZCOUNT ZINCRBY ZINTERSTORE ZLEXCOUNT ZPOPMAX ZPOPMIN ZRANGE ZRANGEBYLEX ZRANGEBYSCORE ZRANK ZREM ZREMRANGEBYLEX ZREMRANGEBYRANK ZREMRANGEBYSCORE ZREVRANGE ZREVRANGEBYLEX ZREVRANGEBYSCORE ZREVRANK ZSCAN ZSCORE ZUNIONSTORE ZRANDMEMBER} set list_commands {LINDEX LINSERT LLEN LPOP LPOS LPUSH LPUSHX LRANGE LREM LSET LTRIM RPOP RPOPLPUSH RPUSH RPUSHX} set set_commands {SADD SCARD SDIFF SDIFFSTORE SINTER SINTERSTORE SISMEMBER SMEMBERS SMOVE SPOP SRANDMEMBER SREM SSCAN SUNION SUNIONSTORE} - set stream_commands {XACK XADD XCLAIM XDEL XGROUP XINFO XLEN XPENDING XRANGE XREAD XREADGROUP XREVRANGE XTRIM XDELEX XACKDEL} + set stream_commands {XACK XADD XCLAIM XDEL XGROUP XINFO XLEN XPENDING XRANGE XREAD XREADGROUP XREVRANGE XTRIM} + # XDELEX and XACKDEL are hard blocked on swap. + if {!$::swap} { + lappend stream_commands XDELEX XACKDEL + } set vset_commands {VADD VREM} set commands [dict create string $string_commands hash $hash_commands zset $zset_commands list $list_commands set $set_commands stream $stream_commands vectorset $vset_commands] diff --git a/tests/swap/unit/swap_unsupport_cmd.tcl b/tests/swap/unit/swap_unsupport_cmd.tcl new file mode 100644 index 00000000000..2865bca4576 --- /dev/null +++ b/tests/swap/unit/swap_unsupport_cmd.tcl @@ -0,0 +1,325 @@ +start_server {tags {"swap-unsupport-cmd"}} { + r config set swap-soft-block-cmd-enabled yes + + test {stream write rejected when soft block enabled} { + catch {r xadd s * f v} err + assert_match "*not supported by swap*" $err + assert_equal 0 [r exists s] + assert_equal [get_info_property r Swap swap_cmd_stats reject_blocked] 1 + assert_equal [get_info_property r Swap swap_cmd_stats allowed_blocked] 0 + assert_equal [get_info_property r Swap swap_cmd_stats master_allowed] 0 + } + + test {swap_cmd_stats fields appear in info} { + set info [r info swap] + assert_match "*swap_cmd_stats:*" $info + assert_match "*reject_blocked=*" $info + assert_match "*allowed_blocked=*" $info + assert_match "*master_allowed=*" $info + } + + test {lua redis.call xadd rejected when soft block enabled} { + catch {r eval {return redis.call('xadd','s','*','f','v')} 0} err + assert_match "*not supported by swap*" $err + assert_equal 0 [r exists s] + assert_equal [get_info_property r Swap swap_cmd_stats reject_blocked] 2 + } + + test {lua redis.pcall xadd is blocked and the script can continue} { + assert_equal ok [r eval {redis.pcall('xadd','s','*','f','v'); redis.call('set','k','1'); return 'ok'} 0] + assert_equal 0 [r exists s] + assert_equal 1 [r exists k] + assert_equal [get_info_property r Swap swap_cmd_stats reject_blocked] 3 + } + + test {lua redis.call stops the script before the blocked xadd} { + catch {r eval {redis.call('set','k2','1'); return redis.call('xadd','s','*','f','v')} 0} err + assert_match "*not supported by swap*" $err + assert_equal 1 [r exists k2] + assert_equal 0 [r exists s] + assert_equal [get_info_property r Swap swap_cmd_stats reject_blocked] 4 + } + + test {lua evalsha xadd rejected when soft block enabled} { + set sha [r script load {return redis.call('xadd',KEYS[1],'*','f','v')}] + catch {r evalsha $sha 1 s} err + assert_match "*not supported by swap*" $err + assert_equal 0 [r exists s] + assert_equal [get_info_property r Swap swap_cmd_stats reject_blocked] 5 + } + + test {lua command without the soft-block flag is not counted} { + assert_equal PONG [r eval {return redis.call('ping')} 0] + assert_equal [get_info_property r Swap swap_cmd_stats reject_blocked] 5 + assert_equal [get_info_property r Swap swap_cmd_stats allowed_blocked] 0 + assert_equal [get_info_property r Swap swap_cmd_stats master_allowed] 0 + } + + test {multi xadd rejected when soft block enabled} { + r multi + catch {r xadd s * f v} err + assert_match "*not supported by swap*" $err + r discard + assert_equal 0 [r exists s] + assert_equal [get_info_property r Swap swap_cmd_stats reject_blocked] 6 + assert_equal [get_info_property r Swap swap_cmd_stats allowed_blocked] 0 + assert_equal [get_info_property r Swap swap_cmd_stats master_allowed] 0 + } + + test {multi command without the soft-block flag is not counted} { + r multi + r set k3 1 + r exec + assert_equal 1 [r get k3] + assert_equal [get_info_property r Swap swap_cmd_stats reject_blocked] 6 + assert_equal [get_info_property r Swap swap_cmd_stats allowed_blocked] 0 + assert_equal [get_info_property r Swap swap_cmd_stats master_allowed] 0 + } +} + +start_server {tags {"swap-unsupport-cmd"}} { + r config set swap-soft-block-cmd-enabled no + + test {stream write allowed when soft block disabled is counted} { + r xadd s * f v + r xadd s * f v + assert_equal [r xlen s] 2 + assert_equal [get_info_property r Swap swap_cmd_stats allowed_blocked] 2 + assert_equal [get_info_property r Swap swap_cmd_stats reject_blocked] 0 + assert_equal [get_info_property r Swap swap_cmd_stats master_allowed] 0 + } + + test {lua stream write allowed when soft block disabled is counted} { + r eval {return redis.call('xadd','slua','*','f','v')} 0 + r eval {return redis.pcall('xadd','slua','*','f','v')} 0 + assert_equal [r xlen slua] 2 + assert_equal [get_info_property r Swap swap_cmd_stats allowed_blocked] 4 + assert_equal [get_info_property r Swap swap_cmd_stats reject_blocked] 0 + assert_equal [get_info_property r Swap swap_cmd_stats master_allowed] 0 + } + + test {multi stream write allowed when soft block disabled is counted} { + r multi + r xadd smulti * f v + r xadd smulti * f v + r exec + assert_equal [r xlen smulti] 2 + assert_equal [get_info_property r Swap swap_cmd_stats allowed_blocked] 6 + assert_equal [get_info_property r Swap swap_cmd_stats reject_blocked] 0 + assert_equal [get_info_property r Swap swap_cmd_stats master_allowed] 0 + } +} + +start_server {tags {"swap-unsupport-cmd"} overrides {save ""}} { + set master [srv 0 client] + set master_host [srv 0 host] + set master_port [srv 0 port] + + start_server {tags {"swap-unsupport-cmd"} overrides {save ""}} { + set slave [srv 0 client] + $slave slaveof $master_host $master_port + wait_for_sync $slave + + # Master accepts the write so it is replicated. + # Slave rejects the same write from a local client. + $master config set swap-soft-block-cmd-enabled no + $slave config set swap-soft-block-cmd-enabled yes + $slave config set replica-read-only no + + test {master stream write is applied on slave and counted as master_allowed} { + $master xadd s * f v + wait_for_ofs_sync $master $slave + + assert_equal [$slave xlen s] 1 + assert_equal [get_info_property $slave Swap swap_cmd_stats master_allowed] 1 + assert_equal [get_info_property $slave Swap swap_cmd_stats reject_blocked] 0 + assert_equal [get_info_property $slave Swap swap_cmd_stats allowed_blocked] 0 + assert_equal [get_info_property $master Swap swap_cmd_stats allowed_blocked] 1 + assert_equal [get_info_property $master Swap swap_cmd_stats reject_blocked] 0 + assert_equal [get_info_property $master Swap swap_cmd_stats master_allowed] 0 + + catch {$slave xadd s * f local} err + assert_match "*not supported by swap*" $err + assert_equal [get_info_property $slave Swap swap_cmd_stats reject_blocked] 1 + assert_equal [get_info_property $slave Swap swap_cmd_stats allowed_blocked] 0 + assert_equal [get_info_property $slave Swap swap_cmd_stats master_allowed] 1 + assert_equal [$slave xlen s] 1 + } + + test {lua xadd from master is applied on slave and counted as master_allowed} { + $master eval {return redis.call('xadd','slua','*','f','v')} 0 + wait_for_ofs_sync $master $slave + + assert_equal [$slave xlen slua] 1 + assert_equal [get_info_property $slave Swap swap_cmd_stats master_allowed] 2 + assert_equal [get_info_property $slave Swap swap_cmd_stats reject_blocked] 1 + assert_equal [get_info_property $slave Swap swap_cmd_stats allowed_blocked] 0 + assert_equal [get_info_property $master Swap swap_cmd_stats allowed_blocked] 2 + assert_equal [get_info_property $master Swap swap_cmd_stats reject_blocked] 0 + assert_equal [get_info_property $master Swap swap_cmd_stats master_allowed] 0 + + catch {$slave eval {return redis.call('xadd','slua','*','f','local')} 0} err + assert_match "*not supported by swap*" $err + assert_equal [get_info_property $slave Swap swap_cmd_stats reject_blocked] 2 + assert_equal [get_info_property $slave Swap swap_cmd_stats allowed_blocked] 0 + assert_equal [get_info_property $slave Swap swap_cmd_stats master_allowed] 2 + assert_equal [$slave xlen slua] 1 + } + + test {multi xadd from master is applied on slave and counted as master_allowed} { + $master multi + $master xadd smulti * f v + $master exec + wait_for_ofs_sync $master $slave + + assert_equal [$slave xlen smulti] 1 + assert_equal [get_info_property $slave Swap swap_cmd_stats master_allowed] 3 + assert_equal [get_info_property $slave Swap swap_cmd_stats reject_blocked] 2 + assert_equal [get_info_property $slave Swap swap_cmd_stats allowed_blocked] 0 + assert_equal [get_info_property $master Swap swap_cmd_stats allowed_blocked] 3 + assert_equal [get_info_property $master Swap swap_cmd_stats reject_blocked] 0 + assert_equal [get_info_property $master Swap swap_cmd_stats master_allowed] 0 + + $slave multi + catch {$slave xadd smulti * f local} err + assert_match "*not supported by swap*" $err + $slave discard + assert_equal [get_info_property $slave Swap swap_cmd_stats reject_blocked] 3 + assert_equal [get_info_property $slave Swap swap_cmd_stats allowed_blocked] 0 + assert_equal [get_info_property $slave Swap swap_cmd_stats master_allowed] 3 + assert_equal [$slave xlen smulti] 1 + } + + test {hard blocked hexpire is rejected on master and is not replicated} { + $master hset h f v + wait_for_ofs_sync $master $slave + assert_equal v [$slave hget h f] + + catch {$master hexpire h 100 FIELDS 1 f} err + assert_match "*not supported by swap*" $err + assert_equal [get_info_property $master Swap swap_cmd_stats reject_blocked] 1 + assert_equal [get_info_property $master Swap swap_cmd_stats allowed_blocked] 3 + assert_equal [get_info_property $master Swap swap_cmd_stats master_allowed] 0 + assert_equal v [$master hget h f] + assert_equal v [$slave hget h f] + assert_equal [get_info_property $slave Swap swap_cmd_stats master_allowed] 3 + assert_equal [get_info_property $slave Swap swap_cmd_stats reject_blocked] 3 + assert_equal [get_info_property $slave Swap swap_cmd_stats allowed_blocked] 0 + + catch {$slave hexpire h 100 FIELDS 1 f} err + assert_match "*not supported by swap*" $err + assert_equal [get_info_property $slave Swap swap_cmd_stats reject_blocked] 4 + assert_equal [get_info_property $slave Swap swap_cmd_stats allowed_blocked] 0 + assert_equal [get_info_property $slave Swap swap_cmd_stats master_allowed] 3 + assert_equal v [$slave hget h f] + } + } +} + +# Hash field expiry commands are hard blocked: a local client is rejected +# whether or not swap-soft-block-cmd-enabled is on. Replication from master +# and AOF replay are the only paths that still apply them. +proc assert_hash_field_expiry_hard_blocked {r key} { + set cmds [list \ + [list hexpire $key 100 FIELDS 1 f] \ + [list hpexpire $key 100000 FIELDS 1 f] \ + [list hexpireat $key 2000000000 FIELDS 1 f] \ + [list hpexpireat $key 2000000000000 FIELDS 1 f] \ + [list hpersist $key FIELDS 1 f] \ + [list hsetex $key EX 10 FIELDS 1 f v2] \ + [list hgetex $key EX 10 FIELDS 1 f]] + set before [get_info_property $r Swap swap_cmd_stats reject_blocked] + foreach cmd $cmds { + catch {$r {*}$cmd} err + assert_match "*not supported by swap*" $err + } + assert_equal [expr {$before + [llength $cmds]}] \ + [get_info_property $r Swap swap_cmd_stats reject_blocked] + assert_equal v [$r hget $key f] +} + +start_server {tags {"swap-unsupport-cmd"}} { + r config set swap-soft-block-cmd-enabled yes + + test {hash field expiry commands are hard blocked when soft block enabled} { + r hset h f v + assert_hash_field_expiry_hard_blocked r h + assert_equal [get_info_property r Swap swap_cmd_stats reject_blocked] 7 + assert_equal [get_info_property r Swap swap_cmd_stats allowed_blocked] 0 + assert_equal [get_info_property r Swap swap_cmd_stats master_allowed] 0 + } + + test {lua redis.call hexpire rejected when soft block enabled} { + r hset hlua f v + catch {r eval {return redis.call('hexpire','hlua','100','FIELDS','1','f')} 0} err + assert_match "*not supported by swap*" $err + assert_equal v [r hget hlua f] + assert_equal [get_info_property r Swap swap_cmd_stats reject_blocked] 8 + } + + test {lua redis.pcall hsetex is blocked and the script can continue} { + r hset hpcall f v + assert_equal ok [r eval {redis.pcall('hsetex','hpcall','EX','10','FIELDS','1','f','v2'); redis.call('set','kpcall','1'); return 'ok'} 0] + assert_equal v [r hget hpcall f] + assert_equal 1 [r get kpcall] + assert_equal [get_info_property r Swap swap_cmd_stats reject_blocked] 9 + } + + test {lua redis.call stops the script before the blocked hexpire} { + r hset hstop f v + catch {r eval {redis.call('set','kstop','1'); return redis.call('hpersist','hstop','FIELDS','1','f')} 0} err + assert_match "*not supported by swap*" $err + assert_equal 1 [r get kstop] + assert_equal v [r hget hstop f] + assert_equal [get_info_property r Swap swap_cmd_stats reject_blocked] 10 + } + + test {multi hexpire rejected when soft block enabled} { + r hset hmulti f v + r multi + catch {r hexpire hmulti 100 FIELDS 1 f} err + assert_match "*not supported by swap*" $err + r discard + assert_equal v [r hget hmulti f] + assert_equal [get_info_property r Swap swap_cmd_stats reject_blocked] 11 + assert_equal [get_info_property r Swap swap_cmd_stats allowed_blocked] 0 + assert_equal [get_info_property r Swap swap_cmd_stats master_allowed] 0 + } +} + +start_server {tags {"swap-unsupport-cmd"}} { + r config set swap-soft-block-cmd-enabled no + + test {hash field expiry commands stay rejected when soft block disabled} { + r hset h f v + r xadd s * f v + assert_equal [get_info_property r Swap swap_cmd_stats allowed_blocked] 1 + assert_equal [get_info_property r Swap swap_cmd_stats reject_blocked] 0 + + assert_hash_field_expiry_hard_blocked r h + assert_equal [get_info_property r Swap swap_cmd_stats reject_blocked] 7 + assert_equal [get_info_property r Swap swap_cmd_stats allowed_blocked] 1 + assert_equal [get_info_property r Swap swap_cmd_stats master_allowed] 0 + assert_equal [r xlen s] 1 + } + + test {lua hexpire stays rejected when soft block disabled} { + catch {r eval {return redis.call('hexpire','h','100','FIELDS','1','f')} 0} err + assert_match "*not supported by swap*" $err + assert_equal ok [r eval {redis.pcall('hgetex','h','EX','10','FIELDS','1','f'); return 'ok'} 0] + assert_equal v [r hget h f] + assert_equal [get_info_property r Swap swap_cmd_stats reject_blocked] 9 + assert_equal [get_info_property r Swap swap_cmd_stats allowed_blocked] 1 + } + + test {multi hexpire stays rejected when soft block disabled} { + r multi + catch {r hsetex h EX 10 FIELDS 1 f v2} err + assert_match "*not supported by swap*" $err + r discard + assert_equal v [r hget h f] + assert_equal [get_info_property r Swap swap_cmd_stats reject_blocked] 10 + assert_equal [get_info_property r Swap swap_cmd_stats allowed_blocked] 1 + assert_equal [get_info_property r Swap swap_cmd_stats master_allowed] 0 + } +} \ No newline at end of file diff --git a/tests/unit/moduleapi/hash.tcl b/tests/unit/moduleapi/hash.tcl index bb866bbff07..086c566ed62 100644 --- a/tests/unit/moduleapi/hash.tcl +++ b/tests/unit/moduleapi/hash.tcl @@ -34,7 +34,7 @@ start_server {tags {"modules"}} { assert_equal 0 [r hash.set H2 "n" f1 yy] assert_equal "f1 f2 v2 yy" [lsort [r hgetall H2]] r debug set-active-expire 1 - } {OK} {needs:debug} + } {OK} {needs:debug memonly} test {Module hash - set XX of expired field gets failed as expected} { r debug set-active-expire 0 @@ -58,7 +58,7 @@ start_server {tags {"modules"}} { assert_equal 2 [r hlen H2] r debug set-active-expire 1 - } {OK} {needs:debug} + } {OK} {needs:debug memonly} test {Module hash - test open key with REDISMODULE_OPEN_KEY_ACCESS_EXPIRED to scan expired fields} { r debug set-active-expire 0 @@ -79,7 +79,7 @@ start_server {tags {"modules"}} { # Scan again expired fields with flag REDISMODULE_OPEN_KEY_ACCESS_EXPIRED assert_equal "f3 v3" [lsort [r hash.hscan_expired H1]] r debug set-active-expire 1 - } + } {undefined} {memonly} test {Module hash - test open key with REDISMODULE_OPEN_KEY_ACCESS_EXPIRED to scan expired key} { r debug set-active-expire 0 @@ -115,7 +115,7 @@ start_server {tags {"modules"}} { # Assert that variadic version of hget_expire works as well assert_equal [r hash.hget_two_expire H1 f1 f2] [list [r hash.hget_expire H1 f1] [r hash.hget_expire H1 f2]] - } + } {undefined} {memonly} test {Module hash - Read minimum expiration time} { r del H1 @@ -136,7 +136,7 @@ start_server {tags {"modules"}} { r del H2 r hash.set H2 "n" f1 v1 assert_equal [r hash.hget_min_expire H2] -1 - } + } {undefined} {memonly} test {Module hash - KEYSIZES is updated as expected} { proc run_cmd_verify_hist {cmd expOutput {retries 1}} { @@ -158,16 +158,19 @@ start_server {tags {"modules"}} { run_cmd_verify_hist {r hash.set H1 "n" f2 v2} {db0_HASH:1=1,2=1} run_cmd_verify_hist {r hash.set H1 "a" f3 v3 f4 v4} {db0_HASH:1=1,4=1} run_cmd_verify_hist {r del H1} {db0_HASH:1=1} - run_cmd_verify_hist {r del H2} {} - - # Check lazy expire - r debug set-active-expire 0 - run_cmd_verify_hist {r hash.set H1 "n" f1 v1} {db0_HASH:1=1} - run_cmd_verify_hist {r hpexpire H1 1 FIELDS 1 f1} {db0_HASH:1=1} - run_cmd_verify_hist {after 5} {db0_HASH:1=1} - run_cmd_verify_hist {r hash.hget_expired H1 f1} {db0_HASH:1=1} - r debug set-active-expire 1 - run_cmd_verify_hist {after 5} {} 50 + run_cmd_verify_hist {r del H2} {} + + # Field expiry is hard blocked on swap. Histogram checks above still run. + if {!$::swap} { + # Check lazy expire + r debug set-active-expire 0 + run_cmd_verify_hist {r hash.set H1 "n" f1 v1} {db0_HASH:1=1} + run_cmd_verify_hist {r hpexpire H1 1 FIELDS 1 f1} {db0_HASH:1=1} + run_cmd_verify_hist {after 5} {db0_HASH:1=1} + run_cmd_verify_hist {r hash.hget_expired H1 f1} {db0_HASH:1=1} + r debug set-active-expire 1 + run_cmd_verify_hist {after 5} {} 50 + } } test "Unload the module - hash" { diff --git a/tests/unit/moduleapi/scan.tcl b/tests/unit/moduleapi/scan.tcl index 6c26de4438c..63290f9edd0 100644 --- a/tests/unit/moduleapi/scan.tcl +++ b/tests/unit/moduleapi/scan.tcl @@ -34,7 +34,7 @@ start_server {tags {"modules"}} { assert_encoding listpackex hh r debug set-active-expire 1 lsort [r scan.scan_key hh] - } {{f1 v1} {f2 v2}} {needs:debug} + } {{f1 v1} {f2 v2}} {needs:debug memonly} test {Module scan hash dict} { r config set hash-max-ziplist-entries 2 @@ -53,7 +53,7 @@ start_server {tags {"modules"}} { assert_encoding hashtable hh after 10 lsort [r scan.scan_key hh] - } {{f1 v1} {f2 v2}} + } {{f1 v1} {f2 v2}} {memonly} test {Module scan hash with hexpire can return no items} { r del hh @@ -63,7 +63,7 @@ start_server {tags {"modules"}} { after 10 assert_equal [r scan.scan_key hh] {} r debug set-active-expire 1 - } {OK} {needs:debug} + } {OK} {needs:debug memonly} test {Module scan zset listpack} { r zadd zz 1 f1 2 f2 diff --git a/tests/unit/pubsub.tcl b/tests/unit/pubsub.tcl index 1262e4ad1c8..f237501743f 100644 --- a/tests/unit/pubsub.tcl +++ b/tests/unit/pubsub.tcl @@ -631,7 +631,7 @@ start_server {tags {"pubsub network"}} { r debug set-active-expire 1 $rd1 close - } {0} {needs:debug} + } {0} {needs:debug memonly} test "Keyspace notifications: expired events (triggered expire)" { r config set notify-keyspace-events Ex diff --git a/tests/unit/type/stream-cgroups.tcl b/tests/unit/type/stream-cgroups.tcl index e24985ac16a..ec98e965982 100644 --- a/tests/unit/type/stream-cgroups.tcl +++ b/tests/unit/type/stream-cgroups.tcl @@ -564,13 +564,22 @@ start_server { r XREADGROUP GROUP mygroup consumer1 STREAMS mystream > assert {[lindex [r XPENDING mystream mygroup] 0] == 5} - # Try to delete a message with ACKED - should fail because both groups have references - assert_equal {2 2 2 2 2} [r XDELEX mystream ACKED IDS 5 0-1 1-1 2-1 3-1 4-1] + # XDELEX is hard blocked on swap. Non-swap still checks that ACKED + # cannot delete while the group holds references. + if {!$::swap} { + # Try to delete a message with ACKED - should fail because both groups have references + assert_equal {2 2 2 2 2} [r XDELEX mystream ACKED IDS 5 0-1 1-1 2-1 3-1 4-1] + } # Destroy one consumer group, and then we can delete all the entries with ACKED. r XGROUP DESTROY mystream mygroup - assert_equal {1 1 1 1 1} [r XDELEX mystream ACKED IDS 5 0-1 1-1 2-1 3-1 4-1] - assert_equal 0 [r XLEN mystream] + if {!$::swap} { + assert_equal {1 1 1 1 1} [r XDELEX mystream ACKED IDS 5 0-1 1-1 2-1 3-1 4-1] + assert_equal 0 [r XLEN mystream] + } else { + assert_equal {} [r XINFO GROUPS mystream] + assert_equal 5 [r XLEN mystream] + } } test {RENAME can unblock XREADGROUP with data} { @@ -1531,7 +1540,7 @@ start_server { } } - start_server {} { + start_server {tags {memonly}} { test "XACKDEL wrong number of args" { assert_error {*wrong number of arguments for 'xackdel' command} {r XACKDEL} assert_error {*wrong number of arguments for 'xackdel' command} {r XACKDEL s} diff --git a/tests/unit/type/stream.tcl b/tests/unit/type/stream.tcl index 8f6630595c7..fb7889b5362 100644 --- a/tests/unit/type/stream.tcl +++ b/tests/unit/type/stream.tcl @@ -1140,7 +1140,7 @@ start_server {tags {"stream"}} { } } -start_server {tags {"stream"}} { +start_server {tags {"stream" memonly}} { test "XDELEX wrong number of args" { assert_error {*wrong number of arguments for 'xdelex' command} {r XDELEX s DELREF} }