mirror of
https://github.com/redis/redis.git
synced 2026-01-10 10:08:09 -05:00
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:
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user