Expose "touches-arbitrary-keys" flag to Redis modules (#14290)

This commit adds support for the "touches-arbitrary-keys" command flag
in Redis modules, allowing module commands to be properly marked when
they modify keys not explicitly provided as arguments, to avoid wrapping
replicated commands with MULTI/EXEC.

Changes:
- Added "touches-arbitrary-keys" flag parsing in
commandFlagsFromString()
- Updated module command documentation to describe the new flag
- Added test implementation in zset module with zset.delall command to
demonstrate and verify the flag functionality

The zset.delall command serves as a test case that scans the keyspace
and deletes all zset-type keys, properly using the new flag since it
modifies keys not provided via argv.

This commit adds a new `zset.delall` command to the zset test module
that iterates through the keyspace and deletes all keys of type "zset".

Key changes:
- Added zset_delall() function that uses RedisModule_Scan to iterate
through all keys in the keyspace
- Added zset_delall_callback() that checks each key's type and deletes
zset keys using RedisModule_Call with "DEL" command
- Registered the new command with "write touches-arbitrary-keys" flags
since it modifies arbitrary keys not provided via argv
- Added support for "touches-arbitrary-keys" flag in module command
parsing
- Added comprehensive tests for the new functionality

The command returns the number of deleted zset keys and properly handles
replication by using the "s!" format specifier with RedisModule_Call to
ensure DEL commands are replicated to slaves and AOF.

Usage: ZSET.DELALL
Returns: Integer count of deleted zset keys
This commit is contained in:
guybe7
2025-08-20 02:32:24 +03:00
committed by GitHub
parent b9d9d4000b
commit ca9ede6968
3 changed files with 142 additions and 2 deletions

View File

@@ -1165,7 +1165,8 @@ int64_t commandFlagsFromString(char *s) {
else if (!strcasecmp(t,"no-cluster")) flags |= CMD_MODULE_NO_CLUSTER;
else if (!strcasecmp(t,"no-mandatory-keys")) flags |= CMD_NO_MANDATORY_KEYS;
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,"internal")) flags |= (CMD_INTERNAL|CMD_NOSCRIPT); /* We also disallow internal commands in scripts. */
else if (!strcasecmp(t,"touches-arbitrary-keys")) flags |= CMD_TOUCHES_ARBITRARY_KEYS;
else break;
}
sdsfreesplitres(tokens,count);
@@ -1253,6 +1254,8 @@ RedisModuleCommand *moduleCreateCommandProxy(struct RedisModule *module, sds dec
* * **"internal"**: Internal command, one that should not be exposed to the user connections.
* For example, module commands that are called by the modules,
* commands that do not perform ACL validations (relying on earlier checks)
* * **"touches-arbitrary-keys"**: This command may modify arbitrary keys (i.e. not provided via argv).
* This flag is used so we don't wrap the replicated commands with MULTI/EXEC.
*
* The last three parameters specify which arguments of the new command are
* Redis keys. See https://redis.io/commands/command for more information.

View File

@@ -69,6 +69,91 @@ int zset_incrby(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
return RedisModule_ReplyWithError(ctx, "ERR ZsetIncrby failed");
}
/* Structure to hold data for the delall scan callback */
typedef struct {
RedisModuleCtx *ctx;
RedisModuleString **keys_to_delete;
size_t keys_capacity;
size_t keys_count;
} zset_delall_data;
/* Callback function for scanning keys and collecting zset keys to delete */
void zset_delall_callback(RedisModuleCtx *ctx, RedisModuleString *keyname, RedisModuleKey *key, void *privdata) {
zset_delall_data *data = privdata;
int was_opened = 0;
/* Open the key if it wasn't already opened */
if (!key) {
key = RedisModule_OpenKey(ctx, keyname, REDISMODULE_READ);
was_opened = 1;
}
/* Check if the key is a zset and add it to the list */
if (RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_ZSET) {
/* Expand the array if needed */
if (data->keys_count >= data->keys_capacity) {
data->keys_capacity = data->keys_capacity ? data->keys_capacity * 2 : 16;
data->keys_to_delete = RedisModule_Realloc(data->keys_to_delete,
data->keys_capacity * sizeof(RedisModuleString*));
}
/* Store the key name (retain it so it doesn't get freed) */
data->keys_to_delete[data->keys_count] = keyname;
RedisModule_RetainString(ctx, keyname);
data->keys_count++;
}
/* Close the key if we opened it */
if (was_opened) {
RedisModule_CloseKey(key);
}
}
/* ZSET.DELALL
*
* Iterates through the keyspace and deletes all keys of type "zset".
* Returns the number of deleted keys.
*/
int zset_delall(RedisModuleCtx *ctx, REDISMODULE_ATTR_UNUSED RedisModuleString **argv, int argc) {
if (argc != 1) return RedisModule_WrongArity(ctx);
RedisModule_AutoMemory(ctx);
zset_delall_data data = {
.ctx = ctx,
.keys_to_delete = NULL,
.keys_capacity = 0,
.keys_count = 0
};
/* Create a scan cursor and iterate through all keys */
RedisModuleScanCursor *cursor = RedisModule_ScanCursorCreate();
while (RedisModule_Scan(ctx, cursor, zset_delall_callback, &data));
RedisModule_ScanCursorDestroy(cursor);
/* Delete all the collected zset keys after scan is complete */
size_t deleted_count = 0;
for (size_t i = 0; i < data.keys_count; i++) {
RedisModuleCallReply *reply = RedisModule_Call(ctx, "DEL", "s!", data.keys_to_delete[i]);
if (reply && RedisModule_CallReplyType(reply) == REDISMODULE_REPLY_INTEGER) {
long long del_result = RedisModule_CallReplyInteger(reply);
if (del_result > 0) {
deleted_count++;
}
}
if (reply) {
RedisModule_FreeCallReply(reply);
}
RedisModule_FreeString(ctx, data.keys_to_delete[i]);
}
/* Free the keys array */
if (data.keys_to_delete) {
RedisModule_Free(data.keys_to_delete);
}
return RedisModule_ReplyWithLongLong(ctx, deleted_count);
}
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
REDISMODULE_NOT_USED(argv);
REDISMODULE_NOT_USED(argc);
@@ -87,5 +172,9 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
1, 1, 1) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx, "zset.delall", zset_delall, "write touches-arbitrary-keys",
0, 0, 0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
return REDISMODULE_OK;
}

