summaryrefslogtreecommitdiff
path: root/examples/redis-unstable/tests/modules
diff options
context:
space:
mode:
Diffstat (limited to 'examples/redis-unstable/tests/modules')
-rw-r--r--examples/redis-unstable/tests/modules/Makefile106
-rw-r--r--examples/redis-unstable/tests/modules/aclcheck.c365
-rw-r--r--examples/redis-unstable/tests/modules/atomicslotmigration.c594
-rw-r--r--examples/redis-unstable/tests/modules/auth.c286
-rw-r--r--examples/redis-unstable/tests/modules/basics.c1051
-rw-r--r--examples/redis-unstable/tests/modules/blockedclient.c723
-rw-r--r--examples/redis-unstable/tests/modules/blockonbackground.c333
-rw-r--r--examples/redis-unstable/tests/modules/blockonkeys.c645
-rw-r--r--examples/redis-unstable/tests/modules/cmdintrospection.c226
-rw-r--r--examples/redis-unstable/tests/modules/commandfilter.c251
-rw-r--r--examples/redis-unstable/tests/modules/configaccess.c353
-rw-r--r--examples/redis-unstable/tests/modules/crash.c311
-rw-r--r--examples/redis-unstable/tests/modules/datatype.c323
-rw-r--r--examples/redis-unstable/tests/modules/datatype2.c739
-rw-r--r--examples/redis-unstable/tests/modules/defragtest.c475
-rw-r--r--examples/redis-unstable/tests/modules/eventloop.c276
-rw-r--r--examples/redis-unstable/tests/modules/fork.c96
-rw-r--r--examples/redis-unstable/tests/modules/getchannels.c69
-rw-r--r--examples/redis-unstable/tests/modules/getkeys.c178
-rw-r--r--examples/redis-unstable/tests/modules/hash.c244
-rw-r--r--examples/redis-unstable/tests/modules/hooks.c496
-rw-r--r--examples/redis-unstable/tests/modules/infotest.c119
-rw-r--r--examples/redis-unstable/tests/modules/internalsecret.c154
-rw-r--r--examples/redis-unstable/tests/modules/keyspace_events.c479
-rw-r--r--examples/redis-unstable/tests/modules/keyspecs.c236
-rw-r--r--examples/redis-unstable/tests/modules/list.c252
-rw-r--r--examples/redis-unstable/tests/modules/mallocsize.c237
-rw-r--r--examples/redis-unstable/tests/modules/misc.c642
-rw-r--r--examples/redis-unstable/tests/modules/moduleauthtwo.c43
-rw-r--r--examples/redis-unstable/tests/modules/moduleconfigs.c287
-rw-r--r--examples/redis-unstable/tests/modules/moduleconfigstwo.c39
-rw-r--r--examples/redis-unstable/tests/modules/postnotifications.c289
-rw-r--r--examples/redis-unstable/tests/modules/propagate.c403
-rw-r--r--examples/redis-unstable/tests/modules/publish.c57
-rw-r--r--examples/redis-unstable/tests/modules/rdbloadsave.c162
-rw-r--r--examples/redis-unstable/tests/modules/reply.c214
-rw-r--r--examples/redis-unstable/tests/modules/scan.c121
-rw-r--r--examples/redis-unstable/tests/modules/stream.c258
-rw-r--r--examples/redis-unstable/tests/modules/subcommands.c119
-rw-r--r--examples/redis-unstable/tests/modules/test_keymeta.c585
-rw-r--r--examples/redis-unstable/tests/modules/test_lazyfree.c196
-rw-r--r--examples/redis-unstable/tests/modules/testrdb.c405
-rw-r--r--examples/redis-unstable/tests/modules/timer.c102
-rw-r--r--examples/redis-unstable/tests/modules/usercall.c230
-rw-r--r--examples/redis-unstable/tests/modules/zset.c180
45 files changed, 13949 insertions, 0 deletions
diff --git a/examples/redis-unstable/tests/modules/Makefile b/examples/redis-unstable/tests/modules/Makefile
new file mode 100644
index 0000000..f141234
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/Makefile
@@ -0,0 +1,106 @@
+
+# find the OS
+uname_S := $(shell sh -c 'uname -s 2>/dev/null || echo not')
+
+warning_cflags = -W -Wall -Wno-missing-field-initializers
+ifeq ($(uname_S),Darwin)
+ SHOBJ_CFLAGS ?= $(warning_cflags) -dynamic -fno-common -g -ggdb -std=gnu11 -O2
+ SHOBJ_LDFLAGS ?= -bundle -undefined dynamic_lookup
+else # Linux, others
+ SHOBJ_CFLAGS ?= $(warning_cflags) -fno-common -g -ggdb -std=gnu11 -O2
+ SHOBJ_LDFLAGS ?= -shared
+endif
+
+CLANG := $(findstring clang,$(shell sh -c '$(CC) --version | head -1'))
+
+ifeq ($(SANITIZER),memory)
+ifeq (clang, $(CLANG))
+ LD=clang
+ MALLOC=libc
+ CFLAGS+=-fsanitize=memory -fsanitize-memory-track-origins=2 -fno-sanitize-recover=all -fno-omit-frame-pointer
+ LDFLAGS+=-fsanitize=memory
+else
+ $(error "MemorySanitizer needs to be compiled and linked with clang. Please use CC=clang")
+endif
+endif
+
+
+# This is a hack to override the default CC. When running with SANITIZER=memory
+# tough we want to keep the compiler as clang as MSan is not supported for gcc
+ifeq ($(uname_S),Linux)
+ifneq ($(SANITIZER),memory)
+ LD = gcc
+ CC = gcc
+endif
+endif
+
+# OS X 11.x doesn't have /usr/lib/libSystem.dylib and needs an explicit setting.
+ifeq ($(uname_S),Darwin)
+ifeq ("$(wildcard /usr/lib/libSystem.dylib)","")
+LIBS = -L /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib -lsystem
+endif
+endif
+
+TEST_MODULES = \
+ commandfilter.so \
+ basics.so \
+ testrdb.so \
+ fork.so \
+ infotest.so \
+ propagate.so \
+ misc.so \
+ hooks.so \
+ blockonkeys.so \
+ blockonbackground.so \
+ scan.so \
+ datatype.so \
+ datatype2.so \
+ auth.so \
+ keyspace_events.so \
+ blockedclient.so \
+ getkeys.so \
+ getchannels.so \
+ test_lazyfree.so \
+ timer.so \
+ defragtest.so \
+ keyspecs.so \
+ hash.so \
+ zset.so \
+ stream.so \
+ mallocsize.so \
+ aclcheck.so \
+ list.so \
+ subcommands.so \
+ reply.so \
+ cmdintrospection.so \
+ eventloop.so \
+ moduleconfigs.so \
+ moduleconfigstwo.so \
+ publish.so \
+ usercall.so \
+ postnotifications.so \
+ moduleauthtwo.so \
+ rdbloadsave.so \
+ crash.so \
+ internalsecret.so \
+ configaccess.so \
+ test_keymeta.so \
+ atomicslotmigration.so
+
+.PHONY: all
+
+all: $(TEST_MODULES)
+
+32bit:
+ $(MAKE) CFLAGS="-m32" LDFLAGS="-m32"
+
+%.xo: %.c ../../src/redismodule.h
+ $(CC) -I../../src $(CFLAGS) $(SHOBJ_CFLAGS) -fPIC -c $< -o $@
+
+%.so: %.xo
+ $(LD) -o $@ $^ $(SHOBJ_LDFLAGS) $(LDFLAGS) $(LIBS)
+
+.PHONY: clean
+
+clean:
+ rm -f $(TEST_MODULES) $(TEST_MODULES:.so=.xo)
diff --git a/examples/redis-unstable/tests/modules/aclcheck.c b/examples/redis-unstable/tests/modules/aclcheck.c
new file mode 100644
index 0000000..d7e4e3b
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/aclcheck.c
@@ -0,0 +1,365 @@
+
+#include "redismodule.h"
+#include <errno.h>
+#include <assert.h>
+#include <string.h>
+#include <strings.h>
+
+/* A wrap for SET command with ACL check on the key. */
+int set_aclcheck_key(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc < 4) {
+ return RedisModule_WrongArity(ctx);
+ }
+
+ int permissions;
+ const char *flags = RedisModule_StringPtrLen(argv[1], NULL);
+
+ if (!strcasecmp(flags, "W")) {
+ permissions = REDISMODULE_CMD_KEY_UPDATE;
+ } else if (!strcasecmp(flags, "R")) {
+ permissions = REDISMODULE_CMD_KEY_ACCESS;
+ } else if (!strcasecmp(flags, "*")) {
+ permissions = REDISMODULE_CMD_KEY_UPDATE | REDISMODULE_CMD_KEY_ACCESS;
+ } else if (!strcasecmp(flags, "~")) {
+ permissions = 0; /* Requires either read or write */
+ } else {
+ RedisModule_ReplyWithError(ctx, "INVALID FLAGS");
+ return REDISMODULE_OK;
+ }
+
+ /* Check that the key can be accessed */
+ RedisModuleString *user_name = RedisModule_GetCurrentUserName(ctx);
+ RedisModuleUser *user = RedisModule_GetModuleUserFromUserName(user_name);
+ int ret = RedisModule_ACLCheckKeyPermissions(user, argv[2], permissions);
+ if (ret != 0) {
+ RedisModule_ReplyWithError(ctx, "DENIED KEY");
+ RedisModule_FreeModuleUser(user);
+ RedisModule_FreeString(ctx, user_name);
+ return REDISMODULE_OK;
+ }
+
+ RedisModuleCallReply *rep = RedisModule_Call(ctx, "SET", "v", argv + 2, (size_t)argc - 2);
+ if (!rep) {
+ RedisModule_ReplyWithError(ctx, "NULL reply returned");
+ } else {
+ RedisModule_ReplyWithCallReply(ctx, rep);
+ RedisModule_FreeCallReply(rep);
+ }
+
+ RedisModule_FreeModuleUser(user);
+ RedisModule_FreeString(ctx, user_name);
+ return REDISMODULE_OK;
+}
+
+/* A wrap for SET command with ACL check on the key. */
+int set_aclcheck_prefixkey(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc < 4) {
+ return RedisModule_WrongArity(ctx);
+ }
+
+ int permissions;
+ const char *flags = RedisModule_StringPtrLen(argv[1], NULL);
+
+ if (!strcasecmp(flags, "W")) {
+ permissions = REDISMODULE_CMD_KEY_UPDATE;
+ } else if (!strcasecmp(flags, "R")) {
+ permissions = REDISMODULE_CMD_KEY_ACCESS;
+ } else if (!strcasecmp(flags, "*")) {
+ permissions = REDISMODULE_CMD_KEY_UPDATE | REDISMODULE_CMD_KEY_ACCESS;
+ } else if (!strcasecmp(flags, "~")) {
+ permissions = 0; /* Requires either read or write */
+ } else {
+ RedisModule_ReplyWithError(ctx, "INVALID FLAGS");
+ return REDISMODULE_OK;
+ }
+
+ /* Check that the key can be accessed */
+ RedisModuleString *user_name = RedisModule_GetCurrentUserName(ctx);
+ RedisModuleUser *user = RedisModule_GetModuleUserFromUserName(user_name);
+ int ret = RedisModule_ACLCheckKeyPrefixPermissions(user, argv[2], permissions);
+ if (ret != 0) {
+ RedisModule_ReplyWithError(ctx, "DENIED KEY");
+ RedisModule_FreeModuleUser(user);
+ RedisModule_FreeString(ctx, user_name);
+ return REDISMODULE_OK;
+ }
+
+ RedisModuleCallReply *rep = RedisModule_Call(ctx, "SET", "v", argv + 3, (size_t)argc - 3);
+ if (!rep) {
+ RedisModule_ReplyWithError(ctx, "NULL reply returned");
+ } else {
+ RedisModule_ReplyWithCallReply(ctx, rep);
+ RedisModule_FreeCallReply(rep);
+ }
+
+ RedisModule_FreeModuleUser(user);
+ RedisModule_FreeString(ctx, user_name);
+ return REDISMODULE_OK;
+}
+
+/* A wrap for PUBLISH command with ACL check on the channel. */
+int publish_aclcheck_channel(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 3) {
+ return RedisModule_WrongArity(ctx);
+ }
+
+ /* Check that the pubsub channel can be accessed */
+ RedisModuleString *user_name = RedisModule_GetCurrentUserName(ctx);
+ RedisModuleUser *user = RedisModule_GetModuleUserFromUserName(user_name);
+ int ret = RedisModule_ACLCheckChannelPermissions(user, argv[1], REDISMODULE_CMD_CHANNEL_SUBSCRIBE);
+ if (ret != 0) {
+ RedisModule_ReplyWithError(ctx, "DENIED CHANNEL");
+ RedisModule_FreeModuleUser(user);
+ RedisModule_FreeString(ctx, user_name);
+ return REDISMODULE_OK;
+ }
+
+ RedisModuleCallReply *rep = RedisModule_Call(ctx, "PUBLISH", "v", argv + 1, (size_t)argc - 1);
+ if (!rep) {
+ RedisModule_ReplyWithError(ctx, "NULL reply returned");
+ } else {
+ RedisModule_ReplyWithCallReply(ctx, rep);
+ RedisModule_FreeCallReply(rep);
+ }
+
+ RedisModule_FreeModuleUser(user);
+ RedisModule_FreeString(ctx, user_name);
+ return REDISMODULE_OK;
+}
+
+/* A wrap for RM_Call that check first that the command can be executed */
+int rm_call_aclcheck_cmd(RedisModuleCtx *ctx, RedisModuleUser *user, RedisModuleString **argv, int argc) {
+ if (argc < 2) {
+ return RedisModule_WrongArity(ctx);
+ }
+
+ /* Check that the command can be executed */
+ int ret = RedisModule_ACLCheckCommandPermissions(user, argv + 1, argc - 1);
+ if (ret != 0) {
+ RedisModule_ReplyWithError(ctx, "DENIED CMD");
+ /* Add entry to ACL log */
+ RedisModule_ACLAddLogEntry(ctx, user, argv[1], REDISMODULE_ACL_LOG_CMD);
+ return REDISMODULE_OK;
+ }
+
+ const char* cmd = RedisModule_StringPtrLen(argv[1], NULL);
+
+ RedisModuleCallReply* rep = RedisModule_Call(ctx, cmd, "v", argv + 2, (size_t)argc - 2);
+ if(!rep){
+ RedisModule_ReplyWithError(ctx, "NULL reply returned");
+ }else{
+ RedisModule_ReplyWithCallReply(ctx, rep);
+ RedisModule_FreeCallReply(rep);
+ }
+
+ return REDISMODULE_OK;
+}
+
+int rm_call_aclcheck_cmd_default_user(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ RedisModuleString *user_name = RedisModule_GetCurrentUserName(ctx);
+ RedisModuleUser *user = RedisModule_GetModuleUserFromUserName(user_name);
+
+ int res = rm_call_aclcheck_cmd(ctx, user, argv, argc);
+
+ RedisModule_FreeModuleUser(user);
+ RedisModule_FreeString(ctx, user_name);
+ return res;
+}
+
+int rm_call_aclcheck_cmd_module_user(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ /* Create a user and authenticate */
+ RedisModuleUser *user = RedisModule_CreateModuleUser("testuser1");
+ RedisModule_SetModuleUserACL(user, "allcommands");
+ RedisModule_SetModuleUserACL(user, "allkeys");
+ RedisModule_SetModuleUserACL(user, "on");
+ RedisModule_AuthenticateClientWithUser(ctx, user, NULL, NULL, NULL);
+
+ int res = rm_call_aclcheck_cmd(ctx, user, argv, argc);
+
+ /* authenticated back to "default" user (so once we free testuser1 we will not disconnected */
+ RedisModule_AuthenticateClientWithACLUser(ctx, "default", 7, NULL, NULL, NULL);
+ RedisModule_FreeModuleUser(user);
+ return res;
+}
+
+int rm_call_aclcheck_with_errors(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ if(argc < 2){
+ return RedisModule_WrongArity(ctx);
+ }
+
+ const char* cmd = RedisModule_StringPtrLen(argv[1], NULL);
+
+ RedisModuleCallReply* rep = RedisModule_Call(ctx, cmd, "vEC", argv + 2, (size_t)argc - 2);
+ RedisModule_ReplyWithCallReply(ctx, rep);
+ RedisModule_FreeCallReply(rep);
+ return REDISMODULE_OK;
+}
+
+/* A wrap for RM_Call that pass the 'C' flag to do ACL check on the command. */
+int rm_call_aclcheck(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ if(argc < 2){
+ return RedisModule_WrongArity(ctx);
+ }
+
+ const char* cmd = RedisModule_StringPtrLen(argv[1], NULL);
+
+ RedisModuleCallReply* rep = RedisModule_Call(ctx, cmd, "vC", argv + 2, (size_t)argc - 2);
+ if(!rep) {
+ char err[100];
+ switch (errno) {
+ case EACCES:
+ RedisModule_ReplyWithError(ctx, "ERR NOPERM");
+ break;
+ default:
+ snprintf(err, sizeof(err) - 1, "ERR errno=%d", errno);
+ RedisModule_ReplyWithError(ctx, err);
+ break;
+ }
+ } else {
+ RedisModule_ReplyWithCallReply(ctx, rep);
+ RedisModule_FreeCallReply(rep);
+ }
+
+ return REDISMODULE_OK;
+}
+
+int module_test_acl_category(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+int commandBlockCheck(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ int response_ok = 0;
+ int result = RedisModule_CreateCommand(ctx,"command.that.should.fail", module_test_acl_category, "", 0, 0, 0);
+ response_ok |= (result == REDISMODULE_OK);
+
+ result = RedisModule_AddACLCategory(ctx,"blockedcategory");
+ response_ok |= (result == REDISMODULE_OK);
+
+ RedisModuleCommand *parent = RedisModule_GetCommand(ctx,"block.commands.outside.onload");
+ result = RedisModule_SetCommandACLCategories(parent, "write");
+ response_ok |= (result == REDISMODULE_OK);
+
+ result = RedisModule_CreateSubcommand(parent,"subcommand.that.should.fail",module_test_acl_category,"",0,0,0);
+ response_ok |= (result == REDISMODULE_OK);
+
+ /* This validates that it's not possible to create commands or add
+ * a new ACL Category outside OnLoad function.
+ * thus returns an error if they succeed. */
+ if (response_ok) {
+ RedisModule_ReplyWithError(ctx, "UNEXPECTEDOK");
+ } else {
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ }
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+
+ if (RedisModule_Init(ctx,"aclcheck",1,REDISMODULE_APIVER_1)== REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (argc > 1) return RedisModule_WrongArity(ctx);
+
+ /* When that flag is passed, we try to create too many categories,
+ * and the test expects this to fail. In this case redis returns REDISMODULE_ERR
+ * and set errno to ENOMEM*/
+ if (argc == 1) {
+ long long fail_flag = 0;
+ RedisModule_StringToLongLong(argv[0], &fail_flag);
+ if (fail_flag) {
+ for (size_t j = 0; j < 45; j++) {
+ RedisModuleString* name = RedisModule_CreateStringPrintf(ctx, "customcategory%zu", j);
+ if (RedisModule_AddACLCategory(ctx, RedisModule_StringPtrLen(name, NULL)) == REDISMODULE_ERR) {
+ RedisModule_Assert(errno == ENOMEM);
+ RedisModule_FreeString(ctx, name);
+ return REDISMODULE_ERR;
+ }
+ RedisModule_FreeString(ctx, name);
+ }
+ }
+ }
+
+ if (RedisModule_CreateCommand(ctx,"aclcheck.set.check.key", set_aclcheck_key,"write",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"aclcheck.set.check.prefixkey", set_aclcheck_prefixkey,"write",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"block.commands.outside.onload", commandBlockCheck,"write",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"aclcheck.module.command.aclcategories.write", module_test_acl_category,"write",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ RedisModuleCommand *aclcategories_write = RedisModule_GetCommand(ctx,"aclcheck.module.command.aclcategories.write");
+
+ if (RedisModule_SetCommandACLCategories(aclcategories_write, "write") == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"aclcheck.module.command.aclcategories.write.function.read.category", module_test_acl_category,"write",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ RedisModuleCommand *read_category = RedisModule_GetCommand(ctx,"aclcheck.module.command.aclcategories.write.function.read.category");
+
+ if (RedisModule_SetCommandACLCategories(read_category, "read") == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"aclcheck.module.command.aclcategories.read.only.category", module_test_acl_category,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ RedisModuleCommand *read_only_category = RedisModule_GetCommand(ctx,"aclcheck.module.command.aclcategories.read.only.category");
+
+ if (RedisModule_SetCommandACLCategories(read_only_category, "read") == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"aclcheck.publish.check.channel", publish_aclcheck_channel,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"aclcheck.rm_call.check.cmd", rm_call_aclcheck_cmd_default_user,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"aclcheck.rm_call.check.cmd.module.user", rm_call_aclcheck_cmd_module_user,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"aclcheck.rm_call", rm_call_aclcheck,
+ "write",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"aclcheck.rm_call_with_errors", rm_call_aclcheck_with_errors,
+ "write",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ /* This validates that, when module tries to add a category with invalid characters,
+ * redis returns REDISMODULE_ERR and set errno to `EINVAL` */
+ if (RedisModule_AddACLCategory(ctx,"!nval!dch@r@cter$") == REDISMODULE_ERR)
+ RedisModule_Assert(errno == EINVAL);
+ else
+ return REDISMODULE_ERR;
+
+ /* This validates that, when module tries to add a category that already exists,
+ * redis returns REDISMODULE_ERR and set errno to `EBUSY` */
+ if (RedisModule_AddACLCategory(ctx,"write") == REDISMODULE_ERR)
+ RedisModule_Assert(errno == EBUSY);
+ else
+ return REDISMODULE_ERR;
+
+ if (RedisModule_AddACLCategory(ctx,"foocategory") == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"aclcheck.module.command.test.add.new.aclcategories", module_test_acl_category,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ RedisModuleCommand *test_add_new_aclcategories = RedisModule_GetCommand(ctx,"aclcheck.module.command.test.add.new.aclcategories");
+
+ if (RedisModule_SetCommandACLCategories(test_add_new_aclcategories, "foocategory") == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/atomicslotmigration.c b/examples/redis-unstable/tests/modules/atomicslotmigration.c
new file mode 100644
index 0000000..83393cd
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/atomicslotmigration.c
@@ -0,0 +1,594 @@
+#include "redismodule.h"
+
+#include <stdlib.h>
+#include <memory.h>
+#include <errno.h>
+
+#define MAX_EVENTS 1024
+
+/* Log of cluster events. */
+const char *clusterEventLog[MAX_EVENTS];
+int numClusterEvents = 0;
+
+/* Log of cluster trim events. */
+const char *clusterTrimEventLog[MAX_EVENTS];
+int numClusterTrimEvents = 0;
+
+/* Log of last deleted key event. */
+const char *lastDeletedKeyLog = NULL;
+
+/* Flag to disable trim. */
+int disableTrimFlag = 0;
+
+int replicateModuleCommand = 0; /* Enable or disable module command replication. */
+RedisModuleString *moduleCommandKeyName = NULL; /* Key name to replicate. */
+RedisModuleString *moduleCommandKeyVal = NULL; /* Key value to replicate. */
+
+/* Enable or disable module command replication. */
+int replicate_module_command(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 4) {
+ RedisModule_ReplyWithError(ctx, "ERR wrong number of arguments");
+ return REDISMODULE_OK;
+ }
+
+ long long enable = 0;
+ if (RedisModule_StringToLongLong(argv[1], &enable) != REDISMODULE_OK) {
+ RedisModule_ReplyWithError(ctx, "ERR enable value");
+ return REDISMODULE_OK;
+ }
+ replicateModuleCommand = (enable != 0);
+
+ /* Set the key name and value to replicate. */
+ if (moduleCommandKeyName) RedisModule_FreeString(ctx, moduleCommandKeyName);
+ if (moduleCommandKeyVal) RedisModule_FreeString(ctx, moduleCommandKeyVal);
+ moduleCommandKeyName = RedisModule_CreateStringFromString(ctx, argv[2]);
+ moduleCommandKeyVal = RedisModule_CreateStringFromString(ctx, argv[3]);
+
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+int lpush_and_replicate_crossslot_command(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 3) return RedisModule_WrongArity(ctx);
+
+ /* LPUSH */
+ RedisModuleCallReply *rep = RedisModule_Call(ctx, "LPUSH", "!ss", argv[1], argv[2]);
+ RedisModule_Assert(RedisModule_CallReplyType(rep) != REDISMODULE_REPLY_ERROR);
+ RedisModule_FreeCallReply(rep);
+
+ /* Replicate cross slot command */
+ int ret = RedisModule_Replicate(ctx, "MSET", "cccccc", "key1", "val1", "key2", "val2", "key3", "val3");
+ RedisModule_Assert(ret == REDISMODULE_OK);
+
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+int testClusterGetLocalSlotRanges(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ static int use_auto_memory = 0;
+ use_auto_memory = !use_auto_memory;
+
+ RedisModuleSlotRangeArray *slots;
+ if (use_auto_memory) {
+ RedisModule_AutoMemory(ctx);
+ slots = RedisModule_ClusterGetLocalSlotRanges(ctx);
+ } else {
+ slots = RedisModule_ClusterGetLocalSlotRanges(NULL);
+ }
+
+ RedisModule_ReplyWithArray(ctx, slots->num_ranges);
+ for (int i = 0; i < slots->num_ranges; i++) {
+ RedisModule_ReplyWithArray(ctx, 2);
+ RedisModule_ReplyWithLongLong(ctx, slots->ranges[i].start);
+ RedisModule_ReplyWithLongLong(ctx, slots->ranges[i].end);
+ }
+ if (!use_auto_memory)
+ RedisModule_ClusterFreeSlotRanges(NULL, slots);
+ return REDISMODULE_OK;
+}
+
+/* Helper function to check if a slot range array contains a given slot. */
+int slotRangeArrayContains(RedisModuleSlotRangeArray *sra, unsigned int slot) {
+ for (int i = 0; i < sra->num_ranges; i++)
+ if (sra->ranges[i].start <= slot && sra->ranges[i].end >= slot)
+ return 1;
+ return 0;
+}
+
+/* Sanity check. */
+int sanity(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ RedisModule_Assert(RedisModule_ClusterCanAccessKeysInSlot(-1) == 0);
+ RedisModule_Assert(RedisModule_ClusterCanAccessKeysInSlot(16384) == 0);
+ RedisModule_Assert(RedisModule_ClusterCanAccessKeysInSlot(100000) == 0);
+
+ /* Call with invalid args. */
+ errno = 0;
+ RedisModule_Assert(RedisModule_ClusterPropagateForSlotMigration(NULL, NULL, NULL) == REDISMODULE_ERR);
+ RedisModule_Assert(errno == EINVAL);
+
+ /* Call with invalid args. */
+ errno = 0;
+ RedisModule_Assert(RedisModule_ClusterPropagateForSlotMigration(ctx, NULL, NULL) == REDISMODULE_ERR);
+ RedisModule_Assert(errno == EINVAL);
+
+ /* Call with invalid args. */
+ errno = 0;
+ RedisModule_Assert(RedisModule_ClusterPropagateForSlotMigration(NULL, "asm.keyless_cmd", "") == REDISMODULE_ERR);
+ RedisModule_Assert(errno == EINVAL);
+
+ /* Call outside of slot migration. */
+ errno = 0;
+ RedisModule_Assert(RedisModule_ClusterPropagateForSlotMigration(ctx, "asm.keyless_cmd", "") == REDISMODULE_ERR);
+ RedisModule_Assert(errno == EBADF);
+
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+/* Command to test RM_ClusterCanAccessKeysInSlot(). */
+int testClusterCanAccessKeysInSlot(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argc);
+ long long slot = 0;
+
+ if (RedisModule_StringToLongLong(argv[1],&slot) != REDISMODULE_OK) {
+ return RedisModule_ReplyWithError(ctx,"ERR invalid slot");
+ }
+ RedisModule_ReplyWithLongLong(ctx, RedisModule_ClusterCanAccessKeysInSlot(slot));
+ return REDISMODULE_OK;
+}
+
+/* Generate a string representation of the info struct and subevent.
+ e.g. 'sub: cluster-slot-migration-import-started, task_id: aeBd..., slots: 0-100,200-300' */
+const char *clusterAsmInfoToString(RedisModuleClusterSlotMigrationInfo *info, uint64_t sub) {
+ char buf[1024] = {0};
+
+ if (sub == REDISMODULE_SUBEVENT_CLUSTER_SLOT_MIGRATION_IMPORT_STARTED)
+ snprintf(buf, sizeof(buf), "sub: cluster-slot-migration-import-started, ");
+ else if (sub == REDISMODULE_SUBEVENT_CLUSTER_SLOT_MIGRATION_IMPORT_FAILED)
+ snprintf(buf, sizeof(buf), "sub: cluster-slot-migration-import-failed, ");
+ else if (sub == REDISMODULE_SUBEVENT_CLUSTER_SLOT_MIGRATION_IMPORT_COMPLETED)
+ snprintf(buf, sizeof(buf), "sub: cluster-slot-migration-import-completed, ");
+ else if (sub == REDISMODULE_SUBEVENT_CLUSTER_SLOT_MIGRATION_MIGRATE_STARTED)
+ snprintf(buf, sizeof(buf), "sub: cluster-slot-migration-migrate-started, ");
+ else if (sub == REDISMODULE_SUBEVENT_CLUSTER_SLOT_MIGRATION_MIGRATE_FAILED)
+ snprintf(buf, sizeof(buf), "sub: cluster-slot-migration-migrate-failed, ");
+ else if (sub == REDISMODULE_SUBEVENT_CLUSTER_SLOT_MIGRATION_MIGRATE_COMPLETED)
+ snprintf(buf, sizeof(buf), "sub: cluster-slot-migration-migrate-completed, ");
+ else {
+ RedisModule_Assert(0);
+ }
+ snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), "source_node_id:%.40s, destination_node_id:%.40s, ",
+ info->source_node_id, info->destination_node_id);
+ snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), "task_id:%s, slots:", info->task_id);
+ for (int i = 0; i < info->slots->num_ranges; i++) {
+ RedisModuleSlotRange *sr = &info->slots->ranges[i];
+ snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), "%d-%d", sr->start, sr->end);
+ if (i != info->slots->num_ranges - 1)
+ snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), ",");
+ }
+ return RedisModule_Strdup(buf);
+}
+
+/* Generate a string representation of the info struct and subevent.
+ e.g. 'sub: cluster-slot-migration-trim-started, task_id: aeBd..., slots:0-100,200-300' */
+const char *clusterTrimInfoToString(RedisModuleClusterSlotMigrationTrimInfo *info, uint64_t sub) {
+ RedisModule_Assert(info);
+ char buf[1024] = {0};
+
+ if (sub == REDISMODULE_SUBEVENT_CLUSTER_SLOT_MIGRATION_TRIM_BACKGROUND)
+ snprintf(buf, sizeof(buf), "sub: cluster-slot-migration-trim-background, ");
+ else if (sub == REDISMODULE_SUBEVENT_CLUSTER_SLOT_MIGRATION_TRIM_STARTED)
+ snprintf(buf, sizeof(buf), "sub: cluster-slot-migration-trim-started, ");
+ else if (sub == REDISMODULE_SUBEVENT_CLUSTER_SLOT_MIGRATION_TRIM_COMPLETED)
+ snprintf(buf, sizeof(buf), "sub: cluster-slot-migration-trim-completed, ");
+ else {
+ RedisModule_Assert(0);
+ }
+ snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), "slots:");
+ for (int i = 0; i < info->slots->num_ranges; i++) {
+ RedisModuleSlotRange *sr = &info->slots->ranges[i];
+ snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), "%d-%d", sr->start, sr->end);
+ if (i != info->slots->num_ranges - 1)
+ snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), ",");
+ }
+ return RedisModule_Strdup(buf);
+}
+
+static void testReplicatingOutsideSlotRange(RedisModuleCtx *ctx, RedisModuleClusterSlotMigrationInfo *info) {
+ int slot = 0;
+ while (slot >= 0 && slot <= 16383) {
+ if (!slotRangeArrayContains(info->slots, slot)) {
+ break;
+ }
+ slot++;
+ }
+ char buf[128] = {0};
+ const char *prefix = RedisModule_ClusterCanonicalKeyNameInSlot(slot);
+ snprintf(buf, sizeof(buf), "{%s}%s", prefix, "modulekey");
+ errno = 0;
+ int ret = RedisModule_ClusterPropagateForSlotMigration(ctx, "SET", "cc", buf, "value");
+ RedisModule_Assert(ret == REDISMODULE_ERR);
+ RedisModule_Assert(errno == ERANGE);
+}
+
+static void testReplicatingCrossslotCommand(RedisModuleCtx *ctx) {
+ errno = 0;
+ int ret = RedisModule_ClusterPropagateForSlotMigration(ctx, "MSET", "cccccc", "key1", "val1", "key2", "val2", "key3", "val3");
+ RedisModule_Assert(ret == REDISMODULE_ERR);
+ RedisModule_Assert(errno == ENOTSUP);
+}
+
+static void testReplicatingUnknownCommand(RedisModuleCtx *ctx) {
+ errno = 0;
+ int ret = RedisModule_ClusterPropagateForSlotMigration(ctx, "unknowncommand", "");
+ RedisModule_Assert(ret == REDISMODULE_ERR);
+ RedisModule_Assert(errno == ENOENT);
+}
+
+static void testNonFatalScenarios(RedisModuleCtx *ctx, RedisModuleClusterSlotMigrationInfo *info) {
+ testReplicatingOutsideSlotRange(ctx, info);
+ testReplicatingCrossslotCommand(ctx);
+ testReplicatingUnknownCommand(ctx);
+}
+
+int disableTrimCmd(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ disableTrimFlag = 1;
+ /* Only disable when MIGRATE_COMPLETED for simulating recommended usage. */
+ // RedisModule_ClusterDisableTrim(ctx)
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+int enableTrimCmd(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ disableTrimFlag = 0;
+ RedisModule_Assert(RedisModule_ClusterEnableTrim(ctx) == REDISMODULE_OK);
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+int trimInProgressCmd(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ uint64_t flags = RedisModule_GetContextFlags(ctx);
+ RedisModule_ReplyWithLongLong(ctx, !!(flags & REDISMODULE_CTX_FLAGS_TRIM_IN_PROGRESS));
+ return REDISMODULE_OK;
+}
+
+void clusterEventCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data) {
+ REDISMODULE_NOT_USED(ctx);
+ int ret;
+
+ RedisModule_Assert(RedisModule_IsSubEventSupported(e, sub));
+
+ if (e.id == REDISMODULE_EVENT_CLUSTER_SLOT_MIGRATION) {
+ RedisModuleClusterSlotMigrationInfo *info = data;
+
+ if (sub == REDISMODULE_SUBEVENT_CLUSTER_SLOT_MIGRATION_MIGRATE_MODULE_PROPAGATE) {
+ /* Test some non-fatal scenarios. */
+ testNonFatalScenarios(ctx, info);
+
+ if (replicateModuleCommand == 0) return;
+
+ /* Replicate a keyless command. */
+ ret = RedisModule_ClusterPropagateForSlotMigration(ctx, "asm.keyless_cmd", "");
+ RedisModule_Assert(ret == REDISMODULE_OK);
+
+ /* Propagate configured key and value. */
+ ret = RedisModule_ClusterPropagateForSlotMigration(ctx, "SET", "ss", moduleCommandKeyName, moduleCommandKeyVal);
+ RedisModule_Assert(ret == REDISMODULE_OK);
+ } else {
+ /* Log the event. */
+ if (numClusterEvents >= MAX_EVENTS) return;
+ clusterEventLog[numClusterEvents++] = clusterAsmInfoToString(info, sub);
+
+ if (sub == REDISMODULE_SUBEVENT_CLUSTER_SLOT_MIGRATION_MIGRATE_COMPLETED) {
+ /* If users ask to disable trim, we disable trim. */
+ if (disableTrimFlag) {
+ RedisModule_Assert(RedisModule_ClusterDisableTrim(ctx) == REDISMODULE_OK);
+ }
+ }
+ }
+ }
+}
+
+int getPendingTrimKeyCmd(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2) {
+ RedisModule_ReplyWithError(ctx, "ERR wrong number of arguments");
+ return REDISMODULE_ERR;
+ }
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1],
+ REDISMODULE_READ | REDISMODULE_OPEN_KEY_ACCESS_TRIMMED);
+ if (!key) {
+ RedisModule_ReplyWithNull(ctx);
+ return REDISMODULE_OK;
+ }
+ if (RedisModule_KeyType(key) != REDISMODULE_KEYTYPE_STRING) {
+ RedisModule_ReplyWithError(ctx, "key is not a string");
+ return REDISMODULE_ERR;
+ }
+ size_t len;
+ const char *value = RedisModule_StringDMA(key, &len, 0);
+ RedisModule_ReplyWithStringBuffer(ctx, value, len);
+ RedisModule_CloseKey(key);
+ return REDISMODULE_OK;
+}
+
+void clusterTrimEventCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data) {
+ REDISMODULE_NOT_USED(ctx);
+
+ RedisModule_Assert(RedisModule_IsSubEventSupported(e, sub));
+
+ if (e.id == REDISMODULE_EVENT_CLUSTER_SLOT_MIGRATION_TRIM) {
+ /* Log the event. */
+ if (numClusterTrimEvents >= MAX_EVENTS) return;
+ RedisModuleClusterSlotMigrationTrimInfo *info = data;
+ clusterTrimEventLog[numClusterTrimEvents++] = clusterTrimInfoToString(info, sub);
+ }
+}
+
+static int keyspaceNotificationTrimmedCallback(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key) {
+ REDISMODULE_NOT_USED(ctx);
+
+ RedisModule_Assert(type == REDISMODULE_NOTIFY_KEY_TRIMMED);
+ RedisModule_Assert(strcmp(event, "key_trimmed") == 0);
+
+ if (numClusterTrimEvents >= MAX_EVENTS) return REDISMODULE_OK;
+
+ /* Log the trimmed key event. */
+ size_t len;
+ const char *key_str = RedisModule_StringPtrLen(key, &len);
+
+ char buf[1024] = {0};
+ snprintf(buf, sizeof(buf), "keyspace: key_trimmed, key: %s", key_str);
+
+ clusterTrimEventLog[numClusterTrimEvents++] = RedisModule_Strdup(buf);
+ return REDISMODULE_OK;
+}
+
+/* ASM.PARENT SET key value (just proxy to Redis SET) */
+static int asmParentSet(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 4) return RedisModule_WrongArity(ctx);
+ RedisModuleCallReply *reply = RedisModule_Call(ctx, "SET", "ss", argv[2], argv[3]);
+ if (!reply) return RedisModule_ReplyWithError(ctx, "ERR internal");
+ RedisModule_ReplyWithCallReply(ctx, reply);
+ RedisModule_FreeCallReply(reply);
+ RedisModule_ReplicateVerbatim(ctx);
+ return REDISMODULE_OK;
+}
+
+/* Clear both the cluster and trim event logs. */
+int clearEventLog(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ for (int i = 0; i < numClusterEvents; i++)
+ RedisModule_Free((void *)clusterEventLog[i]);
+ numClusterEvents = 0;
+
+ for (int i = 0; i < numClusterTrimEvents; i++)
+ RedisModule_Free((void *)clusterTrimEventLog[i]);
+ numClusterTrimEvents = 0;
+
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+/* Reply with the cluster event log. */
+int getClusterEventLog(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(ctx);
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ RedisModule_ReplyWithArray(ctx, numClusterEvents);
+ for (int i = 0; i < numClusterEvents; i++)
+ RedisModule_ReplyWithStringBuffer(ctx, clusterEventLog[i], strlen(clusterEventLog[i]));
+ return REDISMODULE_OK;
+}
+
+/* Reply with the cluster trim event log. */
+int getClusterTrimEventLog(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(ctx);
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ RedisModule_ReplyWithArray(ctx, numClusterTrimEvents);
+ for (int i = 0; i < numClusterTrimEvents; i++)
+ RedisModule_ReplyWithStringBuffer(ctx, clusterTrimEventLog[i], strlen(clusterTrimEventLog[i]));
+ return REDISMODULE_OK;
+}
+
+/* A keyless command to test module command replication. */
+int moduledata = 0;
+int keylessCmd(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(ctx);
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ moduledata++;
+ RedisModule_ReplyWithLongLong(ctx, moduledata);
+ return REDISMODULE_OK;
+}
+int readkeylessCmdVal(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(ctx);
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ RedisModule_ReplyWithLongLong(ctx, moduledata);
+ return REDISMODULE_OK;
+}
+
+int subscribeTrimmedEvent(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(ctx);
+ if (argc != 2)
+ return RedisModule_WrongArity(ctx);
+
+ long long subscribe = 0;
+ if (RedisModule_StringToLongLong(argv[1], &subscribe) != REDISMODULE_OK) {
+ RedisModule_ReplyWithError(ctx, "ERR subscribe value");
+ return REDISMODULE_OK;
+ }
+
+ if (subscribe) {
+ /* Unsubscribe first to avoid duplicate subscription. */
+ RedisModule_UnsubscribeFromKeyspaceEvents(ctx, REDISMODULE_NOTIFY_KEY_TRIMMED, keyspaceNotificationTrimmedCallback);
+ int ret = RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_KEY_TRIMMED, keyspaceNotificationTrimmedCallback);
+ RedisModule_Assert(ret == REDISMODULE_OK);
+ } else {
+ int ret = RedisModule_UnsubscribeFromKeyspaceEvents(ctx, REDISMODULE_NOTIFY_KEY_TRIMMED, keyspaceNotificationTrimmedCallback);
+ RedisModule_Assert(ret == REDISMODULE_OK);
+ }
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+void keyEventCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data) {
+ REDISMODULE_NOT_USED(ctx);
+ REDISMODULE_NOT_USED(e);
+
+ if (sub == REDISMODULE_SUBEVENT_KEY_DELETED) {
+ RedisModuleKeyInfoV1 *ei = data;
+ RedisModuleKey *kp = ei->key;
+ RedisModuleString *key = (RedisModuleString *) RedisModule_GetKeyNameFromModuleKey(kp);
+ size_t keylen;
+ const char *keyname = RedisModule_StringPtrLen(key, &keylen);
+
+ /* Verify value can be read. It will be used to verify key's value can
+ * be read in a trim callback. */
+ size_t valuelen = 0;
+ const char *value = "";
+ RedisModuleKey *mk = RedisModule_OpenKey(ctx, key, REDISMODULE_READ);
+ if (RedisModule_KeyType(mk) == REDISMODULE_KEYTYPE_STRING) {
+ value = RedisModule_StringDMA(mk, &valuelen, 0);
+ }
+ RedisModule_CloseKey(mk);
+
+ char buf[1024] = {0};
+ snprintf(buf, sizeof(buf), "keyevent: key: %.*s, value: %.*s", (int) keylen, keyname, (int)valuelen, value);
+
+ if (lastDeletedKeyLog) RedisModule_Free((void *)lastDeletedKeyLog);
+ lastDeletedKeyLog = RedisModule_Strdup(buf);
+ }
+}
+
+int getLastDeletedKey(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(ctx);
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ if (lastDeletedKeyLog) {
+ RedisModule_ReplyWithStringBuffer(ctx, lastDeletedKeyLog, strlen(lastDeletedKeyLog));
+ } else {
+ RedisModule_ReplyWithNull(ctx);
+ }
+ return REDISMODULE_OK;
+}
+
+int asmGetCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(ctx);
+
+ if (argc != 2) return RedisModule_WrongArity(ctx);
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ);
+ if (key == NULL) {
+ RedisModule_ReplyWithNull(ctx);
+ return REDISMODULE_OK;
+ }
+
+ RedisModule_Assert(RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_STRING);
+ size_t len;
+ const char *value = RedisModule_StringDMA(key, &len, 0);
+ RedisModule_ReplyWithStringBuffer(ctx, value, len);
+ RedisModule_CloseKey(key);
+
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ if (RedisModule_Init(ctx, "asm", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "asm.cluster_can_access_keys_in_slot", testClusterCanAccessKeysInSlot, "", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "asm.clear_event_log", clearEventLog, "", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "asm.get_cluster_event_log", getClusterEventLog, "", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "asm.get_cluster_trim_event_log", getClusterTrimEventLog, "", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "asm.keyless_cmd", keylessCmd, "write", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "asm.disable_trim", disableTrimCmd, "", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "asm.enable_trim", enableTrimCmd, "", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "asm.read_pending_trim_key", getPendingTrimKeyCmd, "readonly", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "asm.trim_in_progress", trimInProgressCmd, "", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "asm.read_keyless_cmd_val", readkeylessCmdVal, "", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "asm.sanity", sanity, "", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "asm.subscribe_trimmed_event", subscribeTrimmedEvent, "", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "asm.replicate_module_command", replicate_module_command, "", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "asm.lpush_replicate_crossslot_command", lpush_and_replicate_crossslot_command, "write", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "asm.cluster_get_local_slot_ranges", testClusterGetLocalSlotRanges, "", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "asm.get_last_deleted_key", getLastDeletedKey, "", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "asm.get", asmGetCommand, "", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "asm.parent", NULL, "", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ RedisModuleCommand *parent = RedisModule_GetCommand(ctx, "asm.parent");
+ if (!parent) return REDISMODULE_ERR;
+
+ /* Subcommand: ASM.PARENT SET (write) */
+ if (RedisModule_CreateSubcommand(parent, "set", asmParentSet, "write fast", 2, 2, 1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_SubscribeToServerEvent(ctx, RedisModuleEvent_ClusterSlotMigration, clusterEventCallback) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_SubscribeToServerEvent(ctx, RedisModuleEvent_ClusterSlotMigrationTrim, clusterTrimEventCallback) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_KEY_TRIMMED, keyspaceNotificationTrimmedCallback) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_SubscribeToServerEvent(ctx, RedisModuleEvent_Key, keyEventCallback) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/auth.c b/examples/redis-unstable/tests/modules/auth.c
new file mode 100644
index 0000000..cc2378e
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/auth.c
@@ -0,0 +1,286 @@
+/* define macros for having usleep */
+#define _BSD_SOURCE
+#define _DEFAULT_SOURCE
+
+#include "redismodule.h"
+
+#include <string.h>
+#include <unistd.h>
+#include <pthread.h>
+
+#define UNUSED(V) ((void) V)
+
+// A simple global user
+static RedisModuleUser *global = NULL;
+static long long client_change_delta = 0;
+static pthread_t tid;
+
+void UserChangedCallback(uint64_t client_id, void *privdata) {
+ REDISMODULE_NOT_USED(privdata);
+ REDISMODULE_NOT_USED(client_id);
+ client_change_delta++;
+}
+
+int Auth_CreateModuleUser(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ if (global) {
+ RedisModule_FreeModuleUser(global);
+ }
+
+ global = RedisModule_CreateModuleUser("global");
+ RedisModule_SetModuleUserACL(global, "allcommands");
+ RedisModule_SetModuleUserACL(global, "allkeys");
+ RedisModule_SetModuleUserACL(global, "on");
+
+ return RedisModule_ReplyWithSimpleString(ctx, "OK");
+}
+
+int Auth_AuthModuleUser(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ uint64_t client_id;
+ RedisModule_AuthenticateClientWithUser(ctx, global, UserChangedCallback, NULL, &client_id);
+
+ return RedisModule_ReplyWithLongLong(ctx, (uint64_t) client_id);
+}
+
+int Auth_AuthRealUser(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2) return RedisModule_WrongArity(ctx);
+
+ size_t length;
+ uint64_t client_id;
+
+ RedisModuleString *user_string = argv[1];
+ const char *name = RedisModule_StringPtrLen(user_string, &length);
+
+ if (RedisModule_AuthenticateClientWithACLUser(ctx, name, length,
+ UserChangedCallback, NULL, &client_id) == REDISMODULE_ERR) {
+ return RedisModule_ReplyWithError(ctx, "Invalid user");
+ }
+
+ return RedisModule_ReplyWithLongLong(ctx, (uint64_t) client_id);
+}
+
+/* This command redacts every other arguments and returns OK */
+int Auth_RedactedAPI(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ for(int i = argc - 1; i > 0; i -= 2) {
+ int result = RedisModule_RedactClientCommandArgument(ctx, i);
+ RedisModule_Assert(result == REDISMODULE_OK);
+ }
+ return RedisModule_ReplyWithSimpleString(ctx, "OK");
+}
+
+int Auth_ChangeCount(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ long long result = client_change_delta;
+ client_change_delta = 0;
+ return RedisModule_ReplyWithLongLong(ctx, result);
+}
+
+/* The Module functionality below validates that module authentication callbacks can be registered
+ * to support both non-blocking and blocking module based authentication. */
+
+/* Non Blocking Module Auth callback / implementation. */
+int auth_cb(RedisModuleCtx *ctx, RedisModuleString *username, RedisModuleString *password, RedisModuleString **err) {
+ const char *user = RedisModule_StringPtrLen(username, NULL);
+ const char *pwd = RedisModule_StringPtrLen(password, NULL);
+ if (!strcmp(user,"foo") && !strcmp(pwd,"allow")) {
+ RedisModule_AuthenticateClientWithACLUser(ctx, "foo", 3, NULL, NULL, NULL);
+ return REDISMODULE_AUTH_HANDLED;
+ }
+ else if (!strcmp(user,"foo") && !strcmp(pwd,"deny")) {
+ RedisModuleString *log = RedisModule_CreateString(ctx, "Module Auth", 11);
+ RedisModule_ACLAddLogEntryByUserName(ctx, username, log, REDISMODULE_ACL_LOG_AUTH);
+ RedisModule_FreeString(ctx, log);
+ const char *err_msg = "Auth denied by Misc Module.";
+ *err = RedisModule_CreateString(ctx, err_msg, strlen(err_msg));
+ return REDISMODULE_AUTH_HANDLED;
+ }
+ return REDISMODULE_AUTH_NOT_HANDLED;
+}
+
+int test_rm_register_auth_cb(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ RedisModule_RegisterAuthCallback(ctx, auth_cb);
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+/*
+ * The thread entry point that actually executes the blocking part of the AUTH command.
+ * This function sleeps for 0.5 seconds and then unblocks the client which will later call
+ * `AuthBlock_Reply`.
+ * `arg` is expected to contain the RedisModuleBlockedClient, username, and password.
+ */
+void *AuthBlock_ThreadMain(void *arg) {
+ usleep(500000);
+ void **targ = arg;
+ RedisModuleBlockedClient *bc = targ[0];
+ int result = 2;
+ const char *user = RedisModule_StringPtrLen(targ[1], NULL);
+ const char *pwd = RedisModule_StringPtrLen(targ[2], NULL);
+ if (!strcmp(user,"foo") && !strcmp(pwd,"block_allow")) {
+ result = 1;
+ }
+ else if (!strcmp(user,"foo") && !strcmp(pwd,"block_deny")) {
+ result = 0;
+ }
+ else if (!strcmp(user,"foo") && !strcmp(pwd,"block_abort")) {
+ RedisModule_BlockedClientMeasureTimeEnd(bc);
+ RedisModule_AbortBlock(bc);
+ goto cleanup;
+ }
+ /* Provide the result to the blocking reply cb. */
+ void **replyarg = RedisModule_Alloc(sizeof(void*));
+ replyarg[0] = (void *) (uintptr_t) result;
+ RedisModule_BlockedClientMeasureTimeEnd(bc);
+ RedisModule_UnblockClient(bc, replyarg);
+cleanup:
+ /* Free the username and password and thread / arg data. */
+ RedisModule_FreeString(NULL, targ[1]);
+ RedisModule_FreeString(NULL, targ[2]);
+ RedisModule_Free(targ);
+ return NULL;
+}
+
+/*
+ * Reply callback for a blocking AUTH command. This is called when the client is unblocked.
+ */
+int AuthBlock_Reply(RedisModuleCtx *ctx, RedisModuleString *username, RedisModuleString *password, RedisModuleString **err) {
+ REDISMODULE_NOT_USED(password);
+ void **targ = RedisModule_GetBlockedClientPrivateData(ctx);
+ int result = (uintptr_t) targ[0];
+ size_t userlen = 0;
+ const char *user = RedisModule_StringPtrLen(username, &userlen);
+ /* Handle the success case by authenticating. */
+ if (result == 1) {
+ RedisModule_AuthenticateClientWithACLUser(ctx, user, userlen, NULL, NULL, NULL);
+ return REDISMODULE_AUTH_HANDLED;
+ }
+ /* Handle the Error case by denying auth */
+ else if (result == 0) {
+ RedisModuleString *log = RedisModule_CreateString(ctx, "Module Auth", 11);
+ RedisModule_ACLAddLogEntryByUserName(ctx, username, log, REDISMODULE_ACL_LOG_AUTH);
+ RedisModule_FreeString(ctx, log);
+ const char *err_msg = "Auth denied by Misc Module.";
+ *err = RedisModule_CreateString(ctx, err_msg, strlen(err_msg));
+ return REDISMODULE_AUTH_HANDLED;
+ }
+ /* "Skip" Authentication */
+ return REDISMODULE_AUTH_NOT_HANDLED;
+}
+
+/* Private data freeing callback for Module Auth. */
+void AuthBlock_FreeData(RedisModuleCtx *ctx, void *privdata) {
+ REDISMODULE_NOT_USED(ctx);
+ RedisModule_Free(privdata);
+}
+
+/* Callback triggered when the engine attempts module auth
+ * Return code here is one of the following: Auth succeeded, Auth denied,
+ * Auth not handled, Auth blocked.
+ * The Module can have auth succeed / denied here itself, but this is an example
+ * of blocking module auth.
+ */
+int blocking_auth_cb(RedisModuleCtx *ctx, RedisModuleString *username, RedisModuleString *password, RedisModuleString **err) {
+ REDISMODULE_NOT_USED(username);
+ REDISMODULE_NOT_USED(password);
+ REDISMODULE_NOT_USED(err);
+ /* Block the client from the Module. */
+ RedisModuleBlockedClient *bc = RedisModule_BlockClientOnAuth(ctx, AuthBlock_Reply, AuthBlock_FreeData);
+ int ctx_flags = RedisModule_GetContextFlags(ctx);
+ if (ctx_flags & REDISMODULE_CTX_FLAGS_MULTI || ctx_flags & REDISMODULE_CTX_FLAGS_LUA) {
+ /* Clean up by using RedisModule_UnblockClient since we attempted blocking the client. */
+ RedisModule_UnblockClient(bc, NULL);
+ return REDISMODULE_AUTH_HANDLED;
+ }
+
+ /* Another blocking auth cb may have spawned a thread, we'll just wait for it
+ * to finish here */
+ if (tid) pthread_join(tid, NULL);
+
+ RedisModule_BlockedClientMeasureTimeStart(bc);
+
+ /* Allocate memory for information needed. */
+ void **targ = RedisModule_Alloc(sizeof(void*)*3);
+ targ[0] = bc;
+ targ[1] = RedisModule_CreateStringFromString(NULL, username);
+ targ[2] = RedisModule_CreateStringFromString(NULL, password);
+
+ /* Create bg thread and pass the blockedclient, username and password to it. */
+ if (pthread_create(&tid, NULL, AuthBlock_ThreadMain, targ) != 0) {
+ RedisModule_AbortBlock(bc);
+
+ /* These are freed in AuthBlock_ThreadMain but since we failed to spawn
+ * the thread need to free them here. */
+ RedisModule_FreeString(NULL, targ[1]);
+ RedisModule_FreeString(NULL, targ[2]);
+ RedisModule_Free(targ);
+ }
+
+ return REDISMODULE_AUTH_HANDLED;
+}
+
+int test_rm_register_blocking_auth_cb(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ RedisModule_RegisterAuthCallback(ctx, blocking_auth_cb);
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+/* This function must be present on each Redis module. It is used in order to
+ * register the commands into the Redis server. */
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ if (RedisModule_Init(ctx,"testacl",1,REDISMODULE_APIVER_1)
+ == REDISMODULE_ERR) return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"auth.authrealuser",
+ Auth_AuthRealUser,"no-auth",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"auth.createmoduleuser",
+ Auth_CreateModuleUser,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"auth.authmoduleuser",
+ Auth_AuthModuleUser,"no-auth",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"auth.changecount",
+ Auth_ChangeCount,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"auth.redact",
+ Auth_RedactedAPI,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"testmoduleone.rm_register_auth_cb",
+ test_rm_register_auth_cb,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"testmoduleone.rm_register_blocking_auth_cb",
+ test_rm_register_blocking_auth_cb,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnUnload(RedisModuleCtx *ctx) {
+ UNUSED(ctx);
+
+ if (tid) pthread_join(tid, NULL);
+
+ if (global)
+ RedisModule_FreeModuleUser(global);
+
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/basics.c b/examples/redis-unstable/tests/modules/basics.c
new file mode 100644
index 0000000..85c6196
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/basics.c
@@ -0,0 +1,1051 @@
+/* Module designed to test the Redis modules subsystem.
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (c) 2016-Present, Redis Ltd.
+ * All rights reserved.
+ *
+ * Licensed under your choice of (a) the Redis Source Available License 2.0
+ * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
+ * GNU Affero General Public License v3 (AGPLv3).
+ */
+
+#include "redismodule.h"
+#include <string.h>
+#include <stdlib.h>
+
+/* --------------------------------- Helpers -------------------------------- */
+
+/* Return true if the reply and the C null term string matches. */
+int TestMatchReply(RedisModuleCallReply *reply, char *str) {
+ RedisModuleString *mystr;
+ mystr = RedisModule_CreateStringFromCallReply(reply);
+ if (!mystr) return 0;
+ const char *ptr = RedisModule_StringPtrLen(mystr,NULL);
+ return strcmp(ptr,str) == 0;
+}
+
+/* ------------------------------- Test units ------------------------------- */
+
+/* TEST.CALL -- Test Call() API. */
+int TestCall(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ RedisModule_AutoMemory(ctx);
+ RedisModuleCallReply *reply;
+
+ RedisModule_Call(ctx,"DEL","c","mylist");
+ RedisModuleString *mystr = RedisModule_CreateString(ctx,"foo",3);
+ RedisModule_Call(ctx,"RPUSH","csl","mylist",mystr,(long long)1234);
+ reply = RedisModule_Call(ctx,"LRANGE","ccc","mylist","0","-1");
+ long long items = RedisModule_CallReplyLength(reply);
+ if (items != 2) goto fail;
+
+ RedisModuleCallReply *item0, *item1;
+
+ item0 = RedisModule_CallReplyArrayElement(reply,0);
+ item1 = RedisModule_CallReplyArrayElement(reply,1);
+ if (!TestMatchReply(item0,"foo")) goto fail;
+ if (!TestMatchReply(item1,"1234")) goto fail;
+
+ RedisModule_ReplyWithSimpleString(ctx,"OK");
+ return REDISMODULE_OK;
+
+fail:
+ RedisModule_ReplyWithSimpleString(ctx,"ERR");
+ return REDISMODULE_OK;
+}
+
+int TestCallResp3Attribute(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ RedisModule_AutoMemory(ctx);
+ RedisModuleCallReply *reply;
+
+ reply = RedisModule_Call(ctx,"DEBUG","3cc" ,"PROTOCOL", "attrib"); /* 3 stands for resp 3 reply */
+ if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_STRING) goto fail;
+
+ /* make sure we can not reply to resp2 client with resp3 (it might be a string but it contains attribute) */
+ if (RedisModule_ReplyWithCallReply(ctx, reply) != REDISMODULE_ERR) goto fail;
+
+ if (!TestMatchReply(reply,"Some real reply following the attribute")) goto fail;
+
+ reply = RedisModule_CallReplyAttribute(reply);
+ if (!reply || RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_ATTRIBUTE) goto fail;
+ /* make sure we can not reply to resp2 client with resp3 attribute */
+ if (RedisModule_ReplyWithCallReply(ctx, reply) != REDISMODULE_ERR) goto fail;
+ if (RedisModule_CallReplyLength(reply) != 1) goto fail;
+
+ RedisModuleCallReply *key, *val;
+ if (RedisModule_CallReplyAttributeElement(reply,0,&key,&val) != REDISMODULE_OK) goto fail;
+ if (!TestMatchReply(key,"key-popularity")) goto fail;
+ if (RedisModule_CallReplyType(val) != REDISMODULE_REPLY_ARRAY) goto fail;
+ if (RedisModule_CallReplyLength(val) != 2) goto fail;
+ if (!TestMatchReply(RedisModule_CallReplyArrayElement(val, 0),"key:123")) goto fail;
+ if (!TestMatchReply(RedisModule_CallReplyArrayElement(val, 1),"90")) goto fail;
+
+ RedisModule_ReplyWithSimpleString(ctx,"OK");
+ return REDISMODULE_OK;
+
+fail:
+ RedisModule_ReplyWithSimpleString(ctx,"ERR");
+ return REDISMODULE_OK;
+}
+
+int TestGetResp(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ int flags = RedisModule_GetContextFlags(ctx);
+
+ if (flags & REDISMODULE_CTX_FLAGS_RESP3) {
+ RedisModule_ReplyWithLongLong(ctx, 3);
+ } else {
+ RedisModule_ReplyWithLongLong(ctx, 2);
+ }
+
+ return REDISMODULE_OK;
+}
+
+int TestCallRespAutoMode(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ RedisModule_AutoMemory(ctx);
+ RedisModuleCallReply *reply;
+
+ RedisModule_Call(ctx,"DEL","c","myhash");
+ RedisModule_Call(ctx,"HSET","ccccc","myhash", "f1", "v1", "f2", "v2");
+ /* 0 stands for auto mode, we will get the reply in the same format as the client */
+ reply = RedisModule_Call(ctx,"HGETALL","0c" ,"myhash");
+ RedisModule_ReplyWithCallReply(ctx, reply);
+ return REDISMODULE_OK;
+}
+
+int TestCallResp3Map(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ RedisModule_AutoMemory(ctx);
+ RedisModuleCallReply *reply;
+
+ RedisModule_Call(ctx,"DEL","c","myhash");
+ RedisModule_Call(ctx,"HSET","ccccc","myhash", "f1", "v1", "f2", "v2");
+ reply = RedisModule_Call(ctx,"HGETALL","3c" ,"myhash"); /* 3 stands for resp 3 reply */
+ if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_MAP) goto fail;
+
+ /* make sure we can not reply to resp2 client with resp3 map */
+ if (RedisModule_ReplyWithCallReply(ctx, reply) != REDISMODULE_ERR) goto fail;
+
+ long long items = RedisModule_CallReplyLength(reply);
+ if (items != 2) goto fail;
+
+ RedisModuleCallReply *key0, *key1;
+ RedisModuleCallReply *val0, *val1;
+ if (RedisModule_CallReplyMapElement(reply,0,&key0,&val0) != REDISMODULE_OK) goto fail;
+ if (RedisModule_CallReplyMapElement(reply,1,&key1,&val1) != REDISMODULE_OK) goto fail;
+ if (!TestMatchReply(key0,"f1")) goto fail;
+ if (!TestMatchReply(key1,"f2")) goto fail;
+ if (!TestMatchReply(val0,"v1")) goto fail;
+ if (!TestMatchReply(val1,"v2")) goto fail;
+
+ RedisModule_ReplyWithSimpleString(ctx,"OK");
+ return REDISMODULE_OK;
+
+fail:
+ RedisModule_ReplyWithSimpleString(ctx,"ERR");
+ return REDISMODULE_OK;
+}
+
+int TestCallResp3Bool(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ RedisModule_AutoMemory(ctx);
+ RedisModuleCallReply *reply;
+
+ reply = RedisModule_Call(ctx,"DEBUG","3cc" ,"PROTOCOL", "true"); /* 3 stands for resp 3 reply */
+ if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_BOOL) goto fail;
+ /* make sure we can not reply to resp2 client with resp3 bool */
+ if (RedisModule_ReplyWithCallReply(ctx, reply) != REDISMODULE_ERR) goto fail;
+
+ if (!RedisModule_CallReplyBool(reply)) goto fail;
+ reply = RedisModule_Call(ctx,"DEBUG","3cc" ,"PROTOCOL", "false"); /* 3 stands for resp 3 reply */
+ if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_BOOL) goto fail;
+ if (RedisModule_CallReplyBool(reply)) goto fail;
+
+ RedisModule_ReplyWithSimpleString(ctx,"OK");
+ return REDISMODULE_OK;
+
+fail:
+ RedisModule_ReplyWithSimpleString(ctx,"ERR");
+ return REDISMODULE_OK;
+}
+
+int TestCallResp3Null(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ RedisModule_AutoMemory(ctx);
+ RedisModuleCallReply *reply;
+
+ reply = RedisModule_Call(ctx,"DEBUG","3cc" ,"PROTOCOL", "null"); /* 3 stands for resp 3 reply */
+ if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_NULL) goto fail;
+
+ /* make sure we can not reply to resp2 client with resp3 null */
+ if (RedisModule_ReplyWithCallReply(ctx, reply) != REDISMODULE_ERR) goto fail;
+
+ RedisModule_ReplyWithSimpleString(ctx,"OK");
+ return REDISMODULE_OK;
+
+fail:
+ RedisModule_ReplyWithSimpleString(ctx,"ERR");
+ return REDISMODULE_OK;
+}
+
+int TestCallReplyWithNestedReply(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ RedisModule_AutoMemory(ctx);
+ RedisModuleCallReply *reply;
+
+ RedisModule_Call(ctx,"DEL","c","mylist");
+ RedisModule_Call(ctx,"RPUSH","ccl","mylist","test",(long long)1234);
+ reply = RedisModule_Call(ctx,"LRANGE","ccc","mylist","0","-1");
+ if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_ARRAY) goto fail;
+ if (RedisModule_CallReplyLength(reply) < 1) goto fail;
+ RedisModuleCallReply *nestedReply = RedisModule_CallReplyArrayElement(reply, 0);
+
+ RedisModule_ReplyWithCallReply(ctx,nestedReply);
+ return REDISMODULE_OK;
+
+fail:
+ RedisModule_ReplyWithSimpleString(ctx,"ERR");
+ return REDISMODULE_OK;
+}
+
+int TestCallReplyWithArrayReply(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ RedisModule_AutoMemory(ctx);
+ RedisModuleCallReply *reply;
+
+ RedisModule_Call(ctx,"DEL","c","mylist");
+ RedisModule_Call(ctx,"RPUSH","ccl","mylist","test",(long long)1234);
+ reply = RedisModule_Call(ctx,"LRANGE","ccc","mylist","0","-1");
+ if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_ARRAY) goto fail;
+
+ RedisModule_ReplyWithCallReply(ctx,reply);
+ return REDISMODULE_OK;
+
+fail:
+ RedisModule_ReplyWithSimpleString(ctx,"ERR");
+ return REDISMODULE_OK;
+}
+
+int TestCallResp3Double(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ RedisModule_AutoMemory(ctx);
+ RedisModuleCallReply *reply;
+
+ reply = RedisModule_Call(ctx,"DEBUG","3cc" ,"PROTOCOL", "double"); /* 3 stands for resp 3 reply */
+ if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_DOUBLE) goto fail;
+
+ /* make sure we can not reply to resp2 client with resp3 double*/
+ if (RedisModule_ReplyWithCallReply(ctx, reply) != REDISMODULE_ERR) goto fail;
+
+ double d = RedisModule_CallReplyDouble(reply);
+ /* we compare strings, since comparing doubles directly can fail in various architectures, e.g. 32bit */
+ char got[30], expected[30];
+ snprintf(got, sizeof(got), "%.17g", d);
+ snprintf(expected, sizeof(expected), "%.17g", 3.141);
+ if (strcmp(got, expected) != 0) goto fail;
+ RedisModule_ReplyWithSimpleString(ctx,"OK");
+ return REDISMODULE_OK;
+
+fail:
+ RedisModule_ReplyWithSimpleString(ctx,"ERR");
+ return REDISMODULE_OK;
+}
+
+int TestCallResp3BigNumber(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ RedisModule_AutoMemory(ctx);
+ RedisModuleCallReply *reply;
+
+ reply = RedisModule_Call(ctx,"DEBUG","3cc" ,"PROTOCOL", "bignum"); /* 3 stands for resp 3 reply */
+ if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_BIG_NUMBER) goto fail;
+
+ /* make sure we can not reply to resp2 client with resp3 big number */
+ if (RedisModule_ReplyWithCallReply(ctx, reply) != REDISMODULE_ERR) goto fail;
+
+ size_t len;
+ const char* big_num = RedisModule_CallReplyBigNumber(reply, &len);
+ RedisModule_ReplyWithStringBuffer(ctx,big_num,len);
+ return REDISMODULE_OK;
+
+fail:
+ RedisModule_ReplyWithSimpleString(ctx,"ERR");
+ return REDISMODULE_OK;
+}
+
+int TestCallResp3Verbatim(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ RedisModule_AutoMemory(ctx);
+ RedisModuleCallReply *reply;
+
+ reply = RedisModule_Call(ctx,"DEBUG","3cc" ,"PROTOCOL", "verbatim"); /* 3 stands for resp 3 reply */
+ if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_VERBATIM_STRING) goto fail;
+
+ /* make sure we can not reply to resp2 client with resp3 verbatim string */
+ if (RedisModule_ReplyWithCallReply(ctx, reply) != REDISMODULE_ERR) goto fail;
+
+ const char* format;
+ size_t len;
+ const char* str = RedisModule_CallReplyVerbatim(reply, &len, &format);
+ RedisModuleString *s = RedisModule_CreateStringPrintf(ctx, "%.*s:%.*s", 3, format, (int)len, str);
+ RedisModule_ReplyWithString(ctx,s);
+ return REDISMODULE_OK;
+
+fail:
+ RedisModule_ReplyWithSimpleString(ctx,"ERR");
+ return REDISMODULE_OK;
+}
+
+int TestCallResp3Set(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ RedisModule_AutoMemory(ctx);
+ RedisModuleCallReply *reply;
+
+ RedisModule_Call(ctx,"DEL","c","myset");
+ RedisModule_Call(ctx,"sadd","ccc","myset", "v1", "v2");
+ reply = RedisModule_Call(ctx,"smembers","3c" ,"myset"); // N stands for resp 3 reply
+ if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_SET) goto fail;
+
+ /* make sure we can not reply to resp2 client with resp3 set */
+ if (RedisModule_ReplyWithCallReply(ctx, reply) != REDISMODULE_ERR) goto fail;
+
+ long long items = RedisModule_CallReplyLength(reply);
+ if (items != 2) goto fail;
+
+ RedisModuleCallReply *val0, *val1;
+
+ val0 = RedisModule_CallReplySetElement(reply,0);
+ val1 = RedisModule_CallReplySetElement(reply,1);
+
+ /*
+ * The order of elements on sets are not promised so we just
+ * veridy that the reply matches one of the elements.
+ */
+ if (!TestMatchReply(val0,"v1") && !TestMatchReply(val0,"v2")) goto fail;
+ if (!TestMatchReply(val1,"v1") && !TestMatchReply(val1,"v2")) goto fail;
+
+ RedisModule_ReplyWithSimpleString(ctx,"OK");
+ return REDISMODULE_OK;
+
+fail:
+ RedisModule_ReplyWithSimpleString(ctx,"ERR");
+ return REDISMODULE_OK;
+}
+
+/* TEST.STRING.APPEND -- Test appending to an existing string object. */
+int TestStringAppend(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ RedisModuleString *s = RedisModule_CreateString(ctx,"foo",3);
+ RedisModule_StringAppendBuffer(ctx,s,"bar",3);
+ RedisModule_ReplyWithString(ctx,s);
+ RedisModule_FreeString(ctx,s);
+ return REDISMODULE_OK;
+}
+
+/* TEST.STRING.APPEND.AM -- Test append with retain when auto memory is on. */
+int TestStringAppendAM(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ RedisModule_AutoMemory(ctx);
+ RedisModuleString *s = RedisModule_CreateString(ctx,"foo",3);
+ RedisModule_RetainString(ctx,s);
+ RedisModule_TrimStringAllocation(s); /* Mostly NOP, but exercises the API function */
+ RedisModule_StringAppendBuffer(ctx,s,"bar",3);
+ RedisModule_ReplyWithString(ctx,s);
+ RedisModule_FreeString(ctx,s);
+ return REDISMODULE_OK;
+}
+
+/* TEST.STRING.TRIM -- Test we trim a string with free space. */
+int TestTrimString(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ RedisModuleString *s = RedisModule_CreateString(ctx,"foo",3);
+ char *tmp = RedisModule_Alloc(1024);
+ RedisModule_StringAppendBuffer(ctx,s,tmp,1024);
+ size_t string_len = RedisModule_MallocSizeString(s);
+ RedisModule_TrimStringAllocation(s);
+ size_t len_after_trim = RedisModule_MallocSizeString(s);
+
+ /* Determine if using jemalloc memory allocator. */
+ RedisModuleServerInfoData *info = RedisModule_GetServerInfo(ctx, "memory");
+ const char *field = RedisModule_ServerInfoGetFieldC(info, "mem_allocator");
+ int use_jemalloc = !strncmp(field, "jemalloc", 8);
+
+ /* Jemalloc will reallocate `s` from 2k to 1k after RedisModule_TrimStringAllocation(),
+ * but non-jemalloc memory allocators may keep the old size. */
+ if ((use_jemalloc && len_after_trim < string_len) ||
+ (!use_jemalloc && len_after_trim <= string_len))
+ {
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ } else {
+ RedisModule_ReplyWithError(ctx, "String was not trimmed as expected.");
+ }
+ RedisModule_FreeServerInfo(ctx, info);
+ RedisModule_Free(tmp);
+ RedisModule_FreeString(ctx,s);
+ return REDISMODULE_OK;
+}
+
+/* TEST.STRING.PRINTF -- Test string formatting. */
+int TestStringPrintf(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ RedisModule_AutoMemory(ctx);
+ if (argc < 3) {
+ return RedisModule_WrongArity(ctx);
+ }
+ RedisModuleString *s = RedisModule_CreateStringPrintf(ctx,
+ "Got %d args. argv[1]: %s, argv[2]: %s",
+ argc,
+ RedisModule_StringPtrLen(argv[1], NULL),
+ RedisModule_StringPtrLen(argv[2], NULL)
+ );
+
+ RedisModule_ReplyWithString(ctx,s);
+
+ return REDISMODULE_OK;
+}
+
+int failTest(RedisModuleCtx *ctx, const char *msg) {
+ RedisModule_ReplyWithError(ctx, msg);
+ return REDISMODULE_ERR;
+}
+
+int TestUnlink(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ RedisModule_AutoMemory(ctx);
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ RedisModuleKey *k = RedisModule_OpenKey(ctx, RedisModule_CreateStringPrintf(ctx, "unlinked"), REDISMODULE_WRITE | REDISMODULE_READ);
+ if (!k) return failTest(ctx, "Could not create key");
+
+ if (REDISMODULE_ERR == RedisModule_StringSet(k, RedisModule_CreateStringPrintf(ctx, "Foobar"))) {
+ return failTest(ctx, "Could not set string value");
+ }
+
+ RedisModuleCallReply *rep = RedisModule_Call(ctx, "EXISTS", "c", "unlinked");
+ if (!rep || RedisModule_CallReplyInteger(rep) != 1) {
+ return failTest(ctx, "Key does not exist before unlink");
+ }
+
+ if (REDISMODULE_ERR == RedisModule_UnlinkKey(k)) {
+ return failTest(ctx, "Could not unlink key");
+ }
+
+ rep = RedisModule_Call(ctx, "EXISTS", "c", "unlinked");
+ if (!rep || RedisModule_CallReplyInteger(rep) != 0) {
+ return failTest(ctx, "Could not verify key to be unlinked");
+ }
+ return RedisModule_ReplyWithSimpleString(ctx, "OK");
+}
+
+int TestNestedCallReplyArrayElement(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ RedisModule_AutoMemory(ctx);
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ RedisModuleString *expect_key = RedisModule_CreateString(ctx, "mykey", strlen("mykey"));
+ RedisModule_SelectDb(ctx, 1);
+ RedisModule_Call(ctx, "LPUSH", "sc", expect_key, "myvalue");
+
+ RedisModuleCallReply *scan_reply = RedisModule_Call(ctx, "SCAN", "l", (long long)0);
+ RedisModule_Assert(scan_reply != NULL && RedisModule_CallReplyType(scan_reply) == REDISMODULE_REPLY_ARRAY);
+ RedisModule_Assert(RedisModule_CallReplyLength(scan_reply) == 2);
+
+ long long scan_cursor;
+ RedisModuleCallReply *cursor_reply = RedisModule_CallReplyArrayElement(scan_reply, 0);
+ RedisModule_Assert(RedisModule_CallReplyType(cursor_reply) == REDISMODULE_REPLY_STRING);
+ RedisModule_Assert(RedisModule_StringToLongLong(RedisModule_CreateStringFromCallReply(cursor_reply), &scan_cursor) == REDISMODULE_OK);
+ RedisModule_Assert(scan_cursor == 0);
+
+ RedisModuleCallReply *keys_reply = RedisModule_CallReplyArrayElement(scan_reply, 1);
+ RedisModule_Assert(RedisModule_CallReplyType(keys_reply) == REDISMODULE_REPLY_ARRAY);
+ RedisModule_Assert( RedisModule_CallReplyLength(keys_reply) == 1);
+
+ RedisModuleCallReply *key_reply = RedisModule_CallReplyArrayElement(keys_reply, 0);
+ RedisModule_Assert(RedisModule_CallReplyType(key_reply) == REDISMODULE_REPLY_STRING);
+ RedisModuleString *key = RedisModule_CreateStringFromCallReply(key_reply);
+ RedisModule_Assert(RedisModule_StringCompare(key, expect_key) == 0);
+
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+/* TEST.STRING.TRUNCATE -- Test truncating an existing string object. */
+int TestStringTruncate(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ RedisModule_AutoMemory(ctx);
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ RedisModule_Call(ctx, "SET", "cc", "foo", "abcde");
+ RedisModuleKey *k = RedisModule_OpenKey(ctx, RedisModule_CreateStringPrintf(ctx, "foo"), REDISMODULE_READ | REDISMODULE_WRITE);
+ if (!k) return failTest(ctx, "Could not create key");
+
+ size_t len = 0;
+ char* s;
+
+ /* expand from 5 to 8 and check null pad */
+ if (REDISMODULE_ERR == RedisModule_StringTruncate(k, 8)) {
+ return failTest(ctx, "Could not truncate string value (8)");
+ }
+ s = RedisModule_StringDMA(k, &len, REDISMODULE_READ);
+ if (!s) {
+ return failTest(ctx, "Failed to read truncated string (8)");
+ } else if (len != 8) {
+ return failTest(ctx, "Failed to expand string value (8)");
+ } else if (0 != strncmp(s, "abcde\0\0\0", 8)) {
+ return failTest(ctx, "Failed to null pad string value (8)");
+ }
+
+ /* shrink from 8 to 4 */
+ if (REDISMODULE_ERR == RedisModule_StringTruncate(k, 4)) {
+ return failTest(ctx, "Could not truncate string value (4)");
+ }
+ s = RedisModule_StringDMA(k, &len, REDISMODULE_READ);
+ if (!s) {
+ return failTest(ctx, "Failed to read truncated string (4)");
+ } else if (len != 4) {
+ return failTest(ctx, "Failed to shrink string value (4)");
+ } else if (0 != strncmp(s, "abcd", 4)) {
+ return failTest(ctx, "Failed to truncate string value (4)");
+ }
+
+ /* shrink to 0 */
+ if (REDISMODULE_ERR == RedisModule_StringTruncate(k, 0)) {
+ return failTest(ctx, "Could not truncate string value (0)");
+ }
+ s = RedisModule_StringDMA(k, &len, REDISMODULE_READ);
+ if (!s) {
+ return failTest(ctx, "Failed to read truncated string (0)");
+ } else if (len != 0) {
+ return failTest(ctx, "Failed to shrink string value to (0)");
+ }
+
+ return RedisModule_ReplyWithSimpleString(ctx, "OK");
+}
+
+int NotifyCallback(RedisModuleCtx *ctx, int type, const char *event,
+ RedisModuleString *key) {
+ RedisModule_AutoMemory(ctx);
+ /* Increment a counter on the notifications: for each key notified we
+ * increment a counter */
+ RedisModule_Log(ctx, "notice", "Got event type %d, event %s, key %s", type,
+ event, RedisModule_StringPtrLen(key, NULL));
+
+ RedisModule_Call(ctx, "HINCRBY", "csc", "notifications", key, "1");
+ return REDISMODULE_OK;
+}
+
+/* TEST.NOTIFICATIONS -- Test Keyspace Notifications. */
+int TestNotifications(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ RedisModule_AutoMemory(ctx);
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+#define FAIL(msg, ...) \
+ { \
+ RedisModule_Log(ctx, "warning", "Failed NOTIFY Test. Reason: " #msg, ##__VA_ARGS__); \
+ goto err; \
+ }
+ RedisModule_Call(ctx, "FLUSHDB", "");
+
+ RedisModule_Call(ctx, "SET", "cc", "foo", "bar");
+ RedisModule_Call(ctx, "SET", "cc", "foo", "baz");
+ RedisModule_Call(ctx, "SADD", "cc", "bar", "x");
+ RedisModule_Call(ctx, "SADD", "cc", "bar", "y");
+
+ RedisModule_Call(ctx, "HSET", "ccc", "baz", "x", "y");
+ /* LPUSH should be ignored and not increment any counters */
+ RedisModule_Call(ctx, "LPUSH", "cc", "l", "y");
+ RedisModule_Call(ctx, "LPUSH", "cc", "l", "y");
+
+ /* Miss some keys intentionally so we will get a "keymiss" notification. */
+ RedisModule_Call(ctx, "GET", "c", "nosuchkey");
+ RedisModule_Call(ctx, "SMEMBERS", "c", "nosuchkey");
+
+ size_t sz;
+ const char *rep;
+ RedisModuleCallReply *r = RedisModule_Call(ctx, "HGET", "cc", "notifications", "foo");
+ if (r == NULL || RedisModule_CallReplyType(r) != REDISMODULE_REPLY_STRING) {
+ FAIL("Wrong or no reply for foo");
+ } else {
+ rep = RedisModule_CallReplyStringPtr(r, &sz);
+ if (sz != 1 || *rep != '2') {
+ FAIL("Got reply '%s'. expected '2'", RedisModule_CallReplyStringPtr(r, NULL));
+ }
+ }
+
+ r = RedisModule_Call(ctx, "HGET", "cc", "notifications", "bar");
+ if (r == NULL || RedisModule_CallReplyType(r) != REDISMODULE_REPLY_STRING) {
+ FAIL("Wrong or no reply for bar");
+ } else {
+ rep = RedisModule_CallReplyStringPtr(r, &sz);
+ if (sz != 1 || *rep != '2') {
+ FAIL("Got reply '%s'. expected '2'", rep);
+ }
+ }
+
+ r = RedisModule_Call(ctx, "HGET", "cc", "notifications", "baz");
+ if (r == NULL || RedisModule_CallReplyType(r) != REDISMODULE_REPLY_STRING) {
+ FAIL("Wrong or no reply for baz");
+ } else {
+ rep = RedisModule_CallReplyStringPtr(r, &sz);
+ if (sz != 1 || *rep != '1') {
+ FAIL("Got reply '%.*s'. expected '1'", (int)sz, rep);
+ }
+ }
+ /* For l we expect nothing since we didn't subscribe to list events */
+ r = RedisModule_Call(ctx, "HGET", "cc", "notifications", "l");
+ if (r == NULL || RedisModule_CallReplyType(r) != REDISMODULE_REPLY_NULL) {
+ FAIL("Wrong reply for l");
+ }
+
+ r = RedisModule_Call(ctx, "HGET", "cc", "notifications", "nosuchkey");
+ if (r == NULL || RedisModule_CallReplyType(r) != REDISMODULE_REPLY_STRING) {
+ FAIL("Wrong or no reply for nosuchkey");
+ } else {
+ rep = RedisModule_CallReplyStringPtr(r, &sz);
+ if (sz != 1 || *rep != '2') {
+ FAIL("Got reply '%.*s'. expected '2'", (int)sz, rep);
+ }
+ }
+
+ RedisModule_Call(ctx, "FLUSHDB", "");
+
+ return RedisModule_ReplyWithSimpleString(ctx, "OK");
+err:
+ RedisModule_Call(ctx, "FLUSHDB", "");
+
+ return RedisModule_ReplyWithSimpleString(ctx, "ERR");
+}
+
+/* TEST.CTXFLAGS -- Test GetContextFlags. */
+int TestCtxFlags(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argc);
+ REDISMODULE_NOT_USED(argv);
+
+ RedisModule_AutoMemory(ctx);
+
+ int ok = 1;
+ const char *errString = NULL;
+#undef FAIL
+#define FAIL(msg) \
+ { \
+ ok = 0; \
+ errString = msg; \
+ goto end; \
+ }
+
+ int flags = RedisModule_GetContextFlags(ctx);
+ if (flags == 0) {
+ FAIL("Got no flags");
+ }
+
+ if (flags & REDISMODULE_CTX_FLAGS_LUA) FAIL("Lua flag was set");
+ if (flags & REDISMODULE_CTX_FLAGS_MULTI) FAIL("Multi flag was set");
+
+ if (flags & REDISMODULE_CTX_FLAGS_AOF) FAIL("AOF Flag was set")
+ /* Enable AOF to test AOF flags */
+ RedisModule_Call(ctx, "config", "ccc", "set", "appendonly", "yes");
+ flags = RedisModule_GetContextFlags(ctx);
+ if (!(flags & REDISMODULE_CTX_FLAGS_AOF)) FAIL("AOF Flag not set after config set");
+
+ /* Disable RDB saving and test the flag. */
+ RedisModule_Call(ctx, "config", "ccc", "set", "save", "");
+ flags = RedisModule_GetContextFlags(ctx);
+ if (flags & REDISMODULE_CTX_FLAGS_RDB) FAIL("RDB Flag was set");
+ /* Enable RDB to test RDB flags */
+ RedisModule_Call(ctx, "config", "ccc", "set", "save", "900 1");
+ flags = RedisModule_GetContextFlags(ctx);
+ if (!(flags & REDISMODULE_CTX_FLAGS_RDB)) FAIL("RDB Flag was not set after config set");
+
+ if (!(flags & REDISMODULE_CTX_FLAGS_MASTER)) FAIL("Master flag was not set");
+ if (flags & REDISMODULE_CTX_FLAGS_SLAVE) FAIL("Slave flag was set");
+ if (flags & REDISMODULE_CTX_FLAGS_READONLY) FAIL("Read-only flag was set");
+ if (flags & REDISMODULE_CTX_FLAGS_CLUSTER) FAIL("Cluster flag was set");
+
+ /* Disable maxmemory and test the flag. (it is implicitly set in 32bit builds. */
+ RedisModule_Call(ctx, "config", "ccc", "set", "maxmemory", "0");
+ flags = RedisModule_GetContextFlags(ctx);
+ if (flags & REDISMODULE_CTX_FLAGS_MAXMEMORY) FAIL("Maxmemory flag was set");
+
+ /* Enable maxmemory and test the flag. */
+ RedisModule_Call(ctx, "config", "ccc", "set", "maxmemory", "100000000");
+ flags = RedisModule_GetContextFlags(ctx);
+ if (!(flags & REDISMODULE_CTX_FLAGS_MAXMEMORY))
+ FAIL("Maxmemory flag was not set after config set");
+
+ if (flags & REDISMODULE_CTX_FLAGS_EVICT) FAIL("Eviction flag was set");
+ RedisModule_Call(ctx, "config", "ccc", "set", "maxmemory-policy", "allkeys-lru");
+ flags = RedisModule_GetContextFlags(ctx);
+ if (!(flags & REDISMODULE_CTX_FLAGS_EVICT)) FAIL("Eviction flag was not set after config set");
+
+end:
+ /* Revert config changes */
+ RedisModule_Call(ctx, "config", "ccc", "set", "appendonly", "no");
+ RedisModule_Call(ctx, "config", "ccc", "set", "save", "");
+ RedisModule_Call(ctx, "config", "ccc", "set", "maxmemory", "0");
+ RedisModule_Call(ctx, "config", "ccc", "set", "maxmemory-policy", "noeviction");
+
+ if (!ok) {
+ RedisModule_Log(ctx, "warning", "Failed CTXFLAGS Test. Reason: %s", errString);
+ return RedisModule_ReplyWithSimpleString(ctx, "ERR");
+ }
+
+ return RedisModule_ReplyWithSimpleString(ctx, "OK");
+}
+
+/* ----------------------------- Test framework ----------------------------- */
+
+/* Return 1 if the reply matches the specified string, otherwise log errors
+ * in the server log and return 0. */
+int TestAssertErrorReply(RedisModuleCtx *ctx, RedisModuleCallReply *reply, char *str, size_t len) {
+ RedisModuleString *mystr, *expected;
+ if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_ERROR) {
+ return 0;
+ }
+
+ mystr = RedisModule_CreateStringFromCallReply(reply);
+ expected = RedisModule_CreateString(ctx,str,len);
+ if (RedisModule_StringCompare(mystr,expected) != 0) {
+ const char *mystr_ptr = RedisModule_StringPtrLen(mystr,NULL);
+ const char *expected_ptr = RedisModule_StringPtrLen(expected,NULL);
+ RedisModule_Log(ctx,"warning",
+ "Unexpected Error reply reply '%s' (instead of '%s')",
+ mystr_ptr, expected_ptr);
+ return 0;
+ }
+ return 1;
+}
+
+int TestAssertStringReply(RedisModuleCtx *ctx, RedisModuleCallReply *reply, char *str, size_t len) {
+ RedisModuleString *mystr, *expected;
+
+ if (RedisModule_CallReplyType(reply) == REDISMODULE_REPLY_ERROR) {
+ RedisModule_Log(ctx,"warning","Test error reply: %s",
+ RedisModule_CallReplyStringPtr(reply, NULL));
+ return 0;
+ } else if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_STRING) {
+ RedisModule_Log(ctx,"warning","Unexpected reply type %d",
+ RedisModule_CallReplyType(reply));
+ return 0;
+ }
+ mystr = RedisModule_CreateStringFromCallReply(reply);
+ expected = RedisModule_CreateString(ctx,str,len);
+ if (RedisModule_StringCompare(mystr,expected) != 0) {
+ const char *mystr_ptr = RedisModule_StringPtrLen(mystr,NULL);
+ const char *expected_ptr = RedisModule_StringPtrLen(expected,NULL);
+ RedisModule_Log(ctx,"warning",
+ "Unexpected string reply '%s' (instead of '%s')",
+ mystr_ptr, expected_ptr);
+ return 0;
+ }
+ return 1;
+}
+
+/* Return 1 if the reply matches the specified integer, otherwise log errors
+ * in the server log and return 0. */
+int TestAssertIntegerReply(RedisModuleCtx *ctx, RedisModuleCallReply *reply, long long expected) {
+ if (RedisModule_CallReplyType(reply) == REDISMODULE_REPLY_ERROR) {
+ RedisModule_Log(ctx,"warning","Test error reply: %s",
+ RedisModule_CallReplyStringPtr(reply, NULL));
+ return 0;
+ } else if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_INTEGER) {
+ RedisModule_Log(ctx,"warning","Unexpected reply type %d",
+ RedisModule_CallReplyType(reply));
+ return 0;
+ }
+ long long val = RedisModule_CallReplyInteger(reply);
+ if (val != expected) {
+ RedisModule_Log(ctx,"warning",
+ "Unexpected integer reply '%lld' (instead of '%lld')",
+ val, expected);
+ return 0;
+ }
+ return 1;
+}
+
+/* Replies "yes", "no" otherwise if the context may execute debug commands */
+int TestCanDebug(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ int flags = RedisModule_GetContextFlags(ctx);
+ int allFlags = RedisModule_GetContextFlagsAll();
+ if ((allFlags & REDISMODULE_CTX_FLAGS_DEBUG_ENABLED) &&
+ (flags & REDISMODULE_CTX_FLAGS_DEBUG_ENABLED)) {
+ RedisModule_ReplyWithSimpleString(ctx, "yes");
+ } else {
+ RedisModule_ReplyWithSimpleString(ctx, "no");
+ }
+ return REDISMODULE_OK;
+}
+
+#define T(name,...) \
+ do { \
+ RedisModule_Log(ctx,"warning","Testing %s", name); \
+ reply = RedisModule_Call(ctx,name,__VA_ARGS__); \
+ } while (0)
+
+/* TEST.BASICS -- Run all the tests.
+ * Note: it is useful to run these tests from the module rather than TCL
+ * since it's easier to check the reply types like that make a distinction
+ * between 0 and "0", etc. */
+int TestBasics(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ RedisModule_AutoMemory(ctx);
+ RedisModuleCallReply *reply;
+
+ /* Make sure the DB is empty before to proceed. */
+ T("dbsize","");
+ if (!TestAssertIntegerReply(ctx,reply,0)) goto fail;
+
+ T("ping","");
+ if (!TestAssertStringReply(ctx,reply,"PONG",4)) goto fail;
+
+ T("test.call","");
+ if (!TestAssertStringReply(ctx,reply,"OK",2)) goto fail;
+
+ T("test.callresp3map","");
+ if (!TestAssertStringReply(ctx,reply,"OK",2)) goto fail;
+
+ T("test.callresp3set","");
+ if (!TestAssertStringReply(ctx,reply,"OK",2)) goto fail;
+
+ T("test.callresp3double","");
+ if (!TestAssertStringReply(ctx,reply,"OK",2)) goto fail;
+
+ T("test.callresp3bool","");
+ if (!TestAssertStringReply(ctx,reply,"OK",2)) goto fail;
+
+ T("test.callresp3null","");
+ if (!TestAssertStringReply(ctx,reply,"OK",2)) goto fail;
+
+ T("test.callreplywithnestedreply","");
+ if (!TestAssertStringReply(ctx,reply,"test",4)) goto fail;
+
+ T("test.callreplywithbignumberreply","");
+ if (!TestAssertStringReply(ctx,reply,"1234567999999999999999999999999999999",37)) goto fail;
+
+ T("test.callreplywithverbatimstringreply","");
+ if (!TestAssertStringReply(ctx,reply,"txt:This is a verbatim\nstring",29)) goto fail;
+
+ T("test.ctxflags","");
+ if (!TestAssertStringReply(ctx,reply,"OK",2)) goto fail;
+
+ T("test.string.append","");
+ if (!TestAssertStringReply(ctx,reply,"foobar",6)) goto fail;
+
+ T("test.string.truncate","");
+ if (!TestAssertStringReply(ctx,reply,"OK",2)) goto fail;
+
+ T("test.unlink","");
+ if (!TestAssertStringReply(ctx,reply,"OK",2)) goto fail;
+
+ T("test.nestedcallreplyarray","");
+ if (!TestAssertStringReply(ctx,reply,"OK",2)) goto fail;
+
+ T("test.string.append.am","");
+ if (!TestAssertStringReply(ctx,reply,"foobar",6)) goto fail;
+
+ T("test.string.trim","");
+ if (!TestAssertStringReply(ctx,reply,"OK",2)) goto fail;
+
+ T("test.string.printf", "cc", "foo", "bar");
+ if (!TestAssertStringReply(ctx,reply,"Got 3 args. argv[1]: foo, argv[2]: bar",38)) goto fail;
+
+ T("test.notify", "");
+ if (!TestAssertStringReply(ctx,reply,"OK",2)) goto fail;
+
+ T("test.callreplywitharrayreply", "");
+ if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_ARRAY) goto fail;
+ if (RedisModule_CallReplyLength(reply) != 2) goto fail;
+ if (!TestAssertStringReply(ctx,RedisModule_CallReplyArrayElement(reply, 0),"test",4)) goto fail;
+ if (!TestAssertStringReply(ctx,RedisModule_CallReplyArrayElement(reply, 1),"1234",4)) goto fail;
+
+ T("foo", "E");
+ if (!TestAssertErrorReply(ctx,reply,"ERR unknown command 'foo'",25)) goto fail;
+
+ T("set", "Ec", "x");
+ if (!TestAssertErrorReply(ctx,reply,"ERR wrong number of arguments for 'set' command",47)) goto fail;
+
+ T("shutdown", "SE");
+ if (!TestAssertErrorReply(ctx,reply,"ERR command 'shutdown' is not allowed on script mode",52)) goto fail;
+
+ T("set", "WEcc", "x", "1");
+ if (!TestAssertErrorReply(ctx,reply,"ERR Write command 'set' was called while write is not allowed.",62)) goto fail;
+
+ RedisModule_ReplyWithSimpleString(ctx,"ALL TESTS PASSED");
+ return REDISMODULE_OK;
+
+fail:
+ RedisModule_ReplyWithSimpleString(ctx,
+ "SOME TEST DID NOT PASS! Check server logs");
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ if (RedisModule_Init(ctx,"test",1,REDISMODULE_APIVER_1)
+ == REDISMODULE_ERR) return REDISMODULE_ERR;
+
+ /* Perform RM_Call inside the RedisModule_OnLoad
+ * to verify that it works as expected without crashing.
+ * The tests will verify it on different configurations
+ * options (cluster/no cluster). A simple ping command
+ * is enough for this test. */
+ RedisModuleCallReply *reply = RedisModule_Call(ctx, "ping", "");
+ if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_STRING) {
+ RedisModule_FreeCallReply(reply);
+ return REDISMODULE_ERR;
+ }
+ size_t len;
+ const char *reply_str = RedisModule_CallReplyStringPtr(reply, &len);
+ if (len != 4) {
+ RedisModule_FreeCallReply(reply);
+ return REDISMODULE_ERR;
+ }
+ if (memcmp(reply_str, "PONG", 4) != 0) {
+ RedisModule_FreeCallReply(reply);
+ return REDISMODULE_ERR;
+ }
+ RedisModule_FreeCallReply(reply);
+
+ if (RedisModule_CreateCommand(ctx,"test.call",
+ TestCall,"write deny-oom",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"test.callresp3map",
+ TestCallResp3Map,"write deny-oom",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"test.callresp3attribute",
+ TestCallResp3Attribute,"write deny-oom",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"test.callresp3set",
+ TestCallResp3Set,"write deny-oom",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"test.callresp3double",
+ TestCallResp3Double,"write deny-oom",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"test.callresp3bool",
+ TestCallResp3Bool,"write deny-oom",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"test.callresp3null",
+ TestCallResp3Null,"write deny-oom",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"test.callreplywitharrayreply",
+ TestCallReplyWithArrayReply,"write deny-oom",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"test.callreplywithnestedreply",
+ TestCallReplyWithNestedReply,"write deny-oom",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"test.callreplywithbignumberreply",
+ TestCallResp3BigNumber,"write deny-oom",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"test.callreplywithverbatimstringreply",
+ TestCallResp3Verbatim,"write deny-oom",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"test.string.append",
+ TestStringAppend,"write deny-oom",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"test.string.trim",
+ TestTrimString,"write deny-oom",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"test.string.append.am",
+ TestStringAppendAM,"write deny-oom",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"test.string.truncate",
+ TestStringTruncate,"write deny-oom",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"test.string.printf",
+ TestStringPrintf,"write deny-oom",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"test.ctxflags",
+ TestCtxFlags,"readonly",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"test.unlink",
+ TestUnlink,"write deny-oom",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"test.nestedcallreplyarray",
+ TestNestedCallReplyArrayElement,"write deny-oom",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"test.basics",
+ TestBasics,"write",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ /* the following commands are used by an external test and should not be added to TestBasics */
+ if (RedisModule_CreateCommand(ctx,"test.rmcallautomode",
+ TestCallRespAutoMode,"write",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"test.getresp",
+ TestGetResp,"readonly",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"test.candebug",
+ TestCanDebug,"readonly",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ RedisModule_SubscribeToKeyspaceEvents(ctx,
+ REDISMODULE_NOTIFY_HASH |
+ REDISMODULE_NOTIFY_SET |
+ REDISMODULE_NOTIFY_STRING |
+ REDISMODULE_NOTIFY_KEY_MISS,
+ NotifyCallback);
+ if (RedisModule_CreateCommand(ctx,"test.notify",
+ TestNotifications,"write deny-oom",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/blockedclient.c b/examples/redis-unstable/tests/modules/blockedclient.c
new file mode 100644
index 0000000..dc226ee
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/blockedclient.c
@@ -0,0 +1,723 @@
+/* define macros for having usleep */
+#define _BSD_SOURCE
+#define _DEFAULT_SOURCE
+#include <unistd.h>
+
+#include "redismodule.h"
+#include <assert.h>
+#include <stdio.h>
+#include <pthread.h>
+#include <strings.h>
+
+#define UNUSED(V) ((void) V)
+
+/* used to test processing events during slow bg operation */
+static volatile int g_slow_bg_operation = 0;
+static volatile int g_is_in_slow_bg_operation = 0;
+
+void *sub_worker(void *arg) {
+ // Get Redis module context
+ RedisModuleCtx *ctx = (RedisModuleCtx *)arg;
+
+ // Try acquiring GIL
+ int res = RedisModule_ThreadSafeContextTryLock(ctx);
+
+ // GIL is already taken by the calling thread expecting to fail.
+ assert(res != REDISMODULE_OK);
+
+ return NULL;
+}
+
+void *worker(void *arg) {
+ // Retrieve blocked client
+ RedisModuleBlockedClient *bc = (RedisModuleBlockedClient *)arg;
+
+ // Get Redis module context
+ RedisModuleCtx *ctx = RedisModule_GetThreadSafeContext(bc);
+
+ // Acquire GIL
+ RedisModule_ThreadSafeContextLock(ctx);
+
+ // Create another thread which will try to acquire the GIL
+ pthread_t tid;
+ int res = pthread_create(&tid, NULL, sub_worker, ctx);
+ assert(res == 0);
+
+ // Wait for thread
+ pthread_join(tid, NULL);
+
+ // Release GIL
+ RedisModule_ThreadSafeContextUnlock(ctx);
+
+ // Reply to client
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+
+ // Unblock client
+ RedisModule_UnblockClient(bc, NULL);
+
+ // Free the Redis module context
+ RedisModule_FreeThreadSafeContext(ctx);
+
+ return NULL;
+}
+
+int acquire_gil(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ UNUSED(argv);
+ UNUSED(argc);
+
+ int flags = RedisModule_GetContextFlags(ctx);
+ int allFlags = RedisModule_GetContextFlagsAll();
+ if ((allFlags & REDISMODULE_CTX_FLAGS_MULTI) &&
+ (flags & REDISMODULE_CTX_FLAGS_MULTI)) {
+ RedisModule_ReplyWithSimpleString(ctx, "Blocked client is not supported inside multi");
+ return REDISMODULE_OK;
+ }
+
+ if ((allFlags & REDISMODULE_CTX_FLAGS_DENY_BLOCKING) &&
+ (flags & REDISMODULE_CTX_FLAGS_DENY_BLOCKING)) {
+ RedisModule_ReplyWithSimpleString(ctx, "Blocked client is not allowed");
+ return REDISMODULE_OK;
+ }
+
+ /* This command handler tries to acquire the GIL twice
+ * once in the worker thread using "RedisModule_ThreadSafeContextLock"
+ * second in the sub-worker thread
+ * using "RedisModule_ThreadSafeContextTryLock"
+ * as the GIL is already locked. */
+ RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx, NULL, NULL, NULL, 0);
+
+ pthread_t tid;
+ int res = pthread_create(&tid, NULL, worker, bc);
+ assert(res == 0);
+ pthread_detach(tid);
+
+ return REDISMODULE_OK;
+}
+
+typedef struct {
+ RedisModuleString **argv;
+ int argc;
+ RedisModuleBlockedClient *bc;
+} bg_call_data;
+
+void *bg_call_worker(void *arg) {
+ bg_call_data *bg = arg;
+ RedisModuleBlockedClient *bc = bg->bc;
+
+ // Get Redis module context
+ RedisModuleCtx *ctx = RedisModule_GetThreadSafeContext(bg->bc);
+
+ // Acquire GIL
+ RedisModule_ThreadSafeContextLock(ctx);
+
+ // Test slow operation yielding
+ if (g_slow_bg_operation) {
+ g_is_in_slow_bg_operation = 1;
+ while (g_slow_bg_operation) {
+ RedisModule_Yield(ctx, REDISMODULE_YIELD_FLAG_CLIENTS, "Slow module operation");
+ usleep(1000);
+ }
+ g_is_in_slow_bg_operation = 0;
+ }
+
+ // Call the command
+ const char *module_cmd = RedisModule_StringPtrLen(bg->argv[0], NULL);
+ int cmd_pos = 1;
+ RedisModuleString *format_redis_str = RedisModule_CreateString(NULL, "v", 1);
+ if (!strcasecmp(module_cmd, "do_bg_rm_call_format")) {
+ cmd_pos = 2;
+ size_t format_len;
+ const char *format = RedisModule_StringPtrLen(bg->argv[1], &format_len);
+ RedisModule_StringAppendBuffer(NULL, format_redis_str, format, format_len);
+ RedisModule_StringAppendBuffer(NULL, format_redis_str, "E", 1);
+ }
+ const char *format = RedisModule_StringPtrLen(format_redis_str, NULL);
+ const char *cmd = RedisModule_StringPtrLen(bg->argv[cmd_pos], NULL);
+ RedisModuleCallReply *rep = RedisModule_Call(ctx, cmd, format, bg->argv + cmd_pos + 1, (size_t)bg->argc - cmd_pos - 1);
+ RedisModule_FreeString(NULL, format_redis_str);
+
+ /* Free the arguments within GIL to prevent simultaneous freeing in main thread. */
+ for (int i=0; i<bg->argc; i++)
+ RedisModule_FreeString(ctx, bg->argv[i]);
+ RedisModule_Free(bg->argv);
+ RedisModule_Free(bg);
+
+ // Release GIL
+ RedisModule_ThreadSafeContextUnlock(ctx);
+
+ // Reply to client
+ if (!rep) {
+ RedisModule_ReplyWithError(ctx, "NULL reply returned");
+ } else {
+ RedisModule_ReplyWithCallReply(ctx, rep);
+ RedisModule_FreeCallReply(rep);
+ }
+
+ // Unblock client
+ RedisModule_UnblockClient(bc, NULL);
+
+ // Free the Redis module context
+ RedisModule_FreeThreadSafeContext(ctx);
+
+ return NULL;
+}
+
+int do_bg_rm_call(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ UNUSED(argv);
+ UNUSED(argc);
+
+ /* Make sure we're not trying to block a client when we shouldn't */
+ int flags = RedisModule_GetContextFlags(ctx);
+ int allFlags = RedisModule_GetContextFlagsAll();
+ if ((allFlags & REDISMODULE_CTX_FLAGS_MULTI) &&
+ (flags & REDISMODULE_CTX_FLAGS_MULTI)) {
+ RedisModule_ReplyWithSimpleString(ctx, "Blocked client is not supported inside multi");
+ return REDISMODULE_OK;
+ }
+ if ((allFlags & REDISMODULE_CTX_FLAGS_DENY_BLOCKING) &&
+ (flags & REDISMODULE_CTX_FLAGS_DENY_BLOCKING)) {
+ RedisModule_ReplyWithSimpleString(ctx, "Blocked client is not allowed");
+ return REDISMODULE_OK;
+ }
+
+ /* Make a copy of the arguments and pass them to the thread. */
+ bg_call_data *bg = RedisModule_Alloc(sizeof(bg_call_data));
+ bg->argv = RedisModule_Alloc(sizeof(RedisModuleString*)*argc);
+ bg->argc = argc;
+ for (int i=0; i<argc; i++)
+ bg->argv[i] = RedisModule_HoldString(ctx, argv[i]);
+
+ /* Block the client */
+ bg->bc = RedisModule_BlockClient(ctx, NULL, NULL, NULL, 0);
+
+ /* Start a thread to handle the request */
+ pthread_t tid;
+ int res = pthread_create(&tid, NULL, bg_call_worker, bg);
+ assert(res == 0);
+ pthread_detach(tid);
+
+ return REDISMODULE_OK;
+}
+
+int do_rm_call(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){
+ UNUSED(argv);
+ UNUSED(argc);
+
+ if(argc < 2){
+ return RedisModule_WrongArity(ctx);
+ }
+
+ const char* cmd = RedisModule_StringPtrLen(argv[1], NULL);
+
+ RedisModuleCallReply* rep = RedisModule_Call(ctx, cmd, "Ev", argv + 2, (size_t)argc - 2);
+ if(!rep){
+ RedisModule_ReplyWithError(ctx, "NULL reply returned");
+ }else{
+ RedisModule_ReplyWithCallReply(ctx, rep);
+ RedisModule_FreeCallReply(rep);
+ }
+
+ return REDISMODULE_OK;
+}
+
+static void rm_call_async_send_reply(RedisModuleCtx *ctx, RedisModuleCallReply *reply) {
+ RedisModule_ReplyWithCallReply(ctx, reply);
+ RedisModule_FreeCallReply(reply);
+}
+
+/* Called when the command that was blocked on 'RM_Call' gets unblocked
+ * and send the reply to the blocked client. */
+static void rm_call_async_on_unblocked(RedisModuleCtx *ctx, RedisModuleCallReply *reply, void *private_data) {
+ UNUSED(ctx);
+ RedisModuleBlockedClient *bc = private_data;
+ RedisModuleCtx *bctx = RedisModule_GetThreadSafeContext(bc);
+ rm_call_async_send_reply(bctx, reply);
+ RedisModule_FreeThreadSafeContext(bctx);
+ RedisModule_UnblockClient(bc, RedisModule_BlockClientGetPrivateData(bc));
+}
+
+int do_rm_call_async_fire_and_forget(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){
+ UNUSED(argv);
+ UNUSED(argc);
+
+ if(argc < 2){
+ return RedisModule_WrongArity(ctx);
+ }
+ const char* cmd = RedisModule_StringPtrLen(argv[1], NULL);
+
+ RedisModuleCallReply* rep = RedisModule_Call(ctx, cmd, "!KEv", argv + 2, (size_t)argc - 2);
+
+ if(RedisModule_CallReplyType(rep) != REDISMODULE_REPLY_PROMISE) {
+ RedisModule_ReplyWithCallReply(ctx, rep);
+ } else {
+ RedisModule_ReplyWithSimpleString(ctx, "Blocked");
+ }
+ RedisModule_FreeCallReply(rep);
+
+ return REDISMODULE_OK;
+}
+
+static void do_rm_call_async_free_pd(RedisModuleCtx * ctx, void *pd) {
+ UNUSED(ctx);
+ RedisModule_FreeCallReply(pd);
+}
+
+static void do_rm_call_async_disconnect(RedisModuleCtx *ctx, struct RedisModuleBlockedClient *bc) {
+ UNUSED(ctx);
+ RedisModuleCallReply* rep = RedisModule_BlockClientGetPrivateData(bc);
+ RedisModule_CallReplyPromiseAbort(rep, NULL);
+ RedisModule_FreeCallReply(rep);
+ RedisModule_AbortBlock(bc);
+}
+
+/*
+ * Callback for do_rm_call_async / do_rm_call_async_script_mode
+ * Gets the command to invoke as the first argument to the command and runs it,
+ * passing the rest of the arguments to the command invocation.
+ * If the command got blocked, blocks the client and unblock it when the command gets unblocked,
+ * this allows check the K (allow blocking) argument to RM_Call.
+ */
+int do_rm_call_async(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){
+ UNUSED(argv);
+ UNUSED(argc);
+
+ if(argc < 2){
+ return RedisModule_WrongArity(ctx);
+ }
+
+ size_t format_len = 0;
+ char format[6] = {0};
+
+ if (!(RedisModule_GetContextFlags(ctx) & REDISMODULE_CTX_FLAGS_DENY_BLOCKING)) {
+ /* We are allowed to block the client so we can allow RM_Call to also block us */
+ format[format_len++] = 'K';
+ }
+
+ const char* invoked_cmd = RedisModule_StringPtrLen(argv[0], NULL);
+ if (strcasecmp(invoked_cmd, "do_rm_call_async_script_mode") == 0) {
+ format[format_len++] = 'S';
+ }
+
+ format[format_len++] = 'E';
+ format[format_len++] = 'v';
+ if (strcasecmp(invoked_cmd, "do_rm_call_async_no_replicate") != 0) {
+ /* Notice, without the '!' flag we will have inconsistency between master and replica.
+ * This is used only to check '!' flag correctness on blocked commands. */
+ format[format_len++] = '!';
+ }
+
+ const char* cmd = RedisModule_StringPtrLen(argv[1], NULL);
+
+ RedisModuleCallReply* rep = RedisModule_Call(ctx, cmd, format, argv + 2, (size_t)argc - 2);
+
+ if(RedisModule_CallReplyType(rep) != REDISMODULE_REPLY_PROMISE) {
+ rm_call_async_send_reply(ctx, rep);
+ } else {
+ RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx, NULL, NULL, do_rm_call_async_free_pd, 0);
+ RedisModule_SetDisconnectCallback(bc, do_rm_call_async_disconnect);
+ RedisModule_BlockClientSetPrivateData(bc, rep);
+ RedisModule_CallReplyPromiseSetUnblockHandler(rep, rm_call_async_on_unblocked, bc);
+ }
+
+ return REDISMODULE_OK;
+}
+
+typedef struct ThreadedAsyncRMCallCtx{
+ RedisModuleBlockedClient *bc;
+ RedisModuleCallReply *reply;
+} ThreadedAsyncRMCallCtx;
+
+void *send_async_reply(void *arg) {
+ ThreadedAsyncRMCallCtx *ta_rm_call_ctx = arg;
+ rm_call_async_on_unblocked(NULL, ta_rm_call_ctx->reply, ta_rm_call_ctx->bc);
+ RedisModule_Free(ta_rm_call_ctx);
+ return NULL;
+}
+
+/* Called when the command that was blocked on 'RM_Call' gets unblocked
+ * and schedule a thread to send the reply to the blocked client. */
+static void rm_call_async_reply_on_thread(RedisModuleCtx *ctx, RedisModuleCallReply *reply, void *private_data) {
+ UNUSED(ctx);
+ ThreadedAsyncRMCallCtx *ta_rm_call_ctx = RedisModule_Alloc(sizeof(*ta_rm_call_ctx));
+ ta_rm_call_ctx->bc = private_data;
+ ta_rm_call_ctx->reply = reply;
+ pthread_t tid;
+ int res = pthread_create(&tid, NULL, send_async_reply, ta_rm_call_ctx);
+ assert(res == 0);
+ pthread_detach(tid);
+}
+
+/*
+ * Callback for do_rm_call_async_on_thread.
+ * Gets the command to invoke as the first argument to the command and runs it,
+ * passing the rest of the arguments to the command invocation.
+ * If the command got blocked, blocks the client and unblock on a background thread.
+ * this allows check the K (allow blocking) argument to RM_Call, and make sure that the reply
+ * that passes to unblock handler is owned by the handler and are not attached to any
+ * context that might be freed after the callback ends.
+ */
+int do_rm_call_async_on_thread(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){
+ UNUSED(argv);
+ UNUSED(argc);
+
+ if(argc < 2){
+ return RedisModule_WrongArity(ctx);
+ }
+
+ const char* cmd = RedisModule_StringPtrLen(argv[1], NULL);
+
+ RedisModuleCallReply* rep = RedisModule_Call(ctx, cmd, "KEv", argv + 2, (size_t)argc - 2);
+
+ if(RedisModule_CallReplyType(rep) != REDISMODULE_REPLY_PROMISE) {
+ rm_call_async_send_reply(ctx, rep);
+ } else {
+ RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx, NULL, NULL, NULL, 0);
+ RedisModule_CallReplyPromiseSetUnblockHandler(rep, rm_call_async_reply_on_thread, bc);
+ RedisModule_FreeCallReply(rep);
+ }
+
+ return REDISMODULE_OK;
+}
+
+/* Private data for wait_and_do_rm_call_async that holds information about:
+ * 1. the block client, to unblock when done.
+ * 2. the arguments, contains the command to run using RM_Call */
+typedef struct WaitAndDoRMCallCtx {
+ RedisModuleBlockedClient *bc;
+ RedisModuleString **argv;
+ int argc;
+} WaitAndDoRMCallCtx;
+
+/*
+ * This callback will be called when the 'wait' command invoke on 'wait_and_do_rm_call_async' will finish.
+ * This callback will continue the execution flow just like 'do_rm_call_async' command.
+ */
+static void wait_and_do_rm_call_async_on_unblocked(RedisModuleCtx *ctx, RedisModuleCallReply *reply, void *private_data) {
+ WaitAndDoRMCallCtx *wctx = private_data;
+ if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_INTEGER) {
+ goto done;
+ }
+
+ if (RedisModule_CallReplyInteger(reply) != 1) {
+ goto done;
+ }
+
+ RedisModule_FreeCallReply(reply);
+ reply = NULL;
+
+ const char* cmd = RedisModule_StringPtrLen(wctx->argv[0], NULL);
+ reply = RedisModule_Call(ctx, cmd, "!EKv", wctx->argv + 1, (size_t)wctx->argc - 1);
+
+done:
+ if(RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_PROMISE) {
+ RedisModuleCtx *bctx = RedisModule_GetThreadSafeContext(wctx->bc);
+ rm_call_async_send_reply(bctx, reply);
+ RedisModule_FreeThreadSafeContext(bctx);
+ RedisModule_UnblockClient(wctx->bc, NULL);
+ } else {
+ RedisModule_CallReplyPromiseSetUnblockHandler(reply, rm_call_async_on_unblocked, wctx->bc);
+ RedisModule_FreeCallReply(reply);
+ }
+ for (int i = 0 ; i < wctx->argc ; ++i) {
+ RedisModule_FreeString(NULL, wctx->argv[i]);
+ }
+ RedisModule_Free(wctx->argv);
+ RedisModule_Free(wctx);
+}
+
+/*
+ * Callback for wait_and_do_rm_call
+ * Gets the command to invoke as the first argument, runs 'wait'
+ * command (using the K flag to RM_Call). Once the wait finished, runs the
+ * command that was given (just like 'do_rm_call_async').
+ */
+int wait_and_do_rm_call_async(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ UNUSED(argv);
+ UNUSED(argc);
+
+ if(argc < 2){
+ return RedisModule_WrongArity(ctx);
+ }
+
+ int flags = RedisModule_GetContextFlags(ctx);
+ if (flags & REDISMODULE_CTX_FLAGS_DENY_BLOCKING) {
+ return RedisModule_ReplyWithError(ctx, "Err can not run wait, blocking is not allowed.");
+ }
+
+ RedisModuleCallReply* rep = RedisModule_Call(ctx, "wait", "!EKcc", "1", "0");
+ if(RedisModule_CallReplyType(rep) != REDISMODULE_REPLY_PROMISE) {
+ rm_call_async_send_reply(ctx, rep);
+ } else {
+ RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx, NULL, NULL, NULL, 0);
+ WaitAndDoRMCallCtx *wctx = RedisModule_Alloc(sizeof(*wctx));
+ *wctx = (WaitAndDoRMCallCtx){
+ .bc = bc,
+ .argv = RedisModule_Alloc((argc - 1) * sizeof(RedisModuleString*)),
+ .argc = argc - 1,
+ };
+
+ for (int i = 1 ; i < argc ; ++i) {
+ wctx->argv[i - 1] = RedisModule_HoldString(NULL, argv[i]);
+ }
+ RedisModule_CallReplyPromiseSetUnblockHandler(rep, wait_and_do_rm_call_async_on_unblocked, wctx);
+ RedisModule_FreeCallReply(rep);
+ }
+
+ return REDISMODULE_OK;
+}
+
+static void blpop_and_set_multiple_keys_on_unblocked(RedisModuleCtx *ctx, RedisModuleCallReply *reply, void *private_data) {
+ /* ignore the reply */
+ RedisModule_FreeCallReply(reply);
+ WaitAndDoRMCallCtx *wctx = private_data;
+ for (int i = 0 ; i < wctx->argc ; i += 2) {
+ RedisModuleCallReply* rep = RedisModule_Call(ctx, "set", "!ss", wctx->argv[i], wctx->argv[i + 1]);
+ RedisModule_FreeCallReply(rep);
+ }
+
+ RedisModuleCtx *bctx = RedisModule_GetThreadSafeContext(wctx->bc);
+ RedisModule_ReplyWithSimpleString(bctx, "OK");
+ RedisModule_FreeThreadSafeContext(bctx);
+ RedisModule_UnblockClient(wctx->bc, NULL);
+
+ for (int i = 0 ; i < wctx->argc ; ++i) {
+ RedisModule_FreeString(NULL, wctx->argv[i]);
+ }
+ RedisModule_Free(wctx->argv);
+ RedisModule_Free(wctx);
+
+}
+
+/*
+ * Performs a blpop command on a given list and when unblocked set multiple string keys.
+ * This command allows checking that the unblock callback is performed as a unit
+ * and its effect are replicated to the replica and AOF wrapped with multi exec.
+ */
+int blpop_and_set_multiple_keys(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ UNUSED(argv);
+ UNUSED(argc);
+
+ if(argc < 2 || argc % 2 != 0){
+ return RedisModule_WrongArity(ctx);
+ }
+
+ int flags = RedisModule_GetContextFlags(ctx);
+ if (flags & REDISMODULE_CTX_FLAGS_DENY_BLOCKING) {
+ return RedisModule_ReplyWithError(ctx, "Err can not run wait, blocking is not allowed.");
+ }
+
+ RedisModuleCallReply* rep = RedisModule_Call(ctx, "blpop", "!EKsc", argv[1], "0");
+ if(RedisModule_CallReplyType(rep) != REDISMODULE_REPLY_PROMISE) {
+ rm_call_async_send_reply(ctx, rep);
+ } else {
+ RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx, NULL, NULL, NULL, 0);
+ WaitAndDoRMCallCtx *wctx = RedisModule_Alloc(sizeof(*wctx));
+ *wctx = (WaitAndDoRMCallCtx){
+ .bc = bc,
+ .argv = RedisModule_Alloc((argc - 2) * sizeof(RedisModuleString*)),
+ .argc = argc - 2,
+ };
+
+ for (int i = 0 ; i < argc - 2 ; ++i) {
+ wctx->argv[i] = RedisModule_HoldString(NULL, argv[i + 2]);
+ }
+ RedisModule_CallReplyPromiseSetUnblockHandler(rep, blpop_and_set_multiple_keys_on_unblocked, wctx);
+ RedisModule_FreeCallReply(rep);
+ }
+
+ return REDISMODULE_OK;
+}
+
+/* simulate a blocked client replying to a thread safe context without creating a thread */
+int do_fake_bg_true(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ UNUSED(argv);
+ UNUSED(argc);
+
+ RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx, NULL, NULL, NULL, 0);
+ RedisModuleCtx *bctx = RedisModule_GetThreadSafeContext(bc);
+
+ RedisModule_ReplyWithBool(bctx, 1);
+
+ RedisModule_FreeThreadSafeContext(bctx);
+ RedisModule_UnblockClient(bc, NULL);
+
+ return REDISMODULE_OK;
+}
+
+
+/* this flag is used to work with busy commands, that might take a while
+ * and ability to stop the busy work with a different command*/
+static volatile int abort_flag = 0;
+
+int slow_fg_command(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+ long long block_time = 0;
+ if (RedisModule_StringToLongLong(argv[1], &block_time) != REDISMODULE_OK) {
+ RedisModule_ReplyWithError(ctx, "Invalid integer value");
+ return REDISMODULE_OK;
+ }
+
+ uint64_t start_time = RedisModule_MonotonicMicroseconds();
+ /* when not blocking indefinitely, we don't process client commands in this test. */
+ int yield_flags = block_time? REDISMODULE_YIELD_FLAG_NONE: REDISMODULE_YIELD_FLAG_CLIENTS;
+ while (!abort_flag) {
+ RedisModule_Yield(ctx, yield_flags, "Slow module operation");
+ usleep(1000);
+ if (block_time && RedisModule_MonotonicMicroseconds() - start_time > (uint64_t)block_time)
+ break;
+ }
+
+ abort_flag = 0;
+ RedisModule_ReplyWithLongLong(ctx, 1);
+ return REDISMODULE_OK;
+}
+
+int stop_slow_fg_command(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ abort_flag = 1;
+ RedisModule_ReplyWithLongLong(ctx, 1);
+ return REDISMODULE_OK;
+}
+
+/* used to enable or disable slow operation in do_bg_rm_call */
+static int set_slow_bg_operation(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+ long long ll;
+ if (RedisModule_StringToLongLong(argv[1], &ll) != REDISMODULE_OK) {
+ RedisModule_ReplyWithError(ctx, "Invalid integer value");
+ return REDISMODULE_OK;
+ }
+ g_slow_bg_operation = ll;
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+/* used to test if we reached the slow operation in do_bg_rm_call */
+static int is_in_slow_bg_operation(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ UNUSED(argv);
+ if (argc != 1) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ RedisModule_ReplyWithLongLong(ctx, g_is_in_slow_bg_operation);
+ return REDISMODULE_OK;
+}
+
+static void timer_callback(RedisModuleCtx *ctx, void *data)
+{
+ UNUSED(ctx);
+
+ RedisModuleBlockedClient *bc = data;
+
+ // Get Redis module context
+ RedisModuleCtx *reply_ctx = RedisModule_GetThreadSafeContext(bc);
+
+ // Reply to client
+ RedisModule_ReplyWithSimpleString(reply_ctx, "OK");
+
+ // Unblock client
+ RedisModule_UnblockClient(bc, NULL);
+
+ // Free the Redis module context
+ RedisModule_FreeThreadSafeContext(reply_ctx);
+}
+
+/* unblock_by_timer <period_ms> <timeout_ms>
+ * period_ms is the period of the timer.
+ * timeout_ms is the blocking timeout. */
+int unblock_by_timer(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ if (argc != 3)
+ return RedisModule_WrongArity(ctx);
+
+ long long period;
+ long long timeout;
+ if (RedisModule_StringToLongLong(argv[1],&period) != REDISMODULE_OK)
+ return RedisModule_ReplyWithError(ctx,"ERR invalid period");
+ if (RedisModule_StringToLongLong(argv[2],&timeout) != REDISMODULE_OK) {
+ return RedisModule_ReplyWithError(ctx,"ERR invalid timeout");
+ }
+
+ RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx, NULL, NULL, NULL, timeout);
+ RedisModule_CreateTimer(ctx, period, timer_callback, bc);
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ if (RedisModule_Init(ctx, "blockedclient", 1, REDISMODULE_APIVER_1)== REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "acquire_gil", acquire_gil, "", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "do_rm_call", do_rm_call,
+ "write", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "do_rm_call_async", do_rm_call_async,
+ "write", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "do_rm_call_async_on_thread", do_rm_call_async_on_thread,
+ "write", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "do_rm_call_async_script_mode", do_rm_call_async,
+ "write", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "do_rm_call_async_no_replicate", do_rm_call_async,
+ "write", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "do_rm_call_fire_and_forget", do_rm_call_async_fire_and_forget,
+ "write", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "wait_and_do_rm_call", wait_and_do_rm_call_async,
+ "write", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "blpop_and_set_multiple_keys", blpop_and_set_multiple_keys,
+ "write", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "do_bg_rm_call", do_bg_rm_call, "", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "do_bg_rm_call_format", do_bg_rm_call, "", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "do_fake_bg_true", do_fake_bg_true, "", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "slow_fg_command", slow_fg_command,"", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "stop_slow_fg_command", stop_slow_fg_command,"allow-busy", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "set_slow_bg_operation", set_slow_bg_operation, "allow-busy", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "is_in_slow_bg_operation", is_in_slow_bg_operation, "allow-busy", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "unblock_by_timer", unblock_by_timer, "", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/blockonbackground.c b/examples/redis-unstable/tests/modules/blockonbackground.c
new file mode 100644
index 0000000..7aeb011
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/blockonbackground.c
@@ -0,0 +1,333 @@
+#define _XOPEN_SOURCE 700
+#include "redismodule.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <pthread.h>
+#include <time.h>
+
+#define UNUSED(x) (void)(x)
+
+typedef struct {
+ /* Mutex for protecting RedisModule_BlockedClientMeasureTime*() API from race
+ * conditions due to timeout callback triggered in the main thread. */
+ pthread_mutex_t measuretime_mutex;
+ int measuretime_completed; /* Indicates that time measure has ended and will not continue further */
+ int myint; /* Used for replying */
+} BlockPrivdata;
+
+void blockClientPrivdataInit(RedisModuleBlockedClient *bc) {
+ BlockPrivdata *block_privdata = RedisModule_Calloc(1, sizeof(*block_privdata));
+ block_privdata->measuretime_mutex = (pthread_mutex_t)PTHREAD_MUTEX_INITIALIZER;
+ RedisModule_BlockClientSetPrivateData(bc, block_privdata);
+}
+
+void blockClientMeasureTimeStart(RedisModuleBlockedClient *bc, BlockPrivdata *block_privdata) {
+ pthread_mutex_lock(&block_privdata->measuretime_mutex);
+ RedisModule_BlockedClientMeasureTimeStart(bc);
+ pthread_mutex_unlock(&block_privdata->measuretime_mutex);
+}
+
+void blockClientMeasureTimeEnd(RedisModuleBlockedClient *bc, BlockPrivdata *block_privdata, int completed) {
+ pthread_mutex_lock(&block_privdata->measuretime_mutex);
+ if (!block_privdata->measuretime_completed) {
+ RedisModule_BlockedClientMeasureTimeEnd(bc);
+ if (completed) block_privdata->measuretime_completed = 1;
+ }
+ pthread_mutex_unlock(&block_privdata->measuretime_mutex);
+}
+
+/* Reply callback for blocking command BLOCK.DEBUG */
+int HelloBlock_Reply(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ UNUSED(argv);
+ UNUSED(argc);
+ BlockPrivdata *block_privdata = RedisModule_GetBlockedClientPrivateData(ctx);
+ return RedisModule_ReplyWithLongLong(ctx,block_privdata->myint);
+}
+
+/* Timeout callback for blocking command BLOCK.DEBUG */
+int HelloBlock_Timeout(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ UNUSED(argv);
+ UNUSED(argc);
+ RedisModuleBlockedClient *bc = RedisModule_GetBlockedClientHandle(ctx);
+ BlockPrivdata *block_privdata = RedisModule_GetBlockedClientPrivateData(ctx);
+ blockClientMeasureTimeEnd(bc, block_privdata, 1);
+ return RedisModule_ReplyWithSimpleString(ctx,"Request timedout");
+}
+
+/* Private data freeing callback for BLOCK.DEBUG command. */
+void HelloBlock_FreeData(RedisModuleCtx *ctx, void *privdata) {
+ UNUSED(ctx);
+ BlockPrivdata *block_privdata = privdata;
+ pthread_mutex_destroy(&block_privdata->measuretime_mutex);
+ RedisModule_Free(privdata);
+}
+
+/* Private data freeing callback for BLOCK.BLOCK command. */
+void HelloBlock_FreeStringData(RedisModuleCtx *ctx, void *privdata) {
+ RedisModule_FreeString(ctx, (RedisModuleString*)privdata);
+}
+
+/* The thread entry point that actually executes the blocking part
+ * of the command BLOCK.DEBUG. */
+void *BlockDebug_ThreadMain(void *arg) {
+ void **targ = arg;
+ RedisModuleBlockedClient *bc = targ[0];
+ long long delay = (unsigned long)targ[1];
+ long long enable_time_track = (unsigned long)targ[2];
+ BlockPrivdata *block_privdata = RedisModule_BlockClientGetPrivateData(bc);
+
+ if (enable_time_track)
+ blockClientMeasureTimeStart(bc, block_privdata);
+ RedisModule_Free(targ);
+
+ struct timespec ts;
+ ts.tv_sec = delay / 1000;
+ ts.tv_nsec = (delay % 1000) * 1000000;
+ nanosleep(&ts, NULL);
+ if (enable_time_track)
+ blockClientMeasureTimeEnd(bc, block_privdata, 0);
+ block_privdata->myint = rand();
+ RedisModule_UnblockClient(bc,block_privdata);
+ return NULL;
+}
+
+/* The thread entry point that actually executes the blocking part
+ * of the command BLOCK.DOUBLE_DEBUG. */
+void *DoubleBlock_ThreadMain(void *arg) {
+ void **targ = arg;
+ RedisModuleBlockedClient *bc = targ[0];
+ long long delay = (unsigned long)targ[1];
+ BlockPrivdata *block_privdata = RedisModule_BlockClientGetPrivateData(bc);
+ blockClientMeasureTimeStart(bc, block_privdata);
+ RedisModule_Free(targ);
+ struct timespec ts;
+ ts.tv_sec = delay / 1000;
+ ts.tv_nsec = (delay % 1000) * 1000000;
+ nanosleep(&ts, NULL);
+ blockClientMeasureTimeEnd(bc, block_privdata, 0);
+ /* call again RedisModule_BlockedClientMeasureTimeStart() and
+ * RedisModule_BlockedClientMeasureTimeEnd and ensure that the
+ * total execution time is 2x the delay. */
+ blockClientMeasureTimeStart(bc, block_privdata);
+ nanosleep(&ts, NULL);
+ blockClientMeasureTimeEnd(bc, block_privdata, 0);
+ block_privdata->myint = rand();
+ RedisModule_UnblockClient(bc,block_privdata);
+ return NULL;
+}
+
+void HelloBlock_Disconnected(RedisModuleCtx *ctx, RedisModuleBlockedClient *bc) {
+ RedisModule_Log(ctx,"warning","Blocked client %p disconnected!",
+ (void*)bc);
+}
+
+/* BLOCK.DEBUG <delay_ms> <timeout_ms> -- Block for <count> milliseconds, then reply with
+ * a random number. Timeout is the command timeout, so that you can test
+ * what happens when the delay is greater than the timeout. */
+int HelloBlock_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 3) return RedisModule_WrongArity(ctx);
+ long long delay;
+ long long timeout;
+
+ if (RedisModule_StringToLongLong(argv[1],&delay) != REDISMODULE_OK) {
+ return RedisModule_ReplyWithError(ctx,"ERR invalid count");
+ }
+
+ if (RedisModule_StringToLongLong(argv[2],&timeout) != REDISMODULE_OK) {
+ return RedisModule_ReplyWithError(ctx,"ERR invalid count");
+ }
+
+ pthread_t tid;
+ RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx,HelloBlock_Reply,HelloBlock_Timeout,HelloBlock_FreeData,timeout);
+ blockClientPrivdataInit(bc);
+
+ /* Here we set a disconnection handler, however since this module will
+ * block in sleep() in a thread, there is not much we can do in the
+ * callback, so this is just to show you the API. */
+ RedisModule_SetDisconnectCallback(bc,HelloBlock_Disconnected);
+
+ /* Now that we setup a blocking client, we need to pass the control
+ * to the thread. However we need to pass arguments to the thread:
+ * the delay and a reference to the blocked client handle. */
+ void **targ = RedisModule_Alloc(sizeof(void*)*3);
+ targ[0] = bc;
+ targ[1] = (void*)(unsigned long) delay;
+ // pass 1 as flag to enable time tracking
+ targ[2] = (void*)(unsigned long) 1;
+
+ if (pthread_create(&tid,NULL,BlockDebug_ThreadMain,targ) != 0) {
+ RedisModule_AbortBlock(bc);
+ return RedisModule_ReplyWithError(ctx,"-ERR Can't start thread");
+ }
+ pthread_detach(tid);
+ return REDISMODULE_OK;
+}
+
+/* BLOCK.DEBUG_NOTRACKING <delay_ms> <timeout_ms> -- Block for <count> milliseconds, then reply with
+ * a random number. Timeout is the command timeout, so that you can test
+ * what happens when the delay is greater than the timeout.
+ * this command does not track background time so the background time should no appear in stats*/
+int HelloBlockNoTracking_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 3) return RedisModule_WrongArity(ctx);
+ long long delay;
+ long long timeout;
+
+ if (RedisModule_StringToLongLong(argv[1],&delay) != REDISMODULE_OK) {
+ return RedisModule_ReplyWithError(ctx,"ERR invalid count");
+ }
+
+ if (RedisModule_StringToLongLong(argv[2],&timeout) != REDISMODULE_OK) {
+ return RedisModule_ReplyWithError(ctx,"ERR invalid count");
+ }
+
+ pthread_t tid;
+ RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx,HelloBlock_Reply,HelloBlock_Timeout,HelloBlock_FreeData,timeout);
+ blockClientPrivdataInit(bc);
+
+ /* Here we set a disconnection handler, however since this module will
+ * block in sleep() in a thread, there is not much we can do in the
+ * callback, so this is just to show you the API. */
+ RedisModule_SetDisconnectCallback(bc,HelloBlock_Disconnected);
+
+ /* Now that we setup a blocking client, we need to pass the control
+ * to the thread. However we need to pass arguments to the thread:
+ * the delay and a reference to the blocked client handle. */
+ void **targ = RedisModule_Alloc(sizeof(void*)*3);
+ targ[0] = bc;
+ targ[1] = (void*)(unsigned long) delay;
+ // pass 0 as flag to enable time tracking
+ targ[2] = (void*)(unsigned long) 0;
+
+ if (pthread_create(&tid,NULL,BlockDebug_ThreadMain,targ) != 0) {
+ RedisModule_AbortBlock(bc);
+ return RedisModule_ReplyWithError(ctx,"-ERR Can't start thread");
+ }
+ pthread_detach(tid);
+ return REDISMODULE_OK;
+}
+
+/* BLOCK.DOUBLE_DEBUG <delay_ms> -- Block for 2 x <count> milliseconds,
+ * then reply with a random number.
+ * This command is used to test multiple calls to RedisModule_BlockedClientMeasureTimeStart()
+ * and RedisModule_BlockedClientMeasureTimeEnd() within the same execution. */
+int HelloDoubleBlock_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2) return RedisModule_WrongArity(ctx);
+ long long delay;
+
+ if (RedisModule_StringToLongLong(argv[1],&delay) != REDISMODULE_OK) {
+ return RedisModule_ReplyWithError(ctx,"ERR invalid count");
+ }
+
+ pthread_t tid;
+ RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx,HelloBlock_Reply,HelloBlock_Timeout,HelloBlock_FreeData,0);
+ blockClientPrivdataInit(bc);
+
+ /* Now that we setup a blocking client, we need to pass the control
+ * to the thread. However we need to pass arguments to the thread:
+ * the delay and a reference to the blocked client handle. */
+ void **targ = RedisModule_Alloc(sizeof(void*)*2);
+ targ[0] = bc;
+ targ[1] = (void*)(unsigned long) delay;
+
+ if (pthread_create(&tid,NULL,DoubleBlock_ThreadMain,targ) != 0) {
+ RedisModule_AbortBlock(bc);
+ return RedisModule_ReplyWithError(ctx,"-ERR Can't start thread");
+ }
+ pthread_detach(tid);
+ return REDISMODULE_OK;
+}
+
+RedisModuleBlockedClient *blocked_client = NULL;
+
+/* BLOCK.BLOCK [TIMEOUT] -- Blocks the current client until released
+ * or TIMEOUT seconds. If TIMEOUT is zero, no timeout function is
+ * registered.
+ */
+int Block_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (RedisModule_IsBlockedReplyRequest(ctx)) {
+ RedisModuleString *r = RedisModule_GetBlockedClientPrivateData(ctx);
+ return RedisModule_ReplyWithString(ctx, r);
+ } else if (RedisModule_IsBlockedTimeoutRequest(ctx)) {
+ RedisModule_UnblockClient(blocked_client, NULL); /* Must be called to avoid leaks. */
+ blocked_client = NULL;
+ return RedisModule_ReplyWithSimpleString(ctx, "Timed out");
+ }
+
+ if (argc != 2) return RedisModule_WrongArity(ctx);
+ long long timeout;
+
+ if (RedisModule_StringToLongLong(argv[1], &timeout) != REDISMODULE_OK) {
+ return RedisModule_ReplyWithError(ctx, "ERR invalid timeout");
+ }
+ if (blocked_client) {
+ return RedisModule_ReplyWithError(ctx, "ERR another client already blocked");
+ }
+
+ /* Block client. We use this function as both a reply and optional timeout
+ * callback and differentiate the different code flows above.
+ */
+ blocked_client = RedisModule_BlockClient(ctx, Block_RedisCommand,
+ timeout > 0 ? Block_RedisCommand : NULL, HelloBlock_FreeStringData, timeout);
+ return REDISMODULE_OK;
+}
+
+/* BLOCK.IS_BLOCKED -- Returns 1 if we have a blocked client, or 0 otherwise.
+ */
+int IsBlocked_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ UNUSED(argv);
+ UNUSED(argc);
+ RedisModule_ReplyWithLongLong(ctx, blocked_client ? 1 : 0);
+ return REDISMODULE_OK;
+}
+
+/* BLOCK.RELEASE [reply] -- Releases the blocked client and produce the specified reply.
+ */
+int Release_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2) return RedisModule_WrongArity(ctx);
+ if (!blocked_client) {
+ return RedisModule_ReplyWithError(ctx, "ERR No blocked client");
+ }
+
+ RedisModuleString *replystr = argv[1];
+ RedisModule_RetainString(ctx, replystr);
+ RedisModule_UnblockClient(blocked_client, replystr);
+ blocked_client = NULL;
+
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ UNUSED(argv);
+ UNUSED(argc);
+
+ if (RedisModule_Init(ctx,"block",1,REDISMODULE_APIVER_1)
+ == REDISMODULE_ERR) return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"block.debug",
+ HelloBlock_RedisCommand,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"block.double_debug",
+ HelloDoubleBlock_RedisCommand,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"block.debug_no_track",
+ HelloBlockNoTracking_RedisCommand,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "block.block",
+ Block_RedisCommand, "", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"block.is_blocked",
+ IsBlocked_RedisCommand,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"block.release",
+ Release_RedisCommand,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/blockonkeys.c b/examples/redis-unstable/tests/modules/blockonkeys.c
new file mode 100644
index 0000000..94bb361
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/blockonkeys.c
@@ -0,0 +1,645 @@
+#include "redismodule.h"
+
+#include <string.h>
+#include <strings.h>
+#include <assert.h>
+#include <unistd.h>
+
+#define UNUSED(V) ((void) V)
+
+#define LIST_SIZE 1024
+
+/* The FSL (Fixed-Size List) data type is a low-budget imitation of the
+ * native Redis list, in order to test list-like commands implemented
+ * by a module.
+ * Examples: FSL.PUSH, FSL.BPOP, etc. */
+
+typedef struct {
+ long long list[LIST_SIZE];
+ long long length;
+} fsl_t; /* Fixed-size list */
+
+static RedisModuleType *fsltype = NULL;
+
+fsl_t *fsl_type_create(void) {
+ fsl_t *o;
+ o = RedisModule_Alloc(sizeof(*o));
+ o->length = 0;
+ return o;
+}
+
+void fsl_type_free(fsl_t *o) {
+ RedisModule_Free(o);
+}
+
+/* ========================== "fsltype" type methods ======================= */
+
+void *fsl_rdb_load(RedisModuleIO *rdb, int encver) {
+ if (encver != 0) {
+ return NULL;
+ }
+ fsl_t *fsl = fsl_type_create();
+ fsl->length = RedisModule_LoadUnsigned(rdb);
+ for (long long i = 0; i < fsl->length; i++)
+ fsl->list[i] = RedisModule_LoadSigned(rdb);
+ return fsl;
+}
+
+void fsl_rdb_save(RedisModuleIO *rdb, void *value) {
+ fsl_t *fsl = value;
+ RedisModule_SaveUnsigned(rdb,fsl->length);
+ for (long long i = 0; i < fsl->length; i++)
+ RedisModule_SaveSigned(rdb, fsl->list[i]);
+}
+
+void fsl_aofrw(RedisModuleIO *aof, RedisModuleString *key, void *value) {
+ fsl_t *fsl = value;
+ for (long long i = 0; i < fsl->length; i++)
+ RedisModule_EmitAOF(aof, "FSL.PUSH","sl", key, fsl->list[i]);
+}
+
+void fsl_free(void *value) {
+ fsl_type_free(value);
+}
+
+/* ========================== helper methods ======================= */
+
+/* Wrapper to the boilerplate code of opening a key, checking its type, etc.
+ * Returns 0 if `keyname` exists in the dataset, but it's of the wrong type (i.e. not FSL) */
+int get_fsl(RedisModuleCtx *ctx, RedisModuleString *keyname, int mode, int create, fsl_t **fsl, int reply_on_failure) {
+ *fsl = NULL;
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, keyname, mode);
+
+ if (RedisModule_KeyType(key) != REDISMODULE_KEYTYPE_EMPTY) {
+ /* Key exists */
+ if (RedisModule_ModuleTypeGetType(key) != fsltype) {
+ /* Key is not FSL */
+ RedisModule_CloseKey(key);
+ if (reply_on_failure)
+ RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE);
+ RedisModuleCallReply *reply = RedisModule_Call(ctx, "INCR", "c", "fsl_wrong_type");
+ RedisModule_FreeCallReply(reply);
+ return 0;
+ }
+
+ *fsl = RedisModule_ModuleTypeGetValue(key);
+ if (*fsl && !(*fsl)->length && mode & REDISMODULE_WRITE) {
+ /* Key exists, but it's logically empty */
+ if (create) {
+ create = 0; /* No need to create, key exists in its basic state */
+ } else {
+ RedisModule_DeleteKey(key);
+ *fsl = NULL;
+ }
+ } else {
+ /* Key exists, and has elements in it - no need to create anything */
+ create = 0;
+ }
+ }
+
+ if (create) {
+ *fsl = fsl_type_create();
+ RedisModule_ModuleTypeSetValue(key, fsltype, *fsl);
+ }
+
+ RedisModule_CloseKey(key);
+ return 1;
+}
+
+/* ========================== commands ======================= */
+
+/* FSL.PUSH <key> <int> - Push an integer to the fixed-size list (to the right).
+ * It must be greater than the element in the head of the list. */
+int fsl_push(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 3)
+ return RedisModule_WrongArity(ctx);
+
+ long long ele;
+ if (RedisModule_StringToLongLong(argv[2],&ele) != REDISMODULE_OK)
+ return RedisModule_ReplyWithError(ctx,"ERR invalid integer");
+
+ fsl_t *fsl;
+ if (!get_fsl(ctx, argv[1], REDISMODULE_WRITE, 1, &fsl, 1))
+ return REDISMODULE_OK;
+
+ if (fsl->length == LIST_SIZE)
+ return RedisModule_ReplyWithError(ctx,"ERR list is full");
+
+ if (fsl->length != 0 && fsl->list[fsl->length-1] >= ele)
+ return RedisModule_ReplyWithError(ctx,"ERR new element has to be greater than the head element");
+
+ fsl->list[fsl->length++] = ele;
+ RedisModule_SignalKeyAsReady(ctx, argv[1]);
+
+ RedisModule_ReplicateVerbatim(ctx);
+
+ return RedisModule_ReplyWithSimpleString(ctx, "OK");
+}
+
+typedef struct {
+ RedisModuleString *keyname;
+ long long ele;
+} timer_data_t;
+
+static void timer_callback(RedisModuleCtx *ctx, void *data)
+{
+ timer_data_t *td = data;
+
+ fsl_t *fsl;
+ if (!get_fsl(ctx, td->keyname, REDISMODULE_WRITE, 1, &fsl, 1))
+ return;
+
+ if (fsl->length == LIST_SIZE)
+ return; /* list is full */
+
+ if (fsl->length != 0 && fsl->list[fsl->length-1] >= td->ele)
+ return; /* new element has to be greater than the head element */
+
+ fsl->list[fsl->length++] = td->ele;
+ RedisModule_SignalKeyAsReady(ctx, td->keyname);
+
+ RedisModule_Replicate(ctx, "FSL.PUSH", "sl", td->keyname, td->ele);
+
+ RedisModule_FreeString(ctx, td->keyname);
+ RedisModule_Free(td);
+}
+
+/* FSL.PUSHTIMER <key> <int> <period-in-ms> - Push the number 9000 to the fixed-size list (to the right).
+ * It must be greater than the element in the head of the list. */
+int fsl_pushtimer(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ if (argc != 4)
+ return RedisModule_WrongArity(ctx);
+
+ long long ele;
+ if (RedisModule_StringToLongLong(argv[2],&ele) != REDISMODULE_OK)
+ return RedisModule_ReplyWithError(ctx,"ERR invalid integer");
+
+ long long period;
+ if (RedisModule_StringToLongLong(argv[3],&period) != REDISMODULE_OK)
+ return RedisModule_ReplyWithError(ctx,"ERR invalid period");
+
+ fsl_t *fsl;
+ if (!get_fsl(ctx, argv[1], REDISMODULE_WRITE, 1, &fsl, 1))
+ return REDISMODULE_OK;
+
+ if (fsl->length == LIST_SIZE)
+ return RedisModule_ReplyWithError(ctx,"ERR list is full");
+
+ timer_data_t *td = RedisModule_Alloc(sizeof(*td));
+ td->keyname = argv[1];
+ RedisModule_RetainString(ctx, td->keyname);
+ td->ele = ele;
+
+ RedisModuleTimerID id = RedisModule_CreateTimer(ctx, period, timer_callback, td);
+ RedisModule_ReplyWithLongLong(ctx, id);
+
+ return REDISMODULE_OK;
+}
+
+int bpop_reply_callback(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ RedisModuleString *keyname = RedisModule_GetBlockedClientReadyKey(ctx);
+
+ fsl_t *fsl;
+ if (!get_fsl(ctx, keyname, REDISMODULE_WRITE, 0, &fsl, 0) || !fsl)
+ return REDISMODULE_ERR;
+
+ RedisModule_Assert(fsl->length);
+ RedisModule_ReplyWithLongLong(ctx, fsl->list[--fsl->length]);
+
+ /* I'm lazy so i'll replicate a potentially blocking command, it shouldn't block in this flow. */
+ RedisModule_ReplicateVerbatim(ctx);
+ return REDISMODULE_OK;
+}
+
+int bpop_timeout_callback(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ return RedisModule_ReplyWithSimpleString(ctx, "Request timedout");
+}
+
+/* FSL.BPOP <key> <timeout> [NO_TO_CB]- Block clients until list has two or more elements.
+ * When that happens, unblock client and pop the last two elements (from the right). */
+int fsl_bpop(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc < 3)
+ return RedisModule_WrongArity(ctx);
+
+ long long timeout;
+ if (RedisModule_StringToLongLong(argv[2],&timeout) != REDISMODULE_OK || timeout < 0)
+ return RedisModule_ReplyWithError(ctx,"ERR invalid timeout");
+
+ int to_cb = 1;
+ if (argc == 4) {
+ if (strcasecmp("NO_TO_CB", RedisModule_StringPtrLen(argv[3], NULL)))
+ return RedisModule_ReplyWithError(ctx,"ERR invalid argument");
+ to_cb = 0;
+ }
+
+ fsl_t *fsl;
+ if (!get_fsl(ctx, argv[1], REDISMODULE_WRITE, 0, &fsl, 1))
+ return REDISMODULE_OK;
+
+ if (!fsl) {
+ RedisModule_BlockClientOnKeys(ctx, bpop_reply_callback, to_cb ? bpop_timeout_callback : NULL,
+ NULL, timeout, &argv[1], 1, NULL);
+ } else {
+ RedisModule_Assert(fsl->length);
+ RedisModule_ReplyWithLongLong(ctx, fsl->list[--fsl->length]);
+ /* I'm lazy so i'll replicate a potentially blocking command, it shouldn't block in this flow. */
+ RedisModule_ReplicateVerbatim(ctx);
+ }
+
+ return REDISMODULE_OK;
+}
+
+int bpopgt_reply_callback(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ RedisModuleString *keyname = RedisModule_GetBlockedClientReadyKey(ctx);
+ long long *pgt = RedisModule_GetBlockedClientPrivateData(ctx);
+
+ fsl_t *fsl;
+ if (!get_fsl(ctx, keyname, REDISMODULE_WRITE, 0, &fsl, 0) || !fsl)
+ return RedisModule_ReplyWithError(ctx,"UNBLOCKED key no longer exists");
+
+ if (fsl->list[fsl->length-1] <= *pgt)
+ return REDISMODULE_ERR;
+
+ RedisModule_Assert(fsl->length);
+ RedisModule_ReplyWithLongLong(ctx, fsl->list[--fsl->length]);
+ /* I'm lazy so i'll replicate a potentially blocking command, it shouldn't block in this flow. */
+ RedisModule_ReplicateVerbatim(ctx);
+ return REDISMODULE_OK;
+}
+
+int bpopgt_timeout_callback(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ return RedisModule_ReplyWithSimpleString(ctx, "Request timedout");
+}
+
+void bpopgt_free_privdata(RedisModuleCtx *ctx, void *privdata) {
+ REDISMODULE_NOT_USED(ctx);
+ RedisModule_Free(privdata);
+}
+
+/* FSL.BPOPGT <key> <gt> <timeout> - Block clients until list has an element greater than <gt>.
+ * When that happens, unblock client and pop the last element (from the right). */
+int fsl_bpopgt(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 4)
+ return RedisModule_WrongArity(ctx);
+
+ long long gt;
+ if (RedisModule_StringToLongLong(argv[2],&gt) != REDISMODULE_OK)
+ return RedisModule_ReplyWithError(ctx,"ERR invalid integer");
+
+ long long timeout;
+ if (RedisModule_StringToLongLong(argv[3],&timeout) != REDISMODULE_OK || timeout < 0)
+ return RedisModule_ReplyWithError(ctx,"ERR invalid timeout");
+
+ fsl_t *fsl;
+ if (!get_fsl(ctx, argv[1], REDISMODULE_WRITE, 0, &fsl, 1))
+ return REDISMODULE_OK;
+
+ if (!fsl)
+ return RedisModule_ReplyWithError(ctx,"ERR key must exist");
+
+ if (fsl->list[fsl->length-1] <= gt) {
+ /* We use malloc so the tests in blockedonkeys.tcl can check for memory leaks */
+ long long *pgt = RedisModule_Alloc(sizeof(long long));
+ *pgt = gt;
+ RedisModule_BlockClientOnKeysWithFlags(
+ ctx, bpopgt_reply_callback, bpopgt_timeout_callback,
+ bpopgt_free_privdata, timeout, &argv[1], 1, pgt,
+ REDISMODULE_BLOCK_UNBLOCK_DELETED);
+ } else {
+ RedisModule_Assert(fsl->length);
+ RedisModule_ReplyWithLongLong(ctx, fsl->list[--fsl->length]);
+ /* I'm lazy so i'll replicate a potentially blocking command, it shouldn't block in this flow. */
+ RedisModule_ReplicateVerbatim(ctx);
+ }
+
+ return REDISMODULE_OK;
+}
+
+int bpoppush_reply_callback(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ RedisModuleString *src_keyname = RedisModule_GetBlockedClientReadyKey(ctx);
+ RedisModuleString *dst_keyname = RedisModule_GetBlockedClientPrivateData(ctx);
+
+ fsl_t *src;
+ if (!get_fsl(ctx, src_keyname, REDISMODULE_WRITE, 0, &src, 0) || !src)
+ return REDISMODULE_ERR;
+
+ fsl_t *dst;
+ if (!get_fsl(ctx, dst_keyname, REDISMODULE_WRITE, 1, &dst, 0) || !dst)
+ return REDISMODULE_ERR;
+
+ RedisModule_Assert(src->length);
+ long long ele = src->list[--src->length];
+ dst->list[dst->length++] = ele;
+ RedisModule_SignalKeyAsReady(ctx, dst_keyname);
+ /* I'm lazy so i'll replicate a potentially blocking command, it shouldn't block in this flow. */
+ RedisModule_ReplicateVerbatim(ctx);
+ return RedisModule_ReplyWithLongLong(ctx, ele);
+}
+
+int bpoppush_timeout_callback(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ return RedisModule_ReplyWithSimpleString(ctx, "Request timedout");
+}
+
+void bpoppush_free_privdata(RedisModuleCtx *ctx, void *privdata) {
+ RedisModule_FreeString(ctx, privdata);
+}
+
+/* FSL.BPOPPUSH <src> <dst> <timeout> - Block clients until <src> has an element.
+ * When that happens, unblock client, pop the last element from <src> and push it to <dst>
+ * (from the right). */
+int fsl_bpoppush(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 4)
+ return RedisModule_WrongArity(ctx);
+
+ long long timeout;
+ if (RedisModule_StringToLongLong(argv[3],&timeout) != REDISMODULE_OK || timeout < 0)
+ return RedisModule_ReplyWithError(ctx,"ERR invalid timeout");
+
+ fsl_t *src;
+ if (!get_fsl(ctx, argv[1], REDISMODULE_WRITE, 0, &src, 1))
+ return REDISMODULE_OK;
+
+ if (!src) {
+ /* Retain string for reply callback */
+ RedisModule_RetainString(ctx, argv[2]);
+ /* Key is empty, we must block */
+ RedisModule_BlockClientOnKeys(ctx, bpoppush_reply_callback, bpoppush_timeout_callback,
+ bpoppush_free_privdata, timeout, &argv[1], 1, argv[2]);
+ } else {
+ fsl_t *dst;
+ if (!get_fsl(ctx, argv[2], REDISMODULE_WRITE, 1, &dst, 1))
+ return REDISMODULE_OK;
+
+ RedisModule_Assert(src->length);
+ long long ele = src->list[--src->length];
+ dst->list[dst->length++] = ele;
+ RedisModule_SignalKeyAsReady(ctx, argv[2]);
+ RedisModule_ReplyWithLongLong(ctx, ele);
+ /* I'm lazy so i'll replicate a potentially blocking command, it shouldn't block in this flow. */
+ RedisModule_ReplicateVerbatim(ctx);
+ }
+
+ return REDISMODULE_OK;
+}
+
+/* FSL.GETALL <key> - Reply with an array containing all elements. */
+int fsl_getall(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2)
+ return RedisModule_WrongArity(ctx);
+
+ fsl_t *fsl;
+ if (!get_fsl(ctx, argv[1], REDISMODULE_READ, 0, &fsl, 1))
+ return REDISMODULE_OK;
+
+ if (!fsl)
+ return RedisModule_ReplyWithArray(ctx, 0);
+
+ RedisModule_ReplyWithArray(ctx, fsl->length);
+ for (int i = 0; i < fsl->length; i++)
+ RedisModule_ReplyWithLongLong(ctx, fsl->list[i]);
+ return REDISMODULE_OK;
+}
+
+/* Callback for blockonkeys_popall */
+int blockonkeys_popall_reply_callback(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argc);
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE);
+ if (RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_LIST) {
+ RedisModuleString *elem;
+ long len = 0;
+ RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_ARRAY_LEN);
+ while ((elem = RedisModule_ListPop(key, REDISMODULE_LIST_HEAD)) != NULL) {
+ len++;
+ RedisModule_ReplyWithString(ctx, elem);
+ RedisModule_FreeString(ctx, elem);
+ }
+ /* I'm lazy so i'll replicate a potentially blocking command, it shouldn't block in this flow. */
+ RedisModule_ReplicateVerbatim(ctx);
+ RedisModule_ReplySetArrayLength(ctx, len);
+ } else {
+ RedisModule_ReplyWithError(ctx, "ERR Not a list");
+ }
+ RedisModule_CloseKey(key);
+ return REDISMODULE_OK;
+}
+
+int blockonkeys_popall_timeout_callback(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ return RedisModule_ReplyWithError(ctx, "ERR Timeout");
+}
+
+/* BLOCKONKEYS.POPALL key
+ *
+ * Blocks on an empty key for up to 3 seconds. When unblocked by a list
+ * operation like LPUSH, all the elements are popped and returned. Fails with an
+ * error on timeout. */
+int blockonkeys_popall(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2)
+ return RedisModule_WrongArity(ctx);
+
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ);
+ if (RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_EMPTY) {
+ RedisModule_BlockClientOnKeys(ctx, blockonkeys_popall_reply_callback,
+ blockonkeys_popall_timeout_callback,
+ NULL, 3000, &argv[1], 1, NULL);
+ } else {
+ RedisModule_ReplyWithError(ctx, "ERR Key not empty");
+ }
+ RedisModule_CloseKey(key);
+ return REDISMODULE_OK;
+}
+
+/* BLOCKONKEYS.LPUSH key val [val ..]
+ * BLOCKONKEYS.LPUSH_UNBLOCK key val [val ..]
+ *
+ * A module equivalent of LPUSH. If the name LPUSH_UNBLOCK is used,
+ * RM_SignalKeyAsReady() is also called. */
+int blockonkeys_lpush(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc < 3)
+ return RedisModule_WrongArity(ctx);
+
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE);
+ if (RedisModule_KeyType(key) != REDISMODULE_KEYTYPE_EMPTY &&
+ RedisModule_KeyType(key) != REDISMODULE_KEYTYPE_LIST) {
+ RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE);
+ } else {
+ for (int i = 2; i < argc; i++) {
+ if (RedisModule_ListPush(key, REDISMODULE_LIST_HEAD,
+ argv[i]) != REDISMODULE_OK) {
+ RedisModule_CloseKey(key);
+ return RedisModule_ReplyWithError(ctx, "ERR Push failed");
+ }
+ }
+ }
+ RedisModule_CloseKey(key);
+
+ /* signal key as ready if the command is lpush_unblock */
+ size_t len;
+ const char *str = RedisModule_StringPtrLen(argv[0], &len);
+ if (!strncasecmp(str, "blockonkeys.lpush_unblock", len)) {
+ RedisModule_SignalKeyAsReady(ctx, argv[1]);
+ }
+ RedisModule_ReplicateVerbatim(ctx);
+ return RedisModule_ReplyWithSimpleString(ctx, "OK");
+}
+
+/* Callback for the BLOCKONKEYS.BLPOPN command */
+int blockonkeys_blpopn_reply_callback(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argc);
+ long long n;
+ RedisModule_StringToLongLong(argv[2], &n);
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE);
+ int result;
+ if (RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_LIST &&
+ RedisModule_ValueLength(key) >= (size_t)n) {
+ RedisModule_ReplyWithArray(ctx, n);
+ for (long i = 0; i < n; i++) {
+ RedisModuleString *elem = RedisModule_ListPop(key, REDISMODULE_LIST_HEAD);
+ RedisModule_ReplyWithString(ctx, elem);
+ RedisModule_FreeString(ctx, elem);
+ }
+ /* I'm lazy so i'll replicate a potentially blocking command, it shouldn't block in this flow. */
+ RedisModule_ReplicateVerbatim(ctx);
+ result = REDISMODULE_OK;
+ } else if (RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_LIST ||
+ RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_EMPTY) {
+ const char *module_cmd = RedisModule_StringPtrLen(argv[0], NULL);
+ if (!strcasecmp(module_cmd, "blockonkeys.blpopn_or_unblock"))
+ RedisModule_UnblockClient(RedisModule_GetBlockedClientHandle(ctx), NULL);
+
+ /* continue blocking */
+ result = REDISMODULE_ERR;
+ } else {
+ result = RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE);
+ }
+ RedisModule_CloseKey(key);
+ return result;
+}
+
+int blockonkeys_blpopn_timeout_callback(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ return RedisModule_ReplyWithError(ctx, "ERR Timeout");
+}
+
+int blockonkeys_blpopn_abort_callback(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ return RedisModule_ReplyWithSimpleString(ctx, "Action aborted");
+}
+
+/* BLOCKONKEYS.BLPOPN key N
+ *
+ * Blocks until key has N elements and then pops them or fails after 3 seconds.
+ */
+int blockonkeys_blpopn(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc < 3) return RedisModule_WrongArity(ctx);
+
+ long long n, timeout = 3000LL;
+ if (RedisModule_StringToLongLong(argv[2], &n) != REDISMODULE_OK) {
+ return RedisModule_ReplyWithError(ctx, "ERR Invalid N");
+ }
+
+ if (argc > 3 ) {
+ if (RedisModule_StringToLongLong(argv[3], &timeout) != REDISMODULE_OK) {
+ return RedisModule_ReplyWithError(ctx, "ERR Invalid timeout value");
+ }
+ }
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE);
+ int keytype = RedisModule_KeyType(key);
+ if (keytype != REDISMODULE_KEYTYPE_EMPTY &&
+ keytype != REDISMODULE_KEYTYPE_LIST) {
+ RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE);
+ } else if (keytype == REDISMODULE_KEYTYPE_LIST &&
+ RedisModule_ValueLength(key) >= (size_t)n) {
+ RedisModule_ReplyWithArray(ctx, n);
+ for (long i = 0; i < n; i++) {
+ RedisModuleString *elem = RedisModule_ListPop(key, REDISMODULE_LIST_HEAD);
+ RedisModule_ReplyWithString(ctx, elem);
+ RedisModule_FreeString(ctx, elem);
+ }
+ /* I'm lazy so i'll replicate a potentially blocking command, it shouldn't block in this flow. */
+ RedisModule_ReplicateVerbatim(ctx);
+ } else {
+ RedisModule_BlockClientOnKeys(ctx, blockonkeys_blpopn_reply_callback,
+ timeout ? blockonkeys_blpopn_timeout_callback : blockonkeys_blpopn_abort_callback,
+ NULL, timeout, &argv[1], 1, NULL);
+ }
+ RedisModule_CloseKey(key);
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ if (RedisModule_Init(ctx, "blockonkeys", 1, REDISMODULE_APIVER_1)== REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ RedisModuleTypeMethods tm = {
+ .version = REDISMODULE_TYPE_METHOD_VERSION,
+ .rdb_load = fsl_rdb_load,
+ .rdb_save = fsl_rdb_save,
+ .aof_rewrite = fsl_aofrw,
+ .mem_usage = NULL,
+ .free = fsl_free,
+ .digest = NULL,
+ };
+
+ fsltype = RedisModule_CreateDataType(ctx, "fsltype_t", 0, &tm);
+ if (fsltype == NULL)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"fsl.push",fsl_push,"write",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"fsl.pushtimer",fsl_pushtimer,"write",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"fsl.bpop",fsl_bpop,"write",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"fsl.bpopgt",fsl_bpopgt,"write",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"fsl.bpoppush",fsl_bpoppush,"write",1,2,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"fsl.getall",fsl_getall,"",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "blockonkeys.popall", blockonkeys_popall,
+ "write", 1, 1, 1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "blockonkeys.lpush", blockonkeys_lpush,
+ "write", 1, 1, 1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "blockonkeys.lpush_unblock", blockonkeys_lpush,
+ "write", 1, 1, 1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "blockonkeys.blpopn", blockonkeys_blpopn,
+ "write", 1, 1, 1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "blockonkeys.blpopn_or_unblock", blockonkeys_blpopn,
+ "write", 1, 1, 1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/cmdintrospection.c b/examples/redis-unstable/tests/modules/cmdintrospection.c
new file mode 100644
index 0000000..48208ff
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/cmdintrospection.c
@@ -0,0 +1,226 @@
+#include "redismodule.h"
+
+#define UNUSED(V) ((void) V)
+
+int cmd_xadd(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);
+ if (RedisModule_Init(ctx, "cmdintrospection", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"cmdintrospection.xadd",cmd_xadd,"write deny-oom random fast",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ RedisModuleCommand *xadd = RedisModule_GetCommand(ctx,"cmdintrospection.xadd");
+
+ RedisModuleCommandInfo info = {
+ .version = REDISMODULE_COMMAND_INFO_VERSION,
+ .arity = -5,
+ .summary = "Appends a new message to a stream. Creates the key if it doesn't exist.",
+ .since = "5.0.0",
+ .complexity = "O(1) when adding a new entry, O(N) when trimming where N being the number of entries evicted.",
+ .tips = "nondeterministic_output",
+ .history = (RedisModuleCommandHistoryEntry[]){
+ /* NOTE: All versions specified should be the module's versions, not
+ * Redis'! We use Redis versions in this example for the purpose of
+ * testing (comparing the output with the output of the vanilla
+ * XADD). */
+ {"6.2.0", "Added the `NOMKSTREAM` option, `MINID` trimming strategy and the `LIMIT` option."},
+ {"7.0.0", "Added support for the `<ms>-*` explicit ID form."},
+ {"8.2.0", "Added the `KEEPREF`, `DELREF` and `ACKED` options."},
+ {0}
+ },
+ .key_specs = (RedisModuleCommandKeySpec[]){
+ {
+ .notes = "UPDATE instead of INSERT because of the optional trimming feature",
+ .flags = REDISMODULE_CMD_KEY_RW | REDISMODULE_CMD_KEY_UPDATE,
+ .begin_search_type = REDISMODULE_KSPEC_BS_INDEX,
+ .bs.index.pos = 1,
+ .find_keys_type = REDISMODULE_KSPEC_FK_RANGE,
+ .fk.range = {0,1,0}
+ },
+ {0}
+ },
+ .args = (RedisModuleCommandArg[]){
+ {
+ .name = "key",
+ .type = REDISMODULE_ARG_TYPE_KEY,
+ .key_spec_index = 0
+ },
+ {
+ .name = "nomkstream",
+ .type = REDISMODULE_ARG_TYPE_PURE_TOKEN,
+ .token = "NOMKSTREAM",
+ .since = "6.2.0",
+ .flags = REDISMODULE_CMD_ARG_OPTIONAL
+ },
+ {
+ .name = "condition",
+ .type = REDISMODULE_ARG_TYPE_ONEOF,
+ .flags = REDISMODULE_CMD_ARG_OPTIONAL,
+ .subargs = (RedisModuleCommandArg[]){
+ {
+ .name = "keepref",
+ .type = REDISMODULE_ARG_TYPE_PURE_TOKEN,
+ .token = "KEEPREF"
+ },
+ {
+ .name = "delref",
+ .type = REDISMODULE_ARG_TYPE_PURE_TOKEN,
+ .token = "DELREF"
+ },
+ {
+ .name = "acked",
+ .type = REDISMODULE_ARG_TYPE_PURE_TOKEN,
+ .token = "ACKED"
+ },
+ {0}
+ }
+ },
+ {
+ .name = "idmp",
+ .type = REDISMODULE_ARG_TYPE_ONEOF,
+ .flags = REDISMODULE_CMD_ARG_OPTIONAL,
+ .subargs = (RedisModuleCommandArg[]){
+ {
+ .name = "idmpauto-with-pid",
+ .type = REDISMODULE_ARG_TYPE_BLOCK,
+ .subargs = (RedisModuleCommandArg[]){
+ {
+ .name = "idmpauto-token",
+ .type = REDISMODULE_ARG_TYPE_PURE_TOKEN,
+ .token = "IDMPAUTO"
+ },
+ {
+ .name = "pid",
+ .type = REDISMODULE_ARG_TYPE_STRING,
+ },
+ {0}
+ }
+ },
+ {
+ .name = "idmp-with-pid-iid",
+ .type = REDISMODULE_ARG_TYPE_BLOCK,
+ .subargs = (RedisModuleCommandArg[]){
+ {
+ .name = "idmp-token",
+ .type = REDISMODULE_ARG_TYPE_PURE_TOKEN,
+ .token = "IDMP"
+ },
+ {
+ .name = "pid",
+ .type = REDISMODULE_ARG_TYPE_STRING,
+ },
+ {
+ .name = "iid",
+ .type = REDISMODULE_ARG_TYPE_STRING,
+ },
+ {0}
+ }
+ },
+ {0}
+ }
+ },
+ {
+ .name = "trim",
+ .type = REDISMODULE_ARG_TYPE_BLOCK,
+ .flags = REDISMODULE_CMD_ARG_OPTIONAL,
+ .subargs = (RedisModuleCommandArg[]){
+ {
+ .name = "strategy",
+ .type = REDISMODULE_ARG_TYPE_ONEOF,
+ .subargs = (RedisModuleCommandArg[]){
+ {
+ .name = "maxlen",
+ .type = REDISMODULE_ARG_TYPE_PURE_TOKEN,
+ .token = "MAXLEN",
+ },
+ {
+ .name = "minid",
+ .type = REDISMODULE_ARG_TYPE_PURE_TOKEN,
+ .token = "MINID",
+ .since = "6.2.0",
+ },
+ {0}
+ }
+ },
+ {
+ .name = "operator",
+ .type = REDISMODULE_ARG_TYPE_ONEOF,
+ .flags = REDISMODULE_CMD_ARG_OPTIONAL,
+ .subargs = (RedisModuleCommandArg[]){
+ {
+ .name = "equal",
+ .type = REDISMODULE_ARG_TYPE_PURE_TOKEN,
+ .token = "="
+ },
+ {
+ .name = "approximately",
+ .type = REDISMODULE_ARG_TYPE_PURE_TOKEN,
+ .token = "~"
+ },
+ {0}
+ }
+ },
+ {
+ .name = "threshold",
+ .type = REDISMODULE_ARG_TYPE_STRING,
+ .display_text = "threshold" /* Just for coverage, doesn't have a visible effect */
+ },
+ {
+ .name = "count",
+ .type = REDISMODULE_ARG_TYPE_INTEGER,
+ .token = "LIMIT",
+ .since = "6.2.0",
+ .flags = REDISMODULE_CMD_ARG_OPTIONAL
+ },
+ {0}
+ }
+ },
+ {
+ .name = "id-selector",
+ .type = REDISMODULE_ARG_TYPE_ONEOF,
+ .subargs = (RedisModuleCommandArg[]){
+ {
+ .name = "auto-id",
+ .type = REDISMODULE_ARG_TYPE_PURE_TOKEN,
+ .token = "*"
+ },
+ {
+ .name = "id",
+ .type = REDISMODULE_ARG_TYPE_STRING,
+ },
+ {0}
+ }
+ },
+ {
+ .name = "data",
+ .type = REDISMODULE_ARG_TYPE_BLOCK,
+ .flags = REDISMODULE_CMD_ARG_MULTIPLE,
+ .subargs = (RedisModuleCommandArg[]){
+ {
+ .name = "field",
+ .type = REDISMODULE_ARG_TYPE_STRING,
+ },
+ {
+ .name = "value",
+ .type = REDISMODULE_ARG_TYPE_STRING,
+ },
+ {0}
+ }
+ },
+ {0}
+ }
+ };
+ if (RedisModule_SetCommandInfo(xadd, &info) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/commandfilter.c b/examples/redis-unstable/tests/modules/commandfilter.c
new file mode 100644
index 0000000..333b263
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/commandfilter.c
@@ -0,0 +1,251 @@
+#include "redismodule.h"
+
+#include <string.h>
+#include <strings.h>
+
+static RedisModuleString *log_key_name;
+
+static const char log_command_name[] = "commandfilter.log";
+static const char ping_command_name[] = "commandfilter.ping";
+static const char retained_command_name[] = "commandfilter.retained";
+static const char unregister_command_name[] = "commandfilter.unregister";
+static const char unfiltered_clientid_name[] = "unfilter_clientid";
+static int in_log_command = 0;
+
+unsigned long long unfiltered_clientid = 0;
+
+static RedisModuleCommandFilter *filter, *filter1;
+static RedisModuleString *retained;
+
+int CommandFilter_UnregisterCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ (void) argc;
+ (void) argv;
+
+ RedisModule_ReplyWithLongLong(ctx,
+ RedisModule_UnregisterCommandFilter(ctx, filter));
+
+ return REDISMODULE_OK;
+}
+
+int CommandFilter_PingCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ (void) argc;
+ (void) argv;
+
+ RedisModuleCallReply *reply = RedisModule_Call(ctx, "ping", "c", "@log");
+ if (reply) {
+ RedisModule_ReplyWithCallReply(ctx, reply);
+ RedisModule_FreeCallReply(reply);
+ } else {
+ RedisModule_ReplyWithSimpleString(ctx, "Unknown command or invalid arguments");
+ }
+
+ return REDISMODULE_OK;
+}
+
+int CommandFilter_Retained(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ (void) argc;
+ (void) argv;
+
+ if (retained) {
+ RedisModule_ReplyWithString(ctx, retained);
+ } else {
+ RedisModule_ReplyWithNull(ctx);
+ }
+
+ return REDISMODULE_OK;
+}
+
+int CommandFilter_LogCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ RedisModuleString *s = RedisModule_CreateString(ctx, "", 0);
+
+ int i;
+ for (i = 1; i < argc; i++) {
+ size_t arglen;
+ const char *arg = RedisModule_StringPtrLen(argv[i], &arglen);
+
+ if (i > 1) RedisModule_StringAppendBuffer(ctx, s, " ", 1);
+ RedisModule_StringAppendBuffer(ctx, s, arg, arglen);
+ }
+
+ RedisModuleKey *log = RedisModule_OpenKey(ctx, log_key_name, REDISMODULE_WRITE|REDISMODULE_READ);
+ RedisModule_ListPush(log, REDISMODULE_LIST_HEAD, s);
+ RedisModule_CloseKey(log);
+ RedisModule_FreeString(ctx, s);
+
+ in_log_command = 1;
+
+ size_t cmdlen;
+ const char *cmdname = RedisModule_StringPtrLen(argv[1], &cmdlen);
+ RedisModuleCallReply *reply = RedisModule_Call(ctx, cmdname, "v", &argv[2], (size_t)argc - 2);
+ if (reply) {
+ RedisModule_ReplyWithCallReply(ctx, reply);
+ RedisModule_FreeCallReply(reply);
+ } else {
+ RedisModule_ReplyWithSimpleString(ctx, "Unknown command or invalid arguments");
+ }
+
+ in_log_command = 0;
+
+ return REDISMODULE_OK;
+}
+
+int CommandFilter_UnfilteredClientId(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ if (argc < 2)
+ return RedisModule_WrongArity(ctx);
+
+ long long id;
+ if (RedisModule_StringToLongLong(argv[1], &id) != REDISMODULE_OK) {
+ RedisModule_ReplyWithError(ctx, "invalid client id");
+ return REDISMODULE_OK;
+ }
+ if (id < 0) {
+ RedisModule_ReplyWithError(ctx, "invalid client id");
+ return REDISMODULE_OK;
+ }
+
+ unfiltered_clientid = id;
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+/* Filter to protect against Bug #11894 reappearing
+ *
+ * ensures that the filter is only run the first time through, and not on reprocessing
+ */
+void CommandFilter_BlmoveSwap(RedisModuleCommandFilterCtx *filter)
+{
+ if (RedisModule_CommandFilterArgsCount(filter) != 6)
+ return;
+
+ RedisModuleString *arg = RedisModule_CommandFilterArgGet(filter, 0);
+ size_t arg_len;
+ const char *arg_str = RedisModule_StringPtrLen(arg, &arg_len);
+
+ if (arg_len != 6 || strncmp(arg_str, "blmove", 6))
+ return;
+
+ /*
+ * Swapping directional args (right/left) from source and destination.
+ * need to hold here, can't push into the ArgReplace func, as it will cause other to freed -> use after free
+ */
+ RedisModuleString *dir1 = RedisModule_HoldString(NULL, RedisModule_CommandFilterArgGet(filter, 3));
+ RedisModuleString *dir2 = RedisModule_HoldString(NULL, RedisModule_CommandFilterArgGet(filter, 4));
+ RedisModule_CommandFilterArgReplace(filter, 3, dir2);
+ RedisModule_CommandFilterArgReplace(filter, 4, dir1);
+}
+
+void CommandFilter_CommandFilter(RedisModuleCommandFilterCtx *filter)
+{
+ unsigned long long id = RedisModule_CommandFilterGetClientId(filter);
+ if (id == unfiltered_clientid) return;
+
+ if (in_log_command) return; /* don't process our own RM_Call() from CommandFilter_LogCommand() */
+
+ /* Fun manipulations:
+ * - Remove @delme
+ * - Replace @replaceme
+ * - Append @insertbefore or @insertafter
+ * - Prefix with Log command if @log encountered
+ */
+ int log = 0;
+ int pos = 0;
+ while (pos < RedisModule_CommandFilterArgsCount(filter)) {
+ const RedisModuleString *arg = RedisModule_CommandFilterArgGet(filter, pos);
+ size_t arg_len;
+ const char *arg_str = RedisModule_StringPtrLen(arg, &arg_len);
+
+ if (arg_len == 6 && !memcmp(arg_str, "@delme", 6)) {
+ RedisModule_CommandFilterArgDelete(filter, pos);
+ continue;
+ }
+ if (arg_len == 10 && !memcmp(arg_str, "@replaceme", 10)) {
+ RedisModule_CommandFilterArgReplace(filter, pos,
+ RedisModule_CreateString(NULL, "--replaced--", 12));
+ } else if (arg_len == 13 && !memcmp(arg_str, "@insertbefore", 13)) {
+ RedisModule_CommandFilterArgInsert(filter, pos,
+ RedisModule_CreateString(NULL, "--inserted-before--", 19));
+ pos++;
+ } else if (arg_len == 12 && !memcmp(arg_str, "@insertafter", 12)) {
+ RedisModule_CommandFilterArgInsert(filter, pos + 1,
+ RedisModule_CreateString(NULL, "--inserted-after--", 18));
+ pos++;
+ } else if (arg_len == 7 && !memcmp(arg_str, "@retain", 7)) {
+ if (retained) RedisModule_FreeString(NULL, retained);
+ retained = RedisModule_CommandFilterArgGet(filter, pos + 1);
+ RedisModule_RetainString(NULL, retained);
+ pos++;
+ } else if (arg_len == 4 && !memcmp(arg_str, "@log", 4)) {
+ log = 1;
+ }
+ pos++;
+ }
+
+ if (log) RedisModule_CommandFilterArgInsert(filter, 0,
+ RedisModule_CreateString(NULL, log_command_name, sizeof(log_command_name)-1));
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (RedisModule_Init(ctx,"commandfilter",1,REDISMODULE_APIVER_1)
+ == REDISMODULE_ERR) return REDISMODULE_ERR;
+
+ if (argc != 2 && argc != 3) {
+ RedisModule_Log(ctx, "warning", "Log key name not specified");
+ return REDISMODULE_ERR;
+ }
+
+ long long noself = 0;
+ log_key_name = RedisModule_CreateStringFromString(ctx, argv[0]);
+ RedisModule_StringToLongLong(argv[1], &noself);
+ retained = NULL;
+
+ if (RedisModule_CreateCommand(ctx,log_command_name,
+ CommandFilter_LogCommand,"write deny-oom",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,ping_command_name,
+ CommandFilter_PingCommand,"deny-oom",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,retained_command_name,
+ CommandFilter_Retained,"readonly",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,unregister_command_name,
+ CommandFilter_UnregisterCommand,"write deny-oom",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, unfiltered_clientid_name,
+ CommandFilter_UnfilteredClientId, "admin", 1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if ((filter = RedisModule_RegisterCommandFilter(ctx, CommandFilter_CommandFilter,
+ noself ? REDISMODULE_CMDFILTER_NOSELF : 0))
+ == NULL) return REDISMODULE_ERR;
+
+ if ((filter1 = RedisModule_RegisterCommandFilter(ctx, CommandFilter_BlmoveSwap, 0)) == NULL)
+ return REDISMODULE_ERR;
+
+ if (argc == 3) {
+ const char *ptr = RedisModule_StringPtrLen(argv[2], NULL);
+ if (!strcasecmp(ptr, "noload")) {
+ /* This is a hint that we return ERR at the last moment of OnLoad. */
+ RedisModule_FreeString(ctx, log_key_name);
+ if (retained) RedisModule_FreeString(NULL, retained);
+ return REDISMODULE_ERR;
+ }
+ }
+
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnUnload(RedisModuleCtx *ctx) {
+ RedisModule_FreeString(ctx, log_key_name);
+ if (retained) RedisModule_FreeString(NULL, retained);
+
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/configaccess.c b/examples/redis-unstable/tests/modules/configaccess.c
new file mode 100644
index 0000000..7bc8a48
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/configaccess.c
@@ -0,0 +1,353 @@
+#include "redismodule.h"
+#include <assert.h>
+#include <string.h>
+
+/* See moduleconfigs.c for registering module configs. We need to register some
+ * module configs with our module in order to test the interaction between
+ * module configs and the RM_Get/Set*Config APIs. */
+int configaccess_bool;
+
+int getBoolConfigCommand(const char *name, void *privdata) {
+ REDISMODULE_NOT_USED(name);
+ return (*(int *)privdata);
+}
+
+int setBoolConfigCommand(const char *name, int new, void *privdata, RedisModuleString **err) {
+ REDISMODULE_NOT_USED(name);
+ REDISMODULE_NOT_USED(err);
+ *(int *)privdata = new;
+ return REDISMODULE_OK;
+}
+
+/* Test command for RM_GetConfigType */
+int TestGetConfigType_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2) {
+ return RedisModule_WrongArity(ctx);
+ }
+
+ size_t len;
+ const char *config_name = RedisModule_StringPtrLen(argv[1], &len);
+
+ RedisModuleConfigType type;
+ int res = RedisModule_ConfigGetType(config_name, &type);
+ if (res == REDISMODULE_ERR) {
+ RedisModule_ReplyWithError(ctx, "ERR Config does not exist");
+ return REDISMODULE_ERR;
+ }
+
+ const char *type_str;
+ switch (type) {
+ case REDISMODULE_CONFIG_TYPE_BOOL:
+ type_str = "bool";
+ break;
+ case REDISMODULE_CONFIG_TYPE_NUMERIC:
+ type_str = "numeric";
+ break;
+ case REDISMODULE_CONFIG_TYPE_STRING:
+ type_str = "string";
+ break;
+ case REDISMODULE_CONFIG_TYPE_ENUM:
+ type_str = "enum";
+ break;
+ default:
+ assert(0);
+ break;
+ }
+
+ RedisModule_ReplyWithSimpleString(ctx, type_str);
+ return REDISMODULE_OK;
+}
+
+/* Test command for config iteration */
+int TestConfigIteration_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+
+ if (argc > 2) {
+ return RedisModule_WrongArity(ctx);
+ }
+
+ const char *pattern = NULL;
+ if (argc == 2) {
+ pattern = RedisModule_StringPtrLen(argv[1], NULL);
+ }
+
+ RedisModuleConfigIterator *iter = RedisModule_ConfigIteratorCreate(ctx, pattern);
+ if (!iter) {
+ RedisModule_ReplyWithError(ctx, "ERR Failed to get config iterator");
+ return REDISMODULE_ERR;
+ }
+
+ /* Start array reply for the configs */
+ RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_ARRAY_LEN);
+
+ /* Iterate through the dictionary */
+ const char *config_name = NULL;
+ long count = 0;
+ while ((config_name = RedisModule_ConfigIteratorNext(iter)) != NULL) {
+ RedisModuleString *value = NULL;
+ RedisModule_ConfigGet(ctx, config_name, &value);
+
+ RedisModule_ReplyWithArray(ctx, 2);
+ RedisModule_ReplyWithStringBuffer(ctx, config_name, strlen(config_name));
+ RedisModule_ReplyWithString(ctx, value);
+
+ RedisModule_FreeString(ctx, value);
+ ++count;
+ }
+ RedisModule_ReplySetArrayLength(ctx, count);
+
+ /* Free the iterator */
+ RedisModule_ConfigIteratorRelease(ctx, iter);
+
+ return REDISMODULE_OK;
+}
+
+/* Test command for RM_GetBoolConfig */
+int TestGetBoolConfig_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2) {
+ return RedisModule_WrongArity(ctx);
+ }
+
+ size_t len;
+ const char *config_name = RedisModule_StringPtrLen(argv[1], &len);
+
+ int value;
+ if (RedisModule_ConfigGetBool(ctx, config_name, &value) == REDISMODULE_ERR) {
+ RedisModule_ReplyWithError(ctx, "ERR Failed to get bool config");
+ return REDISMODULE_ERR;
+ }
+
+ RedisModule_ReplyWithLongLong(ctx, value);
+ return REDISMODULE_OK;
+}
+
+/* Test command for RM_GetNumericConfig */
+int TestGetNumericConfig_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2) {
+ return RedisModule_WrongArity(ctx);
+ }
+
+ size_t len;
+ const char *config_name = RedisModule_StringPtrLen(argv[1], &len);
+
+ long long value;
+ if (RedisModule_ConfigGetNumeric(ctx, config_name, &value) == REDISMODULE_ERR) {
+ RedisModule_ReplyWithError(ctx, "ERR Failed to get numeric config");
+ return REDISMODULE_ERR;
+ }
+
+ RedisModule_ReplyWithLongLong(ctx, value);
+ return REDISMODULE_OK;
+}
+
+/* Test command for RM_GetConfig */
+int TestGetConfig_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2) {
+ return RedisModule_WrongArity(ctx);
+ }
+
+ size_t len;
+ const char *config_name = RedisModule_StringPtrLen(argv[1], &len);
+
+ RedisModuleString *value;
+ if (RedisModule_ConfigGet(ctx, config_name, &value) == REDISMODULE_ERR) {
+ RedisModule_ReplyWithError(ctx, "ERR Failed to get string config");
+ return REDISMODULE_ERR;
+ }
+
+ RedisModule_ReplyWithString(ctx, value);
+ RedisModule_FreeString(ctx,value);
+ return REDISMODULE_OK;
+}
+
+/* Test command for RM_GetEnumConfig */
+int TestGetEnumConfig_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2) {
+ return RedisModule_WrongArity(ctx);
+ }
+
+ size_t len;
+ const char *config_name = RedisModule_StringPtrLen(argv[1], &len);
+
+ RedisModuleString *value;
+ if (RedisModule_ConfigGetEnum(ctx, config_name, &value) == REDISMODULE_ERR) {
+ RedisModule_ReplyWithError(ctx, "ERR Failed to get enum name config");
+ return REDISMODULE_ERR;
+ }
+
+ RedisModule_ReplyWithString(ctx, value);
+ RedisModule_Free(value);
+ return REDISMODULE_OK;
+}
+
+/* Test command for RM_SetBoolConfig */
+int TestSetBoolConfig_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 3) {
+ return RedisModule_WrongArity(ctx);
+ }
+
+ size_t name_len, value_len;
+ const char *config_name = RedisModule_StringPtrLen(argv[1], &name_len);
+ const char *config_value = RedisModule_StringPtrLen(argv[2], &value_len);
+
+ int bool_value;
+ if (!strcasecmp(config_value, "yes")) {
+ bool_value = 1;
+ } else if (!strcasecmp(config_value, "no")) {
+ bool_value = 0;
+ } else {
+ bool_value = -1;
+ }
+
+ RedisModuleString *error = NULL;
+ int result = RedisModule_ConfigSetBool(ctx, config_name, bool_value, &error);
+ if (result == REDISMODULE_ERR) {
+ RedisModule_ReplyWithErrorFormat(ctx, "ERR Failed to set bool config %s: %s", config_name, RedisModule_StringPtrLen(error, NULL));
+ RedisModule_FreeString(ctx, error);
+ return REDISMODULE_ERR;
+ }
+
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+/* Test command for RM_SetNumericConfig */
+int TestSetNumericConfig_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 3) {
+ return RedisModule_WrongArity(ctx);
+ }
+
+ size_t name_len;
+ const char *config_name = RedisModule_StringPtrLen(argv[1], &name_len);
+
+ long long value;
+ const char *value_str= RedisModule_StringPtrLen(argv[2], NULL);
+ if (value_str[0] == '-') {
+ if (RedisModule_StringToLongLong(argv[2], &value) != REDISMODULE_OK) {
+ RedisModule_ReplyWithError(ctx, "ERR Invalid numeric value");
+ return REDISMODULE_ERR;
+ }
+ } else {
+ if (RedisModule_StringToULongLong(argv[2], (unsigned long long*)&value) != REDISMODULE_OK) {
+ RedisModule_ReplyWithError(ctx, "ERR Invalid numeric value");
+ return REDISMODULE_ERR;
+ }
+ }
+
+ RedisModuleString *error = NULL;
+ int result = RedisModule_ConfigSetNumeric(ctx, config_name, value, &error);
+ if (result == REDISMODULE_OK) {
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ } else {
+ RedisModule_ReplyWithErrorFormat(ctx, "ERR Failed to set numeric config %s: %s", config_name, RedisModule_StringPtrLen(error, NULL));
+ RedisModule_FreeString(ctx, error);
+ return REDISMODULE_ERR;
+ }
+
+ return REDISMODULE_OK;
+}
+
+/* Test command for RM_SetConfig */
+int TestSetConfig_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 3) {
+ return RedisModule_WrongArity(ctx);
+ }
+
+ size_t name_len;
+ const char *config_name = RedisModule_StringPtrLen(argv[1], &name_len);
+
+ RedisModuleString *error = NULL;
+ int result = RedisModule_ConfigSet(ctx, config_name, argv[2], &error);
+ if (result == REDISMODULE_OK) {
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ } else {
+ RedisModule_ReplyWithErrorFormat(ctx, "ERR Failed to set string config %s: %s", config_name, RedisModule_StringPtrLen(error, NULL));
+ RedisModule_FreeString(ctx, error);
+ return REDISMODULE_ERR;
+ }
+
+ return REDISMODULE_OK;
+}
+
+/* Test command for RM_SetEnumConfig with name */
+int TestSetEnumConfig_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc < 3) {
+ return RedisModule_WrongArity(ctx);
+ }
+
+ const char *config_name = RedisModule_StringPtrLen(argv[1], NULL);
+
+ RedisModuleString *error = NULL;
+ int result = RedisModule_ConfigSetEnum(ctx, config_name, argv[2], &error);
+
+ if (result == REDISMODULE_OK) {
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ } else {
+ RedisModule_ReplyWithErrorFormat(ctx, "ERR Failed to set enum config %s: %s", config_name, RedisModule_StringPtrLen(error, NULL));
+ RedisModule_FreeString(ctx, error);
+ return REDISMODULE_ERR;
+ }
+
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ if (RedisModule_Init(ctx, "configaccess", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "configaccess.getconfigs",
+ TestConfigIteration_RedisCommand, "readonly", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "configaccess.getbool",
+ TestGetBoolConfig_RedisCommand, "readonly", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "configaccess.getnumeric",
+ TestGetNumericConfig_RedisCommand, "readonly", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "configaccess.get",
+ TestGetConfig_RedisCommand, "readonly", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "configaccess.getenum",
+ TestGetEnumConfig_RedisCommand, "readonly", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "configaccess.setbool",
+ TestSetBoolConfig_RedisCommand, "write", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "configaccess.setnumeric",
+ TestSetNumericConfig_RedisCommand, "write", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "configaccess.set",
+ TestSetConfig_RedisCommand, "write", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "configaccess.setenum",
+ TestSetEnumConfig_RedisCommand, "write", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "configaccess.getconfigtype", TestGetConfigType_RedisCommand, "readonly", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_RegisterBoolConfig(ctx, "bool", 1, REDISMODULE_CONFIG_DEFAULT,
+ getBoolConfigCommand, setBoolConfigCommand, NULL, &configaccess_bool) == REDISMODULE_ERR) {
+ RedisModule_Log(ctx, "warning", "Failed to register configaccess_bool");
+ return REDISMODULE_ERR;
+ }
+
+ RedisModule_Log(ctx, "debug", "Loading configaccess module configuration");
+ if (RedisModule_LoadConfigs(ctx) == REDISMODULE_ERR) {
+ RedisModule_Log(ctx, "warning", "Failed to load configaccess module configuration");
+ return REDISMODULE_ERR;
+ }
+
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/crash.c b/examples/redis-unstable/tests/modules/crash.c
new file mode 100644
index 0000000..c5063d0
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/crash.c
@@ -0,0 +1,311 @@
+#include "redismodule.h"
+
+#include <strings.h>
+#include <sys/mman.h>
+
+#define UNUSED(V) ((void) V)
+
+void assertCrash(RedisModuleInfoCtx *ctx, int for_crash_report) {
+ UNUSED(ctx);
+ UNUSED(for_crash_report);
+ RedisModule_Assert(0);
+}
+
+void segfaultCrash(RedisModuleInfoCtx *ctx, int for_crash_report) {
+ UNUSED(ctx);
+ UNUSED(for_crash_report);
+ /* Compiler gives warnings about writing to a random address
+ * e.g "*((char*)-1) = 'x';". As a workaround, we map a read-only area
+ * and try to write there to trigger segmentation fault. */
+ char *p = mmap(NULL, 4096, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ *p = 'x';
+}
+
+int cmd_crash(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ UNUSED(ctx);
+ UNUSED(argv);
+ UNUSED(argc);
+
+ RedisModule_Assert(0);
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ if (RedisModule_Init(ctx,"modulecrash",1,REDISMODULE_APIVER_1)
+ == REDISMODULE_ERR) return REDISMODULE_ERR;
+
+ if (argc >= 1) {
+ if (!strcasecmp(RedisModule_StringPtrLen(argv[0], NULL), "segfault")) {
+ if (RedisModule_RegisterInfoFunc(ctx, segfaultCrash) == REDISMODULE_ERR) return REDISMODULE_ERR;
+ } else if (!strcasecmp(RedisModule_StringPtrLen(argv[0], NULL),"assert")) {
+ if (RedisModule_RegisterInfoFunc(ctx, assertCrash) == REDISMODULE_ERR) return REDISMODULE_ERR;
+ }
+ }
+
+ /* Create modulecrash.xadd command which is similar to xadd command.
+ * It will crash in the command handler to verify we print command tokens
+ * when hide-user-data-from-log config is enabled */
+ RedisModuleCommandInfo info = {
+ .version = REDISMODULE_COMMAND_INFO_VERSION,
+ .arity = -5,
+ .key_specs = (RedisModuleCommandKeySpec[]){
+ {
+ .notes = "UPDATE instead of INSERT because of the optional trimming feature",
+ .flags = REDISMODULE_CMD_KEY_RW | REDISMODULE_CMD_KEY_UPDATE,
+ .begin_search_type = REDISMODULE_KSPEC_BS_INDEX,
+ .bs.index.pos = 1,
+ .find_keys_type = REDISMODULE_KSPEC_FK_RANGE,
+ .fk.range = {0,1,0}
+ },
+ {0}
+ },
+ .args = (RedisModuleCommandArg[]){
+ {
+ .name = "key",
+ .type = REDISMODULE_ARG_TYPE_KEY,
+ .key_spec_index = 0
+ },
+ {
+ .name = "nomkstream",
+ .type = REDISMODULE_ARG_TYPE_PURE_TOKEN,
+ .token = "NOMKSTREAM",
+ .since = "6.2.0",
+ .flags = REDISMODULE_CMD_ARG_OPTIONAL
+ },
+ {
+ .name = "trim",
+ .type = REDISMODULE_ARG_TYPE_BLOCK,
+ .flags = REDISMODULE_CMD_ARG_OPTIONAL,
+ .subargs = (RedisModuleCommandArg[]){
+ {
+ .name = "strategy",
+ .type = REDISMODULE_ARG_TYPE_ONEOF,
+ .subargs = (RedisModuleCommandArg[]){
+ {
+ .name = "maxlen",
+ .type = REDISMODULE_ARG_TYPE_PURE_TOKEN,
+ .token = "MAXLEN",
+ },
+ {
+ .name = "minid",
+ .type = REDISMODULE_ARG_TYPE_PURE_TOKEN,
+ .token = "MINID",
+ .since = "6.2.0",
+ },
+ {0}
+ }
+ },
+ {
+ .name = "operator",
+ .type = REDISMODULE_ARG_TYPE_ONEOF,
+ .flags = REDISMODULE_CMD_ARG_OPTIONAL,
+ .subargs = (RedisModuleCommandArg[]){
+ {
+ .name = "equal",
+ .type = REDISMODULE_ARG_TYPE_PURE_TOKEN,
+ .token = "="
+ },
+ {
+ .name = "approximately",
+ .type = REDISMODULE_ARG_TYPE_PURE_TOKEN,
+ .token = "~"
+ },
+ {0}
+ }
+ },
+ {
+ .name = "threshold",
+ .type = REDISMODULE_ARG_TYPE_STRING,
+ .display_text = "threshold" /* Just for coverage, doesn't have a visible effect */
+ },
+ {
+ .name = "count",
+ .type = REDISMODULE_ARG_TYPE_INTEGER,
+ .token = "LIMIT",
+ .since = "6.2.0",
+ .flags = REDISMODULE_CMD_ARG_OPTIONAL
+ },
+ {0}
+ }
+ },
+ {
+ .name = "id-selector",
+ .type = REDISMODULE_ARG_TYPE_ONEOF,
+ .subargs = (RedisModuleCommandArg[]){
+ {
+ .name = "auto-id",
+ .type = REDISMODULE_ARG_TYPE_PURE_TOKEN,
+ .token = "*"
+ },
+ {
+ .name = "id",
+ .type = REDISMODULE_ARG_TYPE_STRING,
+ },
+ {0}
+ }
+ },
+ {
+ .name = "data",
+ .type = REDISMODULE_ARG_TYPE_BLOCK,
+ .flags = REDISMODULE_CMD_ARG_MULTIPLE,
+ .subargs = (RedisModuleCommandArg[]){
+ {
+ .name = "field",
+ .type = REDISMODULE_ARG_TYPE_STRING,
+ },
+ {
+ .name = "value",
+ .type = REDISMODULE_ARG_TYPE_STRING,
+ },
+ {0}
+ }
+ },
+ {0}
+ }
+ };
+
+ RedisModuleCommand *cmd;
+
+ if (RedisModule_CreateCommand(ctx,"modulecrash.xadd", cmd_crash,"write deny-oom random fast",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ cmd = RedisModule_GetCommand(ctx,"modulecrash.xadd");
+ if (RedisModule_SetCommandInfo(cmd, &info) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ /* Create a subcommand: modulecrash.parent sub
+ * It will crash in the command handler to verify we print subcommand name
+ * when hide-user-data-from-log config is enabled */
+ RedisModuleCommandInfo subcommand_info = {
+ .version = REDISMODULE_COMMAND_INFO_VERSION,
+ .arity = -5,
+ .key_specs = (RedisModuleCommandKeySpec[]){
+ {
+ .flags = REDISMODULE_CMD_KEY_RW | REDISMODULE_CMD_KEY_UPDATE,
+ .begin_search_type = REDISMODULE_KSPEC_BS_INDEX,
+ .bs.index.pos = 1,
+ .find_keys_type = REDISMODULE_KSPEC_FK_RANGE,
+ .fk.range = {0,1,0}
+ },
+ {0}
+ },
+ .args = (RedisModuleCommandArg[]){
+ {
+ .name = "key",
+ .type = REDISMODULE_ARG_TYPE_KEY,
+ .key_spec_index = 0
+ },
+ {
+ .name = "token",
+ .type = REDISMODULE_ARG_TYPE_PURE_TOKEN,
+ .token = "TOKEN",
+ .flags = REDISMODULE_CMD_ARG_OPTIONAL
+ },
+ {
+ .name = "data",
+ .type = REDISMODULE_ARG_TYPE_BLOCK,
+ .subargs = (RedisModuleCommandArg[]){
+ {
+ .name = "field",
+ .type = REDISMODULE_ARG_TYPE_STRING,
+ },
+ {
+ .name = "value",
+ .type = REDISMODULE_ARG_TYPE_STRING,
+ },
+ {0}
+ }
+ },
+ {0}
+ }
+ };
+
+ if (RedisModule_CreateCommand(ctx,"modulecrash.parent",NULL,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ RedisModuleCommand *parent = RedisModule_GetCommand(ctx,"modulecrash.parent");
+
+ if (RedisModule_CreateSubcommand(parent,"subcmd",cmd_crash,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ cmd = RedisModule_GetCommand(ctx,"modulecrash.parent|subcmd");
+ if (RedisModule_SetCommandInfo(cmd, &subcommand_info) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ /* Create modulecrash.zunion command which is similar to zunion command.
+ * It will crash in the command handler to verify we print command tokens
+ * when hide-user-data-from-log config is enabled */
+ RedisModuleCommandInfo zunioninfo = {
+ .version = REDISMODULE_COMMAND_INFO_VERSION,
+ .arity = -5,
+ .key_specs = (RedisModuleCommandKeySpec[]){
+ {
+ .flags = REDISMODULE_CMD_KEY_RO,
+ .begin_search_type = REDISMODULE_KSPEC_BS_INDEX,
+ .bs.index.pos = 1,
+ .find_keys_type = REDISMODULE_KSPEC_FK_KEYNUM,
+ .fk.keynum = {0,1,1}
+ },
+ {0}
+ },
+ .args = (RedisModuleCommandArg[]){
+ {
+ .name = "numkeys",
+ .type = REDISMODULE_ARG_TYPE_INTEGER,
+ },
+ {
+ .name = "key",
+ .type = REDISMODULE_ARG_TYPE_KEY,
+ .key_spec_index = 0,
+ .flags = REDISMODULE_CMD_ARG_MULTIPLE
+ },
+ {
+ .name = "weights",
+ .type = REDISMODULE_ARG_TYPE_INTEGER,
+ .token = "WEIGHTS",
+ .flags = REDISMODULE_CMD_ARG_OPTIONAL | REDISMODULE_CMD_ARG_MULTIPLE
+ },
+ {
+ .name = "aggregate",
+ .type = REDISMODULE_ARG_TYPE_ONEOF,
+ .token = "AGGREGATE",
+ .flags = REDISMODULE_CMD_ARG_OPTIONAL,
+ .subargs = (RedisModuleCommandArg[]){
+ {
+ .name = "sum",
+ .type = REDISMODULE_ARG_TYPE_PURE_TOKEN,
+ .token = "sum"
+ },
+ {
+ .name = "min",
+ .type = REDISMODULE_ARG_TYPE_PURE_TOKEN,
+ .token = "min"
+ },
+ {
+ .name = "max",
+ .type = REDISMODULE_ARG_TYPE_PURE_TOKEN,
+ .token = "max"
+ },
+ {0}
+ }
+ },
+ {
+ .name = "withscores",
+ .type = REDISMODULE_ARG_TYPE_PURE_TOKEN,
+ .token = "WITHSCORES",
+ .flags = REDISMODULE_CMD_ARG_OPTIONAL
+ },
+ {0}
+ }
+ };
+
+ if (RedisModule_CreateCommand(ctx,"modulecrash.zunion", cmd_crash,"readonly",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ cmd = RedisModule_GetCommand(ctx,"modulecrash.zunion");
+ if (RedisModule_SetCommandInfo(cmd, &zunioninfo) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/datatype.c b/examples/redis-unstable/tests/modules/datatype.c
new file mode 100644
index 0000000..05cf233
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/datatype.c
@@ -0,0 +1,323 @@
+/* This module current tests a small subset but should be extended in the future
+ * for general ModuleDataType coverage.
+ */
+
+/* define macros for having usleep */
+#define _BSD_SOURCE
+#define _DEFAULT_SOURCE
+#include <unistd.h>
+
+#include "redismodule.h"
+
+static RedisModuleType *datatype = NULL;
+static int load_encver = 0;
+
+/* used to test processing events during slow loading */
+static volatile int slow_loading = 0;
+static volatile int is_in_slow_loading = 0;
+
+#define DATATYPE_ENC_VER 1
+
+typedef struct {
+ long long intval;
+ RedisModuleString *strval;
+} DataType;
+
+static void *datatype_load(RedisModuleIO *io, int encver) {
+ load_encver = encver;
+ int intval = RedisModule_LoadSigned(io);
+ if (RedisModule_IsIOError(io)) return NULL;
+
+ RedisModuleString *strval = RedisModule_LoadString(io);
+ if (RedisModule_IsIOError(io)) return NULL;
+
+ DataType *dt = (DataType *) RedisModule_Alloc(sizeof(DataType));
+ dt->intval = intval;
+ dt->strval = strval;
+
+ if (slow_loading) {
+ RedisModuleCtx *ctx = RedisModule_GetContextFromIO(io);
+ is_in_slow_loading = 1;
+ while (slow_loading) {
+ RedisModule_Yield(ctx, REDISMODULE_YIELD_FLAG_CLIENTS, "Slow module operation");
+ usleep(1000);
+ }
+ is_in_slow_loading = 0;
+ }
+
+ return dt;
+}
+
+static void datatype_save(RedisModuleIO *io, void *value) {
+ DataType *dt = (DataType *) value;
+ RedisModule_SaveSigned(io, dt->intval);
+ RedisModule_SaveString(io, dt->strval);
+}
+
+static void datatype_free(void *value) {
+ if (value) {
+ DataType *dt = (DataType *) value;
+
+ if (dt->strval) RedisModule_FreeString(NULL, dt->strval);
+ RedisModule_Free(dt);
+ }
+}
+
+static void *datatype_copy(RedisModuleString *fromkey, RedisModuleString *tokey, const void *value) {
+ const DataType *old = value;
+
+ /* Answers to ultimate questions cannot be copied! */
+ if (old->intval == 42)
+ return NULL;
+
+ DataType *new = (DataType *) RedisModule_Alloc(sizeof(DataType));
+
+ new->intval = old->intval;
+ new->strval = RedisModule_CreateStringFromString(NULL, old->strval);
+
+ /* Breaking the rules here! We return a copy that also includes traces
+ * of fromkey/tokey to confirm we get what we expect.
+ */
+ size_t len;
+ const char *str = RedisModule_StringPtrLen(fromkey, &len);
+ RedisModule_StringAppendBuffer(NULL, new->strval, "/", 1);
+ RedisModule_StringAppendBuffer(NULL, new->strval, str, len);
+ RedisModule_StringAppendBuffer(NULL, new->strval, "/", 1);
+ str = RedisModule_StringPtrLen(tokey, &len);
+ RedisModule_StringAppendBuffer(NULL, new->strval, str, len);
+
+ return new;
+}
+
+static int datatype_set(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 4) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ long long intval;
+
+ if (RedisModule_StringToLongLong(argv[2], &intval) != REDISMODULE_OK) {
+ RedisModule_ReplyWithError(ctx, "Invalid integer value");
+ return REDISMODULE_OK;
+ }
+
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE);
+ DataType *dt = RedisModule_Calloc(sizeof(DataType), 1);
+ dt->intval = intval;
+ dt->strval = argv[3];
+ RedisModule_RetainString(ctx, dt->strval);
+
+ RedisModule_ModuleTypeSetValue(key, datatype, dt);
+ RedisModule_CloseKey(key);
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+
+ return REDISMODULE_OK;
+}
+
+static int datatype_restore(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 4) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ long long encver;
+ if (RedisModule_StringToLongLong(argv[3], &encver) != REDISMODULE_OK) {
+ RedisModule_ReplyWithError(ctx, "Invalid integer value");
+ return REDISMODULE_OK;
+ }
+
+ DataType *dt = RedisModule_LoadDataTypeFromStringEncver(argv[2], datatype, encver);
+ if (!dt) {
+ RedisModule_ReplyWithError(ctx, "Invalid data");
+ return REDISMODULE_OK;
+ }
+
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE);
+ RedisModule_ModuleTypeSetValue(key, datatype, dt);
+ RedisModule_CloseKey(key);
+ RedisModule_ReplyWithLongLong(ctx, load_encver);
+
+ return REDISMODULE_OK;
+}
+
+static int datatype_get(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ);
+ DataType *dt = RedisModule_ModuleTypeGetValue(key);
+ RedisModule_CloseKey(key);
+
+ if (!dt) {
+ RedisModule_ReplyWithNullArray(ctx);
+ } else {
+ RedisModule_ReplyWithArray(ctx, 2);
+ RedisModule_ReplyWithLongLong(ctx, dt->intval);
+ RedisModule_ReplyWithString(ctx, dt->strval);
+ }
+ return REDISMODULE_OK;
+}
+
+static int datatype_dump(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ);
+ DataType *dt = RedisModule_ModuleTypeGetValue(key);
+ RedisModule_CloseKey(key);
+
+ RedisModuleString *reply = RedisModule_SaveDataTypeToString(ctx, dt, datatype);
+ if (!reply) {
+ RedisModule_ReplyWithError(ctx, "Failed to save");
+ return REDISMODULE_OK;
+ }
+
+ RedisModule_ReplyWithString(ctx, reply);
+ RedisModule_FreeString(ctx, reply);
+ return REDISMODULE_OK;
+}
+
+static int datatype_swap(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 3) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ RedisModuleKey *a = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE);
+ RedisModuleKey *b = RedisModule_OpenKey(ctx, argv[2], REDISMODULE_WRITE);
+ void *val = RedisModule_ModuleTypeGetValue(a);
+
+ int error = (RedisModule_ModuleTypeReplaceValue(b, datatype, val, &val) == REDISMODULE_ERR ||
+ RedisModule_ModuleTypeReplaceValue(a, datatype, val, NULL) == REDISMODULE_ERR);
+ if (!error)
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ else
+ RedisModule_ReplyWithError(ctx, "ERR failed");
+
+ RedisModule_CloseKey(a);
+ RedisModule_CloseKey(b);
+
+ return REDISMODULE_OK;
+}
+
+/* used to enable or disable slow loading */
+static int datatype_slow_loading(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ long long ll;
+ if (RedisModule_StringToLongLong(argv[1], &ll) != REDISMODULE_OK) {
+ RedisModule_ReplyWithError(ctx, "Invalid integer value");
+ return REDISMODULE_OK;
+ }
+ slow_loading = ll;
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+/* used to test if we reached the slow loading code */
+static int datatype_is_in_slow_loading(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ if (argc != 1) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ RedisModule_ReplyWithLongLong(ctx, is_in_slow_loading);
+ return REDISMODULE_OK;
+}
+
+int createDataTypeBlockCheck(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ static RedisModuleType *datatype_outside_onload = NULL;
+
+ RedisModuleTypeMethods datatype_methods = {
+ .version = REDISMODULE_TYPE_METHOD_VERSION,
+ .rdb_load = datatype_load,
+ .rdb_save = datatype_save,
+ .free = datatype_free,
+ .copy = datatype_copy
+ };
+
+ datatype_outside_onload = RedisModule_CreateDataType(ctx, "test_dt_outside_onload", 1, &datatype_methods);
+
+ /* This validates that it's not possible to create datatype outside OnLoad,
+ * thus returns an error if it succeeds. */
+ if (datatype_outside_onload == NULL) {
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ } else {
+ RedisModule_ReplyWithError(ctx, "UNEXPECTEDOK");
+ }
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ if (RedisModule_Init(ctx,"datatype",DATATYPE_ENC_VER,REDISMODULE_APIVER_1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ /* Creates a command which creates a datatype outside OnLoad() function. */
+ if (RedisModule_CreateCommand(ctx,"block.create.datatype.outside.onload", createDataTypeBlockCheck, "write", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ RedisModule_SetModuleOptions(ctx, REDISMODULE_OPTIONS_HANDLE_IO_ERRORS);
+
+ RedisModuleTypeMethods datatype_methods = {
+ .version = REDISMODULE_TYPE_METHOD_VERSION,
+ .rdb_load = datatype_load,
+ .rdb_save = datatype_save,
+ .free = datatype_free,
+ .copy = datatype_copy
+ };
+
+ datatype = RedisModule_CreateDataType(ctx, "test___dt", 1, &datatype_methods);
+ if (datatype == NULL)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"datatype.set", datatype_set,
+ "write deny-oom", 1, 1, 1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"datatype.get", datatype_get,"",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"datatype.restore", datatype_restore,
+ "write deny-oom", 1, 1, 1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"datatype.dump", datatype_dump,"",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "datatype.swap", datatype_swap,
+ "write", 1, 1, 1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "datatype.slow_loading", datatype_slow_loading,
+ "allow-loading", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "datatype.is_in_slow_loading", datatype_is_in_slow_loading,
+ "allow-loading", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnUnload(RedisModuleCtx *ctx) {
+ REDISMODULE_NOT_USED(ctx);
+ if (datatype) {
+ RedisModule_Free(datatype);
+ datatype = NULL;
+ }
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/datatype2.c b/examples/redis-unstable/tests/modules/datatype2.c
new file mode 100644
index 0000000..bc0dc3d
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/datatype2.c
@@ -0,0 +1,739 @@
+/* This module is used to test a use case of a module that stores information
+ * about keys in global memory, and relies on the enhanced data type callbacks to
+ * get key name and dbid on various operations.
+ *
+ * it simulates a simple memory allocator. The smallest allocation unit of
+ * the allocator is a mem block with a size of 4KB. Multiple mem blocks are combined
+ * using a linked list. These linked lists are placed in a global dict named 'mem_pool'.
+ * Each db has a 'mem_pool'. You can use the 'mem.alloc' command to allocate a specified
+ * number of mem blocks, and use 'mem.free' to release the memory. Use 'mem.write', 'mem.read'
+ * to write and read the specified mem block (note that each mem block can only be written once).
+ * Use 'mem.usage' to get the memory usage under different dbs, and it will return the size
+ * mem blocks and used mem blocks under the db.
+ * The specific structure diagram is as follows:
+ *
+ *
+ * Global variables of the module:
+ *
+ * mem blocks link
+ * ┌─────┬─────┐
+ * │ │ │ ┌───┐ ┌───┐ ┌───┐
+ * │ k1 │ ───┼───►│4KB├───►│4KB├───►│4KB│
+ * │ │ │ └───┘ └───┘ └───┘
+ * ├─────┼─────┤
+ * ┌───────┐ ┌────► │ │ │ ┌───┐ ┌───┐
+ * │ │ │ │ k2 │ ───┼───►│4KB├───►│4KB│
+ * │ db0 ├──────┘ │ │ │ └───┘ └───┘
+ * │ │ ├─────┼─────┤
+ * ├───────┤ │ │ │ ┌───┐ ┌───┐ ┌───┐
+ * │ │ │ k3 │ ───┼───►│4KB├───►│4KB├───►│4KB│
+ * │ db1 ├──►null │ │ │ └───┘ └───┘ └───┘
+ * │ │ └─────┴─────┘
+ * ├───────┤ dict
+ * │ │
+ * │ db2 ├─────────┐
+ * │ │ │
+ * ├───────┤ │ ┌─────┬─────┐
+ * │ │ │ │ │ │ ┌───┐ ┌───┐ ┌───┐
+ * │ db3 ├──►null │ │ k1 │ ───┼───►│4KB├───►│4KB├───►│4KB│
+ * │ │ │ │ │ │ └───┘ └───┘ └───┘
+ * └───────┘ │ ├─────┼─────┤
+ * mem_pool[MAX_DB] │ │ │ │ ┌───┐ ┌───┐
+ * └──►│ k2 │ ───┼───►│4KB├───►│4KB│
+ * │ │ │ └───┘ └───┘
+ * └─────┴─────┘
+ * dict
+ *
+ *
+ * Keys in redis database:
+ *
+ * ┌───────┐
+ * │ size │
+ * ┌───────────►│ used │
+ * │ │ mask │
+ * ┌─────┬─────┐ │ └───────┘ ┌───────┐
+ * │ │ │ │ MemAllocObject │ size │
+ * │ k1 │ ───┼─┘ ┌───────────►│ used │
+ * │ │ │ │ │ mask │
+ * ├─────┼─────┤ ┌───────┐ ┌─────┬─────┐ │ └───────┘
+ * │ │ │ │ size │ │ │ │ │ MemAllocObject
+ * │ k2 │ ───┼─────────────►│ used │ │ k1 │ ───┼─┘
+ * │ │ │ │ mask │ │ │ │
+ * ├─────┼─────┤ └───────┘ ├─────┼─────┤
+ * │ │ │ MemAllocObject │ │ │
+ * │ k3 │ ───┼─┐ │ k2 │ ───┼─┐
+ * │ │ │ │ │ │ │ │
+ * └─────┴─────┘ │ ┌───────┐ └─────┴─────┘ │ ┌───────┐
+ * redis db[0] │ │ size │ redis db[1] │ │ size │
+ * └───────────►│ used │ └───────────►│ used │
+ * │ mask │ │ mask │
+ * └───────┘ └───────┘
+ * MemAllocObject MemAllocObject
+ *
+ **/
+
+#include "redismodule.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <ctype.h>
+#include <string.h>
+#include <stdint.h>
+
+static RedisModuleType *MemAllocType;
+
+#define MAX_DB 16
+RedisModuleDict *mem_pool[MAX_DB];
+typedef struct MemAllocObject {
+ long long size;
+ long long used;
+ uint64_t mask;
+} MemAllocObject;
+
+MemAllocObject *createMemAllocObject(void) {
+ MemAllocObject *o = RedisModule_Calloc(1, sizeof(*o));
+ return o;
+}
+
+/*---------------------------- mem block apis ------------------------------------*/
+#define BLOCK_SIZE 4096
+struct MemBlock {
+ char block[BLOCK_SIZE];
+ struct MemBlock *next;
+};
+
+void MemBlockFree(struct MemBlock *head) {
+ if (head) {
+ struct MemBlock *block = head->next, *next;
+ RedisModule_Free(head);
+ while (block) {
+ next = block->next;
+ RedisModule_Free(block);
+ block = next;
+ }
+ }
+}
+struct MemBlock *MemBlockCreate(long long num) {
+ if (num <= 0) {
+ return NULL;
+ }
+
+ struct MemBlock *head = RedisModule_Calloc(1, sizeof(struct MemBlock));
+ struct MemBlock *block = head;
+ while (--num) {
+ block->next = RedisModule_Calloc(1, sizeof(struct MemBlock));
+ block = block->next;
+ }
+
+ return head;
+}
+
+long long MemBlockNum(const struct MemBlock *head) {
+ long long num = 0;
+ const struct MemBlock *block = head;
+ while (block) {
+ num++;
+ block = block->next;
+ }
+
+ return num;
+}
+
+size_t MemBlockWrite(struct MemBlock *head, long long block_index, const char *data, size_t size) {
+ size_t w_size = 0;
+ struct MemBlock *block = head;
+ while (block_index-- && block) {
+ block = block->next;
+ }
+
+ if (block) {
+ size = size > BLOCK_SIZE ? BLOCK_SIZE:size;
+ memcpy(block->block, data, size);
+ w_size += size;
+ }
+
+ return w_size;
+}
+
+int MemBlockRead(struct MemBlock *head, long long block_index, char *data, size_t size) {
+ size_t r_size = 0;
+ struct MemBlock *block = head;
+ while (block_index-- && block) {
+ block = block->next;
+ }
+
+ if (block) {
+ size = size > BLOCK_SIZE ? BLOCK_SIZE:size;
+ memcpy(data, block->block, size);
+ r_size += size;
+ }
+
+ return r_size;
+}
+
+void MemPoolFreeDb(RedisModuleCtx *ctx, int dbid) {
+ RedisModuleString *key;
+ void *tdata;
+ RedisModuleDictIter *iter = RedisModule_DictIteratorStartC(mem_pool[dbid], "^", NULL, 0);
+ while((key = RedisModule_DictNext(ctx, iter, &tdata)) != NULL) {
+ MemBlockFree((struct MemBlock *)tdata);
+ }
+ RedisModule_DictIteratorStop(iter);
+ RedisModule_FreeDict(NULL, mem_pool[dbid]);
+ mem_pool[dbid] = RedisModule_CreateDict(NULL);
+}
+
+struct MemBlock *MemBlockClone(const struct MemBlock *head) {
+ struct MemBlock *newhead = NULL;
+ if (head) {
+ newhead = RedisModule_Calloc(1, sizeof(struct MemBlock));
+ memcpy(newhead->block, head->block, BLOCK_SIZE);
+ struct MemBlock *newblock = newhead;
+ const struct MemBlock *oldblock = head->next;
+ while (oldblock) {
+ newblock->next = RedisModule_Calloc(1, sizeof(struct MemBlock));
+ newblock = newblock->next;
+ memcpy(newblock->block, oldblock->block, BLOCK_SIZE);
+ oldblock = oldblock->next;
+ }
+ }
+
+ return newhead;
+}
+
+/*---------------------------- event handler ------------------------------------*/
+void swapDbCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data) {
+ REDISMODULE_NOT_USED(ctx);
+ REDISMODULE_NOT_USED(e);
+ REDISMODULE_NOT_USED(sub);
+
+ RedisModuleSwapDbInfo *ei = data;
+
+ // swap
+ RedisModuleDict *tmp = mem_pool[ei->dbnum_first];
+ mem_pool[ei->dbnum_first] = mem_pool[ei->dbnum_second];
+ mem_pool[ei->dbnum_second] = tmp;
+}
+
+void flushdbCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data) {
+ REDISMODULE_NOT_USED(ctx);
+ REDISMODULE_NOT_USED(e);
+ int i;
+ RedisModuleFlushInfo *fi = data;
+
+ RedisModule_AutoMemory(ctx);
+
+ if (sub == REDISMODULE_SUBEVENT_FLUSHDB_START) {
+ if (fi->dbnum != -1) {
+ MemPoolFreeDb(ctx, fi->dbnum);
+ } else {
+ for (i = 0; i < MAX_DB; i++) {
+ MemPoolFreeDb(ctx, i);
+ }
+ }
+ }
+}
+
+/*---------------------------- command implementation ------------------------------------*/
+
+/* MEM.ALLOC key block_num */
+int MemAlloc_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ RedisModule_AutoMemory(ctx);
+
+ if (argc != 3) {
+ return RedisModule_WrongArity(ctx);
+ }
+
+ long long block_num;
+ if ((RedisModule_StringToLongLong(argv[2], &block_num) != REDISMODULE_OK) || block_num <= 0) {
+ return RedisModule_ReplyWithError(ctx, "ERR invalid block_num: must be a value greater than 0");
+ }
+
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ | REDISMODULE_WRITE);
+ int type = RedisModule_KeyType(key);
+ if (type != REDISMODULE_KEYTYPE_EMPTY && RedisModule_ModuleTypeGetType(key) != MemAllocType) {
+ return RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE);
+ }
+
+ MemAllocObject *o;
+ if (type == REDISMODULE_KEYTYPE_EMPTY) {
+ o = createMemAllocObject();
+ RedisModule_ModuleTypeSetValue(key, MemAllocType, o);
+ } else {
+ o = RedisModule_ModuleTypeGetValue(key);
+ }
+
+ struct MemBlock *mem = MemBlockCreate(block_num);
+ RedisModule_Assert(mem != NULL);
+ RedisModule_DictSet(mem_pool[RedisModule_GetSelectedDb(ctx)], argv[1], mem);
+ o->size = block_num;
+ o->used = 0;
+ o->mask = 0;
+
+ RedisModule_ReplyWithLongLong(ctx, block_num);
+ RedisModule_ReplicateVerbatim(ctx);
+ return REDISMODULE_OK;
+}
+
+/* MEM.FREE key */
+int MemFree_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ RedisModule_AutoMemory(ctx);
+
+ if (argc != 2) {
+ return RedisModule_WrongArity(ctx);
+ }
+
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ);
+ int type = RedisModule_KeyType(key);
+ if (type != REDISMODULE_KEYTYPE_EMPTY && RedisModule_ModuleTypeGetType(key) != MemAllocType) {
+ return RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE);
+ }
+
+ int ret = 0;
+ MemAllocObject *o;
+ if (type == REDISMODULE_KEYTYPE_EMPTY) {
+ RedisModule_ReplyWithLongLong(ctx, ret);
+ return REDISMODULE_OK;
+ } else {
+ o = RedisModule_ModuleTypeGetValue(key);
+ }
+
+ int nokey;
+ struct MemBlock *mem = (struct MemBlock *)RedisModule_DictGet(mem_pool[RedisModule_GetSelectedDb(ctx)], argv[1], &nokey);
+ if (!nokey && mem) {
+ RedisModule_DictDel(mem_pool[RedisModule_GetSelectedDb(ctx)], argv[1], NULL);
+ MemBlockFree(mem);
+ o->used = 0;
+ o->size = 0;
+ o->mask = 0;
+ ret = 1;
+ }
+
+ RedisModule_ReplyWithLongLong(ctx, ret);
+ RedisModule_ReplicateVerbatim(ctx);
+ return REDISMODULE_OK;
+}
+
+/* MEM.WRITE key block_index data */
+int MemWrite_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ RedisModule_AutoMemory(ctx);
+
+ if (argc != 4) {
+ return RedisModule_WrongArity(ctx);
+ }
+
+ long long block_index;
+ if ((RedisModule_StringToLongLong(argv[2], &block_index) != REDISMODULE_OK) || block_index < 0) {
+ return RedisModule_ReplyWithError(ctx, "ERR invalid block_index: must be a value greater than 0");
+ }
+
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ | REDISMODULE_WRITE);
+ int type = RedisModule_KeyType(key);
+ if (type != REDISMODULE_KEYTYPE_EMPTY && RedisModule_ModuleTypeGetType(key) != MemAllocType) {
+ return RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE);
+ }
+
+ MemAllocObject *o;
+ if (type == REDISMODULE_KEYTYPE_EMPTY) {
+ return RedisModule_ReplyWithError(ctx, "ERR Memory has not been allocated");
+ } else {
+ o = RedisModule_ModuleTypeGetValue(key);
+ }
+
+ if (o->mask & (1UL << block_index)) {
+ return RedisModule_ReplyWithError(ctx, "ERR block is busy");
+ }
+
+ int ret = 0;
+ int nokey;
+ struct MemBlock *mem = (struct MemBlock *)RedisModule_DictGet(mem_pool[RedisModule_GetSelectedDb(ctx)], argv[1], &nokey);
+ if (!nokey && mem) {
+ size_t len;
+ const char *buf = RedisModule_StringPtrLen(argv[3], &len);
+ ret = MemBlockWrite(mem, block_index, buf, len);
+ o->mask |= (1UL << block_index);
+ o->used++;
+ }
+
+ RedisModule_ReplyWithLongLong(ctx, ret);
+ RedisModule_ReplicateVerbatim(ctx);
+ return REDISMODULE_OK;
+}
+
+/* MEM.READ key block_index */
+int MemRead_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ RedisModule_AutoMemory(ctx);
+
+ if (argc != 3) {
+ return RedisModule_WrongArity(ctx);
+ }
+
+ long long block_index;
+ if ((RedisModule_StringToLongLong(argv[2], &block_index) != REDISMODULE_OK) || block_index < 0) {
+ return RedisModule_ReplyWithError(ctx, "ERR invalid block_index: must be a value greater than 0");
+ }
+
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ);
+ int type = RedisModule_KeyType(key);
+ if (type != REDISMODULE_KEYTYPE_EMPTY && RedisModule_ModuleTypeGetType(key) != MemAllocType) {
+ return RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE);
+ }
+
+ MemAllocObject *o;
+ if (type == REDISMODULE_KEYTYPE_EMPTY) {
+ return RedisModule_ReplyWithError(ctx, "ERR Memory has not been allocated");
+ } else {
+ o = RedisModule_ModuleTypeGetValue(key);
+ }
+
+ if (!(o->mask & (1UL << block_index))) {
+ return RedisModule_ReplyWithNull(ctx);
+ }
+
+ int nokey;
+ struct MemBlock *mem = (struct MemBlock *)RedisModule_DictGet(mem_pool[RedisModule_GetSelectedDb(ctx)], argv[1], &nokey);
+ RedisModule_Assert(nokey == 0 && mem != NULL);
+
+ char buf[BLOCK_SIZE];
+ MemBlockRead(mem, block_index, buf, sizeof(buf));
+
+ /* Assuming that the contents are all c-style strings */
+ RedisModule_ReplyWithStringBuffer(ctx, buf, strlen(buf));
+ return REDISMODULE_OK;
+}
+
+/* MEM.USAGE dbid */
+int MemUsage_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ RedisModule_AutoMemory(ctx);
+
+ if (argc != 2) {
+ return RedisModule_WrongArity(ctx);
+ }
+
+ long long dbid;
+ if ((RedisModule_StringToLongLong(argv[1], (long long *)&dbid) != REDISMODULE_OK)) {
+ return RedisModule_ReplyWithError(ctx, "ERR invalid value: must be a integer");
+ }
+
+ if (dbid < 0 || dbid >= MAX_DB) {
+ return RedisModule_ReplyWithError(ctx, "ERR dbid out of range");
+ }
+
+
+ long long size = 0, used = 0;
+
+ void *data;
+ RedisModuleString *key;
+ RedisModuleDictIter *iter = RedisModule_DictIteratorStartC(mem_pool[dbid], "^", NULL, 0);
+ while((key = RedisModule_DictNext(ctx, iter, &data)) != NULL) {
+ int dbbackup = RedisModule_GetSelectedDb(ctx);
+ RedisModule_SelectDb(ctx, dbid);
+ RedisModuleKey *openkey = RedisModule_OpenKey(ctx, key, REDISMODULE_READ);
+ int type = RedisModule_KeyType(openkey);
+ RedisModule_Assert(type != REDISMODULE_KEYTYPE_EMPTY && RedisModule_ModuleTypeGetType(openkey) == MemAllocType);
+ MemAllocObject *o = RedisModule_ModuleTypeGetValue(openkey);
+ used += o->used;
+ size += o->size;
+ RedisModule_CloseKey(openkey);
+ RedisModule_SelectDb(ctx, dbbackup);
+ }
+ RedisModule_DictIteratorStop(iter);
+
+ RedisModule_ReplyWithArray(ctx, 4);
+ RedisModule_ReplyWithSimpleString(ctx, "total");
+ RedisModule_ReplyWithLongLong(ctx, size);
+ RedisModule_ReplyWithSimpleString(ctx, "used");
+ RedisModule_ReplyWithLongLong(ctx, used);
+ return REDISMODULE_OK;
+}
+
+/* MEM.ALLOCANDWRITE key block_num block_index data block_index data ... */
+int MemAllocAndWrite_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ RedisModule_AutoMemory(ctx);
+
+ if (argc < 3) {
+ return RedisModule_WrongArity(ctx);
+ }
+
+ long long block_num;
+ if ((RedisModule_StringToLongLong(argv[2], &block_num) != REDISMODULE_OK) || block_num <= 0) {
+ return RedisModule_ReplyWithError(ctx, "ERR invalid block_num: must be a value greater than 0");
+ }
+
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ | REDISMODULE_WRITE);
+ int type = RedisModule_KeyType(key);
+ if (type != REDISMODULE_KEYTYPE_EMPTY && RedisModule_ModuleTypeGetType(key) != MemAllocType) {
+ return RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE);
+ }
+
+ MemAllocObject *o;
+ if (type == REDISMODULE_KEYTYPE_EMPTY) {
+ o = createMemAllocObject();
+ RedisModule_ModuleTypeSetValue(key, MemAllocType, o);
+ } else {
+ o = RedisModule_ModuleTypeGetValue(key);
+ }
+
+ struct MemBlock *mem = MemBlockCreate(block_num);
+ RedisModule_Assert(mem != NULL);
+ RedisModule_DictSet(mem_pool[RedisModule_GetSelectedDb(ctx)], argv[1], mem);
+ o->used = 0;
+ o->mask = 0;
+ o->size = block_num;
+
+ int i = 3;
+ long long block_index;
+ for (; i < argc; i++) {
+ /* Security is guaranteed internally, so no security check. */
+ RedisModule_StringToLongLong(argv[i], &block_index);
+ size_t len;
+ const char * buf = RedisModule_StringPtrLen(argv[i + 1], &len);
+ MemBlockWrite(mem, block_index, buf, len);
+ o->used++;
+ o->mask |= (1UL << block_index);
+ }
+
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ RedisModule_ReplicateVerbatim(ctx);
+ return REDISMODULE_OK;
+}
+
+/*---------------------------- type callbacks ------------------------------------*/
+
+void *MemAllocRdbLoad(RedisModuleIO *rdb, int encver) {
+ if (encver != 0) {
+ return NULL;
+ }
+
+ MemAllocObject *o = createMemAllocObject();
+ o->size = RedisModule_LoadSigned(rdb);
+ o->used = RedisModule_LoadSigned(rdb);
+ o->mask = RedisModule_LoadUnsigned(rdb);
+
+ const RedisModuleString *key = RedisModule_GetKeyNameFromIO(rdb);
+ int dbid = RedisModule_GetDbIdFromIO(rdb);
+
+ if (o->size) {
+ size_t size;
+ char *tmpbuf;
+ long long num = o->size;
+ struct MemBlock *head = RedisModule_Calloc(1, sizeof(struct MemBlock));
+ tmpbuf = RedisModule_LoadStringBuffer(rdb, &size);
+ memcpy(head->block, tmpbuf, size > BLOCK_SIZE ? BLOCK_SIZE:size);
+ RedisModule_Free(tmpbuf);
+ struct MemBlock *block = head;
+ while (--num) {
+ block->next = RedisModule_Calloc(1, sizeof(struct MemBlock));
+ block = block->next;
+
+ tmpbuf = RedisModule_LoadStringBuffer(rdb, &size);
+ memcpy(block->block, tmpbuf, size > BLOCK_SIZE ? BLOCK_SIZE:size);
+ RedisModule_Free(tmpbuf);
+ }
+
+ RedisModule_DictSet(mem_pool[dbid], (RedisModuleString *)key, head);
+ }
+
+ return o;
+}
+
+void MemAllocRdbSave(RedisModuleIO *rdb, void *value) {
+ MemAllocObject *o = value;
+ RedisModule_SaveSigned(rdb, o->size);
+ RedisModule_SaveSigned(rdb, o->used);
+ RedisModule_SaveUnsigned(rdb, o->mask);
+
+ const RedisModuleString *key = RedisModule_GetKeyNameFromIO(rdb);
+ int dbid = RedisModule_GetDbIdFromIO(rdb);
+
+ if (o->size) {
+ int nokey;
+ struct MemBlock *mem = (struct MemBlock *)RedisModule_DictGet(mem_pool[dbid], (RedisModuleString *)key, &nokey);
+ RedisModule_Assert(nokey == 0 && mem != NULL);
+
+ struct MemBlock *block = mem;
+ while (block) {
+ RedisModule_SaveStringBuffer(rdb, block->block, BLOCK_SIZE);
+ block = block->next;
+ }
+ }
+}
+
+void MemAllocAofRewrite(RedisModuleIO *aof, RedisModuleString *key, void *value) {
+ MemAllocObject *o = (MemAllocObject *)value;
+ if (o->size) {
+ int dbid = RedisModule_GetDbIdFromIO(aof);
+ int nokey;
+ size_t i = 0, j = 0;
+ struct MemBlock *mem = (struct MemBlock *)RedisModule_DictGet(mem_pool[dbid], (RedisModuleString *)key, &nokey);
+ RedisModule_Assert(nokey == 0 && mem != NULL);
+ size_t array_size = o->size * 2;
+ RedisModuleString ** string_array = RedisModule_Calloc(array_size, sizeof(RedisModuleString *));
+ while (mem) {
+ string_array[i] = RedisModule_CreateStringFromLongLong(NULL, j);
+ string_array[i + 1] = RedisModule_CreateString(NULL, mem->block, BLOCK_SIZE);
+ mem = mem->next;
+ i += 2;
+ j++;
+ }
+ RedisModule_EmitAOF(aof, "mem.allocandwrite", "slv", key, o->size, string_array, array_size);
+ for (i = 0; i < array_size; i++) {
+ RedisModule_FreeString(NULL, string_array[i]);
+ }
+ RedisModule_Free(string_array);
+ } else {
+ RedisModule_EmitAOF(aof, "mem.allocandwrite", "sl", key, o->size);
+ }
+}
+
+void MemAllocFree(void *value) {
+ RedisModule_Free(value);
+}
+
+void MemAllocUnlink(RedisModuleString *key, const void *value) {
+ REDISMODULE_NOT_USED(key);
+ REDISMODULE_NOT_USED(value);
+
+ /* When unlink and unlink2 exist at the same time, we will only call unlink2. */
+ RedisModule_Assert(0);
+}
+
+void MemAllocUnlink2(RedisModuleKeyOptCtx *ctx, const void *value) {
+ MemAllocObject *o = (MemAllocObject *)value;
+
+ const RedisModuleString *key = RedisModule_GetKeyNameFromOptCtx(ctx);
+ int dbid = RedisModule_GetDbIdFromOptCtx(ctx);
+
+ if (o->size) {
+ void *oldval;
+ RedisModule_DictDel(mem_pool[dbid], (RedisModuleString *)key, &oldval);
+ RedisModule_Assert(oldval != NULL);
+ MemBlockFree((struct MemBlock *)oldval);
+ }
+}
+
+void MemAllocDigest(RedisModuleDigest *md, void *value) {
+ MemAllocObject *o = (MemAllocObject *)value;
+ RedisModule_DigestAddLongLong(md, o->size);
+ RedisModule_DigestAddLongLong(md, o->used);
+ RedisModule_DigestAddLongLong(md, o->mask);
+
+ int dbid = RedisModule_GetDbIdFromDigest(md);
+ const RedisModuleString *key = RedisModule_GetKeyNameFromDigest(md);
+
+ if (o->size) {
+ int nokey;
+ struct MemBlock *mem = (struct MemBlock *)RedisModule_DictGet(mem_pool[dbid], (RedisModuleString *)key, &nokey);
+ RedisModule_Assert(nokey == 0 && mem != NULL);
+
+ struct MemBlock *block = mem;
+ while (block) {
+ RedisModule_DigestAddStringBuffer(md, (const char *)block->block, BLOCK_SIZE);
+ block = block->next;
+ }
+ }
+}
+
+void *MemAllocCopy2(RedisModuleKeyOptCtx *ctx, const void *value) {
+ const MemAllocObject *old = value;
+ MemAllocObject *new = createMemAllocObject();
+ new->size = old->size;
+ new->used = old->used;
+ new->mask = old->mask;
+
+ int from_dbid = RedisModule_GetDbIdFromOptCtx(ctx);
+ int to_dbid = RedisModule_GetToDbIdFromOptCtx(ctx);
+ const RedisModuleString *fromkey = RedisModule_GetKeyNameFromOptCtx(ctx);
+ const RedisModuleString *tokey = RedisModule_GetToKeyNameFromOptCtx(ctx);
+
+ if (old->size) {
+ int nokey;
+ struct MemBlock *oldmem = (struct MemBlock *)RedisModule_DictGet(mem_pool[from_dbid], (RedisModuleString *)fromkey, &nokey);
+ RedisModule_Assert(nokey == 0 && oldmem != NULL);
+ struct MemBlock *newmem = MemBlockClone(oldmem);
+ RedisModule_Assert(newmem != NULL);
+ RedisModule_DictSet(mem_pool[to_dbid], (RedisModuleString *)tokey, newmem);
+ }
+
+ return new;
+}
+
+size_t MemAllocMemUsage2(RedisModuleKeyOptCtx *ctx, const void *value, size_t sample_size) {
+ REDISMODULE_NOT_USED(ctx);
+ REDISMODULE_NOT_USED(sample_size);
+ uint64_t size = 0;
+ MemAllocObject *o = (MemAllocObject *)value;
+
+ size += sizeof(*o);
+ size += o->size * sizeof(struct MemBlock);
+
+ return size;
+}
+
+size_t MemAllocMemFreeEffort2(RedisModuleKeyOptCtx *ctx, const void *value) {
+ REDISMODULE_NOT_USED(ctx);
+ MemAllocObject *o = (MemAllocObject *)value;
+ return o->size;
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ if (RedisModule_Init(ctx, "datatype2", 1,REDISMODULE_APIVER_1) == REDISMODULE_ERR) {
+ return REDISMODULE_ERR;
+ }
+
+ RedisModuleTypeMethods tm = {
+ .version = REDISMODULE_TYPE_METHOD_VERSION,
+ .rdb_load = MemAllocRdbLoad,
+ .rdb_save = MemAllocRdbSave,
+ .aof_rewrite = MemAllocAofRewrite,
+ .free = MemAllocFree,
+ .digest = MemAllocDigest,
+ .unlink = MemAllocUnlink,
+ // .defrag = MemAllocDefrag, // Tested in defragtest.c
+ .unlink2 = MemAllocUnlink2,
+ .copy2 = MemAllocCopy2,
+ .mem_usage2 = MemAllocMemUsage2,
+ .free_effort2 = MemAllocMemFreeEffort2,
+ };
+
+ MemAllocType = RedisModule_CreateDataType(ctx, "mem_alloc", 0, &tm);
+ if (MemAllocType == NULL) {
+ return REDISMODULE_ERR;
+ }
+
+ if (RedisModule_CreateCommand(ctx, "mem.alloc", MemAlloc_RedisCommand, "write deny-oom", 1, 1, 1) == REDISMODULE_ERR) {
+ return REDISMODULE_ERR;
+ }
+
+ if (RedisModule_CreateCommand(ctx, "mem.free", MemFree_RedisCommand, "write deny-oom", 1, 1, 1) == REDISMODULE_ERR) {
+ return REDISMODULE_ERR;
+ }
+
+ if (RedisModule_CreateCommand(ctx, "mem.write", MemWrite_RedisCommand, "write deny-oom", 1, 1, 1) == REDISMODULE_ERR) {
+ return REDISMODULE_ERR;
+ }
+
+ if (RedisModule_CreateCommand(ctx, "mem.read", MemRead_RedisCommand, "readonly", 1, 1, 1) == REDISMODULE_ERR) {
+ return REDISMODULE_ERR;
+ }
+
+ if (RedisModule_CreateCommand(ctx, "mem.usage", MemUsage_RedisCommand, "readonly", 1, 1, 1) == REDISMODULE_ERR) {
+ return REDISMODULE_ERR;
+ }
+
+ /* used for internal aof rewrite */
+ if (RedisModule_CreateCommand(ctx, "mem.allocandwrite", MemAllocAndWrite_RedisCommand, "write deny-oom", 1, 1, 1) == REDISMODULE_ERR) {
+ return REDISMODULE_ERR;
+ }
+
+ for(int i = 0; i < MAX_DB; i++){
+ mem_pool[i] = RedisModule_CreateDict(NULL);
+ }
+
+ RedisModule_SubscribeToServerEvent(ctx, RedisModuleEvent_FlushDB, flushdbCallback);
+ RedisModule_SubscribeToServerEvent(ctx, RedisModuleEvent_SwapDB, swapDbCallback);
+
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/defragtest.c b/examples/redis-unstable/tests/modules/defragtest.c
new file mode 100644
index 0000000..8b9e182
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/defragtest.c
@@ -0,0 +1,475 @@
+/* A module that implements defrag callback mechanisms.
+ */
+
+#include "redismodule.h"
+#include <stdlib.h>
+#include <string.h>
+
+#define UNUSED(V) ((void) V)
+
+static RedisModuleType *FragType;
+
+struct FragObject {
+ unsigned long len;
+ void **values;
+ int maxstep;
+};
+
+/* Make sure we get the expected cursor */
+unsigned long int last_set_cursor = 0;
+
+unsigned long int datatype_attempts = 0;
+unsigned long int datatype_defragged = 0;
+unsigned long int datatype_raw_defragged = 0;
+unsigned long int datatype_resumes = 0;
+unsigned long int datatype_wrong_cursor = 0;
+unsigned long int defrag_started = 0;
+unsigned long int defrag_ended = 0;
+unsigned long int global_strings_attempts = 0;
+unsigned long int global_strings_defragged = 0;
+unsigned long int global_dicts_resumes = 0; /* Number of dict defragmentation resumed from a previous break */
+unsigned long int global_subdicts_resumes = 0; /* Number of subdict defragmentation resumed from a previous break */
+unsigned long int global_dicts_attempts = 0; /* Number of attempts to defragment dictionary */
+unsigned long int global_dicts_defragged = 0; /* Number of dictionaries successfully defragmented */
+unsigned long int global_dicts_items_defragged = 0; /* Number of dictionaries items successfully defragmented */
+
+unsigned long global_strings_len = 0;
+RedisModuleString **global_strings = NULL;
+
+unsigned long global_dicts_len = 0;
+RedisModuleDict **global_dicts = NULL;
+
+static void createGlobalStrings(RedisModuleCtx *ctx, unsigned long count)
+{
+ global_strings_len = count;
+ global_strings = RedisModule_Alloc(sizeof(RedisModuleString *) * count);
+
+ for (unsigned long i = 0; i < count; i++) {
+ global_strings[i] = RedisModule_CreateStringFromLongLong(ctx, i);
+ }
+}
+
+static int defragGlobalStrings(RedisModuleDefragCtx *ctx)
+{
+ unsigned long cursor = 0;
+ RedisModule_DefragCursorGet(ctx, &cursor);
+
+ if (!global_strings_len) return 0; /* strings is empty. */
+ RedisModule_Assert(cursor < global_strings_len);
+ for (; cursor < global_strings_len; cursor++) {
+ RedisModuleString *str = global_strings[cursor];
+ if (!str) continue;
+ RedisModuleString *new = RedisModule_DefragRedisModuleString(ctx, str);
+ global_strings_attempts++;
+ if (new != NULL) {
+ global_strings[cursor] = new;
+ global_strings_defragged++;
+ }
+
+ if (RedisModule_DefragShouldStop(ctx)) {
+ RedisModule_DefragCursorSet(ctx, cursor);
+ return 1;
+ }
+ }
+ return 0;
+}
+
+static void createFragGlobalStrings(RedisModuleCtx *ctx) {
+ for (unsigned long i = 0; i < global_strings_len; i++) {
+ if (i % 2 == 1) {
+ RedisModule_FreeString(ctx, global_strings[i]);
+ global_strings[i] = NULL;
+ }
+ }
+}
+
+static void createGlobalDicts(RedisModuleCtx *ctx, unsigned long count) {
+ global_dicts_len = count;
+ global_dicts = RedisModule_Alloc(sizeof(RedisModuleDict *) * count);
+
+ /* Create some nested dictionaries:
+ * - Each main dict contains some subdicts.
+ * - Each sub-dict contains some strings. */
+ for (unsigned long i = 0; i < count; i++) {
+ RedisModuleDict *dict = RedisModule_CreateDict(ctx);
+ for (unsigned long j = 0; j < 10; j++) {
+ /* Create sub dict. */
+ RedisModuleDict *subdict = RedisModule_CreateDict(ctx);
+ for (unsigned long k = 0; k < 10; k++) {
+ RedisModuleString *str = RedisModule_CreateStringFromULongLong(ctx, k);
+ RedisModule_DictSet(subdict, str, str);
+ }
+
+ RedisModuleString *key = RedisModule_CreateStringFromULongLong(ctx, j);
+ RedisModule_DictSet(dict, key, subdict);
+ RedisModule_FreeString(ctx, key);
+ }
+ global_dicts[i] = dict;
+ }
+}
+
+static void freeFragGlobalSubDict(RedisModuleCtx *ctx, RedisModuleDict *subdict) {
+ char *key;
+ size_t keylen;
+ RedisModuleString *str;
+ RedisModuleDictIter *iter = RedisModule_DictIteratorStartC(subdict, "^", NULL, 0);
+ while ((key = RedisModule_DictNextC(iter, &keylen, (void**)&str))) {
+ RedisModule_FreeString(ctx, str);
+ }
+ RedisModule_FreeDict(ctx, subdict);
+ RedisModule_DictIteratorStop(iter);
+}
+
+static void createFragGlobalDicts(RedisModuleCtx *ctx) {
+ char *key;
+ size_t keylen;
+ RedisModuleDict *subdict;
+
+ for (unsigned long i = 0; i < global_dicts_len; i++) {
+ RedisModuleDict *dict = global_dicts[i];
+ if (!dict) continue;
+
+ /* Handle dictionaries differently based on their index in global_dicts array:
+ * 1. For odd indices (i % 2 == 1): Remove the entire dictionary.
+ * 2. For even indices: Keep the dictionary but remove half of its items. */
+ if (i % 2 == 1) {
+ RedisModuleDictIter *iter = RedisModule_DictIteratorStartC(dict, "^", NULL, 0);
+ while ((key = RedisModule_DictNextC(iter, &keylen, (void**)&subdict))) {
+ freeFragGlobalSubDict(ctx, subdict);
+ }
+ RedisModule_FreeDict(ctx, dict);
+ global_dicts[i] = NULL;
+ RedisModule_DictIteratorStop(iter);
+ } else {
+ int key_index = 0;
+ RedisModuleDictIter *iter = RedisModule_DictIteratorStartC(dict, "^", NULL, 0);
+ while ((key = RedisModule_DictNextC(iter, &keylen, (void**)&subdict))) {
+ if (key_index++ % 2 == 1) {
+ freeFragGlobalSubDict(ctx, subdict);
+ RedisModule_DictReplaceC(dict, key, keylen, NULL);
+ }
+ }
+ RedisModule_DictIteratorStop(iter);
+ }
+ }
+}
+
+static int defragGlobalSubDictValueCB(RedisModuleDefragCtx *ctx, void *data, unsigned char *key, size_t keylen, void **newptr) {
+ REDISMODULE_NOT_USED(key);
+ REDISMODULE_NOT_USED(keylen);
+ if (!data) return 0;
+ *newptr = RedisModule_DefragAlloc(ctx, data);
+ return 0;
+}
+
+static int defragGlobalDictValueCB(RedisModuleDefragCtx *ctx, void *data, unsigned char *key, size_t keylen, void **newptr) {
+ REDISMODULE_NOT_USED(key);
+ REDISMODULE_NOT_USED(keylen);
+ static RedisModuleString *seekTo = NULL;
+ RedisModuleDict *subdict = data;
+ if (!subdict) return 0;
+ if (seekTo != NULL) global_subdicts_resumes++;
+
+ *newptr = RedisModule_DefragRedisModuleDict(ctx, subdict, defragGlobalSubDictValueCB, &seekTo);
+ if (*newptr) global_dicts_items_defragged++;
+ /* Return 1 if seekTo is not NULL, indicating this node needs more defrag work. */
+ return seekTo != NULL;
+}
+
+static int defragGlobalDicts(RedisModuleDefragCtx *ctx) {
+ static RedisModuleString *seekTo = NULL;
+ static unsigned long dict_index = 0;
+ unsigned long cursor = 0;
+
+ RedisModule_DefragCursorGet(ctx, &cursor);
+ if (cursor == 0) { /* Start a new defrag. */
+ if (seekTo) {
+ RedisModule_FreeString(NULL, seekTo);
+ seekTo = NULL;
+ }
+ dict_index = 0;
+ } else {
+ global_dicts_resumes++;
+ }
+
+ if (!global_dicts_len) return 0; /* dicts is empty. */
+ RedisModule_Assert(dict_index < global_dicts_len);
+ for (; dict_index < global_dicts_len; dict_index++) {
+ RedisModuleDict *dict = global_dicts[dict_index];
+ if (!dict) continue;
+ RedisModuleDict *new = RedisModule_DefragRedisModuleDict(ctx, dict, defragGlobalDictValueCB, &seekTo);
+ global_dicts_attempts++;
+ if (new != NULL) {
+ global_dicts[dict_index] = new;
+ global_dicts_defragged++;
+ }
+
+ if (seekTo != NULL) {
+ /* Set cursor to 1 to indicate defragmentation is not finished. */
+ RedisModule_DefragCursorSet(ctx, 1);
+ return 1;
+ }
+ }
+
+ /* Set cursor to 0 to indicate completion. */
+ dict_index = 0;
+ RedisModule_DefragCursorSet(ctx, 0);
+ return 0;
+}
+
+typedef enum { DEFRAG_NOT_START, DEFRAG_STRING, DEFRAG_DICT } defrag_module_stage;
+static int defragGlobal(RedisModuleDefragCtx *ctx) {
+ static defrag_module_stage stage = DEFRAG_NOT_START;
+ if (stage == DEFRAG_NOT_START) {
+ stage = DEFRAG_STRING; /* Start a new global defrag. */
+ }
+
+ if (stage == DEFRAG_STRING) {
+ if (defragGlobalStrings(ctx) != 0) return 1;
+ stage = DEFRAG_DICT;
+ }
+ if (stage == DEFRAG_DICT) {
+ if (defragGlobalDicts(ctx) != 0) return 1;
+ stage = DEFRAG_NOT_START;
+ }
+ return 0;
+}
+
+static void defragStart(RedisModuleDefragCtx *ctx) {
+ REDISMODULE_NOT_USED(ctx);
+ defrag_started++;
+}
+
+static void defragEnd(RedisModuleDefragCtx *ctx) {
+ REDISMODULE_NOT_USED(ctx);
+ defrag_ended++;
+}
+
+static void FragInfo(RedisModuleInfoCtx *ctx, int for_crash_report) {
+ REDISMODULE_NOT_USED(for_crash_report);
+
+ RedisModule_InfoAddSection(ctx, "stats");
+ RedisModule_InfoAddFieldLongLong(ctx, "datatype_attempts", datatype_attempts);
+ RedisModule_InfoAddFieldLongLong(ctx, "datatype_defragged", datatype_defragged);
+ RedisModule_InfoAddFieldLongLong(ctx, "datatype_raw_defragged", datatype_raw_defragged);
+ RedisModule_InfoAddFieldLongLong(ctx, "datatype_resumes", datatype_resumes);
+ RedisModule_InfoAddFieldLongLong(ctx, "datatype_wrong_cursor", datatype_wrong_cursor);
+ RedisModule_InfoAddFieldLongLong(ctx, "global_strings_attempts", global_strings_attempts);
+ RedisModule_InfoAddFieldLongLong(ctx, "global_strings_defragged", global_strings_defragged);
+ RedisModule_InfoAddFieldLongLong(ctx, "global_dicts_resumes", global_dicts_resumes);
+ RedisModule_InfoAddFieldLongLong(ctx, "global_subdicts_resumes", global_subdicts_resumes);
+ RedisModule_InfoAddFieldLongLong(ctx, "global_dicts_attempts", global_dicts_attempts);
+ RedisModule_InfoAddFieldLongLong(ctx, "global_dicts_defragged", global_dicts_defragged);
+ RedisModule_InfoAddFieldLongLong(ctx, "global_dicts_items_defragged", global_dicts_items_defragged);
+ RedisModule_InfoAddFieldLongLong(ctx, "defrag_started", defrag_started);
+ RedisModule_InfoAddFieldLongLong(ctx, "defrag_ended", defrag_ended);
+}
+
+struct FragObject *createFragObject(unsigned long len, unsigned long size, int maxstep) {
+ struct FragObject *o = RedisModule_Alloc(sizeof(*o));
+ o->len = len;
+ o->values = RedisModule_Alloc(sizeof(RedisModuleString*) * len);
+ o->maxstep = maxstep;
+
+ for (unsigned long i = 0; i < len; i++) {
+ o->values[i] = RedisModule_Calloc(1, size);
+ }
+
+ return o;
+}
+
+/* FRAG.RESETSTATS */
+static int fragResetStatsCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ datatype_attempts = 0;
+ datatype_defragged = 0;
+ datatype_raw_defragged = 0;
+ datatype_resumes = 0;
+ datatype_wrong_cursor = 0;
+ global_strings_attempts = 0;
+ global_strings_defragged = 0;
+ global_dicts_resumes = 0;
+ global_subdicts_resumes = 0;
+ global_dicts_attempts = 0;
+ global_dicts_defragged = 0;
+ global_dicts_items_defragged = 0;
+ defrag_started = 0;
+ defrag_ended = 0;
+
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+/* FRAG.CREATE key len size maxstep */
+static int fragCreateCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 5)
+ return RedisModule_WrongArity(ctx);
+
+ RedisModuleKey *key = RedisModule_OpenKey(ctx,argv[1],
+ REDISMODULE_READ|REDISMODULE_WRITE);
+ int type = RedisModule_KeyType(key);
+ if (type != REDISMODULE_KEYTYPE_EMPTY)
+ {
+ return RedisModule_ReplyWithError(ctx, "ERR key exists");
+ }
+
+ long long len;
+ if ((RedisModule_StringToLongLong(argv[2], &len) != REDISMODULE_OK)) {
+ return RedisModule_ReplyWithError(ctx, "ERR invalid len");
+ }
+
+ long long size;
+ if ((RedisModule_StringToLongLong(argv[3], &size) != REDISMODULE_OK)) {
+ return RedisModule_ReplyWithError(ctx, "ERR invalid size");
+ }
+
+ long long maxstep;
+ if ((RedisModule_StringToLongLong(argv[4], &maxstep) != REDISMODULE_OK)) {
+ return RedisModule_ReplyWithError(ctx, "ERR invalid maxstep");
+ }
+
+ struct FragObject *o = createFragObject(len, size, maxstep);
+ RedisModule_ModuleTypeSetValue(key, FragType, o);
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ RedisModule_CloseKey(key);
+
+ return REDISMODULE_OK;
+}
+
+/* FRAG.create_frag_global len */
+static int fragCreateGlobalCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ UNUSED(argv);
+ if (argc != 2)
+ return RedisModule_WrongArity(ctx);
+
+ long long glen;
+ if ((RedisModule_StringToLongLong(argv[1], &glen) != REDISMODULE_OK)) {
+ return RedisModule_ReplyWithError(ctx, "ERR invalid len");
+ }
+
+ createGlobalStrings(ctx, glen);
+ createGlobalDicts(ctx, glen);
+ createFragGlobalStrings(ctx);
+ createFragGlobalDicts(ctx);
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+void FragFree(void *value) {
+ struct FragObject *o = value;
+
+ for (unsigned long i = 0; i < o->len; i++)
+ RedisModule_Free(o->values[i]);
+ RedisModule_Free(o->values);
+ RedisModule_Free(o);
+}
+
+size_t FragFreeEffort(RedisModuleString *key, const void *value) {
+ REDISMODULE_NOT_USED(key);
+
+ const struct FragObject *o = value;
+ return o->len;
+}
+
+int FragDefrag(RedisModuleDefragCtx *ctx, RedisModuleString *key, void **value) {
+ unsigned long i = 0;
+ int steps = 0;
+
+ int dbid = RedisModule_GetDbIdFromDefragCtx(ctx);
+ RedisModule_Assert(dbid != -1);
+
+ RedisModule_Log(NULL, "notice", "Defrag key: %s", RedisModule_StringPtrLen(key, NULL));
+
+ /* Attempt to get cursor, validate it's what we're exepcting */
+ if (RedisModule_DefragCursorGet(ctx, &i) == REDISMODULE_OK) {
+ if (i > 0) datatype_resumes++;
+
+ /* Validate we're expecting this cursor */
+ if (i != last_set_cursor) datatype_wrong_cursor++;
+ } else {
+ if (last_set_cursor != 0) datatype_wrong_cursor++;
+ }
+
+ /* Attempt to defrag the object itself */
+ datatype_attempts++;
+ struct FragObject *o = RedisModule_DefragAlloc(ctx, *value);
+ if (o == NULL) {
+ /* Not defragged */
+ o = *value;
+ } else {
+ /* Defragged */
+ *value = o;
+ datatype_defragged++;
+ }
+
+ /* Deep defrag now */
+ for (; i < o->len; i++) {
+ datatype_attempts++;
+ void *new = RedisModule_DefragAlloc(ctx, o->values[i]);
+ if (new) {
+ o->values[i] = new;
+ datatype_defragged++;
+ }
+
+ if ((o->maxstep && ++steps > o->maxstep) ||
+ ((i % 64 == 0) && RedisModule_DefragShouldStop(ctx)))
+ {
+ RedisModule_DefragCursorSet(ctx, i);
+ last_set_cursor = i;
+ return 1;
+ }
+ }
+
+ /* Defrag the values array itself using RedisModule_DefragAllocRaw
+ * and RedisModule_DefragFreeRaw for testing purposes. */
+ void *new_values = RedisModule_DefragAllocRaw(ctx, o->len * sizeof(void*));
+ memcpy(new_values, o->values, o->len * sizeof(void*));
+ RedisModule_DefragFreeRaw(ctx, o->values);
+ o->values = new_values;
+ datatype_raw_defragged++;
+
+ last_set_cursor = 0;
+ return 0;
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ if (RedisModule_Init(ctx, "defragtest", 1, REDISMODULE_APIVER_1)
+ == REDISMODULE_ERR) return REDISMODULE_ERR;
+
+ if (RedisModule_GetTypeMethodVersion() < REDISMODULE_TYPE_METHOD_VERSION) {
+ return REDISMODULE_ERR;
+ }
+
+ RedisModuleTypeMethods tm = {
+ .version = REDISMODULE_TYPE_METHOD_VERSION,
+ .free = FragFree,
+ .free_effort = FragFreeEffort,
+ .defrag = FragDefrag
+ };
+
+ FragType = RedisModule_CreateDataType(ctx, "frag_type", 0, &tm);
+ if (FragType == NULL) return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "frag.create",
+ fragCreateCommand, "write deny-oom", 1, 1, 1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "frag.create_frag_global",
+ fragCreateGlobalCommand, "write deny-oom", 1, 1, 1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "frag.resetstats",
+ fragResetStatsCommand, "write deny-oom", 1, 1, 1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ RedisModule_RegisterInfoFunc(ctx, FragInfo);
+ RedisModule_RegisterDefragFunc2(ctx, defragGlobal);
+ RedisModule_RegisterDefragCallbacks(ctx, defragStart, defragEnd);
+
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/eventloop.c b/examples/redis-unstable/tests/modules/eventloop.c
new file mode 100644
index 0000000..c0cfdf0
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/eventloop.c
@@ -0,0 +1,276 @@
+/* This module contains four tests :
+ * 1- test.sanity : Basic tests for argument validation mostly.
+ * 2- test.sendbytes : Creates a pipe and registers its fds to the event loop,
+ * one end of the pipe for read events and the other end for
+ * the write events. On writable event, data is written. On
+ * readable event data is read. Repeated until all data is
+ * received.
+ * 3- test.iteration : A test for BEFORE_SLEEP and AFTER_SLEEP callbacks.
+ * Counters are incremented each time these events are
+ * fired. They should be equal and increment monotonically.
+ * 4- test.oneshot : Test for oneshot API
+ */
+
+#include "redismodule.h"
+#include <stdlib.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <memory.h>
+#include <errno.h>
+
+int fds[2];
+long long buf_size;
+char *src;
+long long src_offset;
+char *dst;
+long long dst_offset;
+
+RedisModuleBlockedClient *bc;
+RedisModuleCtx *reply_ctx;
+
+void onReadable(int fd, void *user_data, int mask) {
+ REDISMODULE_NOT_USED(mask);
+
+ RedisModule_Assert(strcmp(user_data, "userdataread") == 0);
+
+ while (1) {
+ int rd = read(fd, dst + dst_offset, buf_size - dst_offset);
+ if (rd <= 0)
+ return;
+ dst_offset += rd;
+
+ /* Received all bytes */
+ if (dst_offset == buf_size) {
+ if (memcmp(src, dst, buf_size) == 0)
+ RedisModule_ReplyWithSimpleString(reply_ctx, "OK");
+ else
+ RedisModule_ReplyWithError(reply_ctx, "ERR bytes mismatch");
+
+ RedisModule_EventLoopDel(fds[0], REDISMODULE_EVENTLOOP_READABLE);
+ RedisModule_EventLoopDel(fds[1], REDISMODULE_EVENTLOOP_WRITABLE);
+ RedisModule_Free(src);
+ RedisModule_Free(dst);
+ close(fds[0]);
+ close(fds[1]);
+
+ RedisModule_FreeThreadSafeContext(reply_ctx);
+ RedisModule_UnblockClient(bc, NULL);
+ return;
+ }
+ };
+}
+
+void onWritable(int fd, void *user_data, int mask) {
+ REDISMODULE_NOT_USED(user_data);
+ REDISMODULE_NOT_USED(mask);
+
+ RedisModule_Assert(strcmp(user_data, "userdatawrite") == 0);
+
+ while (1) {
+ /* Check if we sent all data */
+ if (src_offset >= buf_size)
+ return;
+ int written = write(fd, src + src_offset, buf_size - src_offset);
+ if (written <= 0) {
+ return;
+ }
+
+ src_offset += written;
+ };
+}
+
+/* Create a pipe(), register pipe fds to the event loop and send/receive data
+ * using them. */
+int sendbytes(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ if (RedisModule_StringToLongLong(argv[1], &buf_size) != REDISMODULE_OK ||
+ buf_size == 0) {
+ RedisModule_ReplyWithError(ctx, "Invalid integer value");
+ return REDISMODULE_OK;
+ }
+
+ bc = RedisModule_BlockClient(ctx, NULL, NULL, NULL, 0);
+ reply_ctx = RedisModule_GetThreadSafeContext(bc);
+
+ /* Allocate source buffer and write some random data */
+ src = RedisModule_Calloc(1,buf_size);
+ src_offset = 0;
+ memset(src, rand() % 0xFF, buf_size);
+ memcpy(src, "randomtestdata", strlen("randomtestdata"));
+
+ dst = RedisModule_Calloc(1,buf_size);
+ dst_offset = 0;
+
+ /* Create a pipe and register it to the event loop. */
+ if (pipe(fds) < 0) return REDISMODULE_ERR;
+ if (fcntl(fds[0], F_SETFL, O_NONBLOCK) < 0) return REDISMODULE_ERR;
+ if (fcntl(fds[1], F_SETFL, O_NONBLOCK) < 0) return REDISMODULE_ERR;
+
+ if (RedisModule_EventLoopAdd(fds[0], REDISMODULE_EVENTLOOP_READABLE,
+ onReadable, "userdataread") != REDISMODULE_OK) return REDISMODULE_ERR;
+ if (RedisModule_EventLoopAdd(fds[1], REDISMODULE_EVENTLOOP_WRITABLE,
+ onWritable, "userdatawrite") != REDISMODULE_OK) return REDISMODULE_ERR;
+ return REDISMODULE_OK;
+}
+
+int sanity(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ if (pipe(fds) < 0) return REDISMODULE_ERR;
+
+ if (RedisModule_EventLoopAdd(fds[0], 9999999, onReadable, NULL)
+ == REDISMODULE_OK || errno != EINVAL) {
+ RedisModule_ReplyWithError(ctx, "ERR non-existing event type should fail");
+ goto out;
+ }
+ if (RedisModule_EventLoopAdd(-1, REDISMODULE_EVENTLOOP_READABLE, onReadable, NULL)
+ == REDISMODULE_OK || errno != ERANGE) {
+ RedisModule_ReplyWithError(ctx, "ERR out of range fd should fail");
+ goto out;
+ }
+ if (RedisModule_EventLoopAdd(99999999, REDISMODULE_EVENTLOOP_READABLE, onReadable, NULL)
+ == REDISMODULE_OK || errno != ERANGE) {
+ RedisModule_ReplyWithError(ctx, "ERR out of range fd should fail");
+ goto out;
+ }
+ if (RedisModule_EventLoopAdd(fds[0], REDISMODULE_EVENTLOOP_READABLE, NULL, NULL)
+ == REDISMODULE_OK || errno != EINVAL) {
+ RedisModule_ReplyWithError(ctx, "ERR null callback should fail");
+ goto out;
+ }
+ if (RedisModule_EventLoopAdd(fds[0], 9999999, onReadable, NULL)
+ == REDISMODULE_OK || errno != EINVAL) {
+ RedisModule_ReplyWithError(ctx, "ERR non-existing event type should fail");
+ goto out;
+ }
+ if (RedisModule_EventLoopDel(fds[0], REDISMODULE_EVENTLOOP_READABLE)
+ != REDISMODULE_OK || errno != 0) {
+ RedisModule_ReplyWithError(ctx, "ERR del on non-registered fd should not fail");
+ goto out;
+ }
+ if (RedisModule_EventLoopDel(fds[0], 9999999) == REDISMODULE_OK ||
+ errno != EINVAL) {
+ RedisModule_ReplyWithError(ctx, "ERR non-existing event type should fail");
+ goto out;
+ }
+ if (RedisModule_EventLoopDel(-1, REDISMODULE_EVENTLOOP_READABLE)
+ == REDISMODULE_OK || errno != ERANGE) {
+ RedisModule_ReplyWithError(ctx, "ERR out of range fd should fail");
+ goto out;
+ }
+ if (RedisModule_EventLoopDel(99999999, REDISMODULE_EVENTLOOP_READABLE)
+ == REDISMODULE_OK || errno != ERANGE) {
+ RedisModule_ReplyWithError(ctx, "ERR out of range fd should fail");
+ goto out;
+ }
+ if (RedisModule_EventLoopAdd(fds[0], REDISMODULE_EVENTLOOP_READABLE, onReadable, NULL)
+ != REDISMODULE_OK || errno != 0) {
+ RedisModule_ReplyWithError(ctx, "ERR Add failed");
+ goto out;
+ }
+ if (RedisModule_EventLoopAdd(fds[0], REDISMODULE_EVENTLOOP_READABLE, onReadable, NULL)
+ != REDISMODULE_OK || errno != 0) {
+ RedisModule_ReplyWithError(ctx, "ERR Adding same fd twice failed");
+ goto out;
+ }
+ if (RedisModule_EventLoopDel(fds[0], REDISMODULE_EVENTLOOP_READABLE)
+ != REDISMODULE_OK || errno != 0) {
+ RedisModule_ReplyWithError(ctx, "ERR Del failed");
+ goto out;
+ }
+ if (RedisModule_EventLoopAddOneShot(NULL, NULL) == REDISMODULE_OK || errno != EINVAL) {
+ RedisModule_ReplyWithError(ctx, "ERR null callback should fail");
+ goto out;
+ }
+
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+out:
+ close(fds[0]);
+ close(fds[1]);
+ return REDISMODULE_OK;
+}
+
+static long long beforeSleepCount;
+static long long afterSleepCount;
+
+int iteration(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ /* On each event loop iteration, eventloopCallback() is called. We increment
+ * beforeSleepCount and afterSleepCount, so these two should be equal.
+ * We reply with iteration count, caller can test if iteration count
+ * increments monotonically */
+ RedisModule_Assert(beforeSleepCount == afterSleepCount);
+ RedisModule_ReplyWithLongLong(ctx, beforeSleepCount);
+ return REDISMODULE_OK;
+}
+
+void oneshotCallback(void* arg)
+{
+ RedisModule_Assert(strcmp(arg, "userdata") == 0);
+ RedisModule_ReplyWithSimpleString(reply_ctx, "OK");
+ RedisModule_FreeThreadSafeContext(reply_ctx);
+ RedisModule_UnblockClient(bc, NULL);
+}
+
+int oneshot(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ bc = RedisModule_BlockClient(ctx, NULL, NULL, NULL, 0);
+ reply_ctx = RedisModule_GetThreadSafeContext(bc);
+
+ if (RedisModule_EventLoopAddOneShot(oneshotCallback, "userdata") != REDISMODULE_OK) {
+ RedisModule_ReplyWithError(ctx, "ERR oneshot failed");
+ RedisModule_FreeThreadSafeContext(reply_ctx);
+ RedisModule_UnblockClient(bc, NULL);
+ }
+ return REDISMODULE_OK;
+}
+
+void eventloopCallback(struct RedisModuleCtx *ctx, RedisModuleEvent eid, uint64_t subevent, void *data) {
+ REDISMODULE_NOT_USED(ctx);
+ REDISMODULE_NOT_USED(eid);
+ REDISMODULE_NOT_USED(subevent);
+ REDISMODULE_NOT_USED(data);
+
+ RedisModule_Assert(eid.id == REDISMODULE_EVENT_EVENTLOOP);
+ if (subevent == REDISMODULE_SUBEVENT_EVENTLOOP_BEFORE_SLEEP)
+ beforeSleepCount++;
+ else if (subevent == REDISMODULE_SUBEVENT_EVENTLOOP_AFTER_SLEEP)
+ afterSleepCount++;
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ if (RedisModule_Init(ctx,"eventloop",1,REDISMODULE_APIVER_1)
+ == REDISMODULE_ERR) return REDISMODULE_ERR;
+
+ /* Test basics. */
+ if (RedisModule_CreateCommand(ctx, "test.sanity", sanity, "", 0, 0, 0)
+ == REDISMODULE_ERR) return REDISMODULE_ERR;
+
+ /* Register a command to create a pipe() and send data through it by using
+ * event loop API. */
+ if (RedisModule_CreateCommand(ctx, "test.sendbytes", sendbytes, "", 0, 0, 0)
+ == REDISMODULE_ERR) return REDISMODULE_ERR;
+
+ /* Register a command to return event loop iteration count. */
+ if (RedisModule_CreateCommand(ctx, "test.iteration", iteration, "", 0, 0, 0)
+ == REDISMODULE_ERR) return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "test.oneshot", oneshot, "", 0, 0, 0)
+ == REDISMODULE_ERR) return REDISMODULE_ERR;
+
+ if (RedisModule_SubscribeToServerEvent(ctx, RedisModuleEvent_EventLoop,
+ eventloopCallback) != REDISMODULE_OK) return REDISMODULE_ERR;
+
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/fork.c b/examples/redis-unstable/tests/modules/fork.c
new file mode 100644
index 0000000..d7a0d15
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/fork.c
@@ -0,0 +1,96 @@
+
+/* define macros for having usleep */
+#define _BSD_SOURCE
+#define _DEFAULT_SOURCE
+
+#include "redismodule.h"
+#include <string.h>
+#include <assert.h>
+#include <unistd.h>
+
+#define UNUSED(V) ((void) V)
+
+int child_pid = -1;
+int exitted_with_code = -1;
+
+void done_handler(int exitcode, int bysignal, void *user_data) {
+ child_pid = -1;
+ exitted_with_code = exitcode;
+ assert(user_data==(void*)0xdeadbeef);
+ UNUSED(bysignal);
+}
+
+int fork_create(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ long long code_to_exit_with;
+ long long usleep_us;
+ if (argc != 3) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ if(!RMAPI_FUNC_SUPPORTED(RedisModule_Fork)){
+ RedisModule_ReplyWithError(ctx, "Fork api is not supported in the current redis version");
+ return REDISMODULE_OK;
+ }
+
+ RedisModule_StringToLongLong(argv[1], &code_to_exit_with);
+ RedisModule_StringToLongLong(argv[2], &usleep_us);
+ exitted_with_code = -1;
+ int fork_child_pid = RedisModule_Fork(done_handler, (void*)0xdeadbeef);
+ if (fork_child_pid < 0) {
+ RedisModule_ReplyWithError(ctx, "Fork failed");
+ return REDISMODULE_OK;
+ } else if (fork_child_pid > 0) {
+ /* parent */
+ child_pid = fork_child_pid;
+ RedisModule_ReplyWithLongLong(ctx, child_pid);
+ return REDISMODULE_OK;
+ }
+
+ /* child */
+ RedisModule_Log(ctx, "notice", "fork child started");
+ usleep(usleep_us);
+ RedisModule_Log(ctx, "notice", "fork child exiting");
+ RedisModule_ExitFromChild(code_to_exit_with);
+ /* unreachable */
+ return 0;
+}
+
+int fork_exitcode(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ UNUSED(argv);
+ UNUSED(argc);
+ RedisModule_ReplyWithLongLong(ctx, exitted_with_code);
+ return REDISMODULE_OK;
+}
+
+int fork_kill(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ UNUSED(argv);
+ UNUSED(argc);
+ if (RedisModule_KillForkChild(child_pid) != REDISMODULE_OK)
+ RedisModule_ReplyWithError(ctx, "KillForkChild failed");
+ else
+ RedisModule_ReplyWithLongLong(ctx, 1);
+ child_pid = -1;
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ UNUSED(argv);
+ UNUSED(argc);
+ if (RedisModule_Init(ctx,"fork",1,REDISMODULE_APIVER_1)== REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"fork.create", fork_create,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"fork.exitcode", fork_exitcode,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"fork.kill", fork_kill,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/getchannels.c b/examples/redis-unstable/tests/modules/getchannels.c
new file mode 100644
index 0000000..330531d
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/getchannels.c
@@ -0,0 +1,69 @@
+#include "redismodule.h"
+#include <strings.h>
+#include <assert.h>
+#include <unistd.h>
+#include <errno.h>
+
+/* A sample with declarable channels, that are used to validate against ACLs */
+int getChannels_subscribe(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if ((argc - 1) % 3 != 0) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+ char *err = NULL;
+
+ /* getchannels.command [[subscribe|unsubscribe|publish] [pattern|literal] <channel> ...]
+ * This command marks the given channel is accessed based on the
+ * provided modifiers. */
+ for (int i = 1; i < argc; i += 3) {
+ const char *operation = RedisModule_StringPtrLen(argv[i], NULL);
+ const char *type = RedisModule_StringPtrLen(argv[i+1], NULL);
+ int flags = 0;
+
+ if (!strcasecmp(operation, "subscribe")) {
+ flags |= REDISMODULE_CMD_CHANNEL_SUBSCRIBE;
+ } else if (!strcasecmp(operation, "unsubscribe")) {
+ flags |= REDISMODULE_CMD_CHANNEL_UNSUBSCRIBE;
+ } else if (!strcasecmp(operation, "publish")) {
+ flags |= REDISMODULE_CMD_CHANNEL_PUBLISH;
+ } else {
+ err = "Invalid channel operation";
+ break;
+ }
+
+ if (!strcasecmp(type, "literal")) {
+ /* No op */
+ } else if (!strcasecmp(type, "pattern")) {
+ flags |= REDISMODULE_CMD_CHANNEL_PATTERN;
+ } else {
+ err = "Invalid channel type";
+ break;
+ }
+ if (RedisModule_IsChannelsPositionRequest(ctx)) {
+ RedisModule_ChannelAtPosWithFlags(ctx, i+2, flags);
+ }
+ }
+
+ if (!RedisModule_IsChannelsPositionRequest(ctx)) {
+ if (err) {
+ RedisModule_ReplyWithError(ctx, err);
+ } else {
+ /* Normal implementation would go here, but for tests just return okay */
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ }
+ }
+
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ if (RedisModule_Init(ctx, "getchannels", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "getchannels.command", getChannels_subscribe, "getchannels-api", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/getkeys.c b/examples/redis-unstable/tests/modules/getkeys.c
new file mode 100644
index 0000000..cee3b3e
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/getkeys.c
@@ -0,0 +1,178 @@
+
+#include "redismodule.h"
+#include <strings.h>
+#include <assert.h>
+#include <unistd.h>
+#include <errno.h>
+
+#define UNUSED(V) ((void) V)
+
+/* A sample movable keys command that returns a list of all
+ * arguments that follow a KEY argument, i.e.
+ */
+int getkeys_command(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ int i;
+ int count = 0;
+
+ /* Handle getkeys-api introspection */
+ if (RedisModule_IsKeysPositionRequest(ctx)) {
+ for (i = 0; i < argc; i++) {
+ size_t len;
+ const char *str = RedisModule_StringPtrLen(argv[i], &len);
+
+ if (len == 3 && !strncasecmp(str, "key", 3) && i + 1 < argc)
+ RedisModule_KeyAtPos(ctx, i + 1);
+ }
+
+ return REDISMODULE_OK;
+ }
+
+ /* Handle real command invocation */
+ RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_LEN);
+ for (i = 0; i < argc; i++) {
+ size_t len;
+ const char *str = RedisModule_StringPtrLen(argv[i], &len);
+
+ if (len == 3 && !strncasecmp(str, "key", 3) && i + 1 < argc) {
+ RedisModule_ReplyWithString(ctx, argv[i+1]);
+ count++;
+ }
+ }
+ RedisModule_ReplySetArrayLength(ctx, count);
+
+ return REDISMODULE_OK;
+}
+
+int getkeys_command_with_flags(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ int i;
+ int count = 0;
+
+ /* Handle getkeys-api introspection */
+ if (RedisModule_IsKeysPositionRequest(ctx)) {
+ for (i = 0; i < argc; i++) {
+ size_t len;
+ const char *str = RedisModule_StringPtrLen(argv[i], &len);
+
+ if (len == 3 && !strncasecmp(str, "key", 3) && i + 1 < argc)
+ RedisModule_KeyAtPosWithFlags(ctx, i + 1, REDISMODULE_CMD_KEY_RO | REDISMODULE_CMD_KEY_ACCESS);
+ }
+
+ return REDISMODULE_OK;
+ }
+
+ /* Handle real command invocation */
+ RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_LEN);
+ for (i = 0; i < argc; i++) {
+ size_t len;
+ const char *str = RedisModule_StringPtrLen(argv[i], &len);
+
+ if (len == 3 && !strncasecmp(str, "key", 3) && i + 1 < argc) {
+ RedisModule_ReplyWithString(ctx, argv[i+1]);
+ count++;
+ }
+ }
+ RedisModule_ReplySetArrayLength(ctx, count);
+
+ return REDISMODULE_OK;
+}
+
+int getkeys_fixed(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ int i;
+
+ RedisModule_ReplyWithArray(ctx, argc - 1);
+ for (i = 1; i < argc; i++) {
+ RedisModule_ReplyWithString(ctx, argv[i]);
+ }
+ return REDISMODULE_OK;
+}
+
+/* Introspect a command using RM_GetCommandKeys() and returns the list
+ * of keys. Essentially this is COMMAND GETKEYS implemented in a module.
+ * INTROSPECT <with-flags> <cmd> <args>
+ */
+int getkeys_introspect(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ long long with_flags = 0;
+
+ if (argc < 4) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ if (RedisModule_StringToLongLong(argv[1],&with_flags) != REDISMODULE_OK)
+ return RedisModule_ReplyWithError(ctx,"ERR invalid integer");
+
+ int num_keys, *keyflags = NULL;
+ int *keyidx = RedisModule_GetCommandKeysWithFlags(ctx, &argv[2], argc - 2, &num_keys, with_flags ? &keyflags : NULL);
+
+ if (!keyidx) {
+ if (!errno)
+ RedisModule_ReplyWithEmptyArray(ctx);
+ else {
+ char err[100];
+ switch (errno) {
+ case ENOENT:
+ RedisModule_ReplyWithError(ctx, "ERR ENOENT");
+ break;
+ case EINVAL:
+ RedisModule_ReplyWithError(ctx, "ERR EINVAL");
+ break;
+ default:
+ snprintf(err, sizeof(err) - 1, "ERR errno=%d", errno);
+ RedisModule_ReplyWithError(ctx, err);
+ break;
+ }
+ }
+ } else {
+ int i;
+
+ RedisModule_ReplyWithArray(ctx, num_keys);
+ for (i = 0; i < num_keys; i++) {
+ if (!with_flags) {
+ RedisModule_ReplyWithString(ctx, argv[2 + keyidx[i]]);
+ continue;
+ }
+ RedisModule_ReplyWithArray(ctx, 2);
+ RedisModule_ReplyWithString(ctx, argv[2 + keyidx[i]]);
+ char* sflags = "";
+ if (keyflags[i] & REDISMODULE_CMD_KEY_RO)
+ sflags = "RO";
+ else if (keyflags[i] & REDISMODULE_CMD_KEY_RW)
+ sflags = "RW";
+ else if (keyflags[i] & REDISMODULE_CMD_KEY_OW)
+ sflags = "OW";
+ else if (keyflags[i] & REDISMODULE_CMD_KEY_RM)
+ sflags = "RM";
+ RedisModule_ReplyWithCString(ctx, sflags);
+ }
+
+ RedisModule_Free(keyidx);
+ RedisModule_Free(keyflags);
+ }
+
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ UNUSED(argv);
+ UNUSED(argc);
+ if (RedisModule_Init(ctx,"getkeys",1,REDISMODULE_APIVER_1)== REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"getkeys.command", getkeys_command,"getkeys-api",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"getkeys.command_with_flags", getkeys_command_with_flags,"getkeys-api",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"getkeys.fixed", getkeys_fixed,"",2,4,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"getkeys.introspect", getkeys_introspect,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/hash.c b/examples/redis-unstable/tests/modules/hash.c
new file mode 100644
index 0000000..462c21e
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/hash.c
@@ -0,0 +1,244 @@
+#include "redismodule.h"
+#include <strings.h>
+#include <errno.h>
+#include <stdlib.h>
+
+#define UNUSED(x) (void)(x)
+
+/* If a string is ":deleted:", the special value for deleted hash fields is
+ * returned; otherwise the input string is returned. */
+static RedisModuleString *value_or_delete(RedisModuleString *s) {
+ if (!strcasecmp(RedisModule_StringPtrLen(s, NULL), ":delete:"))
+ return REDISMODULE_HASH_DELETE;
+ else
+ return s;
+}
+
+/* HASH.SET key flags field1 value1 [field2 value2 ..]
+ *
+ * Sets 1-4 fields. Returns the same as RedisModule_HashSet().
+ * Flags is a string of "nxa" where n = NX, x = XX, a = COUNT_ALL.
+ * To delete a field, use the value ":delete:".
+ */
+int hash_set(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc < 5 || argc % 2 == 0 || argc > 11)
+ return RedisModule_WrongArity(ctx);
+
+ RedisModule_AutoMemory(ctx);
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE);
+
+ size_t flags_len;
+ const char *flags_str = RedisModule_StringPtrLen(argv[2], &flags_len);
+ int flags = REDISMODULE_HASH_NONE;
+ for (size_t i = 0; i < flags_len; i++) {
+ switch (flags_str[i]) {
+ case 'n': flags |= REDISMODULE_HASH_NX; break;
+ case 'x': flags |= REDISMODULE_HASH_XX; break;
+ case 'a': flags |= REDISMODULE_HASH_COUNT_ALL; break;
+ }
+ }
+
+ /* Test some varargs. (In real-world, use a loop and set one at a time.) */
+ int result;
+ errno = 0;
+ if (argc == 5) {
+ result = RedisModule_HashSet(key, flags,
+ argv[3], value_or_delete(argv[4]),
+ NULL);
+ } else if (argc == 7) {
+ result = RedisModule_HashSet(key, flags,
+ argv[3], value_or_delete(argv[4]),
+ argv[5], value_or_delete(argv[6]),
+ NULL);
+ } else if (argc == 9) {
+ result = RedisModule_HashSet(key, flags,
+ argv[3], value_or_delete(argv[4]),
+ argv[5], value_or_delete(argv[6]),
+ argv[7], value_or_delete(argv[8]),
+ NULL);
+ } else if (argc == 11) {
+ result = RedisModule_HashSet(key, flags,
+ argv[3], value_or_delete(argv[4]),
+ argv[5], value_or_delete(argv[6]),
+ argv[7], value_or_delete(argv[8]),
+ argv[9], value_or_delete(argv[10]),
+ NULL);
+ } else {
+ return RedisModule_ReplyWithError(ctx, "ERR too many fields");
+ }
+
+ /* Check errno */
+ if (result == 0) {
+ if (errno == ENOTSUP)
+ return RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE);
+ else
+ RedisModule_Assert(errno == ENOENT);
+ }
+
+ return RedisModule_ReplyWithLongLong(ctx, result);
+}
+
+RedisModuleKey* openKeyWithMode(RedisModuleCtx *ctx, RedisModuleString *keyName, int mode) {
+ int supportedMode = RedisModule_GetOpenKeyModesAll();
+ if (!(supportedMode & REDISMODULE_READ) || ((supportedMode & mode)!=mode)) {
+ RedisModule_ReplyWithError(ctx, "OpenKey mode is not supported");
+ return NULL;
+ }
+
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, keyName, REDISMODULE_READ | mode);
+ if (!key) {
+ RedisModule_ReplyWithError(ctx, "key not found");
+ return NULL;
+ }
+
+ return key;
+}
+
+int test_open_key_subexpired_hget(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc<3) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ RedisModuleKey *key = openKeyWithMode(ctx, argv[1], REDISMODULE_OPEN_KEY_ACCESS_EXPIRED);
+ if (!key) return REDISMODULE_OK;
+
+ RedisModuleString *value;
+ RedisModule_HashGet(key,REDISMODULE_HASH_NONE,argv[2],&value,NULL);
+
+ /* return the value */
+ if (value) {
+ RedisModule_ReplyWithString(ctx, value);
+ RedisModule_FreeString(ctx, value);
+ } else {
+ RedisModule_ReplyWithNull(ctx);
+ }
+ RedisModule_CloseKey(key);
+ return REDISMODULE_OK;
+}
+
+int test_open_key_hget_expire(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc<3) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ RedisModuleKey *key = openKeyWithMode(ctx, argv[1], REDISMODULE_OPEN_KEY_ACCESS_EXPIRED);
+ if (!key) return REDISMODULE_OK;
+
+ mstime_t expireAt;
+
+ /* Let's test here that we get error if using invalid flags combination */
+ RedisModule_Assert(
+ RedisModule_HashGet(key,
+ REDISMODULE_HASH_EXISTS |
+ REDISMODULE_HASH_EXPIRE_TIME,
+ argv[2], &expireAt, NULL) == REDISMODULE_ERR);
+
+ /* Now let's get the expire time */
+ RedisModule_HashGet(key, REDISMODULE_HASH_EXPIRE_TIME,argv[2],&expireAt,NULL);
+ RedisModule_ReplyWithLongLong(ctx, expireAt);
+ RedisModule_CloseKey(key);
+ return REDISMODULE_OK;
+}
+
+/* Test variadic function to get two expiration times */
+int test_open_key_hget_two_expire(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc<3) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ RedisModuleKey *key = openKeyWithMode(ctx, argv[1], REDISMODULE_OPEN_KEY_ACCESS_EXPIRED);
+ if (!key) return REDISMODULE_OK;
+
+ mstime_t expireAt1, expireAt2;
+ RedisModule_HashGet(key,REDISMODULE_HASH_EXPIRE_TIME,argv[2],&expireAt1,argv[3],&expireAt2,NULL);
+
+ /* return the two expire time */
+ RedisModule_ReplyWithArray(ctx, 2);
+ RedisModule_ReplyWithLongLong(ctx, expireAt1);
+ RedisModule_ReplyWithLongLong(ctx, expireAt2);
+ RedisModule_CloseKey(key);
+ return REDISMODULE_OK;
+}
+
+int test_open_key_hget_min_expire(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc!=2) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ RedisModuleKey *key = openKeyWithMode(ctx, argv[1], REDISMODULE_READ);
+ if (!key) return REDISMODULE_OK;
+
+ volatile mstime_t minExpire = RedisModule_HashFieldMinExpire(key);
+ RedisModule_ReplyWithLongLong(ctx, minExpire);
+ RedisModule_CloseKey(key);
+ return REDISMODULE_OK;
+}
+
+int numReplies;
+void ScanCallback(RedisModuleKey *key, RedisModuleString *field, RedisModuleString *value, void *privdata) {
+ UNUSED(key);
+ RedisModuleCtx *ctx = (RedisModuleCtx *)privdata;
+
+ /* Reply with the field and value (or NULL for sets) */
+ RedisModule_ReplyWithString(ctx, field);
+ if (value) {
+ RedisModule_ReplyWithString(ctx, value);
+ } else {
+ RedisModule_ReplyWithCString(ctx, "(null)");
+ }
+ numReplies+=2;
+}
+
+int test_open_key_access_expired_hscan(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc < 2) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ RedisModuleKey *key = openKeyWithMode(ctx, argv[1], REDISMODULE_OPEN_KEY_ACCESS_EXPIRED);
+
+ if (!key)
+ return RedisModule_ReplyWithError(ctx, "ERR key not exists");
+
+ /* Verify it is a hash */
+ if (RedisModule_KeyType(key) != REDISMODULE_KEYTYPE_HASH) {
+ RedisModule_CloseKey(key);
+ return RedisModule_ReplyWithError(ctx, "ERR key is not a hash");
+ }
+
+ /* Scan the hash and reply pairs of key-value */
+ RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_ARRAY_LEN);
+ numReplies = 0;
+ RedisModuleScanCursor *cursor = RedisModule_ScanCursorCreate();
+ while (RedisModule_ScanKey(key, cursor, ScanCallback, ctx));
+ RedisModule_ScanCursorDestroy(cursor);
+ RedisModule_CloseKey(key);
+ RedisModule_ReplySetArrayLength(ctx, numReplies);
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ if (RedisModule_Init(ctx, "hash", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "hash.set", hash_set, "write", 1, 1, 1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx, "hash.hget_expired", test_open_key_subexpired_hget,"", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx, "hash.hscan_expired", test_open_key_access_expired_hscan,"", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx, "hash.hget_expire", test_open_key_hget_expire,"", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx, "hash.hget_two_expire", test_open_key_hget_two_expire,"", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx, "hash.hget_min_expire", test_open_key_hget_min_expire,"", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/hooks.c b/examples/redis-unstable/tests/modules/hooks.c
new file mode 100644
index 0000000..59c8492
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/hooks.c
@@ -0,0 +1,496 @@
+/* This module is used to test the server events hooks API.
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (c) 2019-Present, Redis Ltd.
+ * All rights reserved.
+ *
+ * Licensed under your choice of (a) the Redis Source Available License 2.0
+ * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
+ * GNU Affero General Public License v3 (AGPLv3).
+ */
+
+#include "redismodule.h"
+#include <stdio.h>
+#include <string.h>
+#include <strings.h>
+#include <assert.h>
+
+/* We need to store events to be able to test and see what we got, and we can't
+ * store them in the key-space since that would mess up rdb loading (duplicates)
+ * and be lost of flushdb. */
+RedisModuleDict *event_log = NULL;
+/* stores all the keys on which we got 'removed' event */
+RedisModuleDict *removed_event_log = NULL;
+/* stores all the subevent on which we got 'removed' event */
+RedisModuleDict *removed_subevent_type = NULL;
+/* stores all the keys on which we got 'removed' event with expiry information */
+RedisModuleDict *removed_expiry_log = NULL;
+
+typedef struct EventElement {
+ long count;
+ RedisModuleString *last_val_string;
+ long last_val_int;
+} EventElement;
+
+void LogStringEvent(RedisModuleCtx *ctx, const char* keyname, const char* data) {
+ EventElement *event = RedisModule_DictGetC(event_log, (void*)keyname, strlen(keyname), NULL);
+ if (!event) {
+ event = RedisModule_Alloc(sizeof(EventElement));
+ memset(event, 0, sizeof(EventElement));
+ RedisModule_DictSetC(event_log, (void*)keyname, strlen(keyname), event);
+ }
+ if (event->last_val_string) RedisModule_FreeString(ctx, event->last_val_string);
+ event->last_val_string = RedisModule_CreateString(ctx, data, strlen(data));
+ event->count++;
+}
+
+void LogNumericEvent(RedisModuleCtx *ctx, const char* keyname, long data) {
+ REDISMODULE_NOT_USED(ctx);
+ EventElement *event = RedisModule_DictGetC(event_log, (void*)keyname, strlen(keyname), NULL);
+ if (!event) {
+ event = RedisModule_Alloc(sizeof(EventElement));
+ memset(event, 0, sizeof(EventElement));
+ RedisModule_DictSetC(event_log, (void*)keyname, strlen(keyname), event);
+ }
+ event->last_val_int = data;
+ event->count++;
+}
+
+void FreeEvent(RedisModuleCtx *ctx, EventElement *event) {
+ if (event->last_val_string)
+ RedisModule_FreeString(ctx, event->last_val_string);
+ RedisModule_Free(event);
+}
+
+int cmdEventCount(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ if (argc != 2){
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ EventElement *event = RedisModule_DictGet(event_log, argv[1], NULL);
+ RedisModule_ReplyWithLongLong(ctx, event? event->count: 0);
+ return REDISMODULE_OK;
+}
+
+int cmdEventLast(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ if (argc != 2){
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ EventElement *event = RedisModule_DictGet(event_log, argv[1], NULL);
+ if (event && event->last_val_string)
+ RedisModule_ReplyWithString(ctx, event->last_val_string);
+ else if (event)
+ RedisModule_ReplyWithLongLong(ctx, event->last_val_int);
+ else
+ RedisModule_ReplyWithNull(ctx);
+ return REDISMODULE_OK;
+}
+
+void clearEvents(RedisModuleCtx *ctx)
+{
+ RedisModuleString *key;
+ EventElement *event;
+ RedisModuleDictIter *iter = RedisModule_DictIteratorStart(event_log, "^", NULL);
+ while((key = RedisModule_DictNext(ctx, iter, (void**)&event)) != NULL) {
+ event->count = 0;
+ event->last_val_int = 0;
+ if (event->last_val_string) RedisModule_FreeString(ctx, event->last_val_string);
+ event->last_val_string = NULL;
+ RedisModule_DictDel(event_log, key, NULL);
+ RedisModule_Free(event);
+ }
+ RedisModule_DictIteratorStop(iter);
+}
+
+int cmdEventsClear(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ REDISMODULE_NOT_USED(argc);
+ REDISMODULE_NOT_USED(argv);
+ clearEvents(ctx);
+ return REDISMODULE_OK;
+}
+
+/* Client state change callback. */
+void clientChangeCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data)
+{
+ REDISMODULE_NOT_USED(e);
+
+ RedisModuleClientInfo *ci = data;
+ char *keyname = (sub == REDISMODULE_SUBEVENT_CLIENT_CHANGE_CONNECTED) ?
+ "client-connected" : "client-disconnected";
+ LogNumericEvent(ctx, keyname, ci->id);
+}
+
+void flushdbCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data)
+{
+ REDISMODULE_NOT_USED(e);
+
+ RedisModuleFlushInfo *fi = data;
+ char *keyname = (sub == REDISMODULE_SUBEVENT_FLUSHDB_START) ?
+ "flush-start" : "flush-end";
+ LogNumericEvent(ctx, keyname, fi->dbnum);
+}
+
+void roleChangeCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data)
+{
+ REDISMODULE_NOT_USED(e);
+ REDISMODULE_NOT_USED(data);
+
+ RedisModuleReplicationInfo *ri = data;
+ char *keyname = (sub == REDISMODULE_EVENT_REPLROLECHANGED_NOW_MASTER) ?
+ "role-master" : "role-replica";
+ LogStringEvent(ctx, keyname, ri->masterhost);
+}
+
+void replicationChangeCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data)
+{
+ REDISMODULE_NOT_USED(e);
+ REDISMODULE_NOT_USED(data);
+
+ char *keyname = (sub == REDISMODULE_SUBEVENT_REPLICA_CHANGE_ONLINE) ?
+ "replica-online" : "replica-offline";
+ LogNumericEvent(ctx, keyname, 0);
+}
+
+void rasterLinkChangeCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data)
+{
+ REDISMODULE_NOT_USED(e);
+ REDISMODULE_NOT_USED(data);
+
+ char *keyname = (sub == REDISMODULE_SUBEVENT_MASTER_LINK_UP) ?
+ "masterlink-up" : "masterlink-down";
+ LogNumericEvent(ctx, keyname, 0);
+}
+
+void persistenceCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data)
+{
+ REDISMODULE_NOT_USED(e);
+ REDISMODULE_NOT_USED(data);
+
+ char *keyname = NULL;
+ switch (sub) {
+ case REDISMODULE_SUBEVENT_PERSISTENCE_RDB_START: keyname = "persistence-rdb-start"; break;
+ case REDISMODULE_SUBEVENT_PERSISTENCE_AOF_START: keyname = "persistence-aof-start"; break;
+ case REDISMODULE_SUBEVENT_PERSISTENCE_SYNC_AOF_START: keyname = "persistence-syncaof-start"; break;
+ case REDISMODULE_SUBEVENT_PERSISTENCE_SYNC_RDB_START: keyname = "persistence-syncrdb-start"; break;
+ case REDISMODULE_SUBEVENT_PERSISTENCE_ENDED: keyname = "persistence-end"; break;
+ case REDISMODULE_SUBEVENT_PERSISTENCE_FAILED: keyname = "persistence-failed"; break;
+ }
+ /* modifying the keyspace from the fork child is not an option, using log instead */
+ RedisModule_Log(ctx, "warning", "module-event-%s", keyname);
+ if (sub == REDISMODULE_SUBEVENT_PERSISTENCE_SYNC_RDB_START ||
+ sub == REDISMODULE_SUBEVENT_PERSISTENCE_SYNC_AOF_START)
+ {
+ LogNumericEvent(ctx, keyname, 0);
+ }
+}
+
+void loadingCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data)
+{
+ REDISMODULE_NOT_USED(e);
+ REDISMODULE_NOT_USED(data);
+
+ char *keyname = NULL;
+ switch (sub) {
+ case REDISMODULE_SUBEVENT_LOADING_RDB_START: keyname = "loading-rdb-start"; break;
+ case REDISMODULE_SUBEVENT_LOADING_AOF_START: keyname = "loading-aof-start"; break;
+ case REDISMODULE_SUBEVENT_LOADING_REPL_START: keyname = "loading-repl-start"; break;
+ case REDISMODULE_SUBEVENT_LOADING_ENDED: keyname = "loading-end"; break;
+ case REDISMODULE_SUBEVENT_LOADING_FAILED: keyname = "loading-failed"; break;
+ }
+ LogNumericEvent(ctx, keyname, 0);
+}
+
+void loadingProgressCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data)
+{
+ REDISMODULE_NOT_USED(e);
+
+ RedisModuleLoadingProgress *ei = data;
+ char *keyname = (sub == REDISMODULE_SUBEVENT_LOADING_PROGRESS_RDB) ?
+ "loading-progress-rdb" : "loading-progress-aof";
+ LogNumericEvent(ctx, keyname, ei->progress);
+}
+
+void shutdownCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data)
+{
+ REDISMODULE_NOT_USED(e);
+ REDISMODULE_NOT_USED(data);
+ REDISMODULE_NOT_USED(sub);
+
+ RedisModule_Log(ctx, "warning", "module-event-%s", "shutdown");
+}
+
+void cronLoopCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data)
+{
+ REDISMODULE_NOT_USED(e);
+ REDISMODULE_NOT_USED(sub);
+
+ RedisModuleCronLoop *ei = data;
+ LogNumericEvent(ctx, "cron-loop", ei->hz);
+}
+
+void moduleChangeCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data)
+{
+ REDISMODULE_NOT_USED(e);
+
+ RedisModuleModuleChange *ei = data;
+ char *keyname = (sub == REDISMODULE_SUBEVENT_MODULE_LOADED) ?
+ "module-loaded" : "module-unloaded";
+ LogStringEvent(ctx, keyname, ei->module_name);
+}
+
+void swapDbCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data)
+{
+ REDISMODULE_NOT_USED(e);
+ REDISMODULE_NOT_USED(sub);
+
+ RedisModuleSwapDbInfo *ei = data;
+ LogNumericEvent(ctx, "swapdb-first", ei->dbnum_first);
+ LogNumericEvent(ctx, "swapdb-second", ei->dbnum_second);
+}
+
+void configChangeCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data)
+{
+ REDISMODULE_NOT_USED(e);
+ if (sub != REDISMODULE_SUBEVENT_CONFIG_CHANGE) {
+ return;
+ }
+
+ RedisModuleConfigChangeV1 *ei = data;
+ LogNumericEvent(ctx, "config-change-count", ei->num_changes);
+ LogStringEvent(ctx, "config-change-first", ei->config_names[0]);
+}
+
+void keyInfoCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data)
+{
+ REDISMODULE_NOT_USED(e);
+
+ RedisModuleKeyInfoV1 *ei = data;
+ RedisModuleKey *kp = ei->key;
+ RedisModuleString *key = (RedisModuleString *) RedisModule_GetKeyNameFromModuleKey(kp);
+ const char *keyname = RedisModule_StringPtrLen(key, NULL);
+ RedisModuleString *event_keyname = RedisModule_CreateStringPrintf(ctx, "key-info-%s", keyname);
+ LogStringEvent(ctx, RedisModule_StringPtrLen(event_keyname, NULL), keyname);
+ RedisModule_FreeString(ctx, event_keyname);
+
+ /* Despite getting a key object from the callback, we also try to re-open it
+ * to make sure the callback is called before it is actually removed from the keyspace. */
+ RedisModuleKey *kp_open = RedisModule_OpenKey(ctx, key, REDISMODULE_READ);
+ assert(RedisModule_ValueLength(kp) == RedisModule_ValueLength(kp_open));
+ RedisModule_CloseKey(kp_open);
+
+ /* We also try to RM_Call a command that accesses that key, also to make sure it's still in the keyspace. */
+ char *size_command = NULL;
+ int key_type = RedisModule_KeyType(kp);
+ if (key_type == REDISMODULE_KEYTYPE_STRING) {
+ size_command = "STRLEN";
+ } else if (key_type == REDISMODULE_KEYTYPE_LIST) {
+ size_command = "LLEN";
+ } else if (key_type == REDISMODULE_KEYTYPE_HASH) {
+ size_command = "HLEN";
+ } else if (key_type == REDISMODULE_KEYTYPE_SET) {
+ size_command = "SCARD";
+ } else if (key_type == REDISMODULE_KEYTYPE_ZSET) {
+ size_command = "ZCARD";
+ } else if (key_type == REDISMODULE_KEYTYPE_STREAM) {
+ size_command = "XLEN";
+ }
+ if (size_command != NULL) {
+ RedisModuleCallReply *reply = RedisModule_Call(ctx, size_command, "s", key);
+ assert(reply != NULL);
+ assert(RedisModule_ValueLength(kp) == (size_t) RedisModule_CallReplyInteger(reply));
+ RedisModule_FreeCallReply(reply);
+ }
+
+ /* Now use the key object we got from the callback for various validations. */
+ RedisModuleString *prev = RedisModule_DictGetC(removed_event_log, (void*)keyname, strlen(keyname), NULL);
+ /* We keep object length */
+ RedisModuleString *v = RedisModule_CreateStringPrintf(ctx, "%zd", RedisModule_ValueLength(kp));
+ /* For string type, we keep value instead of length */
+ if (RedisModule_KeyType(kp) == REDISMODULE_KEYTYPE_STRING) {
+ RedisModule_FreeString(ctx, v);
+ size_t len;
+ /* We need to access the string value with RedisModule_StringDMA.
+ * RedisModule_StringDMA may call dbUnshareStringValue to free the origin object,
+ * so we also can test it. */
+ char *s = RedisModule_StringDMA(kp, &len, REDISMODULE_READ);
+ v = RedisModule_CreateString(ctx, s, len);
+ }
+ RedisModule_DictReplaceC(removed_event_log, (void*)keyname, strlen(keyname), v);
+ if (prev != NULL) {
+ RedisModule_FreeString(ctx, prev);
+ }
+
+ const char *subevent = "deleted";
+ if (sub == REDISMODULE_SUBEVENT_KEY_EXPIRED) {
+ subevent = "expired";
+ } else if (sub == REDISMODULE_SUBEVENT_KEY_EVICTED) {
+ subevent = "evicted";
+ } else if (sub == REDISMODULE_SUBEVENT_KEY_OVERWRITTEN) {
+ subevent = "overwritten";
+ }
+ RedisModule_DictReplaceC(removed_subevent_type, (void*)keyname, strlen(keyname), (void *)subevent);
+
+ RedisModuleString *prevexpire = RedisModule_DictGetC(removed_expiry_log, (void*)keyname, strlen(keyname), NULL);
+ RedisModuleString *expire = RedisModule_CreateStringPrintf(ctx, "%lld", RedisModule_GetAbsExpire(kp));
+ RedisModule_DictReplaceC(removed_expiry_log, (void*)keyname, strlen(keyname), (void *)expire);
+ if (prevexpire != NULL) {
+ RedisModule_FreeString(ctx, prevexpire);
+ }
+}
+
+static int cmdIsKeyRemoved(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){
+ if(argc != 2){
+ return RedisModule_WrongArity(ctx);
+ }
+
+ const char *key = RedisModule_StringPtrLen(argv[1], NULL);
+
+ RedisModuleString *value = RedisModule_DictGetC(removed_event_log, (void*)key, strlen(key), NULL);
+
+ if (value == NULL) {
+ return RedisModule_ReplyWithError(ctx, "ERR Key was not removed");
+ }
+
+ const char *subevent = RedisModule_DictGetC(removed_subevent_type, (void*)key, strlen(key), NULL);
+ RedisModule_ReplyWithArray(ctx, 2);
+ RedisModule_ReplyWithString(ctx, value);
+ RedisModule_ReplyWithSimpleString(ctx, subevent);
+
+ return REDISMODULE_OK;
+}
+
+static int cmdKeyExpiry(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){
+ if(argc != 2){
+ return RedisModule_WrongArity(ctx);
+ }
+
+ const char* key = RedisModule_StringPtrLen(argv[1], NULL);
+ RedisModuleString *expire = RedisModule_DictGetC(removed_expiry_log, (void*)key, strlen(key), NULL);
+ if (expire == NULL) {
+ return RedisModule_ReplyWithError(ctx, "ERR Key was not removed");
+ }
+ RedisModule_ReplyWithString(ctx, expire);
+ return REDISMODULE_OK;
+}
+
+/* This function must be present on each Redis module. It is used in order to
+ * register the commands into the Redis server. */
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+#define VerifySubEventSupported(e, s) \
+ if (!RedisModule_IsSubEventSupported(e, s)) { \
+ return REDISMODULE_ERR; \
+ }
+
+ if (RedisModule_Init(ctx,"testhook",1,REDISMODULE_APIVER_1)
+ == REDISMODULE_ERR) return REDISMODULE_ERR;
+
+ /* Example on how to check if a server sub event is supported */
+ if (!RedisModule_IsSubEventSupported(RedisModuleEvent_ReplicationRoleChanged, REDISMODULE_EVENT_REPLROLECHANGED_NOW_MASTER)) {
+ return REDISMODULE_ERR;
+ }
+
+ /* replication related hooks */
+ RedisModule_SubscribeToServerEvent(ctx,
+ RedisModuleEvent_ReplicationRoleChanged, roleChangeCallback);
+ RedisModule_SubscribeToServerEvent(ctx,
+ RedisModuleEvent_ReplicaChange, replicationChangeCallback);
+ RedisModule_SubscribeToServerEvent(ctx,
+ RedisModuleEvent_MasterLinkChange, rasterLinkChangeCallback);
+
+ /* persistence related hooks */
+ RedisModule_SubscribeToServerEvent(ctx,
+ RedisModuleEvent_Persistence, persistenceCallback);
+ RedisModule_SubscribeToServerEvent(ctx,
+ RedisModuleEvent_Loading, loadingCallback);
+ RedisModule_SubscribeToServerEvent(ctx,
+ RedisModuleEvent_LoadingProgress, loadingProgressCallback);
+
+ /* other hooks */
+ RedisModule_SubscribeToServerEvent(ctx,
+ RedisModuleEvent_ClientChange, clientChangeCallback);
+ RedisModule_SubscribeToServerEvent(ctx,
+ RedisModuleEvent_FlushDB, flushdbCallback);
+ RedisModule_SubscribeToServerEvent(ctx,
+ RedisModuleEvent_Shutdown, shutdownCallback);
+ RedisModule_SubscribeToServerEvent(ctx,
+ RedisModuleEvent_CronLoop, cronLoopCallback);
+
+ RedisModule_SubscribeToServerEvent(ctx,
+ RedisModuleEvent_ModuleChange, moduleChangeCallback);
+ RedisModule_SubscribeToServerEvent(ctx,
+ RedisModuleEvent_SwapDB, swapDbCallback);
+
+ RedisModule_SubscribeToServerEvent(ctx,
+ RedisModuleEvent_Config, configChangeCallback);
+
+ RedisModule_SubscribeToServerEvent(ctx,
+ RedisModuleEvent_Key, keyInfoCallback);
+
+ event_log = RedisModule_CreateDict(ctx);
+ removed_event_log = RedisModule_CreateDict(ctx);
+ removed_subevent_type = RedisModule_CreateDict(ctx);
+ removed_expiry_log = RedisModule_CreateDict(ctx);
+
+ if (RedisModule_CreateCommand(ctx,"hooks.event_count", cmdEventCount,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"hooks.event_last", cmdEventLast,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"hooks.clear", cmdEventsClear,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"hooks.is_key_removed", cmdIsKeyRemoved,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"hooks.pexpireat", cmdKeyExpiry,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (argc == 1) {
+ const char *ptr = RedisModule_StringPtrLen(argv[0], NULL);
+ if (!strcasecmp(ptr, "noload")) {
+ /* This is a hint that we return ERR at the last moment of OnLoad. */
+ RedisModule_FreeDict(ctx, event_log);
+ RedisModule_FreeDict(ctx, removed_event_log);
+ RedisModule_FreeDict(ctx, removed_subevent_type);
+ RedisModule_FreeDict(ctx, removed_expiry_log);
+ return REDISMODULE_ERR;
+ }
+ }
+
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnUnload(RedisModuleCtx *ctx) {
+ clearEvents(ctx);
+ RedisModule_FreeDict(ctx, event_log);
+ event_log = NULL;
+
+ RedisModuleDictIter *iter = RedisModule_DictIteratorStartC(removed_event_log, "^", NULL, 0);
+ char* key;
+ size_t keyLen;
+ RedisModuleString* val;
+ while((key = RedisModule_DictNextC(iter, &keyLen, (void**)&val))){
+ RedisModule_FreeString(ctx, val);
+ }
+ RedisModule_FreeDict(ctx, removed_event_log);
+ RedisModule_DictIteratorStop(iter);
+ removed_event_log = NULL;
+
+ RedisModule_FreeDict(ctx, removed_subevent_type);
+ removed_subevent_type = NULL;
+
+ iter = RedisModule_DictIteratorStartC(removed_expiry_log, "^", NULL, 0);
+ while((key = RedisModule_DictNextC(iter, &keyLen, (void**)&val))){
+ RedisModule_FreeString(ctx, val);
+ }
+ RedisModule_FreeDict(ctx, removed_expiry_log);
+ RedisModule_DictIteratorStop(iter);
+ removed_expiry_log = NULL;
+
+ return REDISMODULE_OK;
+}
+
diff --git a/examples/redis-unstable/tests/modules/infotest.c b/examples/redis-unstable/tests/modules/infotest.c
new file mode 100644
index 0000000..b93a0c4
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/infotest.c
@@ -0,0 +1,119 @@
+#include "redismodule.h"
+
+#include <string.h>
+
+void InfoFunc(RedisModuleInfoCtx *ctx, int for_crash_report) {
+ RedisModule_InfoAddSection(ctx, "");
+ RedisModule_InfoAddFieldLongLong(ctx, "global", -2);
+ RedisModule_InfoAddFieldULongLong(ctx, "uglobal", (unsigned long long)-2);
+
+ RedisModule_InfoAddSection(ctx, "Spanish");
+ RedisModule_InfoAddFieldCString(ctx, "uno", "one");
+ RedisModule_InfoAddFieldLongLong(ctx, "dos", 2);
+
+ RedisModule_InfoAddSection(ctx, "Italian");
+ RedisModule_InfoAddFieldLongLong(ctx, "due", 2);
+ RedisModule_InfoAddFieldDouble(ctx, "tre", 3.3);
+
+ RedisModule_InfoAddSection(ctx, "keyspace");
+ RedisModule_InfoBeginDictField(ctx, "db0");
+ RedisModule_InfoAddFieldLongLong(ctx, "keys", 3);
+ RedisModule_InfoAddFieldLongLong(ctx, "expires", 1);
+ RedisModule_InfoEndDictField(ctx);
+
+ RedisModule_InfoAddSection(ctx, "unsafe");
+ RedisModule_InfoBeginDictField(ctx, "unsafe:field");
+ RedisModule_InfoAddFieldLongLong(ctx, "value", 1);
+ RedisModule_InfoEndDictField(ctx);
+
+ if (for_crash_report) {
+ RedisModule_InfoAddSection(ctx, "Klingon");
+ RedisModule_InfoAddFieldCString(ctx, "one", "wa'");
+ RedisModule_InfoAddFieldCString(ctx, "two", "cha'");
+ RedisModule_InfoAddFieldCString(ctx, "three", "wej");
+ }
+
+}
+
+int info_get(RedisModuleCtx *ctx, RedisModuleString **argv, int argc, char field_type)
+{
+ if (argc != 3 && argc != 4) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+ int err = REDISMODULE_OK;
+ const char *section, *field;
+ section = RedisModule_StringPtrLen(argv[1], NULL);
+ field = RedisModule_StringPtrLen(argv[2], NULL);
+ RedisModuleServerInfoData *info = RedisModule_GetServerInfo(ctx, section);
+ if (field_type=='i') {
+ long long ll = RedisModule_ServerInfoGetFieldSigned(info, field, &err);
+ if (err==REDISMODULE_OK)
+ RedisModule_ReplyWithLongLong(ctx, ll);
+ } else if (field_type=='u') {
+ unsigned long long ll = (unsigned long long)RedisModule_ServerInfoGetFieldUnsigned(info, field, &err);
+ if (err==REDISMODULE_OK)
+ RedisModule_ReplyWithLongLong(ctx, ll);
+ } else if (field_type=='d') {
+ double d = RedisModule_ServerInfoGetFieldDouble(info, field, &err);
+ if (err==REDISMODULE_OK)
+ RedisModule_ReplyWithDouble(ctx, d);
+ } else if (field_type=='c') {
+ const char *str = RedisModule_ServerInfoGetFieldC(info, field);
+ if (str)
+ RedisModule_ReplyWithCString(ctx, str);
+ } else {
+ RedisModuleString *str = RedisModule_ServerInfoGetField(ctx, info, field);
+ if (str) {
+ RedisModule_ReplyWithString(ctx, str);
+ RedisModule_FreeString(ctx, str);
+ } else
+ err=REDISMODULE_ERR;
+ }
+ if (err!=REDISMODULE_OK)
+ RedisModule_ReplyWithError(ctx, "not found");
+ RedisModule_FreeServerInfo(ctx, info);
+ return REDISMODULE_OK;
+}
+
+int info_gets(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ return info_get(ctx, argv, argc, 's');
+}
+
+int info_getc(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ return info_get(ctx, argv, argc, 'c');
+}
+
+int info_geti(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ return info_get(ctx, argv, argc, 'i');
+}
+
+int info_getu(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ return info_get(ctx, argv, argc, 'u');
+}
+
+int info_getd(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ return info_get(ctx, argv, argc, 'd');
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ if (RedisModule_Init(ctx,"infotest",1,REDISMODULE_APIVER_1)
+ == REDISMODULE_ERR) return REDISMODULE_ERR;
+
+ if (RedisModule_RegisterInfoFunc(ctx, InfoFunc) == REDISMODULE_ERR) return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"info.gets", info_gets,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"info.getc", info_getc,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"info.geti", info_geti,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"info.getu", info_getu,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"info.getd", info_getd,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/internalsecret.c b/examples/redis-unstable/tests/modules/internalsecret.c
new file mode 100644
index 0000000..043089c
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/internalsecret.c
@@ -0,0 +1,154 @@
+#include "redismodule.h"
+#include <errno.h>
+
+int InternalAuth_GetInternalSecret(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ /* NOTE: The internal secret SHOULD NOT be exposed by any module. This is
+ done for testing purposes only. */
+ size_t len;
+ const char *secret = RedisModule_GetInternalSecret(ctx, &len);
+ if(secret) {
+ RedisModule_ReplyWithStringBuffer(ctx, secret, len);
+ } else {
+ RedisModule_ReplyWithError(ctx, "ERR no internal secret available");
+ }
+ return REDISMODULE_OK;
+}
+
+int InternalAuth_InternalCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+typedef enum {
+ RM_CALL_REGULAR = 0,
+ RM_CALL_WITHUSER = 1,
+ RM_CALL_WITHDETACHEDCLIENT = 2,
+ RM_CALL_REPLICATED = 3
+} RMCallMode;
+
+int call_rm_call(RedisModuleCtx *ctx, RedisModuleString **argv, int argc, RMCallMode mode) {
+ if(argc < 2){
+ return RedisModule_WrongArity(ctx);
+ }
+ RedisModuleCallReply *rep = NULL;
+ RedisModuleCtx *detached_ctx = NULL;
+ const char* cmd = RedisModule_StringPtrLen(argv[1], NULL);
+
+ switch (mode) {
+ case RM_CALL_REGULAR:
+ // Regular call, with the unrestricted user.
+ rep = RedisModule_Call(ctx, cmd, "vE", argv + 2, (size_t)argc - 2);
+ break;
+ case RM_CALL_WITHUSER:
+ // Simply call the command with the current client.
+ rep = RedisModule_Call(ctx, cmd, "vCE", argv + 2, (size_t)argc - 2);
+ break;
+ case RM_CALL_WITHDETACHEDCLIENT:
+ // Use a context created with the thread-safe-context API
+ detached_ctx = RedisModule_GetThreadSafeContext(NULL);
+ if(!detached_ctx){
+ RedisModule_ReplyWithError(ctx, "ERR failed to create detached context");
+ return REDISMODULE_ERR;
+ }
+ // Dispatch the command with the detached context
+ rep = RedisModule_Call(detached_ctx, cmd, "vCE", argv + 2, (size_t)argc - 2);
+ break;
+ case RM_CALL_REPLICATED:
+ rep = RedisModule_Call(ctx, cmd, "vE", argv + 2, (size_t)argc - 2);
+ }
+
+ if(!rep) {
+ char err[100];
+ switch (errno) {
+ case EACCES:
+ RedisModule_ReplyWithError(ctx, "ERR NOPERM");
+ break;
+ case ENOENT:
+ RedisModule_ReplyWithError(ctx, "ERR unknown command");
+ break;
+ default:
+ snprintf(err, sizeof(err) - 1, "ERR errno=%d", errno);
+ RedisModule_ReplyWithError(ctx, err);
+ break;
+ }
+ } else {
+ RedisModule_ReplyWithCallReply(ctx, rep);
+ RedisModule_FreeCallReply(rep);
+ if (mode == RM_CALL_REPLICATED)
+ RedisModule_ReplicateVerbatim(ctx);
+ }
+
+ if (mode == RM_CALL_WITHDETACHEDCLIENT) {
+ RedisModule_FreeThreadSafeContext(detached_ctx);
+ }
+
+ return REDISMODULE_OK;
+}
+
+int internal_rmcall(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ return call_rm_call(ctx, argv, argc, RM_CALL_REGULAR);
+}
+
+int noninternal_rmcall(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ return call_rm_call(ctx, argv, argc, RM_CALL_REGULAR);
+}
+
+int noninternal_rmcall_withuser(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ return call_rm_call(ctx, argv, argc, RM_CALL_WITHUSER);
+}
+
+int noninternal_rmcall_detachedcontext_withuser(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ return call_rm_call(ctx, argv, argc, RM_CALL_WITHDETACHEDCLIENT);
+}
+
+int internal_rmcall_replicated(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ return call_rm_call(ctx, argv, argc, RM_CALL_REPLICATED);
+}
+
+/* This function must be present on each Redis module. It is used in order to
+ * register the commands into the Redis server. */
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ if (RedisModule_Init(ctx,"testinternalsecret",1,REDISMODULE_APIVER_1)
+ == REDISMODULE_ERR) return REDISMODULE_ERR;
+
+ /* WARNING: A module should NEVER expose the internal secret - this is for
+ * testing purposes only. */
+ if (RedisModule_CreateCommand(ctx,"internalauth.getinternalsecret",
+ InternalAuth_GetInternalSecret,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"internalauth.internalcommand",
+ InternalAuth_InternalCommand,"internal",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"internalauth.internal_rmcall",
+ internal_rmcall,"write internal",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"internalauth.noninternal_rmcall",
+ noninternal_rmcall,"write",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"internalauth.noninternal_rmcall_withuser",
+ noninternal_rmcall_withuser,"write",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"internalauth.noninternal_rmcall_detachedcontext_withuser",
+ noninternal_rmcall_detachedcontext_withuser,"write",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"internalauth.internal_rmcall_replicated",
+ internal_rmcall_replicated,"write internal",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/keyspace_events.c b/examples/redis-unstable/tests/modules/keyspace_events.c
new file mode 100644
index 0000000..146261f
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/keyspace_events.c
@@ -0,0 +1,479 @@
+/* This module is used to test the server keyspace events API.
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (c) 2020-Present, Redis Ltd.
+ * All rights reserved.
+ *
+ * Licensed under your choice of (a) the Redis Source Available License 2.0
+ * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
+ * GNU Affero General Public License v3 (AGPLv3).
+ */
+
+#define _BSD_SOURCE
+#define _DEFAULT_SOURCE /* For usleep */
+
+#include "redismodule.h"
+#include <stdio.h>
+#include <string.h>
+#include <strings.h>
+#include <unistd.h>
+
+ustime_t cached_time = 0;
+
+/** stores all the keys on which we got 'loaded' keyspace notification **/
+RedisModuleDict *loaded_event_log = NULL;
+/** stores all the keys on which we got 'module' keyspace notification **/
+RedisModuleDict *module_event_log = NULL;
+
+/** Counts how many deleted KSN we got on keys with a prefix of "count_dels_" **/
+static size_t dels = 0;
+
+static int KeySpace_NotificationLoaded(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key){
+ REDISMODULE_NOT_USED(ctx);
+ REDISMODULE_NOT_USED(type);
+
+ if(strcmp(event, "loaded") == 0){
+ const char* keyName = RedisModule_StringPtrLen(key, NULL);
+ int nokey;
+ RedisModule_DictGetC(loaded_event_log, (void*)keyName, strlen(keyName), &nokey);
+ if(nokey){
+ RedisModule_DictSetC(loaded_event_log, (void*)keyName, strlen(keyName), RedisModule_HoldString(ctx, key));
+ }
+ }
+
+ return REDISMODULE_OK;
+}
+
+static long long callback_call_count = 0;
+static int KeySpace_NotificationGeneric(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key) {
+ REDISMODULE_NOT_USED(type);
+ callback_call_count++;
+ const char *key_str = RedisModule_StringPtrLen(key, NULL);
+ if (strncmp(key_str, "count_dels_", 11) == 0 && strcmp(event, "del") == 0) {
+ if (RedisModule_GetContextFlags(ctx) & REDISMODULE_CTX_FLAGS_MASTER) {
+ dels++;
+ RedisModule_Replicate(ctx, "keyspace.incr_dels", "");
+ }
+ return REDISMODULE_OK;
+ }
+ if (cached_time) {
+ RedisModule_Assert(cached_time == RedisModule_CachedMicroseconds());
+ usleep(1);
+ RedisModule_Assert(cached_time != RedisModule_Microseconds());
+ }
+
+ if (strcmp(event, "del") == 0) {
+ RedisModuleString *copykey = RedisModule_CreateStringPrintf(ctx, "%s_copy", RedisModule_StringPtrLen(key, NULL));
+ RedisModuleCallReply* rep = RedisModule_Call(ctx, "DEL", "s!", copykey);
+ RedisModule_FreeString(ctx, copykey);
+ RedisModule_FreeCallReply(rep);
+
+ int ctx_flags = RedisModule_GetContextFlags(ctx);
+ if (ctx_flags & REDISMODULE_CTX_FLAGS_LUA) {
+ RedisModuleCallReply* rep = RedisModule_Call(ctx, "INCR", "c", "lua");
+ RedisModule_FreeCallReply(rep);
+ }
+ if (ctx_flags & REDISMODULE_CTX_FLAGS_MULTI) {
+ RedisModuleCallReply* rep = RedisModule_Call(ctx, "INCR", "c", "multi");
+ RedisModule_FreeCallReply(rep);
+ }
+ }
+
+ return REDISMODULE_OK;
+}
+
+static int KeySpace_NotificationExpired(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key) {
+ REDISMODULE_NOT_USED(type);
+ REDISMODULE_NOT_USED(event);
+ REDISMODULE_NOT_USED(key);
+
+ RedisModuleCallReply* rep = RedisModule_Call(ctx, "INCR", "c!", "testkeyspace:expired");
+ RedisModule_FreeCallReply(rep);
+
+ return REDISMODULE_OK;
+}
+
+/* This key miss notification handler is performing a write command inside the notification callback.
+ * Notice, it is discourage and currently wrong to perform a write command inside key miss event.
+ * It can cause read commands to be replicated to the replica/aof. This test is here temporary (for coverage and
+ * verification that it's not crashing). */
+static int KeySpace_NotificationModuleKeyMiss(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key) {
+ REDISMODULE_NOT_USED(type);
+ REDISMODULE_NOT_USED(event);
+ REDISMODULE_NOT_USED(key);
+
+ int flags = RedisModule_GetContextFlags(ctx);
+ if (!(flags & REDISMODULE_CTX_FLAGS_MASTER)) {
+ return REDISMODULE_OK; // ignore the event on replica
+ }
+
+ RedisModuleCallReply* rep = RedisModule_Call(ctx, "incr", "!c", "missed");
+ RedisModule_FreeCallReply(rep);
+
+ return REDISMODULE_OK;
+}
+
+static int KeySpace_NotificationModuleString(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key) {
+ REDISMODULE_NOT_USED(type);
+ REDISMODULE_NOT_USED(event);
+ RedisModuleKey *redis_key = RedisModule_OpenKey(ctx, key, REDISMODULE_READ);
+
+ size_t len = 0;
+ /* RedisModule_StringDMA could change the data format and cause the old robj to be freed.
+ * This code verifies that such format change will not cause any crashes.*/
+ char *data = RedisModule_StringDMA(redis_key, &len, REDISMODULE_READ);
+ int res = strncmp(data, "dummy", 5);
+ REDISMODULE_NOT_USED(res);
+
+ RedisModule_CloseKey(redis_key);
+
+ return REDISMODULE_OK;
+}
+
+static void KeySpace_PostNotificationStringFreePD(void *pd) {
+ RedisModule_FreeString(NULL, pd);
+}
+
+static void KeySpace_PostNotificationString(RedisModuleCtx *ctx, void *pd) {
+ REDISMODULE_NOT_USED(ctx);
+ RedisModuleCallReply* rep = RedisModule_Call(ctx, "incr", "!s", pd);
+ RedisModule_FreeCallReply(rep);
+}
+
+static int KeySpace_NotificationModuleStringPostNotificationJob(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key) {
+ REDISMODULE_NOT_USED(ctx);
+ REDISMODULE_NOT_USED(type);
+ REDISMODULE_NOT_USED(event);
+
+ const char *key_str = RedisModule_StringPtrLen(key, NULL);
+
+ if (strncmp(key_str, "string1_", 8) != 0) {
+ return REDISMODULE_OK;
+ }
+
+ RedisModuleString *new_key = RedisModule_CreateStringPrintf(NULL, "string_changed{%s}", key_str);
+ RedisModule_AddPostNotificationJob(ctx, KeySpace_PostNotificationString, new_key, KeySpace_PostNotificationStringFreePD);
+ return REDISMODULE_OK;
+}
+
+static int KeySpace_NotificationModule(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key) {
+ REDISMODULE_NOT_USED(ctx);
+ REDISMODULE_NOT_USED(type);
+ REDISMODULE_NOT_USED(event);
+
+ const char* keyName = RedisModule_StringPtrLen(key, NULL);
+ int nokey;
+ RedisModule_DictGetC(module_event_log, (void*)keyName, strlen(keyName), &nokey);
+ if(nokey){
+ RedisModule_DictSetC(module_event_log, (void*)keyName, strlen(keyName), RedisModule_HoldString(ctx, key));
+ }
+ return REDISMODULE_OK;
+}
+
+static int cmdNotify(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){
+ if(argc != 2){
+ return RedisModule_WrongArity(ctx);
+ }
+
+ RedisModule_NotifyKeyspaceEvent(ctx, REDISMODULE_NOTIFY_MODULE, "notify", argv[1]);
+ RedisModule_ReplyWithNull(ctx);
+ return REDISMODULE_OK;
+}
+
+static int cmdIsModuleKeyNotified(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){
+ if(argc != 2){
+ return RedisModule_WrongArity(ctx);
+ }
+
+ const char* key = RedisModule_StringPtrLen(argv[1], NULL);
+
+ int nokey;
+ RedisModuleString* keyStr = RedisModule_DictGetC(module_event_log, (void*)key, strlen(key), &nokey);
+
+ RedisModule_ReplyWithArray(ctx, 2);
+ RedisModule_ReplyWithLongLong(ctx, !nokey);
+ if(nokey){
+ RedisModule_ReplyWithNull(ctx);
+ }else{
+ RedisModule_ReplyWithString(ctx, keyStr);
+ }
+ return REDISMODULE_OK;
+}
+
+static int cmdIsKeyLoaded(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){
+ if(argc != 2){
+ return RedisModule_WrongArity(ctx);
+ }
+
+ const char* key = RedisModule_StringPtrLen(argv[1], NULL);
+
+ int nokey;
+ RedisModuleString* keyStr = RedisModule_DictGetC(loaded_event_log, (void*)key, strlen(key), &nokey);
+
+ RedisModule_ReplyWithArray(ctx, 2);
+ RedisModule_ReplyWithLongLong(ctx, !nokey);
+ if(nokey){
+ RedisModule_ReplyWithNull(ctx);
+ }else{
+ RedisModule_ReplyWithString(ctx, keyStr);
+ }
+ return REDISMODULE_OK;
+}
+
+static int cmdDelKeyCopy(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2)
+ return RedisModule_WrongArity(ctx);
+
+ cached_time = RedisModule_CachedMicroseconds();
+
+ RedisModuleCallReply* rep = RedisModule_Call(ctx, "DEL", "s!", argv[1]);
+ if (!rep) {
+ RedisModule_ReplyWithError(ctx, "NULL reply returned");
+ } else {
+ RedisModule_ReplyWithCallReply(ctx, rep);
+ RedisModule_FreeCallReply(rep);
+ }
+ cached_time = 0;
+ return REDISMODULE_OK;
+}
+
+/* Call INCR and propagate using RM_Call with `!`. */
+static int cmdIncrCase1(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2)
+ return RedisModule_WrongArity(ctx);
+
+ RedisModuleCallReply* rep = RedisModule_Call(ctx, "INCR", "s!", argv[1]);
+ if (!rep) {
+ RedisModule_ReplyWithError(ctx, "NULL reply returned");
+ } else {
+ RedisModule_ReplyWithCallReply(ctx, rep);
+ RedisModule_FreeCallReply(rep);
+ }
+ return REDISMODULE_OK;
+}
+
+/* Call INCR and propagate using RM_Replicate. */
+static int cmdIncrCase2(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2)
+ return RedisModule_WrongArity(ctx);
+
+ RedisModuleCallReply* rep = RedisModule_Call(ctx, "INCR", "s", argv[1]);
+ if (!rep) {
+ RedisModule_ReplyWithError(ctx, "NULL reply returned");
+ } else {
+ RedisModule_ReplyWithCallReply(ctx, rep);
+ RedisModule_FreeCallReply(rep);
+ }
+ RedisModule_Replicate(ctx, "INCR", "s", argv[1]);
+ return REDISMODULE_OK;
+}
+
+/* Call INCR and propagate using RM_ReplicateVerbatim. */
+static int cmdIncrCase3(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2)
+ return RedisModule_WrongArity(ctx);
+
+ RedisModuleCallReply* rep = RedisModule_Call(ctx, "INCR", "s", argv[1]);
+ if (!rep) {
+ RedisModule_ReplyWithError(ctx, "NULL reply returned");
+ } else {
+ RedisModule_ReplyWithCallReply(ctx, rep);
+ RedisModule_FreeCallReply(rep);
+ }
+ RedisModule_ReplicateVerbatim(ctx);
+ return REDISMODULE_OK;
+}
+
+static int cmdIncrDels(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ dels++;
+ return RedisModule_ReplyWithSimpleString(ctx, "OK");
+}
+
+static int cmdGetDels(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ return RedisModule_ReplyWithLongLong(ctx, dels);
+}
+
+static RedisModuleNotificationFunc get_callback_for_event(int event_mask) {
+ switch(event_mask) {
+ case REDISMODULE_NOTIFY_LOADED:
+ return KeySpace_NotificationLoaded;
+ case REDISMODULE_NOTIFY_GENERIC:
+ return KeySpace_NotificationGeneric;
+ case REDISMODULE_NOTIFY_EXPIRED:
+ return KeySpace_NotificationExpired;
+ case REDISMODULE_NOTIFY_MODULE:
+ return KeySpace_NotificationModule;
+ case REDISMODULE_NOTIFY_KEY_MISS:
+ return KeySpace_NotificationModuleKeyMiss;
+ case REDISMODULE_NOTIFY_STRING:
+ // We have two callbacks for STRING events in your OnLoad,
+ // For simplicity, pick the first:
+ return KeySpace_NotificationModuleString;
+ default:
+ return NULL;
+ }
+}
+
+int GetCallbackCountCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ RedisModule_ReplyWithLongLong(ctx, callback_call_count);
+ return REDISMODULE_OK;
+}
+
+static int CmdUnsub(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2) {
+ return RedisModule_WrongArity(ctx);
+ }
+
+ long long event_mask;
+ if (RedisModule_StringToLongLong(argv[1], &event_mask) != REDISMODULE_OK) {
+ return RedisModule_ReplyWithError(ctx, "ERR invalid event mask");
+ }
+
+ RedisModuleNotificationFunc cb = get_callback_for_event((int)event_mask);
+ if (cb == NULL) {
+ return RedisModule_ReplyWithError(ctx, "ERR unknown event mask");
+ }
+
+ if (RedisModule_UnsubscribeFromKeyspaceEvents(ctx, (int)event_mask, cb) != REDISMODULE_OK) {
+ return RedisModule_ReplyWithError(ctx, "ERR unsubscribe failed");
+ }
+
+ return RedisModule_ReplyWithSimpleString(ctx, "OK");
+}
+/* This function must be present on each Redis module. It is used in order to
+ * register the commands into the Redis server. */
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (RedisModule_Init(ctx,"testkeyspace",1,REDISMODULE_APIVER_1) == REDISMODULE_ERR){
+ return REDISMODULE_ERR;
+ }
+
+ loaded_event_log = RedisModule_CreateDict(ctx);
+ module_event_log = RedisModule_CreateDict(ctx);
+
+ int keySpaceAll = RedisModule_GetKeyspaceNotificationFlagsAll();
+
+ if (!(keySpaceAll & REDISMODULE_NOTIFY_LOADED)) {
+ // REDISMODULE_NOTIFY_LOADED event are not supported we can not start
+ return REDISMODULE_ERR;
+ }
+
+ if(RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_LOADED, KeySpace_NotificationLoaded) != REDISMODULE_OK){
+ return REDISMODULE_ERR;
+ }
+
+ if(RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_GENERIC, KeySpace_NotificationGeneric) != REDISMODULE_OK){
+ return REDISMODULE_ERR;
+ }
+
+ if(RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_EXPIRED, KeySpace_NotificationExpired) != REDISMODULE_OK){
+ return REDISMODULE_ERR;
+ }
+
+ if(RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_MODULE, KeySpace_NotificationModule) != REDISMODULE_OK){
+ return REDISMODULE_ERR;
+ }
+
+ if(RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_KEY_MISS, KeySpace_NotificationModuleKeyMiss) != REDISMODULE_OK){
+ return REDISMODULE_ERR;
+ }
+
+ if(RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_STRING, KeySpace_NotificationModuleString) != REDISMODULE_OK){
+ return REDISMODULE_ERR;
+ }
+
+ if(RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_STRING, KeySpace_NotificationModuleStringPostNotificationJob) != REDISMODULE_OK){
+ return REDISMODULE_ERR;
+ }
+
+ if (RedisModule_CreateCommand(ctx,"keyspace.notify", cmdNotify,"",0,0,0) == REDISMODULE_ERR){
+ return REDISMODULE_ERR;
+ }
+
+ if (RedisModule_CreateCommand(ctx,"keyspace.is_module_key_notified", cmdIsModuleKeyNotified,"",0,0,0) == REDISMODULE_ERR){
+ return REDISMODULE_ERR;
+ }
+
+ if (RedisModule_CreateCommand(ctx,"keyspace.is_key_loaded", cmdIsKeyLoaded,"",0,0,0) == REDISMODULE_ERR){
+ return REDISMODULE_ERR;
+ }
+
+ if (RedisModule_CreateCommand(ctx, "keyspace.del_key_copy", cmdDelKeyCopy,
+ "write", 0, 0, 0) == REDISMODULE_ERR){
+ return REDISMODULE_ERR;
+ }
+
+ if (RedisModule_CreateCommand(ctx, "keyspace.incr_case1", cmdIncrCase1,
+ "write", 0, 0, 0) == REDISMODULE_ERR){
+ return REDISMODULE_ERR;
+ }
+
+ if (RedisModule_CreateCommand(ctx, "keyspace.incr_case2", cmdIncrCase2,
+ "write", 0, 0, 0) == REDISMODULE_ERR){
+ return REDISMODULE_ERR;
+ }
+
+ if (RedisModule_CreateCommand(ctx, "keyspace.incr_case3", cmdIncrCase3,
+ "write", 0, 0, 0) == REDISMODULE_ERR){
+ return REDISMODULE_ERR;
+ }
+
+ if (RedisModule_CreateCommand(ctx, "keyspace.incr_dels", cmdIncrDels,
+ "write", 0, 0, 0) == REDISMODULE_ERR){
+ return REDISMODULE_ERR;
+ }
+
+ if (RedisModule_CreateCommand(ctx, "keyspace.get_dels", cmdGetDels,
+ "readonly", 0, 0, 0) == REDISMODULE_ERR){
+ return REDISMODULE_ERR;
+ }
+
+ if (RedisModule_CreateCommand(ctx, "keyspace.unsubscribe", CmdUnsub, "write", 0, 0, 0) == REDISMODULE_ERR){
+ return REDISMODULE_ERR;
+ }
+
+ if (RedisModule_CreateCommand(ctx, "keyspace.callback_count", GetCallbackCountCommand, "", 0, 0, 0)== REDISMODULE_ERR){
+ return REDISMODULE_ERR;
+ }
+
+ if (argc == 1) {
+ const char *ptr = RedisModule_StringPtrLen(argv[0], NULL);
+ if (!strcasecmp(ptr, "noload")) {
+ /* This is a hint that we return ERR at the last moment of OnLoad. */
+ RedisModule_FreeDict(ctx, loaded_event_log);
+ RedisModule_FreeDict(ctx, module_event_log);
+ return REDISMODULE_ERR;
+ }
+ }
+
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnUnload(RedisModuleCtx *ctx) {
+ RedisModuleDictIter *iter = RedisModule_DictIteratorStartC(loaded_event_log, "^", NULL, 0);
+ char* key;
+ size_t keyLen;
+ RedisModuleString* val;
+ while((key = RedisModule_DictNextC(iter, &keyLen, (void**)&val))){
+ RedisModule_FreeString(ctx, val);
+ }
+ RedisModule_FreeDict(ctx, loaded_event_log);
+ RedisModule_DictIteratorStop(iter);
+ loaded_event_log = NULL;
+
+ iter = RedisModule_DictIteratorStartC(module_event_log, "^", NULL, 0);
+ while((key = RedisModule_DictNextC(iter, &keyLen, (void**)&val))){
+ RedisModule_FreeString(ctx, val);
+ }
+ RedisModule_FreeDict(ctx, module_event_log);
+ RedisModule_DictIteratorStop(iter);
+ module_event_log = NULL;
+
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/keyspecs.c b/examples/redis-unstable/tests/modules/keyspecs.c
new file mode 100644
index 0000000..0a70de8
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/keyspecs.c
@@ -0,0 +1,236 @@
+#include "redismodule.h"
+
+#define UNUSED(V) ((void) V)
+
+/* This function implements all commands in this module. All we care about is
+ * the COMMAND metadata anyway. */
+int kspec_impl(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ UNUSED(argv);
+ UNUSED(argc);
+
+ /* Handle getkeys-api introspection (for "kspec.nonewithgetkeys") */
+ if (RedisModule_IsKeysPositionRequest(ctx)) {
+ for (int i = 1; i < argc; i += 2)
+ RedisModule_KeyAtPosWithFlags(ctx, i, REDISMODULE_CMD_KEY_RO | REDISMODULE_CMD_KEY_ACCESS);
+
+ return REDISMODULE_OK;
+ }
+
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+int createKspecNone(RedisModuleCtx *ctx) {
+ /* A command without keyspecs; only the legacy (first,last,step) triple (MSET like spec). */
+ if (RedisModule_CreateCommand(ctx,"kspec.none",kspec_impl,"",1,-1,2) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ 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. */
+ if (RedisModule_CreateCommand(ctx,"kspec.tworanges",kspec_impl,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ RedisModuleCommand *command = RedisModule_GetCommand(ctx,"kspec.tworanges");
+ 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 = 2,
+ /* 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 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)
+ return REDISMODULE_ERR;
+
+ RedisModuleCommand *command = RedisModule_GetCommand(ctx,"kspec.keyword");
+ RedisModuleCommandInfo info = {
+ .version = REDISMODULE_COMMAND_INFO_VERSION,
+ .key_specs = (RedisModuleCommandKeySpec[]){
+ {
+ .flags = REDISMODULE_CMD_KEY_RO | REDISMODULE_CMD_KEY_ACCESS,
+ .begin_search_type = REDISMODULE_KSPEC_BS_KEYWORD,
+ .bs.keyword.keyword = "KEYS",
+ .bs.keyword.startfrom = 1,
+ .find_keys_type = REDISMODULE_KSPEC_FK_RANGE,
+ .fk.range = {-1,1,0}
+ },
+ {0}
+ }
+ };
+ if (RedisModule_SetCommandInfo(command, &info) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ return REDISMODULE_OK;
+}
+
+int createKspecComplex1(RedisModuleCtx *ctx) {
+ /* First is a range a single key. The rest are keyword-based specs. */
+ if (RedisModule_CreateCommand(ctx,"kspec.complex1",kspec_impl,"",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ RedisModuleCommand *command = RedisModule_GetCommand(ctx,"kspec.complex1");
+ RedisModuleCommandInfo info = {
+ .version = REDISMODULE_COMMAND_INFO_VERSION,
+ .key_specs = (RedisModuleCommandKeySpec[]){
+ {
+ .flags = REDISMODULE_CMD_KEY_RO,
+ .begin_search_type = REDISMODULE_KSPEC_BS_INDEX,
+ .bs.index.pos = 1,
+ },
+ {
+ .flags = REDISMODULE_CMD_KEY_RW | REDISMODULE_CMD_KEY_UPDATE,
+ .begin_search_type = REDISMODULE_KSPEC_BS_KEYWORD,
+ .bs.keyword.keyword = "STORE",
+ .bs.keyword.startfrom = 2,
+ },
+ {
+ .flags = REDISMODULE_CMD_KEY_RO | REDISMODULE_CMD_KEY_ACCESS,
+ .begin_search_type = REDISMODULE_KSPEC_BS_KEYWORD,
+ .bs.keyword.keyword = "KEYS",
+ .bs.keyword.startfrom = 2,
+ .find_keys_type = REDISMODULE_KSPEC_FK_KEYNUM,
+ .fk.keynum = {0,1,1}
+ },
+ {0}
+ }
+ };
+ if (RedisModule_SetCommandInfo(command, &info) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ return REDISMODULE_OK;
+}
+
+int createKspecComplex2(RedisModuleCtx *ctx) {
+ /* First is not legacy, more than STATIC_KEYS_SPECS_NUM specs */
+ if (RedisModule_CreateCommand(ctx,"kspec.complex2",kspec_impl,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ RedisModuleCommand *command = RedisModule_GetCommand(ctx,"kspec.complex2");
+ RedisModuleCommandInfo info = {
+ .version = REDISMODULE_COMMAND_INFO_VERSION,
+ .key_specs = (RedisModuleCommandKeySpec[]){
+ {
+ .flags = REDISMODULE_CMD_KEY_RW | REDISMODULE_CMD_KEY_UPDATE,
+ .begin_search_type = REDISMODULE_KSPEC_BS_KEYWORD,
+ .bs.keyword.keyword = "STORE",
+ .bs.keyword.startfrom = 5,
+ .find_keys_type = REDISMODULE_KSPEC_FK_RANGE,
+ .fk.range = {0,1,0}
+ },
+ {
+ .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_RO | REDISMODULE_CMD_KEY_ACCESS,
+ .begin_search_type = REDISMODULE_KSPEC_BS_INDEX,
+ .bs.index.pos = 2,
+ .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,
+ .find_keys_type = REDISMODULE_KSPEC_FK_KEYNUM,
+ .fk.keynum = {0,1,1}
+ },
+ {
+ .flags = REDISMODULE_CMD_KEY_RW | REDISMODULE_CMD_KEY_UPDATE,
+ .begin_search_type = REDISMODULE_KSPEC_BS_KEYWORD,
+ .bs.keyword.keyword = "MOREKEYS",
+ .bs.keyword.startfrom = 5,
+ .find_keys_type = REDISMODULE_KSPEC_FK_RANGE,
+ .fk.range = {-1,1,0}
+ },
+ {0}
+ }
+ };
+ if (RedisModule_SetCommandInfo(command, &info) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ if (RedisModule_Init(ctx, "keyspecs", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR)
+ 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;
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/list.c b/examples/redis-unstable/tests/modules/list.c
new file mode 100644
index 0000000..401b2d8
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/list.c
@@ -0,0 +1,252 @@
+#include "redismodule.h"
+#include <assert.h>
+#include <errno.h>
+#include <strings.h>
+
+/* LIST.GETALL key [REVERSE] */
+int list_getall(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc < 2 || argc > 3) return RedisModule_WrongArity(ctx);
+ int reverse = (argc == 3 &&
+ !strcasecmp(RedisModule_StringPtrLen(argv[2], NULL),
+ "REVERSE"));
+ RedisModule_AutoMemory(ctx);
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ);
+ if (RedisModule_KeyType(key) != REDISMODULE_KEYTYPE_LIST) {
+ return RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE);
+ }
+ long n = RedisModule_ValueLength(key);
+ RedisModule_ReplyWithArray(ctx, n);
+ if (!reverse) {
+ for (long i = 0; i < n; i++) {
+ RedisModuleString *elem = RedisModule_ListGet(key, i);
+ RedisModule_ReplyWithString(ctx, elem);
+ RedisModule_FreeString(ctx, elem);
+ }
+ } else {
+ for (long i = -1; i >= -n; i--) {
+ RedisModuleString *elem = RedisModule_ListGet(key, i);
+ RedisModule_ReplyWithString(ctx, elem);
+ RedisModule_FreeString(ctx, elem);
+ }
+ }
+
+ /* Test error condition: index out of bounds */
+ assert(RedisModule_ListGet(key, n) == NULL);
+ assert(errno == EDOM); /* no more elements in list */
+
+ /* RedisModule_CloseKey(key); //implicit, done by auto memory */
+ return REDISMODULE_OK;
+}
+
+/* LIST.EDIT key [REVERSE] cmdstr [value ..]
+ *
+ * cmdstr is a string of the following characters:
+ *
+ * k -- keep
+ * d -- delete
+ * i -- insert value from args
+ * r -- replace with value from args
+ *
+ * The number of occurrences of "i" and "r" in cmdstr) should correspond to the
+ * number of args after cmdstr.
+ *
+ * Reply with a RESP3 Map, containing the number of edits (inserts, replaces, deletes)
+ * performed, as well as the last index and the entry it points to.
+ */
+int list_edit(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc < 3) return RedisModule_WrongArity(ctx);
+ RedisModule_AutoMemory(ctx);
+ int argpos = 1; /* the next arg */
+
+ /* key */
+ int keymode = REDISMODULE_READ | REDISMODULE_WRITE;
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[argpos++], keymode);
+ if (RedisModule_KeyType(key) != REDISMODULE_KEYTYPE_LIST) {
+ return RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE);
+ }
+
+ /* REVERSE */
+ int reverse = 0;
+ if (argc >= 4 &&
+ !strcasecmp(RedisModule_StringPtrLen(argv[argpos], NULL), "REVERSE")) {
+ reverse = 1;
+ argpos++;
+ }
+
+ /* cmdstr */
+ size_t cmdstr_len;
+ const char *cmdstr = RedisModule_StringPtrLen(argv[argpos++], &cmdstr_len);
+
+ /* validate cmdstr vs. argc */
+ long num_req_args = 0;
+ long min_list_length = 0;
+ for (size_t cmdpos = 0; cmdpos < cmdstr_len; cmdpos++) {
+ char c = cmdstr[cmdpos];
+ if (c == 'i' || c == 'r') num_req_args++;
+ if (c == 'd' || c == 'r' || c == 'k') min_list_length++;
+ }
+ if (argc < argpos + num_req_args) {
+ return RedisModule_ReplyWithError(ctx, "ERR too few args");
+ }
+ if ((long)RedisModule_ValueLength(key) < min_list_length) {
+ return RedisModule_ReplyWithError(ctx, "ERR list too short");
+ }
+
+ /* Iterate over the chars in cmdstr (edit instructions) */
+ long long num_inserts = 0, num_deletes = 0, num_replaces = 0;
+ long index = reverse ? -1 : 0;
+ RedisModuleString *value;
+
+ for (size_t cmdpos = 0; cmdpos < cmdstr_len; cmdpos++) {
+ switch (cmdstr[cmdpos]) {
+ case 'i': /* insert */
+ value = argv[argpos++];
+ assert(RedisModule_ListInsert(key, index, value) == REDISMODULE_OK);
+ index += reverse ? -1 : 1;
+ num_inserts++;
+ break;
+ case 'd': /* delete */
+ assert(RedisModule_ListDelete(key, index) == REDISMODULE_OK);
+ num_deletes++;
+ break;
+ case 'r': /* replace */
+ value = argv[argpos++];
+ assert(RedisModule_ListSet(key, index, value) == REDISMODULE_OK);
+ index += reverse ? -1 : 1;
+ num_replaces++;
+ break;
+ case 'k': /* keep */
+ index += reverse ? -1 : 1;
+ break;
+ }
+ }
+
+ RedisModuleString *v = RedisModule_ListGet(key, index);
+ RedisModule_ReplyWithMap(ctx, v ? 5 : 4);
+ RedisModule_ReplyWithCString(ctx, "i");
+ RedisModule_ReplyWithLongLong(ctx, num_inserts);
+ RedisModule_ReplyWithCString(ctx, "d");
+ RedisModule_ReplyWithLongLong(ctx, num_deletes);
+ RedisModule_ReplyWithCString(ctx, "r");
+ RedisModule_ReplyWithLongLong(ctx, num_replaces);
+ RedisModule_ReplyWithCString(ctx, "index");
+ RedisModule_ReplyWithLongLong(ctx, index);
+ if (v) {
+ RedisModule_ReplyWithCString(ctx, "entry");
+ RedisModule_ReplyWithString(ctx, v);
+ RedisModule_FreeString(ctx, v);
+ }
+
+ RedisModule_CloseKey(key);
+ return REDISMODULE_OK;
+}
+
+/* Reply based on errno as set by the List API functions. */
+static int replyByErrno(RedisModuleCtx *ctx) {
+ switch (errno) {
+ case EDOM:
+ return RedisModule_ReplyWithError(ctx, "ERR index out of bounds");
+ case ENOTSUP:
+ return RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE);
+ default: assert(0); /* Can't happen */
+ }
+}
+
+/* LIST.GET key index */
+int list_get(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 3) return RedisModule_WrongArity(ctx);
+ long long index;
+ if (RedisModule_StringToLongLong(argv[2], &index) != REDISMODULE_OK) {
+ return RedisModule_ReplyWithError(ctx, "ERR index must be a number");
+ }
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ);
+ RedisModuleString *value = RedisModule_ListGet(key, index);
+ if (value) {
+ RedisModule_ReplyWithString(ctx, value);
+ RedisModule_FreeString(ctx, value);
+ } else {
+ replyByErrno(ctx);
+ }
+ RedisModule_CloseKey(key);
+ return REDISMODULE_OK;
+}
+
+/* LIST.SET key index value */
+int list_set(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 4) return RedisModule_WrongArity(ctx);
+ long long index;
+ if (RedisModule_StringToLongLong(argv[2], &index) != REDISMODULE_OK) {
+ RedisModule_ReplyWithError(ctx, "ERR index must be a number");
+ return REDISMODULE_OK;
+ }
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE);
+ if (RedisModule_ListSet(key, index, argv[3]) == REDISMODULE_OK) {
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ } else {
+ replyByErrno(ctx);
+ }
+ RedisModule_CloseKey(key);
+ return REDISMODULE_OK;
+}
+
+/* LIST.INSERT key index value
+ *
+ * If index is negative, value is inserted after, otherwise before the element
+ * at index.
+ */
+int list_insert(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 4) return RedisModule_WrongArity(ctx);
+ long long index;
+ if (RedisModule_StringToLongLong(argv[2], &index) != REDISMODULE_OK) {
+ RedisModule_ReplyWithError(ctx, "ERR index must be a number");
+ return REDISMODULE_OK;
+ }
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE);
+ if (RedisModule_ListInsert(key, index, argv[3]) == REDISMODULE_OK) {
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ } else {
+ replyByErrno(ctx);
+ }
+ RedisModule_CloseKey(key);
+ return REDISMODULE_OK;
+}
+
+/* LIST.DELETE key index */
+int list_delete(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 3) return RedisModule_WrongArity(ctx);
+ long long index;
+ if (RedisModule_StringToLongLong(argv[2], &index) != REDISMODULE_OK) {
+ RedisModule_ReplyWithError(ctx, "ERR index must be a number");
+ return REDISMODULE_OK;
+ }
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE);
+ if (RedisModule_ListDelete(key, index) == REDISMODULE_OK) {
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ } else {
+ replyByErrno(ctx);
+ }
+ RedisModule_CloseKey(key);
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ if (RedisModule_Init(ctx, "list", 1, REDISMODULE_APIVER_1) == REDISMODULE_OK &&
+ RedisModule_CreateCommand(ctx, "list.getall", list_getall, "",
+ 1, 1, 1) == REDISMODULE_OK &&
+ RedisModule_CreateCommand(ctx, "list.edit", list_edit, "write",
+ 1, 1, 1) == REDISMODULE_OK &&
+ RedisModule_CreateCommand(ctx, "list.get", list_get, "write",
+ 1, 1, 1) == REDISMODULE_OK &&
+ RedisModule_CreateCommand(ctx, "list.set", list_set, "write",
+ 1, 1, 1) == REDISMODULE_OK &&
+ RedisModule_CreateCommand(ctx, "list.insert", list_insert, "write",
+ 1, 1, 1) == REDISMODULE_OK &&
+ RedisModule_CreateCommand(ctx, "list.delete", list_delete, "write",
+ 1, 1, 1) == REDISMODULE_OK) {
+ return REDISMODULE_OK;
+ } else {
+ return REDISMODULE_ERR;
+ }
+}
diff --git a/examples/redis-unstable/tests/modules/mallocsize.c b/examples/redis-unstable/tests/modules/mallocsize.c
new file mode 100644
index 0000000..a1d31c1
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/mallocsize.c
@@ -0,0 +1,237 @@
+#include "redismodule.h"
+#include <string.h>
+#include <assert.h>
+#include <unistd.h>
+
+#define UNUSED(V) ((void) V)
+
+/* Registered type */
+RedisModuleType *mallocsize_type = NULL;
+
+typedef enum {
+ UDT_RAW,
+ UDT_STRING,
+ UDT_DICT
+} udt_type_t;
+
+typedef struct {
+ void *ptr;
+ size_t len;
+} raw_t;
+
+typedef struct {
+ udt_type_t type;
+ union {
+ raw_t raw;
+ RedisModuleString *str;
+ RedisModuleDict *dict;
+ } data;
+} udt_t;
+
+void udt_free(void *value) {
+ udt_t *udt = value;
+ switch (udt->type) {
+ case (UDT_RAW): {
+ RedisModule_Free(udt->data.raw.ptr);
+ break;
+ }
+ case (UDT_STRING): {
+ RedisModule_FreeString(NULL, udt->data.str);
+ break;
+ }
+ case (UDT_DICT): {
+ RedisModuleString *dk, *dv;
+ RedisModuleDictIter *iter = RedisModule_DictIteratorStartC(udt->data.dict, "^", NULL, 0);
+ while((dk = RedisModule_DictNext(NULL, iter, (void **)&dv)) != NULL) {
+ RedisModule_FreeString(NULL, dk);
+ RedisModule_FreeString(NULL, dv);
+ }
+ RedisModule_DictIteratorStop(iter);
+ RedisModule_FreeDict(NULL, udt->data.dict);
+ break;
+ }
+ }
+ RedisModule_Free(udt);
+}
+
+void udt_rdb_save(RedisModuleIO *rdb, void *value) {
+ udt_t *udt = value;
+ RedisModule_SaveUnsigned(rdb, udt->type);
+ switch (udt->type) {
+ case (UDT_RAW): {
+ RedisModule_SaveStringBuffer(rdb, udt->data.raw.ptr, udt->data.raw.len);
+ break;
+ }
+ case (UDT_STRING): {
+ RedisModule_SaveString(rdb, udt->data.str);
+ break;
+ }
+ case (UDT_DICT): {
+ RedisModule_SaveUnsigned(rdb, RedisModule_DictSize(udt->data.dict));
+ RedisModuleString *dk, *dv;
+ RedisModuleDictIter *iter = RedisModule_DictIteratorStartC(udt->data.dict, "^", NULL, 0);
+ while((dk = RedisModule_DictNext(NULL, iter, (void **)&dv)) != NULL) {
+ RedisModule_SaveString(rdb, dk);
+ RedisModule_SaveString(rdb, dv);
+ RedisModule_FreeString(NULL, dk); /* Allocated by RedisModule_DictNext */
+ }
+ RedisModule_DictIteratorStop(iter);
+ break;
+ }
+ }
+}
+
+void *udt_rdb_load(RedisModuleIO *rdb, int encver) {
+ if (encver != 0)
+ return NULL;
+ udt_t *udt = RedisModule_Alloc(sizeof(*udt));
+ udt->type = RedisModule_LoadUnsigned(rdb);
+ switch (udt->type) {
+ case (UDT_RAW): {
+ udt->data.raw.ptr = RedisModule_LoadStringBuffer(rdb, &udt->data.raw.len);
+ break;
+ }
+ case (UDT_STRING): {
+ udt->data.str = RedisModule_LoadString(rdb);
+ break;
+ }
+ case (UDT_DICT): {
+ long long dict_len = RedisModule_LoadUnsigned(rdb);
+ udt->data.dict = RedisModule_CreateDict(NULL);
+ for (int i = 0; i < dict_len; i += 2) {
+ RedisModuleString *key = RedisModule_LoadString(rdb);
+ RedisModuleString *val = RedisModule_LoadString(rdb);
+ RedisModule_DictSet(udt->data.dict, key, val);
+ }
+ break;
+ }
+ }
+
+ return udt;
+}
+
+size_t udt_mem_usage(RedisModuleKeyOptCtx *ctx, const void *value, size_t sample_size) {
+ UNUSED(ctx);
+ UNUSED(sample_size);
+
+ const udt_t *udt = value;
+ size_t size = sizeof(*udt);
+
+ switch (udt->type) {
+ case (UDT_RAW): {
+ size += RedisModule_MallocSize(udt->data.raw.ptr);
+ break;
+ }
+ case (UDT_STRING): {
+ size += RedisModule_MallocSizeString(udt->data.str);
+ break;
+ }
+ case (UDT_DICT): {
+ void *dk;
+ size_t keylen;
+ RedisModuleString *dv;
+ RedisModuleDictIter *iter = RedisModule_DictIteratorStartC(udt->data.dict, "^", NULL, 0);
+ while((dk = RedisModule_DictNextC(iter, &keylen, (void **)&dv)) != NULL) {
+ size += keylen;
+ size += RedisModule_MallocSizeString(dv);
+ }
+ RedisModule_DictIteratorStop(iter);
+ break;
+ }
+ }
+
+ return size;
+}
+
+/* MALLOCSIZE.SETRAW key len */
+int cmd_setraw(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 3)
+ return RedisModule_WrongArity(ctx);
+
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE);
+
+ udt_t *udt = RedisModule_Alloc(sizeof(*udt));
+ udt->type = UDT_RAW;
+
+ long long raw_len;
+ RedisModule_StringToLongLong(argv[2], &raw_len);
+ udt->data.raw.ptr = RedisModule_Alloc(raw_len);
+ udt->data.raw.len = raw_len;
+
+ RedisModule_ModuleTypeSetValue(key, mallocsize_type, udt);
+ RedisModule_CloseKey(key);
+
+ return RedisModule_ReplyWithSimpleString(ctx, "OK");
+}
+
+/* MALLOCSIZE.SETSTR key string */
+int cmd_setstr(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 3)
+ return RedisModule_WrongArity(ctx);
+
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE);
+
+ udt_t *udt = RedisModule_Alloc(sizeof(*udt));
+ udt->type = UDT_STRING;
+
+ udt->data.str = argv[2];
+ RedisModule_RetainString(ctx, argv[2]);
+
+ RedisModule_ModuleTypeSetValue(key, mallocsize_type, udt);
+ RedisModule_CloseKey(key);
+
+ return RedisModule_ReplyWithSimpleString(ctx, "OK");
+}
+
+/* MALLOCSIZE.SETDICT key field value [field value ...] */
+int cmd_setdict(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc < 4 || argc % 2)
+ return RedisModule_WrongArity(ctx);
+
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE);
+
+ udt_t *udt = RedisModule_Alloc(sizeof(*udt));
+ udt->type = UDT_DICT;
+
+ udt->data.dict = RedisModule_CreateDict(ctx);
+ for (int i = 2; i < argc; i += 2) {
+ RedisModule_DictSet(udt->data.dict, argv[i], argv[i+1]);
+ /* No need to retain argv[i], it is copied as the rax key */
+ RedisModule_RetainString(ctx, argv[i+1]);
+ }
+
+ RedisModule_ModuleTypeSetValue(key, mallocsize_type, udt);
+ RedisModule_CloseKey(key);
+
+ return RedisModule_ReplyWithSimpleString(ctx, "OK");
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ UNUSED(argv);
+ UNUSED(argc);
+ if (RedisModule_Init(ctx,"mallocsize",1,REDISMODULE_APIVER_1)== REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ RedisModuleTypeMethods tm = {
+ .version = REDISMODULE_TYPE_METHOD_VERSION,
+ .rdb_load = udt_rdb_load,
+ .rdb_save = udt_rdb_save,
+ .free = udt_free,
+ .mem_usage2 = udt_mem_usage,
+ };
+
+ mallocsize_type = RedisModule_CreateDataType(ctx, "allocsize", 0, &tm);
+ if (mallocsize_type == NULL)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "mallocsize.setraw", cmd_setraw, "", 1, 1, 1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "mallocsize.setstr", cmd_setstr, "", 1, 1, 1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "mallocsize.setdict", cmd_setdict, "", 1, 1, 1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/misc.c b/examples/redis-unstable/tests/modules/misc.c
new file mode 100644
index 0000000..dbf0fec
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/misc.c
@@ -0,0 +1,642 @@
+#include "redismodule.h"
+
+#include <string.h>
+#include <assert.h>
+#include <unistd.h>
+#include <errno.h>
+#include <limits.h>
+
+#define UNUSED(x) (void)(x)
+
+static int n_events = 0;
+
+static int KeySpace_NotificationModuleKeyMissExpired(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key) {
+ UNUSED(ctx);
+ UNUSED(type);
+ UNUSED(event);
+ UNUSED(key);
+ n_events++;
+ return REDISMODULE_OK;
+}
+
+int test_clear_n_events(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ UNUSED(argv);
+ UNUSED(argc);
+ n_events = 0;
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+int test_get_n_events(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ UNUSED(argv);
+ UNUSED(argc);
+ RedisModule_ReplyWithLongLong(ctx, n_events);
+ return REDISMODULE_OK;
+}
+
+int test_open_key_no_effects(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc<2) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ int supportedMode = RedisModule_GetOpenKeyModesAll();
+ if (!(supportedMode & REDISMODULE_READ) || !(supportedMode & REDISMODULE_OPEN_KEY_NOEFFECTS)) {
+ RedisModule_ReplyWithError(ctx, "OpenKey modes are not supported");
+ return REDISMODULE_OK;
+ }
+
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ | REDISMODULE_OPEN_KEY_NOEFFECTS);
+ if (!key) {
+ RedisModule_ReplyWithError(ctx, "key not found");
+ return REDISMODULE_OK;
+ }
+
+ RedisModule_CloseKey(key);
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+int test_call_generic(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ if (argc<2) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ const char* cmdname = RedisModule_StringPtrLen(argv[1], NULL);
+ RedisModuleCallReply *reply = RedisModule_Call(ctx, cmdname, "v", argv+2, (size_t)argc-2);
+ if (reply) {
+ RedisModule_ReplyWithCallReply(ctx, reply);
+ RedisModule_FreeCallReply(reply);
+ } else {
+ RedisModule_ReplyWithError(ctx, strerror(errno));
+ }
+ return REDISMODULE_OK;
+}
+
+int test_call_info(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ RedisModuleCallReply *reply;
+ if (argc>1)
+ reply = RedisModule_Call(ctx, "info", "s", argv[1]);
+ else
+ reply = RedisModule_Call(ctx, "info", "");
+ if (reply) {
+ RedisModule_ReplyWithCallReply(ctx, reply);
+ RedisModule_FreeCallReply(reply);
+ } else {
+ RedisModule_ReplyWithError(ctx, strerror(errno));
+ }
+ return REDISMODULE_OK;
+}
+
+int test_ld_conv(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ UNUSED(argv);
+ UNUSED(argc);
+ long double ld = 0.00000000000000001L;
+ const char *ldstr = "0.00000000000000001";
+ RedisModuleString *s1 = RedisModule_CreateStringFromLongDouble(ctx, ld, 1);
+ RedisModuleString *s2 =
+ RedisModule_CreateString(ctx, ldstr, strlen(ldstr));
+ if (RedisModule_StringCompare(s1, s2) != 0) {
+ char err[4096];
+ snprintf(err, 4096,
+ "Failed to convert long double to string ('%s' != '%s')",
+ RedisModule_StringPtrLen(s1, NULL),
+ RedisModule_StringPtrLen(s2, NULL));
+ RedisModule_ReplyWithError(ctx, err);
+ goto final;
+ }
+ long double ld2 = 0;
+ if (RedisModule_StringToLongDouble(s2, &ld2) == REDISMODULE_ERR) {
+ RedisModule_ReplyWithError(ctx,
+ "Failed to convert string to long double");
+ goto final;
+ }
+ if (ld2 != ld) {
+ char err[4096];
+ snprintf(err, 4096,
+ "Failed to convert string to long double (%.40Lf != %.40Lf)",
+ ld2,
+ ld);
+ RedisModule_ReplyWithError(ctx, err);
+ goto final;
+ }
+
+ /* Make sure we can't convert a string that has \0 in it */
+ char buf[4] = "123";
+ buf[1] = '\0';
+ RedisModuleString *s3 = RedisModule_CreateString(ctx, buf, 3);
+ long double ld3;
+ if (RedisModule_StringToLongDouble(s3, &ld3) == REDISMODULE_OK) {
+ RedisModule_ReplyWithError(ctx, "Invalid string successfully converted to long double");
+ RedisModule_FreeString(ctx, s3);
+ goto final;
+ }
+ RedisModule_FreeString(ctx, s3);
+
+ RedisModule_ReplyWithLongDouble(ctx, ld2);
+final:
+ RedisModule_FreeString(ctx, s1);
+ RedisModule_FreeString(ctx, s2);
+ return REDISMODULE_OK;
+}
+
+int test_flushall(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ RedisModule_ResetDataset(1, 0);
+ RedisModule_ReplyWithCString(ctx, "Ok");
+ return REDISMODULE_OK;
+}
+
+int test_dbsize(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ long long ll = RedisModule_DbSize(ctx);
+ RedisModule_ReplyWithLongLong(ctx, ll);
+ return REDISMODULE_OK;
+}
+
+int test_randomkey(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ RedisModuleString *str = RedisModule_RandomKey(ctx);
+ RedisModule_ReplyWithString(ctx, str);
+ RedisModule_FreeString(ctx, str);
+ return REDISMODULE_OK;
+}
+
+int test_keyexists(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc < 2) return RedisModule_WrongArity(ctx);
+ RedisModuleString *key = argv[1];
+ int exists = RedisModule_KeyExists(ctx, key);
+ return RedisModule_ReplyWithBool(ctx, exists);
+}
+
+RedisModuleKey *open_key_or_reply(RedisModuleCtx *ctx, RedisModuleString *keyname, int mode) {
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, keyname, mode);
+ if (!key) {
+ RedisModule_ReplyWithError(ctx, "key not found");
+ return NULL;
+ }
+ return key;
+}
+
+int test_getlru(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ if (argc<2) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+ RedisModuleKey *key = open_key_or_reply(ctx, argv[1], REDISMODULE_READ|REDISMODULE_OPEN_KEY_NOTOUCH);
+ mstime_t lru;
+ RedisModule_GetLRU(key, &lru);
+ RedisModule_ReplyWithLongLong(ctx, lru);
+ RedisModule_CloseKey(key);
+ return REDISMODULE_OK;
+}
+
+int test_setlru(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ if (argc<3) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+ RedisModuleKey *key = open_key_or_reply(ctx, argv[1], REDISMODULE_READ|REDISMODULE_OPEN_KEY_NOTOUCH);
+ mstime_t lru;
+ if (RedisModule_StringToLongLong(argv[2], &lru) != REDISMODULE_OK) {
+ RedisModule_ReplyWithError(ctx, "invalid idle time");
+ return REDISMODULE_OK;
+ }
+ int was_set = RedisModule_SetLRU(key, lru)==REDISMODULE_OK;
+ RedisModule_ReplyWithLongLong(ctx, was_set);
+ RedisModule_CloseKey(key);
+ return REDISMODULE_OK;
+}
+
+int test_getlfu(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ if (argc<2) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+ RedisModuleKey *key = open_key_or_reply(ctx, argv[1], REDISMODULE_READ|REDISMODULE_OPEN_KEY_NOTOUCH);
+ mstime_t lfu;
+ RedisModule_GetLFU(key, &lfu);
+ RedisModule_ReplyWithLongLong(ctx, lfu);
+ RedisModule_CloseKey(key);
+ return REDISMODULE_OK;
+}
+
+int test_setlfu(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ if (argc<3) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+ RedisModuleKey *key = open_key_or_reply(ctx, argv[1], REDISMODULE_READ|REDISMODULE_OPEN_KEY_NOTOUCH);
+ mstime_t lfu;
+ if (RedisModule_StringToLongLong(argv[2], &lfu) != REDISMODULE_OK) {
+ RedisModule_ReplyWithError(ctx, "invalid freq");
+ return REDISMODULE_OK;
+ }
+ int was_set = RedisModule_SetLFU(key, lfu)==REDISMODULE_OK;
+ RedisModule_ReplyWithLongLong(ctx, was_set);
+ RedisModule_CloseKey(key);
+ return REDISMODULE_OK;
+}
+
+int test_redisversion(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){
+ (void) argv;
+ (void) argc;
+
+ int version = RedisModule_GetServerVersion();
+ int patch = version & 0x000000ff;
+ int minor = (version & 0x0000ff00) >> 8;
+ int major = (version & 0x00ff0000) >> 16;
+
+ RedisModuleString* vStr = RedisModule_CreateStringPrintf(ctx, "%d.%d.%d", major, minor, patch);
+ RedisModule_ReplyWithString(ctx, vStr);
+ RedisModule_FreeString(ctx, vStr);
+
+ return REDISMODULE_OK;
+}
+
+int test_getclientcert(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ (void) argv;
+ (void) argc;
+
+ RedisModuleString *cert = RedisModule_GetClientCertificate(ctx,
+ RedisModule_GetClientId(ctx));
+ if (!cert) {
+ RedisModule_ReplyWithNull(ctx);
+ } else {
+ RedisModule_ReplyWithString(ctx, cert);
+ RedisModule_FreeString(ctx, cert);
+ }
+
+ return REDISMODULE_OK;
+}
+
+int test_clientinfo(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ (void) argv;
+ (void) argc;
+
+ RedisModuleClientInfoV1 ci = REDISMODULE_CLIENTINFO_INITIALIZER_V1;
+ uint64_t client_id = RedisModule_GetClientId(ctx);
+
+ /* Check expected result from the V1 initializer. */
+ assert(ci.version == 1);
+ /* Trying to populate a future version of the struct should fail. */
+ ci.version = REDISMODULE_CLIENTINFO_VERSION + 1;
+ assert(RedisModule_GetClientInfoById(&ci, client_id) == REDISMODULE_ERR);
+
+ ci.version = 1;
+ if (RedisModule_GetClientInfoById(&ci, client_id) == REDISMODULE_ERR) {
+ RedisModule_ReplyWithError(ctx, "failed to get client info");
+ return REDISMODULE_OK;
+ }
+
+ RedisModule_ReplyWithArray(ctx, 10);
+ char flags[512];
+ snprintf(flags, sizeof(flags) - 1, "%s:%s:%s:%s:%s:%s",
+ ci.flags & REDISMODULE_CLIENTINFO_FLAG_SSL ? "ssl" : "",
+ ci.flags & REDISMODULE_CLIENTINFO_FLAG_PUBSUB ? "pubsub" : "",
+ ci.flags & REDISMODULE_CLIENTINFO_FLAG_BLOCKED ? "blocked" : "",
+ ci.flags & REDISMODULE_CLIENTINFO_FLAG_TRACKING ? "tracking" : "",
+ ci.flags & REDISMODULE_CLIENTINFO_FLAG_UNIXSOCKET ? "unixsocket" : "",
+ ci.flags & REDISMODULE_CLIENTINFO_FLAG_MULTI ? "multi" : "");
+
+ RedisModule_ReplyWithCString(ctx, "flags");
+ RedisModule_ReplyWithCString(ctx, flags);
+ RedisModule_ReplyWithCString(ctx, "id");
+ RedisModule_ReplyWithLongLong(ctx, ci.id);
+ RedisModule_ReplyWithCString(ctx, "addr");
+ RedisModule_ReplyWithCString(ctx, ci.addr);
+ RedisModule_ReplyWithCString(ctx, "port");
+ RedisModule_ReplyWithLongLong(ctx, ci.port);
+ RedisModule_ReplyWithCString(ctx, "db");
+ RedisModule_ReplyWithLongLong(ctx, ci.db);
+
+ return REDISMODULE_OK;
+}
+
+int test_getname(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ (void)argv;
+ if (argc != 1) return RedisModule_WrongArity(ctx);
+ unsigned long long id = RedisModule_GetClientId(ctx);
+ RedisModuleString *name = RedisModule_GetClientNameById(ctx, id);
+ if (name == NULL)
+ return RedisModule_ReplyWithError(ctx, "-ERR No name");
+ RedisModule_ReplyWithString(ctx, name);
+ RedisModule_FreeString(ctx, name);
+ return REDISMODULE_OK;
+}
+
+int test_setname(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2) return RedisModule_WrongArity(ctx);
+ unsigned long long id = RedisModule_GetClientId(ctx);
+ if (RedisModule_SetClientNameById(id, argv[1]) == REDISMODULE_OK)
+ return RedisModule_ReplyWithSimpleString(ctx, "OK");
+ else
+ return RedisModule_ReplyWithError(ctx, strerror(errno));
+}
+
+int test_log_tsctx(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ RedisModuleCtx *tsctx = RedisModule_GetDetachedThreadSafeContext(ctx);
+
+ if (argc != 3) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ char level[50];
+ size_t level_len;
+ const char *level_str = RedisModule_StringPtrLen(argv[1], &level_len);
+ snprintf(level, sizeof(level) - 1, "%.*s", (int) level_len, level_str);
+
+ size_t msg_len;
+ const char *msg_str = RedisModule_StringPtrLen(argv[2], &msg_len);
+
+ RedisModule_Log(tsctx, level, "%.*s", (int) msg_len, msg_str);
+ RedisModule_FreeThreadSafeContext(tsctx);
+
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+int test_weird_cmd(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+int test_monotonic_time(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ RedisModule_ReplyWithLongLong(ctx, RedisModule_MonotonicMicroseconds());
+ return REDISMODULE_OK;
+}
+
+/* wrapper for RM_Call */
+int test_rm_call(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){
+ if(argc < 2){
+ return RedisModule_WrongArity(ctx);
+ }
+
+ const char* cmd = RedisModule_StringPtrLen(argv[1], NULL);
+
+ RedisModuleCallReply* rep = RedisModule_Call(ctx, cmd, "Ev", argv + 2, (size_t)argc - 2);
+ if(!rep){
+ RedisModule_ReplyWithError(ctx, "NULL reply returned");
+ }else{
+ RedisModule_ReplyWithCallReply(ctx, rep);
+ RedisModule_FreeCallReply(rep);
+ }
+
+ return REDISMODULE_OK;
+}
+
+/* wrapper for RM_Call which also replicates the module command */
+int test_rm_call_replicate(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){
+ test_rm_call(ctx, argv, argc);
+ RedisModule_ReplicateVerbatim(ctx);
+
+ return REDISMODULE_OK;
+}
+
+/* wrapper for RM_Call with flags */
+int test_rm_call_flags(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){
+ if(argc < 3){
+ return RedisModule_WrongArity(ctx);
+ }
+
+ /* Append Ev to the provided flags. */
+ RedisModuleString *flags = RedisModule_CreateStringFromString(ctx, argv[1]);
+ RedisModule_StringAppendBuffer(ctx, flags, "Ev", 2);
+
+ const char* flg = RedisModule_StringPtrLen(flags, NULL);
+ const char* cmd = RedisModule_StringPtrLen(argv[2], NULL);
+
+ RedisModuleCallReply* rep = RedisModule_Call(ctx, cmd, flg, argv + 3, (size_t)argc - 3);
+ if(!rep){
+ RedisModule_ReplyWithError(ctx, "NULL reply returned");
+ }else{
+ RedisModule_ReplyWithCallReply(ctx, rep);
+ RedisModule_FreeCallReply(rep);
+ }
+ RedisModule_FreeString(ctx, flags);
+
+ return REDISMODULE_OK;
+}
+
+int test_ull_conv(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ UNUSED(argv);
+ UNUSED(argc);
+ unsigned long long ull = 18446744073709551615ULL;
+ const char *ullstr = "18446744073709551615";
+
+ RedisModuleString *s1 = RedisModule_CreateStringFromULongLong(ctx, ull);
+ RedisModuleString *s2 =
+ RedisModule_CreateString(ctx, ullstr, strlen(ullstr));
+ if (RedisModule_StringCompare(s1, s2) != 0) {
+ char err[4096];
+ snprintf(err, 4096,
+ "Failed to convert unsigned long long to string ('%s' != '%s')",
+ RedisModule_StringPtrLen(s1, NULL),
+ RedisModule_StringPtrLen(s2, NULL));
+ RedisModule_ReplyWithError(ctx, err);
+ goto final;
+ }
+ unsigned long long ull2 = 0;
+ if (RedisModule_StringToULongLong(s2, &ull2) == REDISMODULE_ERR) {
+ RedisModule_ReplyWithError(ctx,
+ "Failed to convert string to unsigned long long");
+ goto final;
+ }
+ if (ull2 != ull) {
+ char err[4096];
+ snprintf(err, 4096,
+ "Failed to convert string to unsigned long long (%llu != %llu)",
+ ull2,
+ ull);
+ RedisModule_ReplyWithError(ctx, err);
+ goto final;
+ }
+
+ /* Make sure we can't convert a string more than ULLONG_MAX or less than 0 */
+ ullstr = "18446744073709551616";
+ RedisModuleString *s3 = RedisModule_CreateString(ctx, ullstr, strlen(ullstr));
+ unsigned long long ull3;
+ if (RedisModule_StringToULongLong(s3, &ull3) == REDISMODULE_OK) {
+ RedisModule_ReplyWithError(ctx, "Invalid string successfully converted to unsigned long long");
+ RedisModule_FreeString(ctx, s3);
+ goto final;
+ }
+ RedisModule_FreeString(ctx, s3);
+ ullstr = "-1";
+ RedisModuleString *s4 = RedisModule_CreateString(ctx, ullstr, strlen(ullstr));
+ unsigned long long ull4;
+ if (RedisModule_StringToULongLong(s4, &ull4) == REDISMODULE_OK) {
+ RedisModule_ReplyWithError(ctx, "Invalid string successfully converted to unsigned long long");
+ RedisModule_FreeString(ctx, s4);
+ goto final;
+ }
+ RedisModule_FreeString(ctx, s4);
+
+ RedisModule_ReplyWithSimpleString(ctx, "ok");
+
+final:
+ RedisModule_FreeString(ctx, s1);
+ RedisModule_FreeString(ctx, s2);
+ return REDISMODULE_OK;
+}
+
+int test_malloc_api(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ UNUSED(argv);
+ UNUSED(argc);
+
+ void *p;
+
+ p = RedisModule_TryAlloc(1024);
+ memset(p, 0, 1024);
+ RedisModule_Free(p);
+
+ p = RedisModule_TryCalloc(1, 1024);
+ memset(p, 1, 1024);
+
+ p = RedisModule_TryRealloc(p, 5 * 1024);
+ memset(p, 1, 5 * 1024);
+ RedisModule_Free(p);
+
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+int test_keyslot(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ /* Static check of the ClusterKeySlot + ClusterKeySlotC + ClusterCanonicalKeyNameInSlot
+ * round-trip for all slots. */
+ for (unsigned int slot = 0; slot < 16384; slot++) {
+ const char *tag = RedisModule_ClusterCanonicalKeyNameInSlot(slot);
+ RedisModuleString *key = RedisModule_CreateStringPrintf(ctx, "x{%s}y", tag);
+ assert(slot == RedisModule_ClusterKeySlot(key));
+ size_t len;
+ const char *key_c = RedisModule_StringPtrLen(key, &len);
+ assert(slot == RedisModule_ClusterKeySlotC(key_c, len));
+ RedisModule_FreeString(ctx, key);
+ }
+ if (argc != 2){
+ return RedisModule_WrongArity(ctx);
+ }
+ unsigned int slot = RedisModule_ClusterKeySlot(argv[1]);
+ return RedisModule_ReplyWithLongLong(ctx, slot);
+}
+
+int only_reply_ok(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+/* Test command for RM_SignalModifiedKey. */
+int test_signalmodifiedkey(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2) return RedisModule_WrongArity(ctx);
+
+ /* Manually signal that the key was modified */
+ RedisModule_SignalModifiedKey(ctx, argv[1]);
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ if (RedisModule_Init(ctx,"misc",1,REDISMODULE_APIVER_1)== REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if(RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_KEY_MISS | REDISMODULE_NOTIFY_EXPIRED, KeySpace_NotificationModuleKeyMissExpired) != REDISMODULE_OK){
+ return REDISMODULE_ERR;
+ }
+
+ if (RedisModule_CreateCommand(ctx,"test.call_generic", test_call_generic,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"test.call_info", test_call_info,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"test.ld_conversion", test_ld_conv, "",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"test.ull_conversion", test_ull_conv, "",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"test.flushall", test_flushall,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"test.dbsize", test_dbsize,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"test.randomkey", test_randomkey,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"test.keyexists", test_keyexists,"",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"test.setlru", test_setlru,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"test.getlru", test_getlru,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"test.setlfu", test_setlfu,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"test.getlfu", test_getlfu,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"test.clientinfo", test_clientinfo,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"test.getname", test_getname,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"test.setname", test_setname,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"test.redisversion", test_redisversion,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"test.getclientcert", test_getclientcert,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"test.log_tsctx", test_log_tsctx,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ /* Add a command with ':' in it's name, so that we can check commandstats sanitization. */
+ if (RedisModule_CreateCommand(ctx,"test.weird:cmd", test_weird_cmd,"readonly",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"test.monotonic_time", test_monotonic_time,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx, "test.rm_call", test_rm_call,"allow-stale", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx, "test.rm_call_flags", test_rm_call_flags,"allow-stale", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx, "test.rm_call_replicate", test_rm_call_replicate,"allow-stale", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx, "test.silent_open_key", test_open_key_no_effects,"", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx, "test.get_n_events", test_get_n_events,"", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx, "test.clear_n_events", test_clear_n_events,"", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx, "test.malloc_api", test_malloc_api,"", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx, "test.keyslot", test_keyslot, "", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx, "test.incompatible_cluster_cmd", only_reply_ok, "", 1, -1, 2) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx, "test.no_cluster_cmd", NULL, "no-cluster", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ RedisModuleCommand *parent = RedisModule_GetCommand(ctx, "test.no_cluster_cmd");
+ if (RedisModule_CreateSubcommand(parent, "set", only_reply_ok, "no-cluster", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx, "test.signalmodifiedkey", test_signalmodifiedkey, "write", 1, 1, 1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/moduleauthtwo.c b/examples/redis-unstable/tests/modules/moduleauthtwo.c
new file mode 100644
index 0000000..0a4f56b
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/moduleauthtwo.c
@@ -0,0 +1,43 @@
+#include "redismodule.h"
+
+#include <string.h>
+
+/* This is a second sample module to validate that module authentication callbacks can be registered
+ * from multiple modules. */
+
+/* Non Blocking Module Auth callback / implementation. */
+int auth_cb(RedisModuleCtx *ctx, RedisModuleString *username, RedisModuleString *password, RedisModuleString **err) {
+ const char *user = RedisModule_StringPtrLen(username, NULL);
+ const char *pwd = RedisModule_StringPtrLen(password, NULL);
+ if (!strcmp(user,"foo") && !strcmp(pwd,"allow_two")) {
+ RedisModule_AuthenticateClientWithACLUser(ctx, "foo", 3, NULL, NULL, NULL);
+ return REDISMODULE_AUTH_HANDLED;
+ }
+ else if (!strcmp(user,"foo") && !strcmp(pwd,"deny_two")) {
+ RedisModuleString *log = RedisModule_CreateString(ctx, "Module Auth", 11);
+ RedisModule_ACLAddLogEntryByUserName(ctx, username, log, REDISMODULE_ACL_LOG_AUTH);
+ RedisModule_FreeString(ctx, log);
+ const char *err_msg = "Auth denied by Misc Module.";
+ *err = RedisModule_CreateString(ctx, err_msg, strlen(err_msg));
+ return REDISMODULE_AUTH_HANDLED;
+ }
+ return REDISMODULE_AUTH_NOT_HANDLED;
+}
+
+int test_rm_register_auth_cb(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ RedisModule_RegisterAuthCallback(ctx, auth_cb);
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ if (RedisModule_Init(ctx,"moduleauthtwo",1,REDISMODULE_APIVER_1)== REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"testmoduletwo.rm_register_auth_cb", test_rm_register_auth_cb,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ return REDISMODULE_OK;
+} \ No newline at end of file
diff --git a/examples/redis-unstable/tests/modules/moduleconfigs.c b/examples/redis-unstable/tests/modules/moduleconfigs.c
new file mode 100644
index 0000000..04974f7
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/moduleconfigs.c
@@ -0,0 +1,287 @@
+#include "redismodule.h"
+#include <strings.h>
+int mutable_bool_val, no_prefix_bool, no_prefix_bool2;
+int immutable_bool_val;
+long long longval, no_prefix_longval;
+long long memval, no_prefix_memval;
+RedisModuleString *strval = NULL;
+RedisModuleString *strval2 = NULL;
+int enumval, no_prefix_enumval;
+int flagsval;
+
+/* Series of get and set callbacks for each type of config, these rely on the privdata ptr
+ * to point to the config, and they register the configs as such. Note that one could also just
+ * use names if they wanted, and store anything in privdata. */
+int getBoolConfigCommand(const char *name, void *privdata) {
+ REDISMODULE_NOT_USED(name);
+ return (*(int *)privdata);
+}
+
+int setBoolConfigCommand(const char *name, int new, void *privdata, RedisModuleString **err) {
+ REDISMODULE_NOT_USED(name);
+ REDISMODULE_NOT_USED(err);
+ *(int *)privdata = new;
+ return REDISMODULE_OK;
+}
+
+long long getNumericConfigCommand(const char *name, void *privdata) {
+ REDISMODULE_NOT_USED(name);
+ return (*(long long *) privdata);
+}
+
+int setNumericConfigCommand(const char *name, long long new, void *privdata, RedisModuleString **err) {
+ REDISMODULE_NOT_USED(name);
+ REDISMODULE_NOT_USED(err);
+ *(long long *)privdata = new;
+ return REDISMODULE_OK;
+}
+
+RedisModuleString *getStringConfigCommand(const char *name, void *privdata) {
+ REDISMODULE_NOT_USED(name);
+ REDISMODULE_NOT_USED(privdata);
+ return strval;
+}
+int setStringConfigCommand(const char *name, RedisModuleString *new, void *privdata, RedisModuleString **err) {
+ REDISMODULE_NOT_USED(name);
+ REDISMODULE_NOT_USED(err);
+ REDISMODULE_NOT_USED(privdata);
+ size_t len;
+ if (!strcasecmp(RedisModule_StringPtrLen(new, &len), "rejectisfreed")) {
+ *err = RedisModule_CreateString(NULL, "Cannot set string to 'rejectisfreed'", 36);
+ return REDISMODULE_ERR;
+ }
+ if (strval) RedisModule_FreeString(NULL, strval);
+ RedisModule_RetainString(NULL, new);
+ strval = new;
+ return REDISMODULE_OK;
+}
+
+int getEnumConfigCommand(const char *name, void *privdata) {
+ REDISMODULE_NOT_USED(name);
+ REDISMODULE_NOT_USED(privdata);
+ return enumval;
+}
+
+int setEnumConfigCommand(const char *name, int val, void *privdata, RedisModuleString **err) {
+ REDISMODULE_NOT_USED(name);
+ REDISMODULE_NOT_USED(err);
+ REDISMODULE_NOT_USED(privdata);
+ enumval = val;
+ return REDISMODULE_OK;
+}
+
+int getFlagsConfigCommand(const char *name, void *privdata) {
+ REDISMODULE_NOT_USED(name);
+ REDISMODULE_NOT_USED(privdata);
+ return flagsval;
+}
+
+int setFlagsConfigCommand(const char *name, int val, void *privdata, RedisModuleString **err) {
+ REDISMODULE_NOT_USED(name);
+ REDISMODULE_NOT_USED(err);
+ REDISMODULE_NOT_USED(privdata);
+ flagsval = val;
+ return REDISMODULE_OK;
+}
+
+int boolApplyFunc(RedisModuleCtx *ctx, void *privdata, RedisModuleString **err) {
+ REDISMODULE_NOT_USED(ctx);
+ REDISMODULE_NOT_USED(privdata);
+ if (mutable_bool_val && immutable_bool_val) {
+ *err = RedisModule_CreateString(NULL, "Bool configs cannot both be yes.", 32);
+ return REDISMODULE_ERR;
+ }
+ return REDISMODULE_OK;
+}
+
+int longlongApplyFunc(RedisModuleCtx *ctx, void *privdata, RedisModuleString **err) {
+ REDISMODULE_NOT_USED(ctx);
+ REDISMODULE_NOT_USED(privdata);
+ if (longval == memval) {
+ *err = RedisModule_CreateString(NULL, "These configs cannot equal each other.", 38);
+ return REDISMODULE_ERR;
+ }
+ return REDISMODULE_OK;
+}
+
+RedisModuleString *getStringConfigUnprefix(const char *name, void *privdata) {
+ REDISMODULE_NOT_USED(name);
+ REDISMODULE_NOT_USED(privdata);
+ return strval2;
+}
+
+int setStringConfigUnprefix(const char *name, RedisModuleString *new, void *privdata, RedisModuleString **err) {
+ REDISMODULE_NOT_USED(name);
+ REDISMODULE_NOT_USED(err);
+ REDISMODULE_NOT_USED(privdata);
+ if (strval2) RedisModule_FreeString(NULL, strval2);
+ RedisModule_RetainString(NULL, new);
+ strval2 = new;
+ return REDISMODULE_OK;
+}
+
+int getEnumConfigUnprefix(const char *name, void *privdata) {
+ REDISMODULE_NOT_USED(name);
+ REDISMODULE_NOT_USED(privdata);
+ return no_prefix_enumval;
+}
+
+int setEnumConfigUnprefix(const char *name, int val, void *privdata, RedisModuleString **err) {
+ REDISMODULE_NOT_USED(name);
+ REDISMODULE_NOT_USED(err);
+ REDISMODULE_NOT_USED(privdata);
+ no_prefix_enumval = val;
+ return REDISMODULE_OK;
+}
+
+int registerBlockCheck(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ int response_ok = 0;
+ int result = RedisModule_RegisterBoolConfig(ctx, "mutable_bool", 1, REDISMODULE_CONFIG_DEFAULT, getBoolConfigCommand, setBoolConfigCommand, boolApplyFunc, &mutable_bool_val);
+ response_ok |= (result == REDISMODULE_OK);
+
+ result = RedisModule_RegisterStringConfig(ctx, "string", "secret password", REDISMODULE_CONFIG_DEFAULT, getStringConfigCommand, setStringConfigCommand, NULL, NULL);
+ response_ok |= (result == REDISMODULE_OK);
+
+ const char *enum_vals[] = {"none", "five", "one", "two", "four"};
+ const int int_vals[] = {0, 5, 1, 2, 4};
+ result = RedisModule_RegisterEnumConfig(ctx, "enum", 1, REDISMODULE_CONFIG_DEFAULT, enum_vals, int_vals, 5, getEnumConfigCommand, setEnumConfigCommand, NULL, NULL);
+ response_ok |= (result == REDISMODULE_OK);
+
+ result = RedisModule_RegisterNumericConfig(ctx, "numeric", -1, REDISMODULE_CONFIG_DEFAULT, -5, 2000, getNumericConfigCommand, setNumericConfigCommand, longlongApplyFunc, &longval);
+ response_ok |= (result == REDISMODULE_OK);
+
+ result = RedisModule_LoadConfigs(ctx);
+ response_ok |= (result == REDISMODULE_OK);
+
+ /* This validates that it's not possible to register/load configs outside OnLoad,
+ * thus returns an error if they succeed. */
+ if (response_ok) {
+ RedisModule_ReplyWithError(ctx, "UNEXPECTEDOK");
+ } else {
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ }
+ return REDISMODULE_OK;
+}
+
+void cleanup(RedisModuleCtx *ctx) {
+ if (strval) {
+ RedisModule_FreeString(ctx, strval);
+ strval = NULL;
+ }
+ if (strval2) {
+ RedisModule_FreeString(ctx, strval2);
+ strval2 = NULL;
+ }
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ if (RedisModule_Init(ctx, "moduleconfigs", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) {
+ RedisModule_Log(ctx, "warning", "Failed to init module");
+ return REDISMODULE_ERR;
+ }
+
+ if (RedisModule_RegisterBoolConfig(ctx, "mutable_bool", 1, REDISMODULE_CONFIG_DEFAULT, getBoolConfigCommand, setBoolConfigCommand, boolApplyFunc, &mutable_bool_val) == REDISMODULE_ERR) {
+ RedisModule_Log(ctx, "warning", "Failed to register mutable_bool");
+ return REDISMODULE_ERR;
+ }
+ /* Immutable config here. */
+ if (RedisModule_RegisterBoolConfig(ctx, "immutable_bool", 0, REDISMODULE_CONFIG_IMMUTABLE, getBoolConfigCommand, setBoolConfigCommand, boolApplyFunc, &immutable_bool_val) == REDISMODULE_ERR) {
+ RedisModule_Log(ctx, "warning", "Failed to register immutable_bool");
+ return REDISMODULE_ERR;
+ }
+ if (RedisModule_RegisterStringConfig(ctx, "string", "secret password", REDISMODULE_CONFIG_DEFAULT, getStringConfigCommand, setStringConfigCommand, NULL, NULL) == REDISMODULE_ERR) {
+ RedisModule_Log(ctx, "warning", "Failed to register string");
+ return REDISMODULE_ERR;
+ }
+
+ /* On the stack to make sure we're copying them. */
+ const char *enum_vals[] = {"none", "five", "one", "two", "four"};
+ const int int_vals[] = {0, 5, 1, 2, 4};
+
+ if (RedisModule_RegisterEnumConfig(ctx, "enum", 1, REDISMODULE_CONFIG_DEFAULT, enum_vals, int_vals, 5, getEnumConfigCommand, setEnumConfigCommand, NULL, NULL) == REDISMODULE_ERR) {
+ RedisModule_Log(ctx, "warning", "Failed to register enum");
+ return REDISMODULE_ERR;
+ }
+ if (RedisModule_RegisterEnumConfig(ctx, "flags", 3, REDISMODULE_CONFIG_DEFAULT | REDISMODULE_CONFIG_BITFLAGS, enum_vals, int_vals, 5, getFlagsConfigCommand, setFlagsConfigCommand, NULL, NULL) == REDISMODULE_ERR) {
+ RedisModule_Log(ctx, "warning", "Failed to register flags");
+ return REDISMODULE_ERR;
+ }
+ /* Memory config here. */
+ if (RedisModule_RegisterNumericConfig(ctx, "memory_numeric", 1024, REDISMODULE_CONFIG_DEFAULT | REDISMODULE_CONFIG_MEMORY, 0, 3000000, getNumericConfigCommand, setNumericConfigCommand, longlongApplyFunc, &memval) == REDISMODULE_ERR) {
+ RedisModule_Log(ctx, "warning", "Failed to register memory_numeric");
+ return REDISMODULE_ERR;
+ }
+ if (RedisModule_RegisterNumericConfig(ctx, "numeric", -1, REDISMODULE_CONFIG_DEFAULT, -5, 2000, getNumericConfigCommand, setNumericConfigCommand, longlongApplyFunc, &longval) == REDISMODULE_ERR) {
+ RedisModule_Log(ctx, "warning", "Failed to register numeric");
+ return REDISMODULE_ERR;
+ }
+
+ /*** unprefixed and aliased configuration ***/
+ if (RedisModule_RegisterBoolConfig(ctx, "unprefix-bool|unprefix-bool-alias", 1, REDISMODULE_CONFIG_DEFAULT|REDISMODULE_CONFIG_UNPREFIXED,
+ getBoolConfigCommand, setBoolConfigCommand, NULL, &no_prefix_bool) == REDISMODULE_ERR) {
+ RedisModule_Log(ctx, "warning", "Failed to register unprefix-bool");
+ return REDISMODULE_ERR;
+ }
+ if (RedisModule_RegisterBoolConfig(ctx, "unprefix-noalias-bool", 1, REDISMODULE_CONFIG_DEFAULT|REDISMODULE_CONFIG_UNPREFIXED,
+ getBoolConfigCommand, setBoolConfigCommand, NULL, &no_prefix_bool2) == REDISMODULE_ERR) {
+ RedisModule_Log(ctx, "warning", "Failed to register unprefix-noalias-bool");
+ return REDISMODULE_ERR;
+ }
+ if (RedisModule_RegisterNumericConfig(ctx, "unprefix.numeric|unprefix.numeric-alias", -1, REDISMODULE_CONFIG_DEFAULT|REDISMODULE_CONFIG_UNPREFIXED,
+ -5, 2000, getNumericConfigCommand, setNumericConfigCommand, NULL, &no_prefix_longval) == REDISMODULE_ERR) {
+ RedisModule_Log(ctx, "warning", "Failed to register unprefix.numeric");
+ return REDISMODULE_ERR;
+ }
+ if (RedisModule_RegisterStringConfig(ctx, "unprefix-string|unprefix.string-alias", "secret unprefix", REDISMODULE_CONFIG_DEFAULT|REDISMODULE_CONFIG_UNPREFIXED,
+ getStringConfigUnprefix, setStringConfigUnprefix, NULL, NULL) == REDISMODULE_ERR) {
+ RedisModule_Log(ctx, "warning", "Failed to register unprefix-string");
+ return REDISMODULE_ERR;
+ }
+ if (RedisModule_RegisterEnumConfig(ctx, "unprefix-enum|unprefix-enum-alias", 1, REDISMODULE_CONFIG_DEFAULT|REDISMODULE_CONFIG_UNPREFIXED,
+ enum_vals, int_vals, 5, getEnumConfigUnprefix, setEnumConfigUnprefix, NULL, NULL) == REDISMODULE_ERR) {
+ RedisModule_Log(ctx, "warning", "Failed to register unprefix-enum");
+ return REDISMODULE_ERR;
+ }
+
+ RedisModule_Log(ctx, "debug", "Registered configuration");
+ size_t len;
+ if (argc && !strcasecmp(RedisModule_StringPtrLen(argv[0], &len), "noload")) {
+ return REDISMODULE_OK;
+ } else if (argc && !strcasecmp(RedisModule_StringPtrLen(argv[0], &len), "override-default")) {
+ if (RedisModule_LoadDefaultConfigs(ctx) == REDISMODULE_ERR) {
+ RedisModule_Log(ctx, "warning", "Failed to load default configuration");
+ goto err;
+ }
+ // simulate configuration values being overwritten by the command line
+ RedisModule_Log(ctx, "debug", "Overriding configuration values");
+ if (strval) RedisModule_FreeString(ctx, strval);
+ strval = RedisModule_CreateString(ctx, "foo", 3);
+ longval = memval = 123;
+ }
+ RedisModule_Log(ctx, "debug", "Loading configuration");
+ if (RedisModule_LoadConfigs(ctx) == REDISMODULE_ERR) {
+ RedisModule_Log(ctx, "warning", "Failed to load configuration");
+ goto err;
+ }
+ /* Creates a command which registers configs outside OnLoad() function. */
+ if (RedisModule_CreateCommand(ctx,"block.register.configs.outside.onload", registerBlockCheck, "write", 0, 0, 0) == REDISMODULE_ERR) {
+ RedisModule_Log(ctx, "warning", "Failed to register command");
+ goto err;
+ }
+
+ return REDISMODULE_OK;
+err:
+ cleanup(ctx);
+ return REDISMODULE_ERR;
+}
+
+int RedisModule_OnUnload(RedisModuleCtx *ctx) {
+ REDISMODULE_NOT_USED(ctx);
+ cleanup(ctx);
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/moduleconfigstwo.c b/examples/redis-unstable/tests/modules/moduleconfigstwo.c
new file mode 100644
index 0000000..c0e8f91
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/moduleconfigstwo.c
@@ -0,0 +1,39 @@
+#include "redismodule.h"
+#include <strings.h>
+
+/* Second module configs module, for testing.
+ * Need to make sure that multiple modules with configs don't interfere with each other */
+int bool_config;
+
+int getBoolConfigCommand(const char *name, void *privdata) {
+ REDISMODULE_NOT_USED(privdata);
+ if (!strcasecmp(name, "test")) {
+ return bool_config;
+ }
+ return 0;
+}
+
+int setBoolConfigCommand(const char *name, int new, void *privdata, RedisModuleString **err) {
+ REDISMODULE_NOT_USED(privdata);
+ REDISMODULE_NOT_USED(err);
+ if (!strcasecmp(name, "test")) {
+ bool_config = new;
+ return REDISMODULE_OK;
+ }
+ return REDISMODULE_ERR;
+}
+
+/* No arguments are expected */
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ if (RedisModule_Init(ctx, "configs", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) return REDISMODULE_ERR;
+
+ if (RedisModule_RegisterBoolConfig(ctx, "test", 1, REDISMODULE_CONFIG_DEFAULT, getBoolConfigCommand, setBoolConfigCommand, NULL, &argc) == REDISMODULE_ERR) {
+ return REDISMODULE_ERR;
+ }
+ if (RedisModule_LoadConfigs(ctx) == REDISMODULE_ERR) {
+ return REDISMODULE_ERR;
+ }
+ return REDISMODULE_OK;
+} \ No newline at end of file
diff --git a/examples/redis-unstable/tests/modules/postnotifications.c b/examples/redis-unstable/tests/modules/postnotifications.c
new file mode 100644
index 0000000..96fb859
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/postnotifications.c
@@ -0,0 +1,289 @@
+/* This module is used to test the server post keyspace jobs API.
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (c) 2020-Present, Redis Ltd.
+ * All rights reserved.
+ *
+ * Licensed under your choice of (a) the Redis Source Available License 2.0
+ * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
+ * GNU Affero General Public License v3 (AGPLv3).
+ */
+
+/* This module allow to verify 'RedisModule_AddPostNotificationJob' by registering to 3
+ * key space event:
+ * * STRINGS - the module register to all strings notifications and set post notification job
+ * that increase a counter indicating how many times the string key was changed.
+ * In addition, it increase another counter that counts the total changes that
+ * was made on all strings keys.
+ * * EXPIRED - the module register to expired event and set post notification job that that
+ * counts the total number of expired events.
+ * * EVICTED - the module register to evicted event and set post notification job that that
+ * counts the total number of evicted events.
+ *
+ * In addition, the module register a new command, 'postnotification.async_set', that performs a set
+ * command from a background thread. This allows to check the 'RedisModule_AddPostNotificationJob' on
+ * notifications that was triggered on a background thread. */
+
+#define _BSD_SOURCE
+#define _DEFAULT_SOURCE /* For usleep */
+
+#include "redismodule.h"
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <pthread.h>
+
+static void KeySpace_PostNotificationStringFreePD(void *pd) {
+ RedisModule_FreeString(NULL, pd);
+}
+
+static void KeySpace_PostNotificationReadKey(RedisModuleCtx *ctx, void *pd) {
+ RedisModuleCallReply* rep = RedisModule_Call(ctx, "get", "!s", pd);
+ RedisModule_FreeCallReply(rep);
+}
+
+static void KeySpace_PostNotificationString(RedisModuleCtx *ctx, void *pd) {
+ REDISMODULE_NOT_USED(ctx);
+ RedisModuleCallReply* rep = RedisModule_Call(ctx, "incr", "!s", pd);
+ RedisModule_FreeCallReply(rep);
+}
+
+static int KeySpace_NotificationExpired(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key){
+ REDISMODULE_NOT_USED(type);
+ REDISMODULE_NOT_USED(event);
+ REDISMODULE_NOT_USED(key);
+
+ RedisModuleString *new_key = RedisModule_CreateString(NULL, "expired", 7);
+ int res = RedisModule_AddPostNotificationJob(ctx, KeySpace_PostNotificationString, new_key, KeySpace_PostNotificationStringFreePD);
+ if (res == REDISMODULE_ERR) KeySpace_PostNotificationStringFreePD(new_key);
+ return REDISMODULE_OK;
+}
+
+static int KeySpace_NotificationEvicted(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key){
+ REDISMODULE_NOT_USED(type);
+ REDISMODULE_NOT_USED(event);
+ REDISMODULE_NOT_USED(key);
+
+ const char *key_str = RedisModule_StringPtrLen(key, NULL);
+
+ if (strncmp(key_str, "evicted", 7) == 0) {
+ return REDISMODULE_OK; /* do not count the evicted key */
+ }
+
+ if (strncmp(key_str, "before_evicted", 14) == 0) {
+ return REDISMODULE_OK; /* do not count the before_evicted key */
+ }
+
+ RedisModuleString *new_key = RedisModule_CreateString(NULL, "evicted", 7);
+ int res = RedisModule_AddPostNotificationJob(ctx, KeySpace_PostNotificationString, new_key, KeySpace_PostNotificationStringFreePD);
+ if (res == REDISMODULE_ERR) KeySpace_PostNotificationStringFreePD(new_key);
+ return REDISMODULE_OK;
+}
+
+static int KeySpace_NotificationString(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key){
+ REDISMODULE_NOT_USED(ctx);
+ REDISMODULE_NOT_USED(type);
+ REDISMODULE_NOT_USED(event);
+
+ const char *key_str = RedisModule_StringPtrLen(key, NULL);
+
+ if (strncmp(key_str, "string_", 7) != 0) {
+ return REDISMODULE_OK;
+ }
+
+ if (strcmp(key_str, "string_total") == 0) {
+ return REDISMODULE_OK;
+ }
+
+ RedisModuleString *new_key;
+ if (strncmp(key_str, "string_changed{", 15) == 0) {
+ new_key = RedisModule_CreateString(NULL, "string_total", 12);
+ } else {
+ new_key = RedisModule_CreateStringPrintf(NULL, "string_changed{%s}", key_str);
+ }
+
+ int res = RedisModule_AddPostNotificationJob(ctx, KeySpace_PostNotificationString, new_key, KeySpace_PostNotificationStringFreePD);
+ if (res == REDISMODULE_ERR) KeySpace_PostNotificationStringFreePD(new_key);
+ return REDISMODULE_OK;
+}
+
+static int KeySpace_LazyExpireInsidePostNotificationJob(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key){
+ REDISMODULE_NOT_USED(ctx);
+ REDISMODULE_NOT_USED(type);
+ REDISMODULE_NOT_USED(event);
+
+ const char *key_str = RedisModule_StringPtrLen(key, NULL);
+
+ if (strncmp(key_str, "read_", 5) != 0) {
+ return REDISMODULE_OK;
+ }
+
+ RedisModuleString *new_key = RedisModule_CreateString(NULL, key_str + 5, strlen(key_str) - 5);;
+ int res = RedisModule_AddPostNotificationJob(ctx, KeySpace_PostNotificationReadKey, new_key, KeySpace_PostNotificationStringFreePD);
+ if (res == REDISMODULE_ERR) KeySpace_PostNotificationStringFreePD(new_key);
+ return REDISMODULE_OK;
+}
+
+static int KeySpace_NestedNotification(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key){
+ REDISMODULE_NOT_USED(ctx);
+ REDISMODULE_NOT_USED(type);
+ REDISMODULE_NOT_USED(event);
+
+ const char *key_str = RedisModule_StringPtrLen(key, NULL);
+
+ if (strncmp(key_str, "write_sync_", 11) != 0) {
+ return REDISMODULE_OK;
+ }
+
+ /* This test was only meant to check REDISMODULE_OPTIONS_ALLOW_NESTED_KEYSPACE_NOTIFICATIONS.
+ * In general it is wrong and discourage to perform any writes inside a notification callback. */
+ RedisModuleString *new_key = RedisModule_CreateString(NULL, key_str + 11, strlen(key_str) - 11);;
+ RedisModuleCallReply* rep = RedisModule_Call(ctx, "set", "!sc", new_key, "1");
+ RedisModule_FreeCallReply(rep);
+ RedisModule_FreeString(NULL, new_key);
+ return REDISMODULE_OK;
+}
+
+static void *KeySpace_PostNotificationsAsyncSetInner(void *arg) {
+ RedisModuleBlockedClient *bc = arg;
+ RedisModuleCtx *ctx = RedisModule_GetThreadSafeContext(bc);
+ RedisModule_ThreadSafeContextLock(ctx);
+ RedisModuleCallReply* rep = RedisModule_Call(ctx, "set", "!cc", "string_x", "1");
+ RedisModule_ThreadSafeContextUnlock(ctx);
+ RedisModule_ReplyWithCallReply(ctx, rep);
+ RedisModule_FreeCallReply(rep);
+
+ RedisModule_UnblockClient(bc, NULL);
+ RedisModule_FreeThreadSafeContext(ctx);
+ return NULL;
+}
+
+static int KeySpace_PostNotificationsAsyncSet(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ if (argc != 1)
+ return RedisModule_WrongArity(ctx);
+
+ pthread_t tid;
+ RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx,NULL,NULL,NULL,0);
+
+ if (pthread_create(&tid,NULL,KeySpace_PostNotificationsAsyncSetInner,bc) != 0) {
+ RedisModule_AbortBlock(bc);
+ return RedisModule_ReplyWithError(ctx,"-ERR Can't start thread");
+ }
+ pthread_detach(tid);
+ return REDISMODULE_OK;
+}
+
+typedef struct KeySpace_EventPostNotificationCtx {
+ RedisModuleString *triggered_on;
+ RedisModuleString *new_key;
+} KeySpace_EventPostNotificationCtx;
+
+static void KeySpace_ServerEventPostNotificationFree(void *pd) {
+ KeySpace_EventPostNotificationCtx *pn_ctx = pd;
+ RedisModule_FreeString(NULL, pn_ctx->new_key);
+ RedisModule_FreeString(NULL, pn_ctx->triggered_on);
+ RedisModule_Free(pn_ctx);
+}
+
+static void KeySpace_ServerEventPostNotification(RedisModuleCtx *ctx, void *pd) {
+ REDISMODULE_NOT_USED(ctx);
+ KeySpace_EventPostNotificationCtx *pn_ctx = pd;
+ RedisModuleCallReply* rep = RedisModule_Call(ctx, "lpush", "!ss", pn_ctx->new_key, pn_ctx->triggered_on);
+ RedisModule_FreeCallReply(rep);
+}
+
+static void KeySpace_ServerEventCallback(RedisModuleCtx *ctx, RedisModuleEvent eid, uint64_t subevent, void *data) {
+ REDISMODULE_NOT_USED(eid);
+ REDISMODULE_NOT_USED(data);
+ if (subevent > 3) {
+ RedisModule_Log(ctx, "warning", "Got an unexpected subevent '%llu'", (unsigned long long)subevent);
+ return;
+ }
+ static const char* events[] = {
+ "before_deleted",
+ "before_expired",
+ "before_evicted",
+ "before_overwritten",
+ };
+
+ const RedisModuleString *key_name = RedisModule_GetKeyNameFromModuleKey(((RedisModuleKeyInfo*)data)->key);
+ const char *key_str = RedisModule_StringPtrLen(key_name, NULL);
+
+ for (int i = 0 ; i < 4 ; ++i) {
+ const char *event = events[i];
+ if (strncmp(key_str, event , strlen(event)) == 0) {
+ return; /* don't log any event on our tracking keys */
+ }
+ }
+
+ KeySpace_EventPostNotificationCtx *pn_ctx = RedisModule_Alloc(sizeof(*pn_ctx));
+ pn_ctx->triggered_on = RedisModule_HoldString(NULL, (RedisModuleString*)key_name);
+ pn_ctx->new_key = RedisModule_CreateString(NULL, events[subevent], strlen(events[subevent]));
+ int res = RedisModule_AddPostNotificationJob(ctx, KeySpace_ServerEventPostNotification, pn_ctx, KeySpace_ServerEventPostNotificationFree);
+ if (res == REDISMODULE_ERR) KeySpace_ServerEventPostNotificationFree(pn_ctx);
+}
+
+/* This function must be present on each Redis module. It is used in order to
+ * register the commands into the Redis server. */
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ if (RedisModule_Init(ctx,"postnotifications",1,REDISMODULE_APIVER_1) == REDISMODULE_ERR){
+ return REDISMODULE_ERR;
+ }
+
+ if (!(RedisModule_GetModuleOptionsAll() & REDISMODULE_OPTIONS_ALLOW_NESTED_KEYSPACE_NOTIFICATIONS)) {
+ return REDISMODULE_ERR;
+ }
+
+ int with_key_events = 0;
+ if (argc >= 1) {
+ const char *arg = RedisModule_StringPtrLen(argv[0], 0);
+ if (strcmp(arg, "with_key_events") == 0) {
+ with_key_events = 1;
+ }
+ }
+
+ RedisModule_SetModuleOptions(ctx, REDISMODULE_OPTIONS_ALLOW_NESTED_KEYSPACE_NOTIFICATIONS);
+
+ if(RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_STRING, KeySpace_NotificationString) != REDISMODULE_OK){
+ return REDISMODULE_ERR;
+ }
+
+ if(RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_STRING, KeySpace_LazyExpireInsidePostNotificationJob) != REDISMODULE_OK){
+ return REDISMODULE_ERR;
+ }
+
+ if(RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_STRING, KeySpace_NestedNotification) != REDISMODULE_OK){
+ return REDISMODULE_ERR;
+ }
+
+ if(RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_EXPIRED, KeySpace_NotificationExpired) != REDISMODULE_OK){
+ return REDISMODULE_ERR;
+ }
+
+ if(RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_EVICTED, KeySpace_NotificationEvicted) != REDISMODULE_OK){
+ return REDISMODULE_ERR;
+ }
+
+ if (with_key_events) {
+ if(RedisModule_SubscribeToServerEvent(ctx, RedisModuleEvent_Key, KeySpace_ServerEventCallback) != REDISMODULE_OK){
+ return REDISMODULE_ERR;
+ }
+ }
+
+ if (RedisModule_CreateCommand(ctx, "postnotification.async_set", KeySpace_PostNotificationsAsyncSet,
+ "write", 0, 0, 0) == REDISMODULE_ERR){
+ return REDISMODULE_ERR;
+ }
+
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnUnload(RedisModuleCtx *ctx) {
+ REDISMODULE_NOT_USED(ctx);
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/propagate.c b/examples/redis-unstable/tests/modules/propagate.c
new file mode 100644
index 0000000..689d688
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/propagate.c
@@ -0,0 +1,403 @@
+/* This module is used to test the propagation (replication + AOF) of
+ * commands, via the RedisModule_Replicate() interface, in asynchronous
+ * contexts, such as callbacks not implementing commands, and thread safe
+ * contexts.
+ *
+ * We create a timer callback and a threads using a thread safe context.
+ * Using both we try to propagate counters increments, and later we check
+ * if the replica contains the changes as expected.
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Copyright (c) 2019-Present, Redis Ltd.
+ * All rights reserved.
+ *
+ * Licensed under your choice of (a) the Redis Source Available License 2.0
+ * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
+ * GNU Affero General Public License v3 (AGPLv3).
+ */
+
+#include "redismodule.h"
+#include <pthread.h>
+#include <errno.h>
+
+#define UNUSED(V) ((void) V)
+
+RedisModuleCtx *detached_ctx = NULL;
+
+static int KeySpace_NotificationGeneric(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key) {
+ REDISMODULE_NOT_USED(type);
+ REDISMODULE_NOT_USED(event);
+ REDISMODULE_NOT_USED(key);
+
+ RedisModuleCallReply* rep = RedisModule_Call(ctx, "INCR", "c!", "notifications");
+ RedisModule_FreeCallReply(rep);
+
+ return REDISMODULE_OK;
+}
+
+/* Timer callback. */
+void timerHandler(RedisModuleCtx *ctx, void *data) {
+ REDISMODULE_NOT_USED(ctx);
+ REDISMODULE_NOT_USED(data);
+
+ static int times = 0;
+
+ RedisModule_Replicate(ctx,"INCR","c","timer");
+ times++;
+
+ if (times < 3)
+ RedisModule_CreateTimer(ctx,100,timerHandler,NULL);
+ else
+ times = 0;
+}
+
+int propagateTestTimerCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ RedisModuleTimerID timer_id =
+ RedisModule_CreateTimer(ctx,100,timerHandler,NULL);
+ REDISMODULE_NOT_USED(timer_id);
+
+ RedisModule_ReplyWithSimpleString(ctx,"OK");
+ return REDISMODULE_OK;
+}
+
+/* Timer callback. */
+void timerNestedHandler(RedisModuleCtx *ctx, void *data) {
+ int repl = (long long)data;
+
+ /* The goal is the trigger a module command that calls RM_Replicate
+ * in order to test MULTI/EXEC structure */
+ RedisModule_Replicate(ctx,"INCRBY","cc","timer-nested-start","1");
+ RedisModuleCallReply *reply = RedisModule_Call(ctx,"propagate-test.nested", repl? "!" : "");
+ RedisModule_FreeCallReply(reply);
+ reply = RedisModule_Call(ctx, "INCR", repl? "c!" : "c", "timer-nested-middle");
+ RedisModule_FreeCallReply(reply);
+ RedisModule_Replicate(ctx,"INCRBY","cc","timer-nested-end","1");
+}
+
+int propagateTestTimerNestedCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ RedisModuleTimerID timer_id =
+ RedisModule_CreateTimer(ctx,100,timerNestedHandler,(void*)0);
+ REDISMODULE_NOT_USED(timer_id);
+
+ RedisModule_ReplyWithSimpleString(ctx,"OK");
+ return REDISMODULE_OK;
+}
+
+int propagateTestTimerNestedReplCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ RedisModuleTimerID timer_id =
+ RedisModule_CreateTimer(ctx,100,timerNestedHandler,(void*)1);
+ REDISMODULE_NOT_USED(timer_id);
+
+ RedisModule_ReplyWithSimpleString(ctx,"OK");
+ return REDISMODULE_OK;
+}
+
+void timerHandlerMaxmemory(RedisModuleCtx *ctx, void *data) {
+ REDISMODULE_NOT_USED(ctx);
+ REDISMODULE_NOT_USED(data);
+
+ RedisModuleCallReply *reply = RedisModule_Call(ctx,"SETEX","ccc!","timer-maxmemory-volatile-start","100","1");
+ RedisModule_FreeCallReply(reply);
+ reply = RedisModule_Call(ctx, "CONFIG", "ccc!", "SET", "maxmemory", "1");
+ RedisModule_FreeCallReply(reply);
+
+ RedisModule_Replicate(ctx, "INCR", "c", "timer-maxmemory-middle");
+
+ reply = RedisModule_Call(ctx,"SETEX","ccc!","timer-maxmemory-volatile-end","100","1");
+ RedisModule_FreeCallReply(reply);
+}
+
+int propagateTestTimerMaxmemoryCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ RedisModuleTimerID timer_id =
+ RedisModule_CreateTimer(ctx,100,timerHandlerMaxmemory,(void*)1);
+ REDISMODULE_NOT_USED(timer_id);
+
+ RedisModule_ReplyWithSimpleString(ctx,"OK");
+ return REDISMODULE_OK;
+}
+
+void timerHandlerEval(RedisModuleCtx *ctx, void *data) {
+ REDISMODULE_NOT_USED(ctx);
+ REDISMODULE_NOT_USED(data);
+
+ RedisModuleCallReply *reply = RedisModule_Call(ctx,"INCRBY","cc!","timer-eval-start","1");
+ RedisModule_FreeCallReply(reply);
+ reply = RedisModule_Call(ctx, "EVAL", "cccc!", "redis.call('set',KEYS[1],ARGV[1])", "1", "foo", "bar");
+ RedisModule_FreeCallReply(reply);
+
+ RedisModule_Replicate(ctx, "INCR", "c", "timer-eval-middle");
+
+ reply = RedisModule_Call(ctx,"INCRBY","cc!","timer-eval-end","1");
+ RedisModule_FreeCallReply(reply);
+}
+
+int propagateTestTimerEvalCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ RedisModuleTimerID timer_id =
+ RedisModule_CreateTimer(ctx,100,timerHandlerEval,(void*)1);
+ REDISMODULE_NOT_USED(timer_id);
+
+ RedisModule_ReplyWithSimpleString(ctx,"OK");
+ return REDISMODULE_OK;
+}
+
+/* The thread entry point. */
+void *threadMain(void *arg) {
+ REDISMODULE_NOT_USED(arg);
+ RedisModuleCtx *ctx = RedisModule_GetThreadSafeContext(NULL);
+ RedisModule_SelectDb(ctx,9); /* Tests ran in database number 9. */
+ for (int i = 0; i < 3; i++) {
+ RedisModule_ThreadSafeContextLock(ctx);
+ RedisModule_Replicate(ctx,"INCR","c","a-from-thread");
+ RedisModuleCallReply *reply = RedisModule_Call(ctx,"INCR","c!","thread-call");
+ RedisModule_FreeCallReply(reply);
+ RedisModule_Replicate(ctx,"INCR","c","b-from-thread");
+ RedisModule_ThreadSafeContextUnlock(ctx);
+ }
+ RedisModule_FreeThreadSafeContext(ctx);
+ return NULL;
+}
+
+int propagateTestThreadCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ pthread_t tid;
+ if (pthread_create(&tid,NULL,threadMain,NULL) != 0)
+ return RedisModule_ReplyWithError(ctx,"-ERR Can't start thread");
+ pthread_detach(tid);
+
+ RedisModule_ReplyWithSimpleString(ctx,"OK");
+ return REDISMODULE_OK;
+}
+
+/* The thread entry point. */
+void *threadDetachedMain(void *arg) {
+ REDISMODULE_NOT_USED(arg);
+ RedisModule_SelectDb(detached_ctx,9); /* Tests ran in database number 9. */
+
+ RedisModule_ThreadSafeContextLock(detached_ctx);
+ RedisModule_Replicate(detached_ctx,"INCR","c","thread-detached-before");
+ RedisModuleCallReply *reply = RedisModule_Call(detached_ctx,"INCR","c!","thread-detached-1");
+ RedisModule_FreeCallReply(reply);
+ reply = RedisModule_Call(detached_ctx,"INCR","c!","thread-detached-2");
+ RedisModule_FreeCallReply(reply);
+ RedisModule_Replicate(detached_ctx,"INCR","c","thread-detached-after");
+ RedisModule_ThreadSafeContextUnlock(detached_ctx);
+
+ return NULL;
+}
+
+int propagateTestDetachedThreadCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ pthread_t tid;
+ if (pthread_create(&tid,NULL,threadDetachedMain,NULL) != 0)
+ return RedisModule_ReplyWithError(ctx,"-ERR Can't start thread");
+ pthread_detach(tid);
+
+ RedisModule_ReplyWithSimpleString(ctx,"OK");
+ return REDISMODULE_OK;
+}
+
+int propagateTestSimpleCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ /* Replicate two commands to test MULTI/EXEC wrapping. */
+ RedisModule_Replicate(ctx,"INCR","c","counter-1");
+ RedisModule_Replicate(ctx,"INCR","c","counter-2");
+ RedisModule_ReplyWithSimpleString(ctx,"OK");
+ return REDISMODULE_OK;
+}
+
+int propagateTestMixedCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ RedisModuleCallReply *reply;
+
+ /* This test mixes multiple propagation systems. */
+ reply = RedisModule_Call(ctx, "INCR", "c!", "using-call");
+ RedisModule_FreeCallReply(reply);
+
+ RedisModule_Replicate(ctx,"INCR","c","counter-1");
+ RedisModule_Replicate(ctx,"INCR","c","counter-2");
+
+ reply = RedisModule_Call(ctx, "INCR", "c!", "after-call");
+ RedisModule_FreeCallReply(reply);
+
+ RedisModule_ReplyWithSimpleString(ctx,"OK");
+ return REDISMODULE_OK;
+}
+
+int propagateTestNestedCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ RedisModuleCallReply *reply;
+
+ /* This test mixes multiple propagation systems. */
+ reply = RedisModule_Call(ctx, "INCR", "c!", "using-call");
+ RedisModule_FreeCallReply(reply);
+
+ reply = RedisModule_Call(ctx,"propagate-test.simple", "!");
+ RedisModule_FreeCallReply(reply);
+
+ RedisModule_Replicate(ctx,"INCR","c","counter-3");
+ RedisModule_Replicate(ctx,"INCR","c","counter-4");
+
+ reply = RedisModule_Call(ctx, "INCR", "c!", "after-call");
+ RedisModule_FreeCallReply(reply);
+
+ reply = RedisModule_Call(ctx, "INCR", "c!", "before-call-2");
+ RedisModule_FreeCallReply(reply);
+
+ reply = RedisModule_Call(ctx, "keyspace.incr_case1", "c!", "asdf"); /* Propagates INCR */
+ RedisModule_FreeCallReply(reply);
+
+ reply = RedisModule_Call(ctx, "keyspace.del_key_copy", "c!", "asdf"); /* Propagates DEL */
+ RedisModule_FreeCallReply(reply);
+
+ reply = RedisModule_Call(ctx, "INCR", "c!", "after-call-2");
+ RedisModule_FreeCallReply(reply);
+
+ RedisModule_ReplyWithSimpleString(ctx,"OK");
+ return REDISMODULE_OK;
+}
+
+int propagateTestIncr(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ REDISMODULE_NOT_USED(argc);
+ RedisModuleCallReply *reply;
+
+ /* This test propagates the module command, not the INCR it executes. */
+ reply = RedisModule_Call(ctx, "INCR", "s", argv[1]);
+ RedisModule_ReplyWithCallReply(ctx,reply);
+ RedisModule_FreeCallReply(reply);
+ RedisModule_ReplicateVerbatim(ctx);
+ return REDISMODULE_OK;
+}
+
+int propagateTestVerbatim(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc < 2){
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ long long replicate_num;
+ RedisModule_StringToLongLong(argv[1], &replicate_num);
+ /* Replicate the command verbatim for the specified number of times. */
+ for (long long i = 0; i < replicate_num; i++)
+ RedisModule_ReplicateVerbatim(ctx);
+ RedisModule_ReplyWithSimpleString(ctx,"OK");
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ if (RedisModule_Init(ctx,"propagate-test",1,REDISMODULE_APIVER_1)
+ == REDISMODULE_ERR) return REDISMODULE_ERR;
+
+ detached_ctx = RedisModule_GetDetachedThreadSafeContext(ctx);
+
+ if (RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_ALL, KeySpace_NotificationGeneric) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"propagate-test.timer",
+ propagateTestTimerCommand,
+ "",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"propagate-test.timer-nested",
+ propagateTestTimerNestedCommand,
+ "",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"propagate-test.timer-nested-repl",
+ propagateTestTimerNestedReplCommand,
+ "",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"propagate-test.timer-maxmemory",
+ propagateTestTimerMaxmemoryCommand,
+ "",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"propagate-test.timer-eval",
+ propagateTestTimerEvalCommand,
+ "",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"propagate-test.thread",
+ propagateTestThreadCommand,
+ "",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"propagate-test.detached-thread",
+ propagateTestDetachedThreadCommand,
+ "",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"propagate-test.simple",
+ propagateTestSimpleCommand,
+ "",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"propagate-test.mixed",
+ propagateTestMixedCommand,
+ "write",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"propagate-test.nested",
+ propagateTestNestedCommand,
+ "write",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"propagate-test.incr",
+ propagateTestIncr,
+ "write",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"propagate-test.verbatim",
+ propagateTestVerbatim,
+ "write",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnUnload(RedisModuleCtx *ctx) {
+ UNUSED(ctx);
+
+ if (detached_ctx)
+ RedisModule_FreeThreadSafeContext(detached_ctx);
+
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/publish.c b/examples/redis-unstable/tests/modules/publish.c
new file mode 100644
index 0000000..ff276d8
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/publish.c
@@ -0,0 +1,57 @@
+#include "redismodule.h"
+#include <string.h>
+#include <assert.h>
+#include <unistd.h>
+
+#define UNUSED(V) ((void) V)
+
+int cmd_publish_classic_multi(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ if (argc < 3)
+ return RedisModule_WrongArity(ctx);
+ RedisModule_ReplyWithArray(ctx, argc-2);
+ for (int i = 2; i < argc; i++) {
+ int receivers = RedisModule_PublishMessage(ctx, argv[1], argv[i]);
+ RedisModule_ReplyWithLongLong(ctx, receivers);
+ }
+ return REDISMODULE_OK;
+}
+
+int cmd_publish_classic(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ if (argc != 3)
+ return RedisModule_WrongArity(ctx);
+
+ int receivers = RedisModule_PublishMessage(ctx, argv[1], argv[2]);
+ RedisModule_ReplyWithLongLong(ctx, receivers);
+ return REDISMODULE_OK;
+}
+
+int cmd_publish_shard(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ if (argc != 3)
+ return RedisModule_WrongArity(ctx);
+
+ int receivers = RedisModule_PublishMessageShard(ctx, argv[1], argv[2]);
+ RedisModule_ReplyWithLongLong(ctx, receivers);
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ UNUSED(argv);
+ UNUSED(argc);
+
+ if (RedisModule_Init(ctx,"publish",1,REDISMODULE_APIVER_1)== REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"publish.classic",cmd_publish_classic,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"publish.classic_multi",cmd_publish_classic_multi,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"publish.shard",cmd_publish_shard,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/rdbloadsave.c b/examples/redis-unstable/tests/modules/rdbloadsave.c
new file mode 100644
index 0000000..687269a
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/rdbloadsave.c
@@ -0,0 +1,162 @@
+#include "redismodule.h"
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <memory.h>
+#include <errno.h>
+
+/* Sanity tests to verify inputs and return values. */
+int sanity(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ RedisModuleRdbStream *s = RedisModule_RdbStreamCreateFromFile("dbnew.rdb");
+
+ /* NULL stream should fail. */
+ if (RedisModule_RdbLoad(ctx, NULL, 0) == REDISMODULE_OK || errno != EINVAL) {
+ RedisModule_ReplyWithError(ctx, strerror(errno));
+ goto out;
+ }
+
+ /* Invalid flags should fail. */
+ if (RedisModule_RdbLoad(ctx, s, 188) == REDISMODULE_OK || errno != EINVAL) {
+ RedisModule_ReplyWithError(ctx, strerror(errno));
+ goto out;
+ }
+
+ /* Missing file should fail. */
+ if (RedisModule_RdbLoad(ctx, s, 0) == REDISMODULE_OK || errno != ENOENT) {
+ RedisModule_ReplyWithError(ctx, strerror(errno));
+ goto out;
+ }
+
+ /* Save RDB file. */
+ if (RedisModule_RdbSave(ctx, s, 0) != REDISMODULE_OK || errno != 0) {
+ RedisModule_ReplyWithError(ctx, strerror(errno));
+ goto out;
+ }
+
+ /* Load the saved RDB file. */
+ if (RedisModule_RdbLoad(ctx, s, 0) != REDISMODULE_OK || errno != 0) {
+ RedisModule_ReplyWithError(ctx, strerror(errno));
+ goto out;
+ }
+
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+
+ out:
+ RedisModule_RdbStreamFree(s);
+ return REDISMODULE_OK;
+}
+
+int cmd_rdbsave(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ size_t len;
+ const char *filename = RedisModule_StringPtrLen(argv[1], &len);
+
+ char tmp[len + 1];
+ memcpy(tmp, filename, len);
+ tmp[len] = '\0';
+
+ RedisModuleRdbStream *stream = RedisModule_RdbStreamCreateFromFile(tmp);
+
+ if (RedisModule_RdbSave(ctx, stream, 0) != REDISMODULE_OK || errno != 0) {
+ RedisModule_ReplyWithError(ctx, strerror(errno));
+ goto out;
+ }
+
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+
+out:
+ RedisModule_RdbStreamFree(stream);
+ return REDISMODULE_OK;
+}
+
+/* Fork before calling RM_RdbSave(). */
+int cmd_rdbsave_fork(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ size_t len;
+ const char *filename = RedisModule_StringPtrLen(argv[1], &len);
+
+ char tmp[len + 1];
+ memcpy(tmp, filename, len);
+ tmp[len] = '\0';
+
+ int fork_child_pid = RedisModule_Fork(NULL, NULL);
+ if (fork_child_pid < 0) {
+ RedisModule_ReplyWithError(ctx, strerror(errno));
+ return REDISMODULE_OK;
+ } else if (fork_child_pid > 0) {
+ /* parent */
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+ }
+
+ RedisModuleRdbStream *stream = RedisModule_RdbStreamCreateFromFile(tmp);
+
+ int ret = 0;
+ if (RedisModule_RdbSave(ctx, stream, 0) != REDISMODULE_OK) {
+ ret = errno;
+ }
+ RedisModule_RdbStreamFree(stream);
+
+ RedisModule_ExitFromChild(ret);
+ return REDISMODULE_OK;
+}
+
+int cmd_rdbload(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ size_t len;
+ const char *filename = RedisModule_StringPtrLen(argv[1], &len);
+
+ char tmp[len + 1];
+ memcpy(tmp, filename, len);
+ tmp[len] = '\0';
+
+ RedisModuleRdbStream *stream = RedisModule_RdbStreamCreateFromFile(tmp);
+
+ if (RedisModule_RdbLoad(ctx, stream, 0) != REDISMODULE_OK || errno != 0) {
+ RedisModule_RdbStreamFree(stream);
+ RedisModule_ReplyWithError(ctx, strerror(errno));
+ return REDISMODULE_OK;
+ }
+
+ RedisModule_RdbStreamFree(stream);
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ if (RedisModule_Init(ctx, "rdbloadsave", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "test.sanity", sanity, "", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "test.rdbsave", cmd_rdbsave, "", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "test.rdbsave_fork", cmd_rdbsave_fork, "", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "test.rdbload", cmd_rdbload, "", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/reply.c b/examples/redis-unstable/tests/modules/reply.c
new file mode 100644
index 0000000..c5baa66
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/reply.c
@@ -0,0 +1,214 @@
+/*
+ * A module the tests RM_ReplyWith family of commands
+ */
+
+#include "redismodule.h"
+#include <math.h>
+
+int rw_string(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2) return RedisModule_WrongArity(ctx);
+
+ return RedisModule_ReplyWithString(ctx, argv[1]);
+}
+
+int rw_cstring(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ if (argc != 1) return RedisModule_WrongArity(ctx);
+
+ return RedisModule_ReplyWithSimpleString(ctx, "A simple string");
+}
+
+int rw_int(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2) return RedisModule_WrongArity(ctx);
+
+ long long integer;
+ if (RedisModule_StringToLongLong(argv[1], &integer) != REDISMODULE_OK)
+ return RedisModule_ReplyWithError(ctx, "Arg cannot be parsed as an integer");
+
+ return RedisModule_ReplyWithLongLong(ctx, integer);
+}
+
+/* When one argument is given, it is returned as a double,
+ * when two arguments are given, it returns a/b. */
+int rw_double(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc==1)
+ return RedisModule_ReplyWithDouble(ctx, NAN);
+
+ if (argc != 2 && argc != 3) return RedisModule_WrongArity(ctx);
+
+ double dbl, dbl2;
+ if (RedisModule_StringToDouble(argv[1], &dbl) != REDISMODULE_OK)
+ return RedisModule_ReplyWithError(ctx, "Arg cannot be parsed as a double");
+ if (argc == 3) {
+ if (RedisModule_StringToDouble(argv[2], &dbl2) != REDISMODULE_OK)
+ return RedisModule_ReplyWithError(ctx, "Arg cannot be parsed as a double");
+ dbl /= dbl2;
+ }
+
+ return RedisModule_ReplyWithDouble(ctx, dbl);
+}
+
+int rw_longdouble(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2) return RedisModule_WrongArity(ctx);
+
+ long double longdbl;
+ if (RedisModule_StringToLongDouble(argv[1], &longdbl) != REDISMODULE_OK)
+ return RedisModule_ReplyWithError(ctx, "Arg cannot be parsed as a double");
+
+ return RedisModule_ReplyWithLongDouble(ctx, longdbl);
+}
+
+int rw_bignumber(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2) return RedisModule_WrongArity(ctx);
+
+ size_t bignum_len;
+ const char *bignum_str = RedisModule_StringPtrLen(argv[1], &bignum_len);
+
+ return RedisModule_ReplyWithBigNumber(ctx, bignum_str, bignum_len);
+}
+
+int rw_array(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2) return RedisModule_WrongArity(ctx);
+
+ long long integer;
+ if (RedisModule_StringToLongLong(argv[1], &integer) != REDISMODULE_OK)
+ return RedisModule_ReplyWithError(ctx, "Arg cannot be parsed as a integer");
+
+ RedisModule_ReplyWithArray(ctx, integer);
+ for (int i = 0; i < integer; ++i) {
+ RedisModule_ReplyWithLongLong(ctx, i);
+ }
+
+ return REDISMODULE_OK;
+}
+
+int rw_map(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2) return RedisModule_WrongArity(ctx);
+
+ long long integer;
+ if (RedisModule_StringToLongLong(argv[1], &integer) != REDISMODULE_OK)
+ return RedisModule_ReplyWithError(ctx, "Arg cannot be parsed as a integer");
+
+ RedisModule_ReplyWithMap(ctx, integer);
+ for (int i = 0; i < integer; ++i) {
+ RedisModule_ReplyWithLongLong(ctx, i);
+ RedisModule_ReplyWithDouble(ctx, i * 1.5);
+ }
+
+ return REDISMODULE_OK;
+}
+
+int rw_set(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2) return RedisModule_WrongArity(ctx);
+
+ long long integer;
+ if (RedisModule_StringToLongLong(argv[1], &integer) != REDISMODULE_OK)
+ return RedisModule_ReplyWithError(ctx, "Arg cannot be parsed as a integer");
+
+ RedisModule_ReplyWithSet(ctx, integer);
+ for (int i = 0; i < integer; ++i) {
+ RedisModule_ReplyWithLongLong(ctx, i);
+ }
+
+ return REDISMODULE_OK;
+}
+
+int rw_attribute(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2) return RedisModule_WrongArity(ctx);
+
+ long long integer;
+ if (RedisModule_StringToLongLong(argv[1], &integer) != REDISMODULE_OK)
+ return RedisModule_ReplyWithError(ctx, "Arg cannot be parsed as a integer");
+
+ if (RedisModule_ReplyWithAttribute(ctx, integer) != REDISMODULE_OK) {
+ return RedisModule_ReplyWithError(ctx, "Attributes aren't supported by RESP 2");
+ }
+
+ for (int i = 0; i < integer; ++i) {
+ RedisModule_ReplyWithLongLong(ctx, i);
+ RedisModule_ReplyWithDouble(ctx, i * 1.5);
+ }
+
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+int rw_bool(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ if (argc != 1) return RedisModule_WrongArity(ctx);
+
+ RedisModule_ReplyWithArray(ctx, 2);
+ RedisModule_ReplyWithBool(ctx, 0);
+ return RedisModule_ReplyWithBool(ctx, 1);
+}
+
+int rw_null(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ if (argc != 1) return RedisModule_WrongArity(ctx);
+
+ return RedisModule_ReplyWithNull(ctx);
+}
+
+int rw_error(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ if (argc != 1) return RedisModule_WrongArity(ctx);
+
+ return RedisModule_ReplyWithError(ctx, "An error");
+}
+
+int rw_error_format(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 3) return RedisModule_WrongArity(ctx);
+
+ return RedisModule_ReplyWithErrorFormat(ctx,
+ RedisModule_StringPtrLen(argv[1], NULL),
+ RedisModule_StringPtrLen(argv[2], NULL));
+}
+
+int rw_verbatim(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2) return RedisModule_WrongArity(ctx);
+
+ size_t verbatim_len;
+ const char *verbatim_str = RedisModule_StringPtrLen(argv[1], &verbatim_len);
+
+ return RedisModule_ReplyWithVerbatimString(ctx, verbatim_str, verbatim_len);
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ if (RedisModule_Init(ctx, "replywith", 1, REDISMODULE_APIVER_1) != REDISMODULE_OK)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"rw.string",rw_string,"",0,0,0) != REDISMODULE_OK)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"rw.cstring",rw_cstring,"",0,0,0) != REDISMODULE_OK)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"rw.bignumber",rw_bignumber,"",0,0,0) != REDISMODULE_OK)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"rw.int",rw_int,"",0,0,0) != REDISMODULE_OK)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"rw.double",rw_double,"",0,0,0) != REDISMODULE_OK)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"rw.longdouble",rw_longdouble,"",0,0,0) != REDISMODULE_OK)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"rw.array",rw_array,"",0,0,0) != REDISMODULE_OK)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"rw.map",rw_map,"",0,0,0) != REDISMODULE_OK)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"rw.attribute",rw_attribute,"",0,0,0) != REDISMODULE_OK)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"rw.set",rw_set,"",0,0,0) != REDISMODULE_OK)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"rw.bool",rw_bool,"",0,0,0) != REDISMODULE_OK)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"rw.null",rw_null,"",0,0,0) != REDISMODULE_OK)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"rw.error",rw_error,"",0,0,0) != REDISMODULE_OK)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"rw.error_format",rw_error_format,"",0,0,0) != REDISMODULE_OK)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"rw.verbatim",rw_verbatim,"",0,0,0) != REDISMODULE_OK)
+ return REDISMODULE_ERR;
+
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/scan.c b/examples/redis-unstable/tests/modules/scan.c
new file mode 100644
index 0000000..1723d30
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/scan.c
@@ -0,0 +1,121 @@
+#include "redismodule.h"
+
+#include <string.h>
+#include <assert.h>
+#include <unistd.h>
+
+typedef struct {
+ size_t nkeys;
+} scan_strings_pd;
+
+void scan_strings_callback(RedisModuleCtx *ctx, RedisModuleString* keyname, RedisModuleKey* key, void *privdata) {
+ scan_strings_pd* pd = privdata;
+ int was_opened = 0;
+ if (!key) {
+ key = RedisModule_OpenKey(ctx, keyname, REDISMODULE_READ);
+ was_opened = 1;
+ }
+
+ if (RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_STRING) {
+ size_t len;
+ char * data = RedisModule_StringDMA(key, &len, REDISMODULE_READ);
+ RedisModule_ReplyWithArray(ctx, 2);
+ RedisModule_ReplyWithString(ctx, keyname);
+ RedisModule_ReplyWithStringBuffer(ctx, data, len);
+ pd->nkeys++;
+ }
+ if (was_opened)
+ RedisModule_CloseKey(key);
+}
+
+int scan_strings(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ scan_strings_pd pd = {
+ .nkeys = 0,
+ };
+
+ RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_LEN);
+
+ RedisModuleScanCursor* cursor = RedisModule_ScanCursorCreate();
+ while(RedisModule_Scan(ctx, cursor, scan_strings_callback, &pd));
+ RedisModule_ScanCursorDestroy(cursor);
+
+ RedisModule_ReplySetArrayLength(ctx, pd.nkeys);
+ return REDISMODULE_OK;
+}
+
+typedef struct {
+ RedisModuleCtx *ctx;
+ size_t nreplies;
+} scan_key_pd;
+
+void scan_key_callback(RedisModuleKey *key, RedisModuleString* field, RedisModuleString* value, void *privdata) {
+ REDISMODULE_NOT_USED(key);
+ scan_key_pd* pd = privdata;
+ RedisModule_ReplyWithArray(pd->ctx, 2);
+ size_t fieldCStrLen;
+
+ // The implementation of RedisModuleString is robj with lots of encodings.
+ // We want to make sure the robj that passes to this callback in
+ // String encoded, this is why we use RedisModule_StringPtrLen and
+ // RedisModule_ReplyWithStringBuffer instead of directly use
+ // RedisModule_ReplyWithString.
+ const char* fieldCStr = RedisModule_StringPtrLen(field, &fieldCStrLen);
+ RedisModule_ReplyWithStringBuffer(pd->ctx, fieldCStr, fieldCStrLen);
+ if(value){
+ size_t valueCStrLen;
+ const char* valueCStr = RedisModule_StringPtrLen(value, &valueCStrLen);
+ RedisModule_ReplyWithStringBuffer(pd->ctx, valueCStr, valueCStrLen);
+ } else {
+ RedisModule_ReplyWithNull(pd->ctx);
+ }
+
+ pd->nreplies++;
+}
+
+int scan_key(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ if (argc != 2) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+ scan_key_pd pd = {
+ .ctx = ctx,
+ .nreplies = 0,
+ };
+
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ);
+ if (!key) {
+ RedisModule_ReplyWithError(ctx, "not found");
+ return REDISMODULE_OK;
+ }
+
+ RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_ARRAY_LEN);
+
+ RedisModuleScanCursor* cursor = RedisModule_ScanCursorCreate();
+ while(RedisModule_ScanKey(key, cursor, scan_key_callback, &pd));
+ RedisModule_ScanCursorDestroy(cursor);
+
+ RedisModule_ReplySetArrayLength(ctx, pd.nreplies);
+ RedisModule_CloseKey(key);
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ if (RedisModule_Init(ctx, "scan", 1, REDISMODULE_APIVER_1)== REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "scan.scan_strings", scan_strings, "", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "scan.scan_key", scan_key, "", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ return REDISMODULE_OK;
+}
+
+
diff --git a/examples/redis-unstable/tests/modules/stream.c b/examples/redis-unstable/tests/modules/stream.c
new file mode 100644
index 0000000..65762a3
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/stream.c
@@ -0,0 +1,258 @@
+#include "redismodule.h"
+
+#include <string.h>
+#include <strings.h>
+#include <assert.h>
+#include <unistd.h>
+#include <errno.h>
+
+/* Command which adds a stream entry with automatic ID, like XADD *.
+ *
+ * Syntax: STREAM.ADD key field1 value1 [ field2 value2 ... ]
+ *
+ * The response is the ID of the added stream entry or an error message.
+ */
+int stream_add(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc < 2 || argc % 2 != 0) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE);
+ RedisModuleStreamID id;
+ if (RedisModule_StreamAdd(key, REDISMODULE_STREAM_ADD_AUTOID, &id,
+ &argv[2], (argc-2)/2) == REDISMODULE_OK) {
+ RedisModuleString *id_str = RedisModule_CreateStringFromStreamID(ctx, &id);
+ RedisModule_ReplyWithString(ctx, id_str);
+ RedisModule_FreeString(ctx, id_str);
+ } else {
+ RedisModule_ReplyWithError(ctx, "ERR StreamAdd failed");
+ }
+ RedisModule_CloseKey(key);
+ return REDISMODULE_OK;
+}
+
+/* Command which adds a stream entry N times.
+ *
+ * Syntax: STREAM.ADD key N field1 value1 [ field2 value2 ... ]
+ *
+ * Returns the number of successfully added entries.
+ */
+int stream_addn(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc < 3 || argc % 2 == 0) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ long long n, i;
+ if (RedisModule_StringToLongLong(argv[2], &n) == REDISMODULE_ERR) {
+ RedisModule_ReplyWithError(ctx, "N must be a number");
+ return REDISMODULE_OK;
+ }
+
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE);
+ for (i = 0; i < n; i++) {
+ if (RedisModule_StreamAdd(key, REDISMODULE_STREAM_ADD_AUTOID, NULL,
+ &argv[3], (argc-3)/2) == REDISMODULE_ERR)
+ break;
+ }
+ RedisModule_ReplyWithLongLong(ctx, i);
+ RedisModule_CloseKey(key);
+ return REDISMODULE_OK;
+}
+
+/* STREAM.DELETE key stream-id */
+int stream_delete(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 3) return RedisModule_WrongArity(ctx);
+ RedisModuleStreamID id;
+ if (RedisModule_StringToStreamID(argv[2], &id) != REDISMODULE_OK) {
+ return RedisModule_ReplyWithError(ctx, "Invalid stream ID");
+ }
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE);
+ if (RedisModule_StreamDelete(key, &id) == REDISMODULE_OK) {
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ } else {
+ RedisModule_ReplyWithError(ctx, "ERR StreamDelete failed");
+ }
+ RedisModule_CloseKey(key);
+ return REDISMODULE_OK;
+}
+
+/* STREAM.RANGE key start-id end-id
+ *
+ * Returns an array of stream items. Each item is an array on the form
+ * [stream-id, [field1, value1, field2, value2, ...]].
+ *
+ * A funny side-effect used for testing RM_StreamIteratorDelete() is that if any
+ * entry has a field named "selfdestruct", the stream entry is deleted. It is
+ * however included in the results of this command.
+ */
+int stream_range(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 4) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ RedisModuleStreamID startid, endid;
+ if (RedisModule_StringToStreamID(argv[2], &startid) != REDISMODULE_OK ||
+ RedisModule_StringToStreamID(argv[3], &endid) != REDISMODULE_OK) {
+ RedisModule_ReplyWithError(ctx, "Invalid stream ID");
+ return REDISMODULE_OK;
+ }
+
+ /* If startid > endid, we swap and set the reverse flag. */
+ int flags = 0;
+ if (startid.ms > endid.ms ||
+ (startid.ms == endid.ms && startid.seq > endid.seq)) {
+ RedisModuleStreamID tmp = startid;
+ startid = endid;
+ endid = tmp;
+ flags |= REDISMODULE_STREAM_ITERATOR_REVERSE;
+ }
+
+ /* Open key and start iterator. */
+ int openflags = REDISMODULE_READ | REDISMODULE_WRITE;
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], openflags);
+ if (RedisModule_StreamIteratorStart(key, flags,
+ &startid, &endid) != REDISMODULE_OK) {
+ /* Key is not a stream, etc. */
+ RedisModule_ReplyWithError(ctx, "ERR StreamIteratorStart failed");
+ RedisModule_CloseKey(key);
+ return REDISMODULE_OK;
+ }
+
+ /* Check error handling: Delete current entry when no current entry. */
+ assert(RedisModule_StreamIteratorDelete(key) ==
+ REDISMODULE_ERR);
+ assert(errno == ENOENT);
+
+ /* Check error handling: Fetch fields when no current entry. */
+ assert(RedisModule_StreamIteratorNextField(key, NULL, NULL) ==
+ REDISMODULE_ERR);
+ assert(errno == ENOENT);
+
+ /* Return array. */
+ RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_LEN);
+ RedisModule_AutoMemory(ctx);
+ RedisModuleStreamID id;
+ long numfields;
+ long len = 0;
+ while (RedisModule_StreamIteratorNextID(key, &id,
+ &numfields) == REDISMODULE_OK) {
+ RedisModule_ReplyWithArray(ctx, 2);
+ RedisModuleString *id_str = RedisModule_CreateStringFromStreamID(ctx, &id);
+ RedisModule_ReplyWithString(ctx, id_str);
+ RedisModule_ReplyWithArray(ctx, numfields * 2);
+ int delete = 0;
+ RedisModuleString *field, *value;
+ for (long i = 0; i < numfields; i++) {
+ assert(RedisModule_StreamIteratorNextField(key, &field, &value) ==
+ REDISMODULE_OK);
+ RedisModule_ReplyWithString(ctx, field);
+ RedisModule_ReplyWithString(ctx, value);
+ /* check if this is a "selfdestruct" field */
+ size_t field_len;
+ const char *field_str = RedisModule_StringPtrLen(field, &field_len);
+ if (!strncmp(field_str, "selfdestruct", field_len)) delete = 1;
+ }
+ if (delete) {
+ assert(RedisModule_StreamIteratorDelete(key) == REDISMODULE_OK);
+ }
+ /* check error handling: no more fields to fetch */
+ assert(RedisModule_StreamIteratorNextField(key, &field, &value) ==
+ REDISMODULE_ERR);
+ assert(errno == ENOENT);
+ len++;
+ }
+ RedisModule_ReplySetArrayLength(ctx, len);
+ RedisModule_StreamIteratorStop(key);
+ RedisModule_CloseKey(key);
+ return REDISMODULE_OK;
+}
+
+/*
+ * STREAM.TRIM key (MAXLEN (=|~) length | MINID (=|~) id)
+ */
+int stream_trim(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 5) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ /* Parse args */
+ int trim_by_id = 0; /* 0 = maxlen, 1 = minid */
+ long long maxlen;
+ RedisModuleStreamID minid;
+ size_t arg_len;
+ const char *arg = RedisModule_StringPtrLen(argv[2], &arg_len);
+ if (!strcasecmp(arg, "minid")) {
+ trim_by_id = 1;
+ if (RedisModule_StringToStreamID(argv[4], &minid) != REDISMODULE_OK) {
+ RedisModule_ReplyWithError(ctx, "ERR Invalid stream ID");
+ return REDISMODULE_OK;
+ }
+ } else if (!strcasecmp(arg, "maxlen")) {
+ if (RedisModule_StringToLongLong(argv[4], &maxlen) == REDISMODULE_ERR) {
+ RedisModule_ReplyWithError(ctx, "ERR Maxlen must be a number");
+ return REDISMODULE_OK;
+ }
+ } else {
+ RedisModule_ReplyWithError(ctx, "ERR Invalid arguments");
+ return REDISMODULE_OK;
+ }
+
+ /* Approx or exact */
+ int flags;
+ arg = RedisModule_StringPtrLen(argv[3], &arg_len);
+ if (arg_len == 1 && arg[0] == '~') {
+ flags = REDISMODULE_STREAM_TRIM_APPROX;
+ } else if (arg_len == 1 && arg[0] == '=') {
+ flags = 0;
+ } else {
+ RedisModule_ReplyWithError(ctx, "ERR Invalid approx-or-exact mark");
+ return REDISMODULE_OK;
+ }
+
+ /* Trim */
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE);
+ long long trimmed;
+ if (trim_by_id) {
+ trimmed = RedisModule_StreamTrimByID(key, flags, &minid);
+ } else {
+ trimmed = RedisModule_StreamTrimByLength(key, flags, maxlen);
+ }
+
+ /* Return result */
+ if (trimmed < 0) {
+ RedisModule_ReplyWithError(ctx, "ERR Trimming failed");
+ } else {
+ RedisModule_ReplyWithLongLong(ctx, trimmed);
+ }
+ RedisModule_CloseKey(key);
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ if (RedisModule_Init(ctx, "stream", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "stream.add", stream_add, "write",
+ 1, 1, 1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx, "stream.addn", stream_addn, "write",
+ 1, 1, 1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx, "stream.delete", stream_delete, "write",
+ 1, 1, 1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx, "stream.range", stream_range, "write",
+ 1, 1, 1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx, "stream.trim", stream_trim, "write",
+ 1, 1, 1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/subcommands.c b/examples/redis-unstable/tests/modules/subcommands.c
new file mode 100644
index 0000000..0961032
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/subcommands.c
@@ -0,0 +1,119 @@
+#include "redismodule.h"
+
+#define UNUSED(V) ((void) V)
+
+int cmd_set(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ UNUSED(argv);
+ UNUSED(argc);
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+int cmd_get(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ UNUSED(argv);
+
+ if (argc > 4) /* For testing */
+ return RedisModule_WrongArity(ctx);
+
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ return REDISMODULE_OK;
+}
+
+int cmd_get_fullname(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ UNUSED(argv);
+ UNUSED(argc);
+
+ const char *command_name = RedisModule_GetCurrentCommandName(ctx);
+ RedisModule_ReplyWithSimpleString(ctx, command_name);
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ if (RedisModule_Init(ctx, "subcommands", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ /* Module command names cannot contain special characters. */
+ RedisModule_Assert(RedisModule_CreateCommand(ctx,"subcommands.char\r",NULL,"",0,0,0) == REDISMODULE_ERR);
+ RedisModule_Assert(RedisModule_CreateCommand(ctx,"subcommands.char\n",NULL,"",0,0,0) == REDISMODULE_ERR);
+ RedisModule_Assert(RedisModule_CreateCommand(ctx,"subcommands.char ",NULL,"",0,0,0) == REDISMODULE_ERR);
+
+ if (RedisModule_CreateCommand(ctx,"subcommands.bitarray",NULL,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ RedisModuleCommand *parent = RedisModule_GetCommand(ctx,"subcommands.bitarray");
+
+ if (RedisModule_CreateSubcommand(parent,"set",cmd_set,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ /* Module subcommand names cannot contain special characters. */
+ RedisModule_Assert(RedisModule_CreateSubcommand(parent,"char|",cmd_set,"",0,0,0) == REDISMODULE_ERR);
+ RedisModule_Assert(RedisModule_CreateSubcommand(parent,"char@",cmd_set,"",0,0,0) == REDISMODULE_ERR);
+ RedisModule_Assert(RedisModule_CreateSubcommand(parent,"char=",cmd_set,"",0,0,0) == REDISMODULE_ERR);
+
+ RedisModuleCommand *subcmd = RedisModule_GetCommand(ctx,"subcommands.bitarray|set");
+ RedisModuleCommandInfo cmd_set_info = {
+ .version = REDISMODULE_COMMAND_INFO_VERSION,
+ .key_specs = (RedisModuleCommandKeySpec[]){
+ {
+ .flags = REDISMODULE_CMD_KEY_RW | REDISMODULE_CMD_KEY_UPDATE,
+ .begin_search_type = REDISMODULE_KSPEC_BS_INDEX,
+ .bs.index.pos = 1,
+ .find_keys_type = REDISMODULE_KSPEC_FK_RANGE,
+ .fk.range = {0,1,0}
+ },
+ {0}
+ }
+ };
+ if (RedisModule_SetCommandInfo(subcmd, &cmd_set_info) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateSubcommand(parent,"get",cmd_get,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ subcmd = RedisModule_GetCommand(ctx,"subcommands.bitarray|get");
+ RedisModuleCommandInfo cmd_get_info = {
+ .version = REDISMODULE_COMMAND_INFO_VERSION,
+ .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}
+ },
+ {0}
+ }
+ };
+ if (RedisModule_SetCommandInfo(subcmd, &cmd_get_info) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ /* Get the name of the command currently running. */
+ if (RedisModule_CreateCommand(ctx,"subcommands.parent_get_fullname",cmd_get_fullname,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ /* Get the name of the subcommand currently running. */
+ if (RedisModule_CreateCommand(ctx,"subcommands.sub",NULL,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ RedisModuleCommand *fullname_parent = RedisModule_GetCommand(ctx,"subcommands.sub");
+ if (RedisModule_CreateSubcommand(fullname_parent,"get_fullname",cmd_get_fullname,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ /* Sanity */
+
+ /* Trying to create the same subcommand fails */
+ RedisModule_Assert(RedisModule_CreateSubcommand(parent,"get",NULL,"",0,0,0) == REDISMODULE_ERR);
+
+ /* Trying to create a sub-subcommand fails */
+ RedisModule_Assert(RedisModule_CreateSubcommand(subcmd,"get",NULL,"",0,0,0) == REDISMODULE_ERR);
+
+ /* Internal container command for testing */
+ if (RedisModule_CreateCommand(ctx,"subcommands.internal_container",NULL,"internal",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ RedisModuleCommand *internal_parent = RedisModule_GetCommand(ctx,"subcommands.internal_container");
+ if (RedisModule_CreateSubcommand(internal_parent,"test",cmd_set,"internal",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/test_keymeta.c b/examples/redis-unstable/tests/modules/test_keymeta.c
new file mode 100644
index 0000000..ca5d622
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/test_keymeta.c
@@ -0,0 +1,585 @@
+/* An example module for attaching metadata to keys.
+ *
+ * This example lets tests create metadata-key classes and then SET and GET metadata
+ * to keys. The 8-byte slot stores a handle to a module-managed allocation; here
+ * we use to attach a string per-key.
+ *
+ * The module pre-registers several metadata classes during initialization and exposes
+ * the following commands (via RedisModule_CreateCommand):
+ *
+ * 1) KEYMETA.REGISTER <4-char-id> <version> [FLAGS]
+ * Register a new metadata-key class during module load.
+ * Returns the <keymeta-class-id> index (Returned from RedisModule_CreateKeyMetaClass)
+ * On failure, returns nil
+ * In a real module it should be registered "automatically" via OnLoad.
+ *
+ * FLAGS (colon-separated):
+ * KEEPONCOPY - Keep metadata on COPY operation
+ * KEEPONRENAME - Keep metadata on RENAME operation
+ * KEEPONMOVE - Keep metadata on MOVE operation
+ * UNLINKFREE - Use unlink callback for async free
+ * RDBLOAD - Enable rdb_load callback (metadata can be loaded from RDB)
+ * RDBSAVE - Enable rdb_save callback (metadata can be saved to RDB)
+ * ALLOWIGNORE - Enable ALLOW_IGNORE flag (graceful discard on load if
+ * class not registered or no rdb_load callback)
+ *
+ * Example: > keymeta.register KMT1 1 KEEPONCOPY:KEEPONRENAME:ALLOWIGNORE:RDBLOAD:RDBSAVE
+ * Example: > keymeta.register KMT2 1 ALLOWIGNORE
+ *
+ * 2) KEYMETA.SET <4-char-id> <key> <string-value>
+ * Set the string value as metadata to given key.
+ * Note:
+ * - If already set earlier, then it is expected that it will released before setting a
+ * new string. That is why this command should start with trying to get first
+ * metadata for given key.
+ *
+ * 3) KEYMETA.GET <4-char-id> <key>
+ * Get the metadata attached to the key for the given class.
+ * Returns a string attached to the given key. Or nil if nothing is attached.
+ *
+ * 4) KEYMETA.UNREGISTER <4-char-id>
+ * This will mark the key metadata class as released. It can later be reused again
+ * by the same class (consider comment above).
+ * Return REDISMODULE_OK/REDISMODULE_ERR.
+ *
+ * 5) KEYMETA.ACTIVE
+ * Return total number of active metadata at the moment.
+ */
+
+#include "redismodule.h"
+#include <string.h>
+#include <stdlib.h>
+#include <assert.h>
+
+/* Virtualize class IDs for testing. Values: 0 unused, 1..7 used, -1 released */
+RedisModuleKeyMetaClassId class_ids[8] = { 0 };
+
+/* Mapping from 4-char-id to class-id */
+typedef struct {
+ char name[5]; /* 4 chars + null terminator */
+ RedisModuleKeyMetaClassId class_id;
+} ClassMapping;
+
+#define MAX_CLASS_MAPPINGS 8
+static ClassMapping class_mappings[MAX_CLASS_MAPPINGS];
+static int num_class_mappings = 0;
+
+/* Reverse lookup: given a class_id, find the 4-char-id name */
+static const char* lookupClassName(RedisModuleKeyMetaClassId class_id) {
+ for (int i = 0; i < num_class_mappings; i++) {
+ if (class_mappings[i].class_id == class_id) {
+ return class_mappings[i].name;
+ }
+ }
+ return NULL;
+}
+
+/* Track active metadata instances (not yet freed) */
+static long long active_metadata_count = 0;
+
+/* Helper functions for class mapping */
+
+/* Add a mapping from 4-char-id to class-id */
+static int addClassMapping(const char *name, RedisModuleKeyMetaClassId class_id) {
+ if (num_class_mappings >= MAX_CLASS_MAPPINGS) {
+ return 0; /* No space */
+ }
+ strncpy(class_mappings[num_class_mappings].name, name, 4);
+ class_mappings[num_class_mappings].name[4] = '\0';
+ class_mappings[num_class_mappings].class_id = class_id;
+ num_class_mappings++;
+ return 1;
+}
+
+/* Lookup class-id by 4-char-id. Returns -1 if not found. */
+static RedisModuleKeyMetaClassId lookupClassId(const char *name) {
+ for (int i = 0; i < num_class_mappings; i++) {
+ if (strncmp(class_mappings[i].name, name, 4) == 0) {
+ return class_mappings[i].class_id;
+ }
+ }
+ return -1;
+}
+
+/* Remove a mapping by 4-char-id */
+static int removeClassMapping(const char *name) {
+ for (int i = 0; i < num_class_mappings; i++) {
+ if (strncmp(class_mappings[i].name, name, 4) == 0) {
+ /* Shift remaining entries down */
+ for (int j = i; j < num_class_mappings - 1; j++) {
+ class_mappings[j] = class_mappings[j + 1];
+ }
+ num_class_mappings--;
+ return 1;
+ }
+ }
+ return 0;
+}
+
+/* Callback functions for metadata lifecycle */
+
+/* Copy callback - called when a key is copied */
+static int KeyMetaCopyCallback(RedisModuleKeyOptCtx *ctx, uint64_t *meta) {
+ REDISMODULE_NOT_USED(ctx);
+ char *str = (char *)*meta;
+ /* Note, condition is redundant since cb only invoked when meta != reset_value */
+ if (str) {
+ char *new_str = strdup(str);
+ *meta = (uint64_t)new_str;
+ active_metadata_count++; /* New metadata instance created */
+ }
+ return 1; /* Keep metadata */
+}
+
+/* Rename callback - called when a key is renamed. */
+static int KeyMetaRenameDiscardCallback(RedisModuleKeyOptCtx *ctx, uint64_t *meta) {
+ REDISMODULE_NOT_USED(ctx);
+ REDISMODULE_NOT_USED(meta);
+ return 0;
+}
+
+/* Unlink callback - called when a key is unlinked */
+static void KeyMetaUnlinkCallback(RedisModuleKeyOptCtx *ctx, uint64_t *meta) {
+ /* Let's challenge and free early on before free callback */
+ /* Note, condition is redundant since cb only invoked when meta != reset_value */
+ if (*meta != 0) {
+ char *str = (char *)*meta;
+ free(str);
+ *meta = 0; /* Set to reset_value !!! */
+ active_metadata_count--; /* Metadata instance freed */
+ }
+ REDISMODULE_NOT_USED(ctx);
+}
+
+/* Free callback - called when metadata needs to be freed */
+static void KeyMetaFreeCallback(const char *keyname, uint64_t meta) {
+ REDISMODULE_NOT_USED(keyname);
+ /* Note, condition is redundant since cb only invoked when meta != reset_value */
+ if (meta != 0) {
+ char *str = (char *)meta;
+ free(str);
+ active_metadata_count--; /* Metadata instance freed */
+ }
+}
+
+static int KeyMetaMoveDiscardCallback(RedisModuleKeyOptCtx *ctx, uint64_t *meta) {
+ REDISMODULE_NOT_USED(ctx);
+ REDISMODULE_NOT_USED(meta);
+ return 0; /* discard metadata */
+}
+
+/* RDB Save Callback - Serialize metadata to RDB
+ * This callback is invoked during RDB save to write the metadata value.
+ *
+ * Parameters:
+ * - rdb: RedisModuleIO context for writing to RDB
+ * - value: The kvobj (key-value object) - not used in this implementation
+ * - meta: Pointer to the 8-byte metadata value (pointer to our string)
+ */
+static void KeyMetaRDBSaveCallback(RedisModuleIO *rdb, void *value, uint64_t *meta) {
+ REDISMODULE_NOT_USED(value);
+
+ /* If metadata is NULL (reset_value), don't save anything */
+ if (*meta == 0) return;
+
+ /* Extract the string from the metadata pointer */
+ char *metadata_string = (char *)*meta;
+
+ /* Save the string to RDB using SaveStringBuffer */
+ RedisModule_SaveStringBuffer(rdb, metadata_string, strlen(metadata_string));
+ /* Save more silly data */
+ RedisModule_SaveSigned(rdb, 1);
+ RedisModule_SaveFloat(rdb, 1.5);
+ RedisModule_SaveLongDouble(rdb, 0.333333333333333333L);
+}
+
+/* RDB Load Callback - Deserialize metadata from RDB
+ * This callback is invoked during RDB load to read the metadata value.
+ *
+ * Parameters:
+ * - rdb: RedisModuleIO context for reading from RDB
+ * - meta: Pointer to store the loaded 8-byte metadata value
+ * - encver: Encoding version (class version from RDB)
+ *
+ * Returns:
+ * - 1: Attach metadata to key (success)
+ * - 0: Ignore/skip metadata (not an error)
+ * - -1: Error - abort RDB load
+ */
+static int KeyMetaRDBLoadCallback(RedisModuleIO *rdb, uint64_t *meta, int encver) {
+ REDISMODULE_NOT_USED(encver);
+
+ /* Load the string from RDB using LoadStringBuffer */
+ size_t len;
+ char *loaded_string = RedisModule_LoadStringBuffer(rdb, &len);
+
+ if (loaded_string == NULL) {
+ /* Error loading string */
+ return -1;
+ }
+
+ /* Allocate and copy the string (LoadStringBuffer returns a buffer that must be freed) */
+ char *metadata_string = malloc(len + 1);
+ if (metadata_string == NULL) {
+ RedisModule_Free(loaded_string);
+ return -1;
+ }
+
+ memcpy(metadata_string, loaded_string, len);
+ metadata_string[len] = '\0';
+ RedisModule_Free(loaded_string);
+
+ /* Load the additional data that was saved (must match rdb_save) */
+ int64_t signed_val = RedisModule_LoadSigned(rdb);
+ float float_val = RedisModule_LoadFloat(rdb);
+ long double ldouble_val = RedisModule_LoadLongDouble(rdb);
+ /* We don't use these values, just need to consume them from the stream */
+ (void)signed_val;
+ (void)float_val;
+ (void)ldouble_val;
+
+ /* Store the pointer in metadata */
+ *meta = (uint64_t)metadata_string;
+ active_metadata_count++; /* New metadata instance created */
+
+ /* Return 1 to attach metadata to the key */
+ return 1;
+}
+
+/* AOF Rewrite Callback - Common implementation for all classes
+ * This callback is invoked during AOF rewrite to emit commands that will
+ * recreate the metadata when the AOF is loaded.
+ *
+ * Parameters:
+ * - aof: RedisModuleIO context for writing to AOF
+ * - value: The kvobj (key-value object) - not used in this implementation
+ * - meta: The 8-byte metadata value (pointer to our string)
+ * - class_id: The class ID for this metadata
+ */
+static void KeyMetaAOFRewriteCallback_Class(RedisModuleIO *aof, void *value, uint64_t meta, RedisModuleKeyMetaClassId class_id) {
+ REDISMODULE_NOT_USED(value);
+
+ /* If metadata is NULL (reset_value), don't emit anything */
+ if (meta == 0) return;
+
+ /* Extract the string from the metadata pointer */
+ char *metadata_string = (char *)meta;
+
+ /* Lookup the 9-byte-id name for this class */
+ const char *class_name = lookupClassName(class_id);
+ if (!class_name) {
+ /* This shouldn't happen, but handle gracefully */
+ return;
+ }
+
+ /* Get the key name from the AOF IO context */
+ const RedisModuleString *key = RedisModule_GetKeyNameFromIO(aof);
+ if (!key) {
+ /* Key name not available - shouldn't happen during AOF rewrite */
+ return;
+ }
+
+ /* Emit the KEYMETA.SET command to recreate this metadata
+ * Format: KEYMETA.SET <9-byte-id> <key> <string-value> */
+ RedisModule_EmitAOF(aof, "KEYMETA.SET", "csc",
+ class_name, /* c: 9-byte-id (C string) */
+ key, /* s: key name (RedisModuleString) */
+ metadata_string); /* c: metadata value (C string) */
+}
+
+/* Individual AOF rewrite callbacks for each class (1-7)
+ * Each callback wraps the common implementation with its specific class ID */
+static void KeyMetaAOFRewriteCb1(RedisModuleIO *aof, void *value, uint64_t meta) {
+ KeyMetaAOFRewriteCallback_Class(aof, value, meta, 1);
+}
+
+static void KeyMetaAOFRewriteCb2(RedisModuleIO *aof, void *value, uint64_t meta) {
+ KeyMetaAOFRewriteCallback_Class(aof, value, meta, 2);
+}
+
+static void KeyMetaAOFRewriteCb3(RedisModuleIO *aof, void *value, uint64_t meta) {
+ KeyMetaAOFRewriteCallback_Class(aof, value, meta, 3);
+}
+
+static void KeyMetaAOFRewriteCb4(RedisModuleIO *aof, void *value, uint64_t meta) {
+ KeyMetaAOFRewriteCallback_Class(aof, value, meta, 4);
+}
+
+static void KeyMetaAOFRewriteCb5(RedisModuleIO *aof, void *value, uint64_t meta) {
+ KeyMetaAOFRewriteCallback_Class(aof, value, meta, 5);
+}
+
+static void KeyMetaAOFRewriteCb6(RedisModuleIO *aof, void *value, uint64_t meta) {
+ KeyMetaAOFRewriteCallback_Class(aof, value, meta, 6);
+}
+
+static void KeyMetaAOFRewriteCb7(RedisModuleIO *aof, void *value, uint64_t meta) {
+ KeyMetaAOFRewriteCallback_Class(aof, value, meta, 7);
+}
+
+/* KEYMETA.REGISTER <4-char-id> <version> [KEEPONCOPY:KEEPONRENAME:UNLINKFREE:ALLOWIGNORE:NORDBLOAD:NORDBSAVE] */
+static int KeyMetaRegister_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc < 3 || argc > 4) {
+ return RedisModule_WrongArity(ctx);
+ }
+
+ /* argv[1]: key metadata class name */
+ size_t namelen;
+ const char *metaname = RedisModule_StringPtrLen(argv[1], &namelen);
+
+ /* argv[2]: key metadata class version */
+ long long metaver;
+ if (RedisModule_StringToLongLong(argv[2], &metaver) != REDISMODULE_OK) {
+ RedisModule_ReplyWithError(ctx, "ERR invalid version number");
+ return REDISMODULE_OK;
+ }
+
+ /* Parse optional callback flags */
+ int keep_on_copy = 0, keep_on_rename = 0, unlink_free = 0, keep_on_move = 0;
+ int allow_ignore = 0; /* Default: ALLOW_IGNORE disabled */
+ int rdb_load = 0; /* Default: rdb_load disabled */
+ int rdb_save = 0; /* Default: rdb_save disabled */
+
+ if (argc == 4) {
+ const char *flags = RedisModule_StringPtrLen(argv[3], NULL);
+ if (strstr(flags, "KEEPONCOPY")) keep_on_copy = 1;
+ if (strstr(flags, "KEEPONRENAME")) keep_on_rename = 1;
+ if (strstr(flags, "UNLINKFREE")) unlink_free = 1;
+ if (strstr(flags, "KEEPONMOVE")) keep_on_move = 1;
+ if (strstr(flags, "ALLOWIGNORE")) allow_ignore = 1; /* Enable ALLOW_IGNORE */
+ if (strstr(flags, "RDBLOAD")) rdb_load = 1; /* Enable rdb_load */
+ if (strstr(flags, "RDBSAVE")) rdb_save = 1; /* Enable rdb_save */
+ }
+
+ /* Setup configuration */
+ RedisModuleKeyMetaClassConfig config = {0};
+ config.version = REDISMODULE_KEY_META_VERSION;
+ config.flags = allow_ignore ? (1 << REDISMODULE_META_ALLOW_IGNORE) : 0;
+ config.reset_value = (uint64_t)NULL; /* NULL pointer means no resource to free */
+ config.rdb_load = rdb_load ? KeyMetaRDBLoadCallback : NULL;
+ config.rdb_save = rdb_save ? KeyMetaRDBSaveCallback : NULL;
+ switch (num_class_mappings + 1) { /* distinct cb per class */
+ case 1: config.aof_rewrite = KeyMetaAOFRewriteCb1; break;
+ case 2: config.aof_rewrite = KeyMetaAOFRewriteCb2; break;
+ case 3: config.aof_rewrite = KeyMetaAOFRewriteCb3; break;
+ case 4: config.aof_rewrite = KeyMetaAOFRewriteCb4; break;
+ case 5: config.aof_rewrite = KeyMetaAOFRewriteCb5; break;
+ case 6: config.aof_rewrite = KeyMetaAOFRewriteCb6; break;
+ case 7: config.aof_rewrite = KeyMetaAOFRewriteCb7; break;
+ default: config.aof_rewrite = NULL; break;
+ }
+ config.free = KeyMetaFreeCallback;
+ config.copy = keep_on_copy ? KeyMetaCopyCallback : NULL;
+ config.rename = keep_on_rename ? NULL : KeyMetaRenameDiscardCallback;
+ config.move = keep_on_move ? NULL : KeyMetaMoveDiscardCallback;
+ config.defrag = NULL;
+ config.unlink = unlink_free ? KeyMetaUnlinkCallback : NULL;
+ config.mem_usage = NULL;
+ config.free_effort = NULL;
+
+ /* Create the metadata class */
+ RedisModuleKeyMetaClassId class_id = RedisModule_CreateKeyMetaClass(ctx, metaname, (int)metaver, &config);
+
+ if (class_id < 0) {
+ RedisModule_ReplyWithError(ctx, "ERR failed to create metadata class");
+ return REDISMODULE_OK;
+ } else {
+ /* Store the mapping from 9-byte-id to class-id */
+ if (!addClassMapping(metaname, class_id)) {
+ RedisModule_ReplyWithError(ctx, "ERR failed to store class mapping");
+ return REDISMODULE_OK;
+ }
+ RedisModule_ReplyWithLongLong(ctx, class_id);
+ }
+
+ return REDISMODULE_OK;
+}
+
+/* KEYMETA.SET <9-byte-id> <key> <string-value> */
+static int KeyMetaSet_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 4) {
+ return RedisModule_WrongArity(ctx);
+ }
+
+ /* Parse arguments */
+ const char *metaname = RedisModule_StringPtrLen(argv[1], NULL);
+ RedisModuleString *keyname = argv[2];
+ const char *value = RedisModule_StringPtrLen(argv[3], NULL);
+
+ /* Lookup the metadata class by name */
+ RedisModuleKeyMetaClassId class_id = lookupClassId(metaname);
+ if (class_id < 0) {
+ RedisModule_ReplyWithError(ctx, "ERR metadata class not found");
+ return REDISMODULE_OK;
+ }
+
+ /* Open the key for writing */
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, keyname, REDISMODULE_READ | REDISMODULE_WRITE);
+
+ if (RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_EMPTY) {
+ RedisModule_ReplyWithNull(ctx);
+ RedisModule_CloseKey(key);
+ return REDISMODULE_OK;
+ }
+
+ /* Check if metadata already exists and free it first.
+ *
+ * Note: The caller is responsible for retrieving and freeing any existing
+ * pointer-based metadata before RM_SetKeyMeta() to a new value
+ */
+ uint64_t meta = 0;
+ if (RedisModule_GetKeyMeta(class_id, key, &meta) == REDISMODULE_OK) {
+ if (meta != 0) {
+ free((char *)meta);
+ active_metadata_count--; /* Old metadata freed */
+ }
+ }
+
+ char *new_str = strdup(value);
+ int res = RedisModule_SetKeyMeta(class_id, key, (uint64_t)new_str);
+
+ if (res == REDISMODULE_OK) {
+ active_metadata_count++; /* New metadata instance created */
+ }
+
+ RedisModule_CloseKey(key);
+
+ if (res == REDISMODULE_OK) {
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ } else {
+ free(new_str);
+ RedisModule_ReplyWithError(ctx, "ERR failed to set metadata");
+ }
+ return REDISMODULE_OK;
+}
+
+/* KEYMETA.GET <9-byte-id> <key> */
+static int KeyMetaGet_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 3) {
+ return RedisModule_WrongArity(ctx);
+ }
+
+ /* Parse arguments */
+ const char *metaname = RedisModule_StringPtrLen(argv[1], NULL);
+ RedisModuleString *keyname = argv[2];
+
+ /* Lookup the metadata class by name */
+ RedisModuleKeyMetaClassId class_id = lookupClassId(metaname);
+ if (class_id < 0) {
+ RedisModule_ReplyWithError(ctx, "ERR metadata class not found");
+ return REDISMODULE_OK;
+ }
+
+ /* Open the key for reading */
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, keyname, REDISMODULE_READ);
+ if (RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_EMPTY) {
+ RedisModule_ReplyWithNull(ctx);
+ RedisModule_CloseKey(key);
+ return REDISMODULE_OK;
+ }
+
+ /* Get the metadata */
+ uint64_t meta = 0;
+ int result = RedisModule_GetKeyMeta(class_id, key, &meta);
+
+ RedisModule_CloseKey(key);
+
+ if (result == REDISMODULE_OK && meta != 0) {
+ char *str = (char *)meta;
+ RedisModule_ReplyWithCString(ctx, str);
+ } else {
+ RedisModule_ReplyWithNull(ctx);
+ }
+
+ return REDISMODULE_OK;
+}
+
+/* KEYMETA.UNREGISTER <9-byte-id> */
+static int KeyMetaUnregister_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2) {
+ return RedisModule_WrongArity(ctx);
+ }
+
+ /* Parse arguments */
+ const char *metaname = RedisModule_StringPtrLen(argv[1], NULL);
+
+ /* Lookup the metadata class by name */
+ RedisModuleKeyMetaClassId class_id = lookupClassId(metaname);
+ if (class_id < 0) {
+ RedisModule_ReplyWithError(ctx, "ERR metadata class not found");
+ return REDISMODULE_OK;
+ }
+
+ /* Release the metadata class */
+ int result = RedisModule_ReleaseKeyMetaClass(class_id);
+
+ if (result == REDISMODULE_OK) {
+ /* Remove the mapping */
+ removeClassMapping(metaname);
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+ } else {
+ RedisModule_ReplyWithError(ctx, "ERR failed to unregister class");
+ }
+ return REDISMODULE_OK;
+}
+
+/* KEYMETA.ACTIVE
+ * Returns the total number of active metadata instances that haven't been freed yet.
+ * This is useful for testing to verify that metadata is properly cleaned up. */
+static int KeyMetaActive_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 1) {
+ return RedisModule_WrongArity(ctx);
+ }
+ REDISMODULE_NOT_USED(argv);
+
+ RedisModule_ReplyWithLongLong(ctx, active_metadata_count);
+ return REDISMODULE_OK;
+}
+
+/* Module initialization */
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ if (RedisModule_Init(ctx, "test_metakey", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) {
+ return REDISMODULE_ERR;
+ }
+
+ /* Register commands */
+ if (RedisModule_CreateCommand(ctx, "keymeta.register",
+ KeyMetaRegister_RedisCommand, "write", 0, 0, 0) == REDISMODULE_ERR) {
+ return REDISMODULE_ERR;
+ }
+
+ if (RedisModule_CreateCommand(ctx, "keymeta.set",
+ KeyMetaSet_RedisCommand, "write deny-oom", 1, 1, 1) == REDISMODULE_ERR) {
+ return REDISMODULE_ERR;
+ }
+
+ if (RedisModule_CreateCommand(ctx, "keymeta.get",
+ KeyMetaGet_RedisCommand, "readonly", 1, 1, 1) == REDISMODULE_ERR) {
+ return REDISMODULE_ERR;
+ }
+
+ if (RedisModule_CreateCommand(ctx, "keymeta.unregister",
+ KeyMetaUnregister_RedisCommand, "write", 0, 0, 0) == REDISMODULE_ERR) {
+ return REDISMODULE_ERR;
+ }
+
+ if (RedisModule_CreateCommand(ctx, "keymeta.active",
+ KeyMetaActive_RedisCommand, "readonly fast", 0, 0, 0) == REDISMODULE_ERR) {
+ return REDISMODULE_ERR;
+ }
+
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnUnload(RedisModuleCtx *ctx) {
+ REDISMODULE_NOT_USED(ctx);
+ long unsigned int i;
+ for (i = 0 ; i < sizeof(class_ids) / sizeof(class_ids[0]); i++) {
+ if (class_ids[i] > 0)
+ RedisModule_ReleaseKeyMetaClass(class_ids[i]);
+ }
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/test_lazyfree.c b/examples/redis-unstable/tests/modules/test_lazyfree.c
new file mode 100644
index 0000000..7ba213f
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/test_lazyfree.c
@@ -0,0 +1,196 @@
+/* This module emulates a linked list for lazyfree testing of modules, which
+ is a simplified version of 'hellotype.c'
+ */
+#include "redismodule.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <ctype.h>
+#include <string.h>
+#include <stdint.h>
+
+static RedisModuleType *LazyFreeLinkType;
+
+struct LazyFreeLinkNode {
+ int64_t value;
+ struct LazyFreeLinkNode *next;
+};
+
+struct LazyFreeLinkObject {
+ struct LazyFreeLinkNode *head;
+ size_t len; /* Number of elements added. */
+};
+
+struct LazyFreeLinkObject *createLazyFreeLinkObject(void) {
+ struct LazyFreeLinkObject *o;
+ o = RedisModule_Alloc(sizeof(*o));
+ o->head = NULL;
+ o->len = 0;
+ return o;
+}
+
+void LazyFreeLinkInsert(struct LazyFreeLinkObject *o, int64_t ele) {
+ struct LazyFreeLinkNode *next = o->head, *newnode, *prev = NULL;
+
+ while(next && next->value < ele) {
+ prev = next;
+ next = next->next;
+ }
+ newnode = RedisModule_Alloc(sizeof(*newnode));
+ newnode->value = ele;
+ newnode->next = next;
+ if (prev) {
+ prev->next = newnode;
+ } else {
+ o->head = newnode;
+ }
+ o->len++;
+}
+
+void LazyFreeLinkReleaseObject(struct LazyFreeLinkObject *o) {
+ struct LazyFreeLinkNode *cur, *next;
+ cur = o->head;
+ while(cur) {
+ next = cur->next;
+ RedisModule_Free(cur);
+ cur = next;
+ }
+ RedisModule_Free(o);
+}
+
+/* LAZYFREELINK.INSERT key value */
+int LazyFreeLinkInsert_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ RedisModule_AutoMemory(ctx); /* Use automatic memory management. */
+
+ if (argc != 3) return RedisModule_WrongArity(ctx);
+ RedisModuleKey *key = RedisModule_OpenKey(ctx,argv[1],
+ REDISMODULE_READ|REDISMODULE_WRITE);
+ int type = RedisModule_KeyType(key);
+ if (type != REDISMODULE_KEYTYPE_EMPTY &&
+ RedisModule_ModuleTypeGetType(key) != LazyFreeLinkType)
+ {
+ return RedisModule_ReplyWithError(ctx,REDISMODULE_ERRORMSG_WRONGTYPE);
+ }
+
+ long long value;
+ if ((RedisModule_StringToLongLong(argv[2],&value) != REDISMODULE_OK)) {
+ return RedisModule_ReplyWithError(ctx,"ERR invalid value: must be a signed 64 bit integer");
+ }
+
+ struct LazyFreeLinkObject *hto;
+ if (type == REDISMODULE_KEYTYPE_EMPTY) {
+ hto = createLazyFreeLinkObject();
+ RedisModule_ModuleTypeSetValue(key,LazyFreeLinkType,hto);
+ } else {
+ hto = RedisModule_ModuleTypeGetValue(key);
+ }
+
+ LazyFreeLinkInsert(hto,value);
+ RedisModule_SignalKeyAsReady(ctx,argv[1]);
+
+ RedisModule_ReplyWithLongLong(ctx,hto->len);
+ RedisModule_ReplicateVerbatim(ctx);
+ return REDISMODULE_OK;
+}
+
+/* LAZYFREELINK.LEN key */
+int LazyFreeLinkLen_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ RedisModule_AutoMemory(ctx); /* Use automatic memory management. */
+
+ if (argc != 2) return RedisModule_WrongArity(ctx);
+ RedisModuleKey *key = RedisModule_OpenKey(ctx,argv[1],
+ REDISMODULE_READ);
+ int type = RedisModule_KeyType(key);
+ if (type != REDISMODULE_KEYTYPE_EMPTY &&
+ RedisModule_ModuleTypeGetType(key) != LazyFreeLinkType)
+ {
+ return RedisModule_ReplyWithError(ctx,REDISMODULE_ERRORMSG_WRONGTYPE);
+ }
+
+ struct LazyFreeLinkObject *hto = RedisModule_ModuleTypeGetValue(key);
+ RedisModule_ReplyWithLongLong(ctx,hto ? hto->len : 0);
+ return REDISMODULE_OK;
+}
+
+void *LazyFreeLinkRdbLoad(RedisModuleIO *rdb, int encver) {
+ if (encver != 0) {
+ return NULL;
+ }
+ uint64_t elements = RedisModule_LoadUnsigned(rdb);
+ struct LazyFreeLinkObject *hto = createLazyFreeLinkObject();
+ while(elements--) {
+ int64_t ele = RedisModule_LoadSigned(rdb);
+ LazyFreeLinkInsert(hto,ele);
+ }
+ return hto;
+}
+
+void LazyFreeLinkRdbSave(RedisModuleIO *rdb, void *value) {
+ struct LazyFreeLinkObject *hto = value;
+ struct LazyFreeLinkNode *node = hto->head;
+ RedisModule_SaveUnsigned(rdb,hto->len);
+ while(node) {
+ RedisModule_SaveSigned(rdb,node->value);
+ node = node->next;
+ }
+}
+
+void LazyFreeLinkAofRewrite(RedisModuleIO *aof, RedisModuleString *key, void *value) {
+ struct LazyFreeLinkObject *hto = value;
+ struct LazyFreeLinkNode *node = hto->head;
+ while(node) {
+ RedisModule_EmitAOF(aof,"LAZYFREELINK.INSERT","sl",key,node->value);
+ node = node->next;
+ }
+}
+
+void LazyFreeLinkFree(void *value) {
+ LazyFreeLinkReleaseObject(value);
+}
+
+size_t LazyFreeLinkFreeEffort(RedisModuleString *key, const void *value) {
+ REDISMODULE_NOT_USED(key);
+ const struct LazyFreeLinkObject *hto = value;
+ return hto->len;
+}
+
+void LazyFreeLinkUnlink(RedisModuleString *key, const void *value) {
+ REDISMODULE_NOT_USED(key);
+ REDISMODULE_NOT_USED(value);
+ /* Here you can know which key and value is about to be freed. */
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ if (RedisModule_Init(ctx,"lazyfreetest",1,REDISMODULE_APIVER_1)
+ == REDISMODULE_ERR) return REDISMODULE_ERR;
+
+ /* We only allow our module to be loaded when the redis core version is greater than the version of my module */
+ if (RedisModule_GetTypeMethodVersion() < REDISMODULE_TYPE_METHOD_VERSION) {
+ return REDISMODULE_ERR;
+ }
+
+ RedisModuleTypeMethods tm = {
+ .version = REDISMODULE_TYPE_METHOD_VERSION,
+ .rdb_load = LazyFreeLinkRdbLoad,
+ .rdb_save = LazyFreeLinkRdbSave,
+ .aof_rewrite = LazyFreeLinkAofRewrite,
+ .free = LazyFreeLinkFree,
+ .free_effort = LazyFreeLinkFreeEffort,
+ .unlink = LazyFreeLinkUnlink,
+ };
+
+ LazyFreeLinkType = RedisModule_CreateDataType(ctx,"test_lazy",0,&tm);
+ if (LazyFreeLinkType == NULL) return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"lazyfreelink.insert",
+ LazyFreeLinkInsert_RedisCommand,"write deny-oom",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"lazyfreelink.len",
+ LazyFreeLinkLen_RedisCommand,"readonly",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/testrdb.c b/examples/redis-unstable/tests/modules/testrdb.c
new file mode 100644
index 0000000..c31aebb
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/testrdb.c
@@ -0,0 +1,405 @@
+#include "redismodule.h"
+
+#include <string.h>
+#include <assert.h>
+
+/* Module configuration, save aux or not? */
+#define CONF_AUX_OPTION_NO_AUX 0
+#define CONF_AUX_OPTION_SAVE2 1 << 0
+#define CONF_AUX_OPTION_BEFORE_KEYSPACE 1 << 1
+#define CONF_AUX_OPTION_AFTER_KEYSPACE 1 << 2
+#define CONF_AUX_OPTION_NO_DATA 1 << 3
+long long conf_aux_count = 0;
+
+/* Registered type */
+RedisModuleType *testrdb_type = NULL;
+
+/* Global values to store and persist to aux */
+RedisModuleString *before_str = NULL;
+RedisModuleString *after_str = NULL;
+
+/* Global values used to keep aux from db being loaded (in case of async_loading) */
+RedisModuleString *before_str_temp = NULL;
+RedisModuleString *after_str_temp = NULL;
+
+/* Indicates whether there is an async replication in progress.
+ * We control this value from RedisModuleEvent_ReplAsyncLoad events. */
+int async_loading = 0;
+
+int n_aux_load_called = 0;
+
+void replAsyncLoadCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data)
+{
+ REDISMODULE_NOT_USED(e);
+ REDISMODULE_NOT_USED(data);
+
+ switch (sub) {
+ case REDISMODULE_SUBEVENT_REPL_ASYNC_LOAD_STARTED:
+ assert(async_loading == 0);
+ async_loading = 1;
+ break;
+ case REDISMODULE_SUBEVENT_REPL_ASYNC_LOAD_ABORTED:
+ /* Discard temp aux */
+ if (before_str_temp)
+ RedisModule_FreeString(ctx, before_str_temp);
+ if (after_str_temp)
+ RedisModule_FreeString(ctx, after_str_temp);
+ before_str_temp = NULL;
+ after_str_temp = NULL;
+
+ async_loading = 0;
+ break;
+ case REDISMODULE_SUBEVENT_REPL_ASYNC_LOAD_COMPLETED:
+ if (before_str)
+ RedisModule_FreeString(ctx, before_str);
+ if (after_str)
+ RedisModule_FreeString(ctx, after_str);
+ before_str = before_str_temp;
+ after_str = after_str_temp;
+
+ before_str_temp = NULL;
+ after_str_temp = NULL;
+
+ async_loading = 0;
+ break;
+ default:
+ assert(0);
+ }
+}
+
+void *testrdb_type_load(RedisModuleIO *rdb, int encver) {
+ int count = RedisModule_LoadSigned(rdb);
+ RedisModuleString *str = RedisModule_LoadString(rdb);
+ float f = RedisModule_LoadFloat(rdb);
+ long double ld = RedisModule_LoadLongDouble(rdb);
+ if (RedisModule_IsIOError(rdb)) {
+ RedisModuleCtx *ctx = RedisModule_GetContextFromIO(rdb);
+ if (str)
+ RedisModule_FreeString(ctx, str);
+ return NULL;
+ }
+ /* Using the values only after checking for io errors. */
+ assert(count==1);
+ assert(encver==1);
+ assert(f==1.5f);
+ assert(ld==0.333333333333333333L);
+ return str;
+}
+
+void testrdb_type_save(RedisModuleIO *rdb, void *value) {
+ RedisModuleString *str = (RedisModuleString*)value;
+ RedisModule_SaveSigned(rdb, 1);
+ RedisModule_SaveString(rdb, str);
+ RedisModule_SaveFloat(rdb, 1.5);
+ RedisModule_SaveLongDouble(rdb, 0.333333333333333333L);
+}
+
+void testrdb_aux_save(RedisModuleIO *rdb, int when) {
+ if (!(conf_aux_count & CONF_AUX_OPTION_BEFORE_KEYSPACE)) assert(when == REDISMODULE_AUX_AFTER_RDB);
+ if (!(conf_aux_count & CONF_AUX_OPTION_AFTER_KEYSPACE)) assert(when == REDISMODULE_AUX_BEFORE_RDB);
+ assert(conf_aux_count!=CONF_AUX_OPTION_NO_AUX);
+ if (when == REDISMODULE_AUX_BEFORE_RDB) {
+ if (before_str) {
+ RedisModule_SaveSigned(rdb, 1);
+ RedisModule_SaveString(rdb, before_str);
+ } else {
+ RedisModule_SaveSigned(rdb, 0);
+ }
+ } else {
+ if (after_str) {
+ RedisModule_SaveSigned(rdb, 1);
+ RedisModule_SaveString(rdb, after_str);
+ } else {
+ RedisModule_SaveSigned(rdb, 0);
+ }
+ }
+}
+
+int testrdb_aux_load(RedisModuleIO *rdb, int encver, int when) {
+ assert(encver == 1);
+ if (!(conf_aux_count & CONF_AUX_OPTION_BEFORE_KEYSPACE)) assert(when == REDISMODULE_AUX_AFTER_RDB);
+ if (!(conf_aux_count & CONF_AUX_OPTION_AFTER_KEYSPACE)) assert(when == REDISMODULE_AUX_BEFORE_RDB);
+ assert(conf_aux_count!=CONF_AUX_OPTION_NO_AUX);
+ RedisModuleCtx *ctx = RedisModule_GetContextFromIO(rdb);
+ if (when == REDISMODULE_AUX_BEFORE_RDB) {
+ if (async_loading == 0) {
+ if (before_str)
+ RedisModule_FreeString(ctx, before_str);
+ before_str = NULL;
+ int count = RedisModule_LoadSigned(rdb);
+ if (RedisModule_IsIOError(rdb))
+ return REDISMODULE_ERR;
+ if (count)
+ before_str = RedisModule_LoadString(rdb);
+ } else {
+ if (before_str_temp)
+ RedisModule_FreeString(ctx, before_str_temp);
+ before_str_temp = NULL;
+ int count = RedisModule_LoadSigned(rdb);
+ if (RedisModule_IsIOError(rdb))
+ return REDISMODULE_ERR;
+ if (count)
+ before_str_temp = RedisModule_LoadString(rdb);
+ }
+ } else {
+ if (async_loading == 0) {
+ if (after_str)
+ RedisModule_FreeString(ctx, after_str);
+ after_str = NULL;
+ int count = RedisModule_LoadSigned(rdb);
+ if (RedisModule_IsIOError(rdb))
+ return REDISMODULE_ERR;
+ if (count)
+ after_str = RedisModule_LoadString(rdb);
+ } else {
+ if (after_str_temp)
+ RedisModule_FreeString(ctx, after_str_temp);
+ after_str_temp = NULL;
+ int count = RedisModule_LoadSigned(rdb);
+ if (RedisModule_IsIOError(rdb))
+ return REDISMODULE_ERR;
+ if (count)
+ after_str_temp = RedisModule_LoadString(rdb);
+ }
+ }
+
+ if (RedisModule_IsIOError(rdb))
+ return REDISMODULE_ERR;
+ return REDISMODULE_OK;
+}
+
+void testrdb_type_free(void *value) {
+ if (value)
+ RedisModule_FreeString(NULL, (RedisModuleString*)value);
+}
+
+int testrdb_set_before(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ if (argc != 2) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ if (before_str)
+ RedisModule_FreeString(ctx, before_str);
+ before_str = argv[1];
+ RedisModule_RetainString(ctx, argv[1]);
+ RedisModule_ReplyWithLongLong(ctx, 1);
+ return REDISMODULE_OK;
+}
+
+int testrdb_get_before(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ REDISMODULE_NOT_USED(argv);
+ if (argc != 1){
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+ if (before_str)
+ RedisModule_ReplyWithString(ctx, before_str);
+ else
+ RedisModule_ReplyWithStringBuffer(ctx, "", 0);
+ return REDISMODULE_OK;
+}
+
+/* For purpose of testing module events, expose variable state during async_loading. */
+int testrdb_async_loading_get_before(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ REDISMODULE_NOT_USED(argv);
+ if (argc != 1){
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+ if (before_str_temp)
+ RedisModule_ReplyWithString(ctx, before_str_temp);
+ else
+ RedisModule_ReplyWithStringBuffer(ctx, "", 0);
+ return REDISMODULE_OK;
+}
+
+int testrdb_set_after(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ if (argc != 2){
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ if (after_str)
+ RedisModule_FreeString(ctx, after_str);
+ after_str = argv[1];
+ RedisModule_RetainString(ctx, argv[1]);
+ RedisModule_ReplyWithLongLong(ctx, 1);
+ return REDISMODULE_OK;
+}
+
+int testrdb_get_after(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ REDISMODULE_NOT_USED(argv);
+ if (argc != 1){
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+ if (after_str)
+ RedisModule_ReplyWithString(ctx, after_str);
+ else
+ RedisModule_ReplyWithStringBuffer(ctx, "", 0);
+ return REDISMODULE_OK;
+}
+
+int testrdb_set_key(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ if (argc != 3){
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE);
+ RedisModuleString *str = RedisModule_ModuleTypeGetValue(key);
+ if (str)
+ RedisModule_FreeString(ctx, str);
+ RedisModule_ModuleTypeSetValue(key, testrdb_type, argv[2]);
+ RedisModule_RetainString(ctx, argv[2]);
+ RedisModule_CloseKey(key);
+ RedisModule_ReplyWithLongLong(ctx, 1);
+ return REDISMODULE_OK;
+}
+
+int testrdb_get_key(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ if (argc != 2){
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ);
+ RedisModuleString *str = RedisModule_ModuleTypeGetValue(key);
+ RedisModule_CloseKey(key);
+ RedisModule_ReplyWithString(ctx, str);
+ return REDISMODULE_OK;
+}
+
+int testrdb_get_n_aux_load_called(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ REDISMODULE_NOT_USED(ctx);
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ RedisModule_ReplyWithLongLong(ctx, n_aux_load_called);
+ return REDISMODULE_OK;
+}
+
+int test2rdb_aux_load(RedisModuleIO *rdb, int encver, int when) {
+ REDISMODULE_NOT_USED(rdb);
+ REDISMODULE_NOT_USED(encver);
+ REDISMODULE_NOT_USED(when);
+ n_aux_load_called++;
+ return REDISMODULE_OK;
+}
+
+void test2rdb_aux_save(RedisModuleIO *rdb, int when) {
+ REDISMODULE_NOT_USED(rdb);
+ REDISMODULE_NOT_USED(when);
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ if (RedisModule_Init(ctx,"testrdb",1,REDISMODULE_APIVER_1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ RedisModule_SetModuleOptions(ctx, REDISMODULE_OPTIONS_HANDLE_IO_ERRORS | REDISMODULE_OPTIONS_HANDLE_REPL_ASYNC_LOAD);
+
+ if (argc > 0)
+ RedisModule_StringToLongLong(argv[0], &conf_aux_count);
+
+ if (conf_aux_count==CONF_AUX_OPTION_NO_AUX) {
+ RedisModuleTypeMethods datatype_methods = {
+ .version = 1,
+ .rdb_load = testrdb_type_load,
+ .rdb_save = testrdb_type_save,
+ .aof_rewrite = NULL,
+ .digest = NULL,
+ .free = testrdb_type_free,
+ };
+
+ testrdb_type = RedisModule_CreateDataType(ctx, "test__rdb", 1, &datatype_methods);
+ if (testrdb_type == NULL)
+ return REDISMODULE_ERR;
+ } else if (!(conf_aux_count & CONF_AUX_OPTION_NO_DATA)) {
+ RedisModuleTypeMethods datatype_methods = {
+ .version = REDISMODULE_TYPE_METHOD_VERSION,
+ .rdb_load = testrdb_type_load,
+ .rdb_save = testrdb_type_save,
+ .aof_rewrite = NULL,
+ .digest = NULL,
+ .free = testrdb_type_free,
+ .aux_load = testrdb_aux_load,
+ .aux_save = testrdb_aux_save,
+ .aux_save_triggers = ((conf_aux_count & CONF_AUX_OPTION_BEFORE_KEYSPACE) ? REDISMODULE_AUX_BEFORE_RDB : 0) |
+ ((conf_aux_count & CONF_AUX_OPTION_AFTER_KEYSPACE) ? REDISMODULE_AUX_AFTER_RDB : 0)
+ };
+
+ if (conf_aux_count & CONF_AUX_OPTION_SAVE2) {
+ datatype_methods.aux_save2 = testrdb_aux_save;
+ }
+
+ testrdb_type = RedisModule_CreateDataType(ctx, "test__rdb", 1, &datatype_methods);
+ if (testrdb_type == NULL)
+ return REDISMODULE_ERR;
+ } else {
+
+ /* Used to verify that aux_save2 api without any data, saves nothing to the RDB. */
+ RedisModuleTypeMethods datatype_methods = {
+ .version = REDISMODULE_TYPE_METHOD_VERSION,
+ .aux_load = test2rdb_aux_load,
+ .aux_save = test2rdb_aux_save,
+ .aux_save_triggers = ((conf_aux_count & CONF_AUX_OPTION_BEFORE_KEYSPACE) ? REDISMODULE_AUX_BEFORE_RDB : 0) |
+ ((conf_aux_count & CONF_AUX_OPTION_AFTER_KEYSPACE) ? REDISMODULE_AUX_AFTER_RDB : 0)
+ };
+ if (conf_aux_count & CONF_AUX_OPTION_SAVE2) {
+ datatype_methods.aux_save2 = test2rdb_aux_save;
+ }
+
+ RedisModule_CreateDataType(ctx, "test__rdb", 1, &datatype_methods);
+ }
+
+ if (RedisModule_CreateCommand(ctx,"testrdb.set.before", testrdb_set_before,"deny-oom",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"testrdb.get.before", testrdb_get_before,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"testrdb.async_loading.get.before", testrdb_async_loading_get_before,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"testrdb.set.after", testrdb_set_after,"deny-oom",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"testrdb.get.after", testrdb_get_after,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"testrdb.set.key", testrdb_set_key,"deny-oom",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"testrdb.get.key", testrdb_get_key,"",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"testrdb.get.n_aux_load_called", testrdb_get_n_aux_load_called,"",1,1,1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ RedisModule_SubscribeToServerEvent(ctx,
+ RedisModuleEvent_ReplAsyncLoad, replAsyncLoadCallback);
+
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnUnload(RedisModuleCtx *ctx) {
+ if (before_str)
+ RedisModule_FreeString(ctx, before_str);
+ if (after_str)
+ RedisModule_FreeString(ctx, after_str);
+ if (before_str_temp)
+ RedisModule_FreeString(ctx, before_str_temp);
+ if (after_str_temp)
+ RedisModule_FreeString(ctx, after_str_temp);
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/timer.c b/examples/redis-unstable/tests/modules/timer.c
new file mode 100644
index 0000000..c9bd636
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/timer.c
@@ -0,0 +1,102 @@
+
+#include "redismodule.h"
+
+static void timer_callback(RedisModuleCtx *ctx, void *data)
+{
+ RedisModuleString *keyname = data;
+ RedisModuleCallReply *reply;
+
+ reply = RedisModule_Call(ctx, "INCR", "s", keyname);
+ if (reply != NULL)
+ RedisModule_FreeCallReply(reply);
+ RedisModule_FreeString(ctx, keyname);
+}
+
+int test_createtimer(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ if (argc != 3) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ long long period;
+ if (RedisModule_StringToLongLong(argv[1], &period) == REDISMODULE_ERR) {
+ RedisModule_ReplyWithError(ctx, "Invalid time specified.");
+ return REDISMODULE_OK;
+ }
+
+ RedisModuleString *keyname = argv[2];
+ RedisModule_RetainString(ctx, keyname);
+
+ RedisModuleTimerID id = RedisModule_CreateTimer(ctx, period, timer_callback, keyname);
+ RedisModule_ReplyWithLongLong(ctx, id);
+
+ return REDISMODULE_OK;
+}
+
+int test_gettimer(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ if (argc != 2) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ long long id;
+ if (RedisModule_StringToLongLong(argv[1], &id) == REDISMODULE_ERR) {
+ RedisModule_ReplyWithError(ctx, "Invalid id specified.");
+ return REDISMODULE_OK;
+ }
+
+ uint64_t remaining;
+ RedisModuleString *keyname;
+ if (RedisModule_GetTimerInfo(ctx, id, &remaining, (void **)&keyname) == REDISMODULE_ERR) {
+ RedisModule_ReplyWithNull(ctx);
+ } else {
+ RedisModule_ReplyWithArray(ctx, 2);
+ RedisModule_ReplyWithString(ctx, keyname);
+ RedisModule_ReplyWithLongLong(ctx, remaining);
+ }
+
+ return REDISMODULE_OK;
+}
+
+int test_stoptimer(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ if (argc != 2) {
+ RedisModule_WrongArity(ctx);
+ return REDISMODULE_OK;
+ }
+
+ long long id;
+ if (RedisModule_StringToLongLong(argv[1], &id) == REDISMODULE_ERR) {
+ RedisModule_ReplyWithError(ctx, "Invalid id specified.");
+ return REDISMODULE_OK;
+ }
+
+ int ret = 0;
+ RedisModuleString *keyname;
+ if (RedisModule_StopTimer(ctx, id, (void **) &keyname) == REDISMODULE_OK) {
+ RedisModule_FreeString(ctx, keyname);
+ ret = 1;
+ }
+
+ RedisModule_ReplyWithLongLong(ctx, ret);
+ return REDISMODULE_OK;
+}
+
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+ if (RedisModule_Init(ctx,"timer",1,REDISMODULE_APIVER_1)== REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"test.createtimer", test_createtimer,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"test.gettimer", test_gettimer,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+ if (RedisModule_CreateCommand(ctx,"test.stoptimer", test_stoptimer,"",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/usercall.c b/examples/redis-unstable/tests/modules/usercall.c
new file mode 100644
index 0000000..cc28817
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/usercall.c
@@ -0,0 +1,230 @@
+#include "redismodule.h"
+#include <pthread.h>
+#include <assert.h>
+
+#define UNUSED(V) ((void) V)
+
+RedisModuleUser *user = NULL;
+
+int call_without_user(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc < 2) {
+ return RedisModule_WrongArity(ctx);
+ }
+
+ const char *cmd = RedisModule_StringPtrLen(argv[1], NULL);
+
+ RedisModuleCallReply *rep = RedisModule_Call(ctx, cmd, "Ev", argv + 2, (size_t)argc - 2);
+ if (!rep) {
+ RedisModule_ReplyWithError(ctx, "NULL reply returned");
+ } else {
+ RedisModule_ReplyWithCallReply(ctx, rep);
+ RedisModule_FreeCallReply(rep);
+ }
+ return REDISMODULE_OK;
+}
+
+int call_with_user_flag(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc < 3) {
+ return RedisModule_WrongArity(ctx);
+ }
+
+ RedisModule_SetContextUser(ctx, user);
+
+ /* Append Ev to the provided flags. */
+ RedisModuleString *flags = RedisModule_CreateStringFromString(ctx, argv[1]);
+ RedisModule_StringAppendBuffer(ctx, flags, "Ev", 2);
+
+ const char* flg = RedisModule_StringPtrLen(flags, NULL);
+ const char* cmd = RedisModule_StringPtrLen(argv[2], NULL);
+
+ RedisModuleCallReply* rep = RedisModule_Call(ctx, cmd, flg, argv + 3, (size_t)argc - 3);
+ if (!rep) {
+ RedisModule_ReplyWithError(ctx, "NULL reply returned");
+ } else {
+ RedisModule_ReplyWithCallReply(ctx, rep);
+ RedisModule_FreeCallReply(rep);
+ }
+ RedisModule_FreeString(ctx, flags);
+
+ return REDISMODULE_OK;
+}
+
+int add_to_acl(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 2) {
+ return RedisModule_WrongArity(ctx);
+ }
+
+ size_t acl_len;
+ const char *acl = RedisModule_StringPtrLen(argv[1], &acl_len);
+
+ RedisModuleString *error;
+ int ret = RedisModule_SetModuleUserACLString(ctx, user, acl, &error);
+ if (ret) {
+ size_t len;
+ const char * e = RedisModule_StringPtrLen(error, &len);
+ RedisModule_ReplyWithError(ctx, e);
+ return REDISMODULE_OK;
+ }
+
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+
+ return REDISMODULE_OK;
+}
+
+int get_acl(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+
+ if (argc != 1) {
+ return RedisModule_WrongArity(ctx);
+ }
+
+ RedisModule_Assert(user != NULL);
+
+ RedisModuleString *acl = RedisModule_GetModuleUserACLString(user);
+
+ RedisModule_ReplyWithString(ctx, acl);
+
+ RedisModule_FreeString(NULL, acl);
+
+ return REDISMODULE_OK;
+}
+
+int reset_user(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+
+ if (argc != 1) {
+ return RedisModule_WrongArity(ctx);
+ }
+
+ if (user != NULL) {
+ RedisModule_FreeModuleUser(user);
+ }
+
+ user = RedisModule_CreateModuleUser("module_user");
+
+ RedisModule_ReplyWithSimpleString(ctx, "OK");
+
+ return REDISMODULE_OK;
+}
+
+typedef struct {
+ RedisModuleString **argv;
+ int argc;
+ RedisModuleBlockedClient *bc;
+} bg_call_data;
+
+void *bg_call_worker(void *arg) {
+ bg_call_data *bg = arg;
+ RedisModuleBlockedClient *bc = bg->bc;
+
+ // Get Redis module context
+ RedisModuleCtx *ctx = RedisModule_GetThreadSafeContext(bg->bc);
+
+ // Acquire GIL
+ RedisModule_ThreadSafeContextLock(ctx);
+
+ // Set user
+ RedisModule_SetContextUser(ctx, user);
+
+ // Call the command
+ size_t format_len;
+ RedisModuleString *format_redis_str = RedisModule_CreateString(NULL, "v", 1);
+ const char *format = RedisModule_StringPtrLen(bg->argv[1], &format_len);
+ RedisModule_StringAppendBuffer(NULL, format_redis_str, format, format_len);
+ RedisModule_StringAppendBuffer(NULL, format_redis_str, "E", 1);
+ format = RedisModule_StringPtrLen(format_redis_str, NULL);
+ const char *cmd = RedisModule_StringPtrLen(bg->argv[2], NULL);
+ RedisModuleCallReply *rep = RedisModule_Call(ctx, cmd, format, bg->argv + 3, (size_t)bg->argc - 3);
+ RedisModule_FreeString(NULL, format_redis_str);
+
+ /* Free the arguments within GIL to prevent simultaneous freeing in main thread. */
+ for (int i=0; i<bg->argc; i++)
+ RedisModule_FreeString(ctx, bg->argv[i]);
+ RedisModule_Free(bg->argv);
+ RedisModule_Free(bg);
+
+ // Release GIL
+ RedisModule_ThreadSafeContextUnlock(ctx);
+
+ // Reply to client
+ if (!rep) {
+ RedisModule_ReplyWithError(ctx, "NULL reply returned");
+ } else {
+ RedisModule_ReplyWithCallReply(ctx, rep);
+ RedisModule_FreeCallReply(rep);
+ }
+
+ // Unblock client
+ RedisModule_UnblockClient(bc, NULL);
+
+ // Free the Redis module context
+ RedisModule_FreeThreadSafeContext(ctx);
+
+ return NULL;
+}
+
+int call_with_user_bg(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+ UNUSED(argv);
+ UNUSED(argc);
+
+ /* Make sure we're not trying to block a client when we shouldn't */
+ int flags = RedisModule_GetContextFlags(ctx);
+ int allFlags = RedisModule_GetContextFlagsAll();
+ if ((allFlags & REDISMODULE_CTX_FLAGS_MULTI) &&
+ (flags & REDISMODULE_CTX_FLAGS_MULTI)) {
+ RedisModule_ReplyWithSimpleString(ctx, "Blocked client is not supported inside multi");
+ return REDISMODULE_OK;
+ }
+ if ((allFlags & REDISMODULE_CTX_FLAGS_DENY_BLOCKING) &&
+ (flags & REDISMODULE_CTX_FLAGS_DENY_BLOCKING)) {
+ RedisModule_ReplyWithSimpleString(ctx, "Blocked client is not allowed");
+ return REDISMODULE_OK;
+ }
+
+ /* Make a copy of the arguments and pass them to the thread. */
+ bg_call_data *bg = RedisModule_Alloc(sizeof(bg_call_data));
+ bg->argv = RedisModule_Alloc(sizeof(RedisModuleString*)*argc);
+ bg->argc = argc;
+ for (int i=0; i<argc; i++)
+ bg->argv[i] = RedisModule_HoldString(ctx, argv[i]);
+
+ /* Block the client */
+ bg->bc = RedisModule_BlockClient(ctx, NULL, NULL, NULL, 0);
+
+ /* Start a thread to handle the request */
+ pthread_t tid;
+ int res = pthread_create(&tid, NULL, bg_call_worker, bg);
+ assert(res == 0);
+ pthread_detach(tid);
+
+ return REDISMODULE_OK;
+}
+
+int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(argc);
+
+ if (RedisModule_Init(ctx,"usercall",1,REDISMODULE_APIVER_1)== REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"usercall.call_without_user", call_without_user,"write",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"usercall.call_with_user_flag", call_with_user_flag,"write",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "usercall.call_with_user_bg", call_with_user_bg, "write", 0, 0, 0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "usercall.add_to_acl", add_to_acl, "write",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"usercall.reset_user", reset_user,"write",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx,"usercall.get_acl", get_acl,"write",0,0,0) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ return REDISMODULE_OK;
+}
diff --git a/examples/redis-unstable/tests/modules/zset.c b/examples/redis-unstable/tests/modules/zset.c
new file mode 100644
index 0000000..adb318d
--- /dev/null
+++ b/examples/redis-unstable/tests/modules/zset.c
@@ -0,0 +1,180 @@
+#include "redismodule.h"
+#include <math.h>
+#include <errno.h>
+
+/* ZSET.REM key element
+ *
+ * Removes an occurrence of an element from a sorted set. Replies with the
+ * number of removed elements (0 or 1).
+ */
+int zset_rem(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 3) return RedisModule_WrongArity(ctx);
+ RedisModule_AutoMemory(ctx);
+ int keymode = REDISMODULE_READ | REDISMODULE_WRITE;
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], keymode);
+ int deleted;
+ if (RedisModule_ZsetRem(key, argv[2], &deleted) == REDISMODULE_OK)
+ return RedisModule_ReplyWithLongLong(ctx, deleted);
+ else
+ return RedisModule_ReplyWithError(ctx, "ERR ZsetRem failed");
+}
+
+/* ZSET.ADD key score member
+ *
+ * Adds a specified member with the specified score to the sorted
+ * set stored at key.
+ */
+int zset_add(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 4) return RedisModule_WrongArity(ctx);
+ RedisModule_AutoMemory(ctx);
+ int keymode = REDISMODULE_READ | REDISMODULE_WRITE;
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], keymode);
+
+ size_t len;
+ double score;
+ char *endptr;
+ const char *str = RedisModule_StringPtrLen(argv[2], &len);
+ score = strtod(str, &endptr);
+ if (*endptr != '\0' || errno == ERANGE)
+ return RedisModule_ReplyWithError(ctx, "value is not a valid float");
+
+ if (RedisModule_ZsetAdd(key, score, argv[3], NULL) == REDISMODULE_OK)
+ return RedisModule_ReplyWithSimpleString(ctx, "OK");
+ else
+ return RedisModule_ReplyWithError(ctx, "ERR ZsetAdd failed");
+}
+
+/* ZSET.INCRBY key member increment
+ *
+ * Increments the score stored at member in the sorted set stored at key by increment.
+ * Replies with the new score of this element.
+ */
+int zset_incrby(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
+ if (argc != 4) return RedisModule_WrongArity(ctx);
+ RedisModule_AutoMemory(ctx);
+ int keymode = REDISMODULE_READ | REDISMODULE_WRITE;
+ RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], keymode);
+
+ size_t len;
+ double score, newscore;
+ char *endptr;
+ const char *str = RedisModule_StringPtrLen(argv[3], &len);
+ score = strtod(str, &endptr);
+ if (*endptr != '\0' || errno == ERANGE)
+ return RedisModule_ReplyWithError(ctx, "value is not a valid float");
+
+ if (RedisModule_ZsetIncrby(key, score, argv[2], NULL, &newscore) == REDISMODULE_OK)
+ return RedisModule_ReplyWithDouble(ctx, newscore);
+ else
+ 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);
+ if (RedisModule_Init(ctx, "zset", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "zset.rem", zset_rem, "write",
+ 1, 1, 1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "zset.add", zset_add, "write",
+ 1, 1, 1) == REDISMODULE_ERR)
+ return REDISMODULE_ERR;
+
+ if (RedisModule_CreateCommand(ctx, "zset.incrby", zset_incrby, "write",
+ 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;
+}