diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-14 13:40:54 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-14 13:40:54 +0000 |
commit | 317c0644ccf108aa23ef3fd8358bd66c2840bfc0 (patch) | |
tree | c417b3d25c86b775989cb5ac042f37611b626c8a /tests/modules | |
parent | Initial commit. (diff) | |
download | redis-upstream/5%7.2.4.tar.xz redis-upstream/5%7.2.4.zip |
Adding upstream version 5:7.2.4.upstream/5%7.2.4
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'tests/modules')
40 files changed, 11032 insertions, 0 deletions
diff --git a/tests/modules/Makefile b/tests/modules/Makefile new file mode 100644 index 0000000..d63c854 --- /dev/null +++ b/tests/modules/Makefile @@ -0,0 +1,83 @@ + +# 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=c99 -O2 + SHOBJ_LDFLAGS ?= -bundle -undefined dynamic_lookup +else # Linux, others + SHOBJ_CFLAGS ?= $(warning_cflags) -fno-common -g -ggdb -std=c99 -O2 + SHOBJ_LDFLAGS ?= -shared +endif + +ifeq ($(uname_S),Linux) + LD = gcc + CC = gcc +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 + +.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/tests/modules/aclcheck.c b/tests/modules/aclcheck.c new file mode 100644 index 0000000..09b525c --- /dev/null +++ b/tests/modules/aclcheck.c @@ -0,0 +1,269 @@ + +#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, 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 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, 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, 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, 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, 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); + + 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 outside OnLoad, + * 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) { + REDISMODULE_NOT_USED(argv); + REDISMODULE_NOT_USED(argc); + + if (RedisModule_Init(ctx,"aclcheck",1,REDISMODULE_APIVER_1)== REDISMODULE_ERR) + return REDISMODULE_ERR; + + if (RedisModule_CreateCommand(ctx,"aclcheck.set.check.key", set_aclcheck_key,"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; + + return REDISMODULE_OK; +} diff --git a/tests/modules/auth.c b/tests/modules/auth.c new file mode 100644 index 0000000..19be95a --- /dev/null +++ b/tests/modules/auth.c @@ -0,0 +1,270 @@ +/* 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; + +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; + } + RedisModule_BlockedClientMeasureTimeStart(bc); + pthread_t tid; + /* 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); + } + 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 (global) + RedisModule_FreeModuleUser(global); + + return REDISMODULE_OK; +} diff --git a/tests/modules/basics.c b/tests/modules/basics.c new file mode 100644 index 0000000..897cb5d --- /dev/null +++ b/tests/modules/basics.c @@ -0,0 +1,1052 @@ +/* Module designed to test the Redis modules subsystem. + * + * ----------------------------------------------------------------------------- + * + * Copyright (c) 2016, Salvatore Sanfilippo <antirez at gmail dot com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#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; +} + +#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', with args beginning with: ",53)) 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; + + 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/tests/modules/blockedclient.c b/tests/modules/blockedclient.c new file mode 100644 index 0000000..92060fd --- /dev/null +++ b/tests/modules/blockedclient.c @@ -0,0 +1,712 @@ +/* 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); + + 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; + + // 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, bg->argc - cmd_pos - 1); + RedisModule_FreeString(NULL, format_redis_str); + + // 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(bg->bc, NULL); + + /* Free the arguments */ + for (int i=0; i<bg->argc; i++) + RedisModule_FreeString(ctx, bg->argv[i]); + RedisModule_Free(bg->argv); + RedisModule_Free(bg); + + // 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); + + 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, 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, 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, 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); +} + +/* + * 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, 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, 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); +} + +int unblock_by_timer(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) +{ + if (argc != 2) + return RedisModule_WrongArity(ctx); + + long long period; + if (RedisModule_StringToLongLong(argv[1],&period) != REDISMODULE_OK) + return RedisModule_ReplyWithError(ctx,"ERR invalid period"); + + RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx, NULL, NULL, NULL, 0); + 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/tests/modules/blockonbackground.c b/tests/modules/blockonbackground.c new file mode 100644 index 0000000..2e3b1a5 --- /dev/null +++ b/tests/modules/blockonbackground.c @@ -0,0 +1,295 @@ +#define _XOPEN_SOURCE 700 +#include "redismodule.h" +#include <stdio.h> +#include <stdlib.h> +#include <pthread.h> +#include <time.h> + +#define UNUSED(x) (void)(x) + +/* Reply callback for blocking command BLOCK.DEBUG */ +int HelloBlock_Reply(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + UNUSED(argv); + UNUSED(argc); + int *myint = RedisModule_GetBlockedClientPrivateData(ctx); + return RedisModule_ReplyWithLongLong(ctx,*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); + RedisModule_BlockedClientMeasureTimeEnd(bc); + return RedisModule_ReplyWithSimpleString(ctx,"Request timedout"); +} + +/* Private data freeing callback for BLOCK.DEBUG command. */ +void HelloBlock_FreeData(RedisModuleCtx *ctx, void *privdata) { + UNUSED(ctx); + 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]; + if (enable_time_track) + RedisModule_BlockedClientMeasureTimeStart(bc); + RedisModule_Free(targ); + + struct timespec ts; + ts.tv_sec = delay / 1000; + ts.tv_nsec = (delay % 1000) * 1000000; + nanosleep(&ts, NULL); + int *r = RedisModule_Alloc(sizeof(int)); + *r = rand(); + if (enable_time_track) + RedisModule_BlockedClientMeasureTimeEnd(bc); + RedisModule_UnblockClient(bc,r); + 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]; + RedisModule_BlockedClientMeasureTimeStart(bc); + RedisModule_Free(targ); + struct timespec ts; + ts.tv_sec = delay / 1000; + ts.tv_nsec = (delay % 1000) * 1000000; + nanosleep(&ts, NULL); + int *r = RedisModule_Alloc(sizeof(int)); + *r = rand(); + RedisModule_BlockedClientMeasureTimeEnd(bc); + /* call again RedisModule_BlockedClientMeasureTimeStart() and + * RedisModule_BlockedClientMeasureTimeEnd and ensure that the + * total execution time is 2x the delay. */ + RedisModule_BlockedClientMeasureTimeStart(bc); + nanosleep(&ts, NULL); + RedisModule_BlockedClientMeasureTimeEnd(bc); + + RedisModule_UnblockClient(bc,r); + 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); + + /* 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"); + } + 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); + + /* 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"); + } + 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); + + /* 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"); + } + 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/tests/modules/blockonkeys.c b/tests/modules/blockonkeys.c new file mode 100644 index 0000000..94bb361 --- /dev/null +++ b/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],>) != 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/tests/modules/cmdintrospection.c b/tests/modules/cmdintrospection.c new file mode 100644 index 0000000..1a5e486 --- /dev/null +++ b/tests/modules/cmdintrospection.c @@ -0,0 +1,158 @@ +#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."}, + {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 = "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/tests/modules/commandfilter.c b/tests/modules/commandfilter.c new file mode 100644 index 0000000..56e517a --- /dev/null +++ b/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], 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/tests/modules/datatype.c b/tests/modules/datatype.c new file mode 100644 index 0000000..408d1a5 --- /dev/null +++ b/tests/modules/datatype.c @@ -0,0 +1,314 @@ +/* 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; +} diff --git a/tests/modules/datatype2.c b/tests/modules/datatype2.c new file mode 100644 index 0000000..bc0dc3d --- /dev/null +++ b/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/tests/modules/defragtest.c b/tests/modules/defragtest.c new file mode 100644 index 0000000..6a02a05 --- /dev/null +++ b/tests/modules/defragtest.c @@ -0,0 +1,235 @@ +/* A module that implements defrag callback mechanisms. + */ + +#include "redismodule.h" +#include <stdlib.h> + +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_resumes = 0; +unsigned long int datatype_wrong_cursor = 0; +unsigned long int global_attempts = 0; +unsigned long int global_defragged = 0; + +int global_strings_len = 0; +RedisModuleString **global_strings = NULL; + +static void createGlobalStrings(RedisModuleCtx *ctx, int count) +{ + global_strings_len = count; + global_strings = RedisModule_Alloc(sizeof(RedisModuleString *) * count); + + for (int i = 0; i < count; i++) { + global_strings[i] = RedisModule_CreateStringFromLongLong(ctx, i); + } +} + +static void defragGlobalStrings(RedisModuleDefragCtx *ctx) +{ + for (int i = 0; i < global_strings_len; i++) { + RedisModuleString *new = RedisModule_DefragRedisModuleString(ctx, global_strings[i]); + global_attempts++; + if (new != NULL) { + global_strings[i] = new; + global_defragged++; + } + } +} + +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_resumes", datatype_resumes); + RedisModule_InfoAddFieldLongLong(ctx, "datatype_wrong_cursor", datatype_wrong_cursor); + RedisModule_InfoAddFieldLongLong(ctx, "global_attempts", global_attempts); + RedisModule_InfoAddFieldLongLong(ctx, "global_defragged", global_defragged); +} + +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_resumes = 0; + datatype_wrong_cursor = 0; + global_attempts = 0; + global_defragged = 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; +} + +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) { + REDISMODULE_NOT_USED(key); + unsigned long i = 0; + int steps = 0; + + int dbid = RedisModule_GetDbIdFromDefragCtx(ctx); + RedisModule_Assert(dbid != -1); + + /* 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; + } + } + + 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; + } + + long long glen; + if (argc != 1 || RedisModule_StringToLongLong(argv[0], &glen) == REDISMODULE_ERR) { + return REDISMODULE_ERR; + } + + createGlobalStrings(ctx, glen); + + 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.resetstats", + fragResetStatsCommand, "write deny-oom", 1, 1, 1) == REDISMODULE_ERR) + return REDISMODULE_ERR; + + RedisModule_RegisterInfoFunc(ctx, FragInfo); + RedisModule_RegisterDefragFunc(ctx, defragGlobalStrings); + + return REDISMODULE_OK; +} diff --git a/tests/modules/eventloop.c b/tests/modules/eventloop.c new file mode 100644 index 0000000..c0cfdf0 --- /dev/null +++ b/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/tests/modules/fork.c b/tests/modules/fork.c new file mode 100644 index 0000000..d7a0d15 --- /dev/null +++ b/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/tests/modules/getchannels.c b/tests/modules/getchannels.c new file mode 100644 index 0000000..330531d --- /dev/null +++ b/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/tests/modules/getkeys.c b/tests/modules/getkeys.c new file mode 100644 index 0000000..cee3b3e --- /dev/null +++ b/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/tests/modules/hash.c b/tests/modules/hash.c new file mode 100644 index 0000000..001a34e --- /dev/null +++ b/tests/modules/hash.c @@ -0,0 +1,90 @@ +#include "redismodule.h" +#include <strings.h> +#include <errno.h> +#include <stdlib.h> + +/* 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); +} + +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_OK && + RedisModule_CreateCommand(ctx, "hash.set", hash_set, "write", + 1, 1, 1) == REDISMODULE_OK) { + return REDISMODULE_OK; + } else { + return REDISMODULE_ERR; + } +} diff --git a/tests/modules/hooks.c b/tests/modules/hooks.c new file mode 100644 index 0000000..fc357d1 --- /dev/null +++ b/tests/modules/hooks.c @@ -0,0 +1,516 @@ +/* This module is used to test the server events hooks API. + * + * ----------------------------------------------------------------------------- + * + * Copyright (c) 2019, Salvatore Sanfilippo <antirez at gmail dot com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#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/tests/modules/infotest.c b/tests/modules/infotest.c new file mode 100644 index 0000000..87a89dc --- /dev/null +++ b/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/tests/modules/keyspace_events.c b/tests/modules/keyspace_events.c new file mode 100644 index 0000000..1a284b5 --- /dev/null +++ b/tests/modules/keyspace_events.c @@ -0,0 +1,440 @@ +/* This module is used to test the server keyspace events API. + * + * ----------------------------------------------------------------------------- + * + * Copyright (c) 2020, Meir Shpilraien <meir at redislabs dot com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#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 int KeySpace_NotificationGeneric(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key) { + REDISMODULE_NOT_USED(type); + 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); +} + +/* 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 (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/tests/modules/keyspecs.c b/tests/modules/keyspecs.c new file mode 100644 index 0000000..0a70de8 --- /dev/null +++ b/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/tests/modules/list.c b/tests/modules/list.c new file mode 100644 index 0000000..401b2d8 --- /dev/null +++ b/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/tests/modules/mallocsize.c b/tests/modules/mallocsize.c new file mode 100644 index 0000000..a1d31c1 --- /dev/null +++ b/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/tests/modules/misc.c b/tests/modules/misc.c new file mode 100644 index 0000000..46bfcb1 --- /dev/null +++ b/tests/modules/misc.c @@ -0,0 +1,571 @@ +#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, 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, 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, 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 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; + + return REDISMODULE_OK; +} diff --git a/tests/modules/moduleauthtwo.c b/tests/modules/moduleauthtwo.c new file mode 100644 index 0000000..0a4f56b --- /dev/null +++ b/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/tests/modules/moduleconfigs.c b/tests/modules/moduleconfigs.c new file mode 100644 index 0000000..2c1737d --- /dev/null +++ b/tests/modules/moduleconfigs.c @@ -0,0 +1,195 @@ +#include "redismodule.h" +#include <strings.h> +int mutable_bool_val; +int immutable_bool_val; +long long longval; +long long memval; +RedisModuleString *strval = NULL; +int 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; +} + +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; +} + +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) return REDISMODULE_ERR; + + if (RedisModule_RegisterBoolConfig(ctx, "mutable_bool", 1, REDISMODULE_CONFIG_DEFAULT, getBoolConfigCommand, setBoolConfigCommand, boolApplyFunc, &mutable_bool_val) == REDISMODULE_ERR) { + return REDISMODULE_ERR; + } + /* Immutable config here. */ + if (RedisModule_RegisterBoolConfig(ctx, "immutable_bool", 0, REDISMODULE_CONFIG_IMMUTABLE, getBoolConfigCommand, setBoolConfigCommand, boolApplyFunc, &immutable_bool_val) == REDISMODULE_ERR) { + return REDISMODULE_ERR; + } + if (RedisModule_RegisterStringConfig(ctx, "string", "secret password", REDISMODULE_CONFIG_DEFAULT, getStringConfigCommand, setStringConfigCommand, NULL, NULL) == REDISMODULE_ERR) { + 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) { + 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) { + 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) { + return REDISMODULE_ERR; + } + if (RedisModule_RegisterNumericConfig(ctx, "numeric", -1, REDISMODULE_CONFIG_DEFAULT, -5, 2000, getNumericConfigCommand, setNumericConfigCommand, longlongApplyFunc, &longval) == REDISMODULE_ERR) { + return REDISMODULE_ERR; + } + size_t len; + if (argc && !strcasecmp(RedisModule_StringPtrLen(argv[0], &len), "noload")) { + return REDISMODULE_OK; + } else if (RedisModule_LoadConfigs(ctx) == REDISMODULE_ERR) { + if (strval) { + RedisModule_FreeString(ctx, strval); + strval = NULL; + } + return REDISMODULE_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) + return REDISMODULE_ERR; + + return REDISMODULE_OK; +} + +int RedisModule_OnUnload(RedisModuleCtx *ctx) { + REDISMODULE_NOT_USED(ctx); + if (strval) { + RedisModule_FreeString(ctx, strval); + strval = NULL; + } + return REDISMODULE_OK; +} diff --git a/tests/modules/moduleconfigstwo.c b/tests/modules/moduleconfigstwo.c new file mode 100644 index 0000000..c0e8f91 --- /dev/null +++ b/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/tests/modules/postnotifications.c b/tests/modules/postnotifications.c new file mode 100644 index 0000000..b4a97cb --- /dev/null +++ b/tests/modules/postnotifications.c @@ -0,0 +1,303 @@ +/* This module is used to test the server post keyspace jobs API. + * + * ----------------------------------------------------------------------------- + * + * Copyright (c) 2020, Meir Shpilraien <meir at redislabs dot com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/* 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); + RedisModule_AddPostNotificationJob(ctx, KeySpace_PostNotificationString, new_key, KeySpace_PostNotificationStringFreePD); + 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); + RedisModule_AddPostNotificationJob(ctx, KeySpace_PostNotificationString, new_key, KeySpace_PostNotificationStringFreePD); + 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); + } + + RedisModule_AddPostNotificationJob(ctx, KeySpace_PostNotificationString, new_key, KeySpace_PostNotificationStringFreePD); + 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);; + RedisModule_AddPostNotificationJob(ctx, KeySpace_PostNotificationReadKey, new_key, KeySpace_PostNotificationStringFreePD); + 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"); + } + 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 '%ld'", 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])); + RedisModule_AddPostNotificationJob(ctx, KeySpace_ServerEventPostNotification, pn_ctx, KeySpace_ServerEventPostNotificationFree); +} + +/* 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/tests/modules/propagate.c b/tests/modules/propagate.c new file mode 100644 index 0000000..d5132a5 --- /dev/null +++ b/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, Salvatore Sanfilippo <antirez at gmail dot com> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#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"); + REDISMODULE_NOT_USED(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"); + REDISMODULE_NOT_USED(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 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; + + return REDISMODULE_OK; +} + +int RedisModule_OnUnload(RedisModuleCtx *ctx) { + UNUSED(ctx); + + if (detached_ctx) + RedisModule_FreeThreadSafeContext(detached_ctx); + + return REDISMODULE_OK; +} diff --git a/tests/modules/publish.c b/tests/modules/publish.c new file mode 100644 index 0000000..ff276d8 --- /dev/null +++ b/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/tests/modules/rdbloadsave.c b/tests/modules/rdbloadsave.c new file mode 100644 index 0000000..687269a --- /dev/null +++ b/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/tests/modules/reply.c b/tests/modules/reply.c new file mode 100644 index 0000000..c5baa66 --- /dev/null +++ b/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/tests/modules/scan.c b/tests/modules/scan.c new file mode 100644 index 0000000..1723d30 --- /dev/null +++ b/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/tests/modules/stream.c b/tests/modules/stream.c new file mode 100644 index 0000000..65762a3 --- /dev/null +++ b/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/tests/modules/subcommands.c b/tests/modules/subcommands.c new file mode 100644 index 0000000..1b2bc51 --- /dev/null +++ b/tests/modules/subcommands.c @@ -0,0 +1,112 @@ +#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); + + return REDISMODULE_OK; +} diff --git a/tests/modules/test_lazyfree.c b/tests/modules/test_lazyfree.c new file mode 100644 index 0000000..7ba213f --- /dev/null +++ b/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/tests/modules/testrdb.c b/tests/modules/testrdb.c new file mode 100644 index 0000000..c31aebb --- /dev/null +++ b/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/tests/modules/timer.c b/tests/modules/timer.c new file mode 100644 index 0000000..c9bd636 --- /dev/null +++ b/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/tests/modules/usercall.c b/tests/modules/usercall.c new file mode 100644 index 0000000..6b23974 --- /dev/null +++ b/tests/modules/usercall.c @@ -0,0 +1,228 @@ +#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, 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, 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; + + // 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, bg->argc - 3); + RedisModule_FreeString(NULL, format_redis_str); + + // 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(bg->bc, NULL); + + /* Free the arguments */ + for (int i=0; i<bg->argc; i++) + RedisModule_FreeString(ctx, bg->argv[i]); + RedisModule_Free(bg->argv); + RedisModule_Free(bg); + + // 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); + + 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/tests/modules/zset.c b/tests/modules/zset.c new file mode 100644 index 0000000..13f2ab3 --- /dev/null +++ b/tests/modules/zset.c @@ -0,0 +1,91 @@ +#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"); +} + +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; + + return REDISMODULE_OK; +} |