View File

@@ -66,7 +66,55 @@ start_server {tags {"modules"}} {
r debug set-active-expire 1
run_cmd_verify_hist {after 5} {} 50
}
test {Module zset DELALL functionality} {
# Clean up any existing keys
r flushall
# Create some zsets and other types of keys
r zadd zset1 100 hello 200 world
r zadd zset2 300 foo 400 bar
r zadd zset3 500 baz
r set string1 "value1"
r hset hash1 field1 value1
r lpush list1 item1
# Verify we have the expected keys
assert_equal 6 [r dbsize]
assert_equal 3 [llength [r keys zset*]]
# Run zset.delall
set deleted [r zset.delall]
assert_equal 3 $deleted
# Verify only zsets were deleted
assert_equal 3 [r dbsize]
assert_equal 0 [llength [r keys zset*]]
assert_equal 1 [r exists string1]
assert_equal 1 [r exists hash1]
assert_equal 1 [r exists list1]
# Test with no zsets
set deleted [r zset.delall]
assert_equal 0 $deleted
assert_equal 3 [r dbsize]
}
test {Module zset DELALL not in transaction} {
set repl [attach_to_replication_stream]
r zadd z1 1 e1
r zadd z2 1 e1
r zset.delall
assert_replication_stream $repl {
{select *}
{zadd z1 1 e1}
{zadd z2 1 e1}
{del z*}
{del z*}
}
close_replication_stream $repl
} {} {needs:repl}
test "Unload the module - zset" {
assert_equal {OK} [r module unload zset]
}