diff options
| author | Mitja Felicijan <mitja.felicijan@gmail.com> | 2026-01-21 22:40:55 +0100 |
|---|---|---|
| committer | Mitja Felicijan <mitja.felicijan@gmail.com> | 2026-01-21 22:40:55 +0100 |
| commit | 5d8dfe892a2ea89f706ee140c3bdcfd89fe03fda (patch) | |
| tree | 1acdfa5220cd13b7be43a2a01368e80d306473ca /examples/redis-unstable/tests/modules | |
| parent | c7ab12bba64d9c20ccd79b132dac475f7bc3923e (diff) | |
| download | crep-5d8dfe892a2ea89f706ee140c3bdcfd89fe03fda.tar.gz | |
Add Redis source code for testing
Diffstat (limited to 'examples/redis-unstable/tests/modules')
45 files changed, 13949 insertions, 0 deletions
diff --git a/examples/redis-unstable/tests/modules/Makefile b/examples/redis-unstable/tests/modules/Makefile new file mode 100644 index 0000000..f141234 --- /dev/null +++ b/examples/redis-unstable/tests/modules/Makefile | |||
| @@ -0,0 +1,106 @@ | |||
| 1 | |||
| 2 | # find the OS | ||
| 3 | uname_S := $(shell sh -c 'uname -s 2>/dev/null || echo not') | ||
| 4 | |||
| 5 | warning_cflags = -W -Wall -Wno-missing-field-initializers | ||
| 6 | ifeq ($(uname_S),Darwin) | ||
| 7 | SHOBJ_CFLAGS ?= $(warning_cflags) -dynamic -fno-common -g -ggdb -std=gnu11 -O2 | ||
| 8 | SHOBJ_LDFLAGS ?= -bundle -undefined dynamic_lookup | ||
| 9 | else # Linux, others | ||
| 10 | SHOBJ_CFLAGS ?= $(warning_cflags) -fno-common -g -ggdb -std=gnu11 -O2 | ||
| 11 | SHOBJ_LDFLAGS ?= -shared | ||
| 12 | endif | ||
| 13 | |||
| 14 | CLANG := $(findstring clang,$(shell sh -c '$(CC) --version | head -1')) | ||
| 15 | |||
| 16 | ifeq ($(SANITIZER),memory) | ||
| 17 | ifeq (clang, $(CLANG)) | ||
| 18 | LD=clang | ||
| 19 | MALLOC=libc | ||
| 20 | CFLAGS+=-fsanitize=memory -fsanitize-memory-track-origins=2 -fno-sanitize-recover=all -fno-omit-frame-pointer | ||
| 21 | LDFLAGS+=-fsanitize=memory | ||
| 22 | else | ||
| 23 | $(error "MemorySanitizer needs to be compiled and linked with clang. Please use CC=clang") | ||
| 24 | endif | ||
| 25 | endif | ||
| 26 | |||
| 27 | |||
| 28 | # This is a hack to override the default CC. When running with SANITIZER=memory | ||
| 29 | # tough we want to keep the compiler as clang as MSan is not supported for gcc | ||
| 30 | ifeq ($(uname_S),Linux) | ||
| 31 | ifneq ($(SANITIZER),memory) | ||
| 32 | LD = gcc | ||
| 33 | CC = gcc | ||
| 34 | endif | ||
| 35 | endif | ||
| 36 | |||
| 37 | # OS X 11.x doesn't have /usr/lib/libSystem.dylib and needs an explicit setting. | ||
| 38 | ifeq ($(uname_S),Darwin) | ||
| 39 | ifeq ("$(wildcard /usr/lib/libSystem.dylib)","") | ||
| 40 | LIBS = -L /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib -lsystem | ||
| 41 | endif | ||
| 42 | endif | ||
| 43 | |||
| 44 | TEST_MODULES = \ | ||
| 45 | commandfilter.so \ | ||
| 46 | basics.so \ | ||
| 47 | testrdb.so \ | ||
| 48 | fork.so \ | ||
| 49 | infotest.so \ | ||
| 50 | propagate.so \ | ||
| 51 | misc.so \ | ||
| 52 | hooks.so \ | ||
| 53 | blockonkeys.so \ | ||
| 54 | blockonbackground.so \ | ||
| 55 | scan.so \ | ||
| 56 | datatype.so \ | ||
| 57 | datatype2.so \ | ||
| 58 | auth.so \ | ||
| 59 | keyspace_events.so \ | ||
| 60 | blockedclient.so \ | ||
| 61 | getkeys.so \ | ||
| 62 | getchannels.so \ | ||
| 63 | test_lazyfree.so \ | ||
| 64 | timer.so \ | ||
| 65 | defragtest.so \ | ||
| 66 | keyspecs.so \ | ||
| 67 | hash.so \ | ||
| 68 | zset.so \ | ||
| 69 | stream.so \ | ||
| 70 | mallocsize.so \ | ||
| 71 | aclcheck.so \ | ||
| 72 | list.so \ | ||
| 73 | subcommands.so \ | ||
| 74 | reply.so \ | ||
| 75 | cmdintrospection.so \ | ||
| 76 | eventloop.so \ | ||
| 77 | moduleconfigs.so \ | ||
| 78 | moduleconfigstwo.so \ | ||
| 79 | publish.so \ | ||
| 80 | usercall.so \ | ||
| 81 | postnotifications.so \ | ||
| 82 | moduleauthtwo.so \ | ||
| 83 | rdbloadsave.so \ | ||
| 84 | crash.so \ | ||
| 85 | internalsecret.so \ | ||
| 86 | configaccess.so \ | ||
| 87 | test_keymeta.so \ | ||
| 88 | atomicslotmigration.so | ||
| 89 | |||
| 90 | .PHONY: all | ||
| 91 | |||
| 92 | all: $(TEST_MODULES) | ||
| 93 | |||
| 94 | 32bit: | ||
| 95 | $(MAKE) CFLAGS="-m32" LDFLAGS="-m32" | ||
| 96 | |||
| 97 | %.xo: %.c ../../src/redismodule.h | ||
| 98 | $(CC) -I../../src $(CFLAGS) $(SHOBJ_CFLAGS) -fPIC -c $< -o $@ | ||
| 99 | |||
| 100 | %.so: %.xo | ||
| 101 | $(LD) -o $@ $^ $(SHOBJ_LDFLAGS) $(LDFLAGS) $(LIBS) | ||
| 102 | |||
| 103 | .PHONY: clean | ||
| 104 | |||
| 105 | clean: | ||
| 106 | rm -f $(TEST_MODULES) $(TEST_MODULES:.so=.xo) | ||
diff --git a/examples/redis-unstable/tests/modules/aclcheck.c b/examples/redis-unstable/tests/modules/aclcheck.c new file mode 100644 index 0000000..d7e4e3b --- /dev/null +++ b/examples/redis-unstable/tests/modules/aclcheck.c | |||
| @@ -0,0 +1,365 @@ | |||
| 1 | |||
| 2 | #include "redismodule.h" | ||
| 3 | #include <errno.h> | ||
| 4 | #include <assert.h> | ||
| 5 | #include <string.h> | ||
| 6 | #include <strings.h> | ||
| 7 | |||
| 8 | /* A wrap for SET command with ACL check on the key. */ | ||
| 9 | int set_aclcheck_key(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 10 | if (argc < 4) { | ||
| 11 | return RedisModule_WrongArity(ctx); | ||
| 12 | } | ||
| 13 | |||
| 14 | int permissions; | ||
| 15 | const char *flags = RedisModule_StringPtrLen(argv[1], NULL); | ||
| 16 | |||
| 17 | if (!strcasecmp(flags, "W")) { | ||
| 18 | permissions = REDISMODULE_CMD_KEY_UPDATE; | ||
| 19 | } else if (!strcasecmp(flags, "R")) { | ||
| 20 | permissions = REDISMODULE_CMD_KEY_ACCESS; | ||
| 21 | } else if (!strcasecmp(flags, "*")) { | ||
| 22 | permissions = REDISMODULE_CMD_KEY_UPDATE | REDISMODULE_CMD_KEY_ACCESS; | ||
| 23 | } else if (!strcasecmp(flags, "~")) { | ||
| 24 | permissions = 0; /* Requires either read or write */ | ||
| 25 | } else { | ||
| 26 | RedisModule_ReplyWithError(ctx, "INVALID FLAGS"); | ||
| 27 | return REDISMODULE_OK; | ||
| 28 | } | ||
| 29 | |||
| 30 | /* Check that the key can be accessed */ | ||
| 31 | RedisModuleString *user_name = RedisModule_GetCurrentUserName(ctx); | ||
| 32 | RedisModuleUser *user = RedisModule_GetModuleUserFromUserName(user_name); | ||
| 33 | int ret = RedisModule_ACLCheckKeyPermissions(user, argv[2], permissions); | ||
| 34 | if (ret != 0) { | ||
| 35 | RedisModule_ReplyWithError(ctx, "DENIED KEY"); | ||
| 36 | RedisModule_FreeModuleUser(user); | ||
| 37 | RedisModule_FreeString(ctx, user_name); | ||
| 38 | return REDISMODULE_OK; | ||
| 39 | } | ||
| 40 | |||
| 41 | RedisModuleCallReply *rep = RedisModule_Call(ctx, "SET", "v", argv + 2, (size_t)argc - 2); | ||
| 42 | if (!rep) { | ||
| 43 | RedisModule_ReplyWithError(ctx, "NULL reply returned"); | ||
| 44 | } else { | ||
| 45 | RedisModule_ReplyWithCallReply(ctx, rep); | ||
| 46 | RedisModule_FreeCallReply(rep); | ||
| 47 | } | ||
| 48 | |||
| 49 | RedisModule_FreeModuleUser(user); | ||
| 50 | RedisModule_FreeString(ctx, user_name); | ||
| 51 | return REDISMODULE_OK; | ||
| 52 | } | ||
| 53 | |||
| 54 | /* A wrap for SET command with ACL check on the key. */ | ||
| 55 | int set_aclcheck_prefixkey(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 56 | if (argc < 4) { | ||
| 57 | return RedisModule_WrongArity(ctx); | ||
| 58 | } | ||
| 59 | |||
| 60 | int permissions; | ||
| 61 | const char *flags = RedisModule_StringPtrLen(argv[1], NULL); | ||
| 62 | |||
| 63 | if (!strcasecmp(flags, "W")) { | ||
| 64 | permissions = REDISMODULE_CMD_KEY_UPDATE; | ||
| 65 | } else if (!strcasecmp(flags, "R")) { | ||
| 66 | permissions = REDISMODULE_CMD_KEY_ACCESS; | ||
| 67 | } else if (!strcasecmp(flags, "*")) { | ||
| 68 | permissions = REDISMODULE_CMD_KEY_UPDATE | REDISMODULE_CMD_KEY_ACCESS; | ||
| 69 | } else if (!strcasecmp(flags, "~")) { | ||
| 70 | permissions = 0; /* Requires either read or write */ | ||
| 71 | } else { | ||
| 72 | RedisModule_ReplyWithError(ctx, "INVALID FLAGS"); | ||
| 73 | return REDISMODULE_OK; | ||
| 74 | } | ||
| 75 | |||
| 76 | /* Check that the key can be accessed */ | ||
| 77 | RedisModuleString *user_name = RedisModule_GetCurrentUserName(ctx); | ||
| 78 | RedisModuleUser *user = RedisModule_GetModuleUserFromUserName(user_name); | ||
| 79 | int ret = RedisModule_ACLCheckKeyPrefixPermissions(user, argv[2], permissions); | ||
| 80 | if (ret != 0) { | ||
| 81 | RedisModule_ReplyWithError(ctx, "DENIED KEY"); | ||
| 82 | RedisModule_FreeModuleUser(user); | ||
| 83 | RedisModule_FreeString(ctx, user_name); | ||
| 84 | return REDISMODULE_OK; | ||
| 85 | } | ||
| 86 | |||
| 87 | RedisModuleCallReply *rep = RedisModule_Call(ctx, "SET", "v", argv + 3, (size_t)argc - 3); | ||
| 88 | if (!rep) { | ||
| 89 | RedisModule_ReplyWithError(ctx, "NULL reply returned"); | ||
| 90 | } else { | ||
| 91 | RedisModule_ReplyWithCallReply(ctx, rep); | ||
| 92 | RedisModule_FreeCallReply(rep); | ||
| 93 | } | ||
| 94 | |||
| 95 | RedisModule_FreeModuleUser(user); | ||
| 96 | RedisModule_FreeString(ctx, user_name); | ||
| 97 | return REDISMODULE_OK; | ||
| 98 | } | ||
| 99 | |||
| 100 | /* A wrap for PUBLISH command with ACL check on the channel. */ | ||
| 101 | int publish_aclcheck_channel(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 102 | if (argc != 3) { | ||
| 103 | return RedisModule_WrongArity(ctx); | ||
| 104 | } | ||
| 105 | |||
| 106 | /* Check that the pubsub channel can be accessed */ | ||
| 107 | RedisModuleString *user_name = RedisModule_GetCurrentUserName(ctx); | ||
| 108 | RedisModuleUser *user = RedisModule_GetModuleUserFromUserName(user_name); | ||
| 109 | int ret = RedisModule_ACLCheckChannelPermissions(user, argv[1], REDISMODULE_CMD_CHANNEL_SUBSCRIBE); | ||
| 110 | if (ret != 0) { | ||
| 111 | RedisModule_ReplyWithError(ctx, "DENIED CHANNEL"); | ||
| 112 | RedisModule_FreeModuleUser(user); | ||
| 113 | RedisModule_FreeString(ctx, user_name); | ||
| 114 | return REDISMODULE_OK; | ||
| 115 | } | ||
| 116 | |||
| 117 | RedisModuleCallReply *rep = RedisModule_Call(ctx, "PUBLISH", "v", argv + 1, (size_t)argc - 1); | ||
| 118 | if (!rep) { | ||
| 119 | RedisModule_ReplyWithError(ctx, "NULL reply returned"); | ||
| 120 | } else { | ||
| 121 | RedisModule_ReplyWithCallReply(ctx, rep); | ||
| 122 | RedisModule_FreeCallReply(rep); | ||
| 123 | } | ||
| 124 | |||
| 125 | RedisModule_FreeModuleUser(user); | ||
| 126 | RedisModule_FreeString(ctx, user_name); | ||
| 127 | return REDISMODULE_OK; | ||
| 128 | } | ||
| 129 | |||
| 130 | /* A wrap for RM_Call that check first that the command can be executed */ | ||
| 131 | int rm_call_aclcheck_cmd(RedisModuleCtx *ctx, RedisModuleUser *user, RedisModuleString **argv, int argc) { | ||
| 132 | if (argc < 2) { | ||
| 133 | return RedisModule_WrongArity(ctx); | ||
| 134 | } | ||
| 135 | |||
| 136 | /* Check that the command can be executed */ | ||
| 137 | int ret = RedisModule_ACLCheckCommandPermissions(user, argv + 1, argc - 1); | ||
| 138 | if (ret != 0) { | ||
| 139 | RedisModule_ReplyWithError(ctx, "DENIED CMD"); | ||
| 140 | /* Add entry to ACL log */ | ||
| 141 | RedisModule_ACLAddLogEntry(ctx, user, argv[1], REDISMODULE_ACL_LOG_CMD); | ||
| 142 | return REDISMODULE_OK; | ||
| 143 | } | ||
| 144 | |||
| 145 | const char* cmd = RedisModule_StringPtrLen(argv[1], NULL); | ||
| 146 | |||
| 147 | RedisModuleCallReply* rep = RedisModule_Call(ctx, cmd, "v", argv + 2, (size_t)argc - 2); | ||
| 148 | if(!rep){ | ||
| 149 | RedisModule_ReplyWithError(ctx, "NULL reply returned"); | ||
| 150 | }else{ | ||
| 151 | RedisModule_ReplyWithCallReply(ctx, rep); | ||
| 152 | RedisModule_FreeCallReply(rep); | ||
| 153 | } | ||
| 154 | |||
| 155 | return REDISMODULE_OK; | ||
| 156 | } | ||
| 157 | |||
| 158 | int rm_call_aclcheck_cmd_default_user(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 159 | RedisModuleString *user_name = RedisModule_GetCurrentUserName(ctx); | ||
| 160 | RedisModuleUser *user = RedisModule_GetModuleUserFromUserName(user_name); | ||
| 161 | |||
| 162 | int res = rm_call_aclcheck_cmd(ctx, user, argv, argc); | ||
| 163 | |||
| 164 | RedisModule_FreeModuleUser(user); | ||
| 165 | RedisModule_FreeString(ctx, user_name); | ||
| 166 | return res; | ||
| 167 | } | ||
| 168 | |||
| 169 | int rm_call_aclcheck_cmd_module_user(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 170 | /* Create a user and authenticate */ | ||
| 171 | RedisModuleUser *user = RedisModule_CreateModuleUser("testuser1"); | ||
| 172 | RedisModule_SetModuleUserACL(user, "allcommands"); | ||
| 173 | RedisModule_SetModuleUserACL(user, "allkeys"); | ||
| 174 | RedisModule_SetModuleUserACL(user, "on"); | ||
| 175 | RedisModule_AuthenticateClientWithUser(ctx, user, NULL, NULL, NULL); | ||
| 176 | |||
| 177 | int res = rm_call_aclcheck_cmd(ctx, user, argv, argc); | ||
| 178 | |||
| 179 | /* authenticated back to "default" user (so once we free testuser1 we will not disconnected */ | ||
| 180 | RedisModule_AuthenticateClientWithACLUser(ctx, "default", 7, NULL, NULL, NULL); | ||
| 181 | RedisModule_FreeModuleUser(user); | ||
| 182 | return res; | ||
| 183 | } | ||
| 184 | |||
| 185 | int rm_call_aclcheck_with_errors(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){ | ||
| 186 | REDISMODULE_NOT_USED(argv); | ||
| 187 | REDISMODULE_NOT_USED(argc); | ||
| 188 | |||
| 189 | if(argc < 2){ | ||
| 190 | return RedisModule_WrongArity(ctx); | ||
| 191 | } | ||
| 192 | |||
| 193 | const char* cmd = RedisModule_StringPtrLen(argv[1], NULL); | ||
| 194 | |||
| 195 | RedisModuleCallReply* rep = RedisModule_Call(ctx, cmd, "vEC", argv + 2, (size_t)argc - 2); | ||
| 196 | RedisModule_ReplyWithCallReply(ctx, rep); | ||
| 197 | RedisModule_FreeCallReply(rep); | ||
| 198 | return REDISMODULE_OK; | ||
| 199 | } | ||
| 200 | |||
| 201 | /* A wrap for RM_Call that pass the 'C' flag to do ACL check on the command. */ | ||
| 202 | int rm_call_aclcheck(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){ | ||
| 203 | REDISMODULE_NOT_USED(argv); | ||
| 204 | REDISMODULE_NOT_USED(argc); | ||
| 205 | |||
| 206 | if(argc < 2){ | ||
| 207 | return RedisModule_WrongArity(ctx); | ||
| 208 | } | ||
| 209 | |||
| 210 | const char* cmd = RedisModule_StringPtrLen(argv[1], NULL); | ||
| 211 | |||
| 212 | RedisModuleCallReply* rep = RedisModule_Call(ctx, cmd, "vC", argv + 2, (size_t)argc - 2); | ||
| 213 | if(!rep) { | ||
| 214 | char err[100]; | ||
| 215 | switch (errno) { | ||
| 216 | case EACCES: | ||
| 217 | RedisModule_ReplyWithError(ctx, "ERR NOPERM"); | ||
| 218 | break; | ||
| 219 | default: | ||
| 220 | snprintf(err, sizeof(err) - 1, "ERR errno=%d", errno); | ||
| 221 | RedisModule_ReplyWithError(ctx, err); | ||
| 222 | break; | ||
| 223 | } | ||
| 224 | } else { | ||
| 225 | RedisModule_ReplyWithCallReply(ctx, rep); | ||
| 226 | RedisModule_FreeCallReply(rep); | ||
| 227 | } | ||
| 228 | |||
| 229 | return REDISMODULE_OK; | ||
| 230 | } | ||
| 231 | |||
| 232 | int module_test_acl_category(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 233 | REDISMODULE_NOT_USED(argv); | ||
| 234 | REDISMODULE_NOT_USED(argc); | ||
| 235 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 236 | return REDISMODULE_OK; | ||
| 237 | } | ||
| 238 | |||
| 239 | int commandBlockCheck(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 240 | REDISMODULE_NOT_USED(argv); | ||
| 241 | REDISMODULE_NOT_USED(argc); | ||
| 242 | int response_ok = 0; | ||
| 243 | int result = RedisModule_CreateCommand(ctx,"command.that.should.fail", module_test_acl_category, "", 0, 0, 0); | ||
| 244 | response_ok |= (result == REDISMODULE_OK); | ||
| 245 | |||
| 246 | result = RedisModule_AddACLCategory(ctx,"blockedcategory"); | ||
| 247 | response_ok |= (result == REDISMODULE_OK); | ||
| 248 | |||
| 249 | RedisModuleCommand *parent = RedisModule_GetCommand(ctx,"block.commands.outside.onload"); | ||
| 250 | result = RedisModule_SetCommandACLCategories(parent, "write"); | ||
| 251 | response_ok |= (result == REDISMODULE_OK); | ||
| 252 | |||
| 253 | result = RedisModule_CreateSubcommand(parent,"subcommand.that.should.fail",module_test_acl_category,"",0,0,0); | ||
| 254 | response_ok |= (result == REDISMODULE_OK); | ||
| 255 | |||
| 256 | /* This validates that it's not possible to create commands or add | ||
| 257 | * a new ACL Category outside OnLoad function. | ||
| 258 | * thus returns an error if they succeed. */ | ||
| 259 | if (response_ok) { | ||
| 260 | RedisModule_ReplyWithError(ctx, "UNEXPECTEDOK"); | ||
| 261 | } else { | ||
| 262 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 263 | } | ||
| 264 | return REDISMODULE_OK; | ||
| 265 | } | ||
| 266 | |||
| 267 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 268 | |||
| 269 | if (RedisModule_Init(ctx,"aclcheck",1,REDISMODULE_APIVER_1)== REDISMODULE_ERR) | ||
| 270 | return REDISMODULE_ERR; | ||
| 271 | |||
| 272 | if (argc > 1) return RedisModule_WrongArity(ctx); | ||
| 273 | |||
| 274 | /* When that flag is passed, we try to create too many categories, | ||
| 275 | * and the test expects this to fail. In this case redis returns REDISMODULE_ERR | ||
| 276 | * and set errno to ENOMEM*/ | ||
| 277 | if (argc == 1) { | ||
| 278 | long long fail_flag = 0; | ||
| 279 | RedisModule_StringToLongLong(argv[0], &fail_flag); | ||
| 280 | if (fail_flag) { | ||
| 281 | for (size_t j = 0; j < 45; j++) { | ||
| 282 | RedisModuleString* name = RedisModule_CreateStringPrintf(ctx, "customcategory%zu", j); | ||
| 283 | if (RedisModule_AddACLCategory(ctx, RedisModule_StringPtrLen(name, NULL)) == REDISMODULE_ERR) { | ||
| 284 | RedisModule_Assert(errno == ENOMEM); | ||
| 285 | RedisModule_FreeString(ctx, name); | ||
| 286 | return REDISMODULE_ERR; | ||
| 287 | } | ||
| 288 | RedisModule_FreeString(ctx, name); | ||
| 289 | } | ||
| 290 | } | ||
| 291 | } | ||
| 292 | |||
| 293 | if (RedisModule_CreateCommand(ctx,"aclcheck.set.check.key", set_aclcheck_key,"write",0,0,0) == REDISMODULE_ERR) | ||
| 294 | return REDISMODULE_ERR; | ||
| 295 | |||
| 296 | if (RedisModule_CreateCommand(ctx,"aclcheck.set.check.prefixkey", set_aclcheck_prefixkey,"write",0,0,0) == REDISMODULE_ERR) | ||
| 297 | return REDISMODULE_ERR; | ||
| 298 | |||
| 299 | if (RedisModule_CreateCommand(ctx,"block.commands.outside.onload", commandBlockCheck,"write",0,0,0) == REDISMODULE_ERR) | ||
| 300 | return REDISMODULE_ERR; | ||
| 301 | |||
| 302 | if (RedisModule_CreateCommand(ctx,"aclcheck.module.command.aclcategories.write", module_test_acl_category,"write",0,0,0) == REDISMODULE_ERR) | ||
| 303 | return REDISMODULE_ERR; | ||
| 304 | RedisModuleCommand *aclcategories_write = RedisModule_GetCommand(ctx,"aclcheck.module.command.aclcategories.write"); | ||
| 305 | |||
| 306 | if (RedisModule_SetCommandACLCategories(aclcategories_write, "write") == REDISMODULE_ERR) | ||
| 307 | return REDISMODULE_ERR; | ||
| 308 | |||
| 309 | if (RedisModule_CreateCommand(ctx,"aclcheck.module.command.aclcategories.write.function.read.category", module_test_acl_category,"write",0,0,0) == REDISMODULE_ERR) | ||
| 310 | return REDISMODULE_ERR; | ||
| 311 | RedisModuleCommand *read_category = RedisModule_GetCommand(ctx,"aclcheck.module.command.aclcategories.write.function.read.category"); | ||
| 312 | |||
| 313 | if (RedisModule_SetCommandACLCategories(read_category, "read") == REDISMODULE_ERR) | ||
| 314 | return REDISMODULE_ERR; | ||
| 315 | |||
| 316 | if (RedisModule_CreateCommand(ctx,"aclcheck.module.command.aclcategories.read.only.category", module_test_acl_category,"",0,0,0) == REDISMODULE_ERR) | ||
| 317 | return REDISMODULE_ERR; | ||
| 318 | RedisModuleCommand *read_only_category = RedisModule_GetCommand(ctx,"aclcheck.module.command.aclcategories.read.only.category"); | ||
| 319 | |||
| 320 | if (RedisModule_SetCommandACLCategories(read_only_category, "read") == REDISMODULE_ERR) | ||
| 321 | return REDISMODULE_ERR; | ||
| 322 | |||
| 323 | if (RedisModule_CreateCommand(ctx,"aclcheck.publish.check.channel", publish_aclcheck_channel,"",0,0,0) == REDISMODULE_ERR) | ||
| 324 | return REDISMODULE_ERR; | ||
| 325 | |||
| 326 | if (RedisModule_CreateCommand(ctx,"aclcheck.rm_call.check.cmd", rm_call_aclcheck_cmd_default_user,"",0,0,0) == REDISMODULE_ERR) | ||
| 327 | return REDISMODULE_ERR; | ||
| 328 | |||
| 329 | if (RedisModule_CreateCommand(ctx,"aclcheck.rm_call.check.cmd.module.user", rm_call_aclcheck_cmd_module_user,"",0,0,0) == REDISMODULE_ERR) | ||
| 330 | return REDISMODULE_ERR; | ||
| 331 | |||
| 332 | if (RedisModule_CreateCommand(ctx,"aclcheck.rm_call", rm_call_aclcheck, | ||
| 333 | "write",0,0,0) == REDISMODULE_ERR) | ||
| 334 | return REDISMODULE_ERR; | ||
| 335 | |||
| 336 | if (RedisModule_CreateCommand(ctx,"aclcheck.rm_call_with_errors", rm_call_aclcheck_with_errors, | ||
| 337 | "write",0,0,0) == REDISMODULE_ERR) | ||
| 338 | return REDISMODULE_ERR; | ||
| 339 | |||
| 340 | /* This validates that, when module tries to add a category with invalid characters, | ||
| 341 | * redis returns REDISMODULE_ERR and set errno to `EINVAL` */ | ||
| 342 | if (RedisModule_AddACLCategory(ctx,"!nval!dch@r@cter$") == REDISMODULE_ERR) | ||
| 343 | RedisModule_Assert(errno == EINVAL); | ||
| 344 | else | ||
| 345 | return REDISMODULE_ERR; | ||
| 346 | |||
| 347 | /* This validates that, when module tries to add a category that already exists, | ||
| 348 | * redis returns REDISMODULE_ERR and set errno to `EBUSY` */ | ||
| 349 | if (RedisModule_AddACLCategory(ctx,"write") == REDISMODULE_ERR) | ||
| 350 | RedisModule_Assert(errno == EBUSY); | ||
| 351 | else | ||
| 352 | return REDISMODULE_ERR; | ||
| 353 | |||
| 354 | if (RedisModule_AddACLCategory(ctx,"foocategory") == REDISMODULE_ERR) | ||
| 355 | return REDISMODULE_ERR; | ||
| 356 | |||
| 357 | if (RedisModule_CreateCommand(ctx,"aclcheck.module.command.test.add.new.aclcategories", module_test_acl_category,"",0,0,0) == REDISMODULE_ERR) | ||
| 358 | return REDISMODULE_ERR; | ||
| 359 | RedisModuleCommand *test_add_new_aclcategories = RedisModule_GetCommand(ctx,"aclcheck.module.command.test.add.new.aclcategories"); | ||
| 360 | |||
| 361 | if (RedisModule_SetCommandACLCategories(test_add_new_aclcategories, "foocategory") == REDISMODULE_ERR) | ||
| 362 | return REDISMODULE_ERR; | ||
| 363 | |||
| 364 | return REDISMODULE_OK; | ||
| 365 | } | ||
diff --git a/examples/redis-unstable/tests/modules/atomicslotmigration.c b/examples/redis-unstable/tests/modules/atomicslotmigration.c new file mode 100644 index 0000000..83393cd --- /dev/null +++ b/examples/redis-unstable/tests/modules/atomicslotmigration.c | |||
| @@ -0,0 +1,594 @@ | |||
| 1 | #include "redismodule.h" | ||
| 2 | |||
| 3 | #include <stdlib.h> | ||
| 4 | #include <memory.h> | ||
| 5 | #include <errno.h> | ||
| 6 | |||
| 7 | #define MAX_EVENTS 1024 | ||
| 8 | |||
| 9 | /* Log of cluster events. */ | ||
| 10 | const char *clusterEventLog[MAX_EVENTS]; | ||
| 11 | int numClusterEvents = 0; | ||
| 12 | |||
| 13 | /* Log of cluster trim events. */ | ||
| 14 | const char *clusterTrimEventLog[MAX_EVENTS]; | ||
| 15 | int numClusterTrimEvents = 0; | ||
| 16 | |||
| 17 | /* Log of last deleted key event. */ | ||
| 18 | const char *lastDeletedKeyLog = NULL; | ||
| 19 | |||
| 20 | /* Flag to disable trim. */ | ||
| 21 | int disableTrimFlag = 0; | ||
| 22 | |||
| 23 | int replicateModuleCommand = 0; /* Enable or disable module command replication. */ | ||
| 24 | RedisModuleString *moduleCommandKeyName = NULL; /* Key name to replicate. */ | ||
| 25 | RedisModuleString *moduleCommandKeyVal = NULL; /* Key value to replicate. */ | ||
| 26 | |||
| 27 | /* Enable or disable module command replication. */ | ||
| 28 | int replicate_module_command(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 29 | if (argc != 4) { | ||
| 30 | RedisModule_ReplyWithError(ctx, "ERR wrong number of arguments"); | ||
| 31 | return REDISMODULE_OK; | ||
| 32 | } | ||
| 33 | |||
| 34 | long long enable = 0; | ||
| 35 | if (RedisModule_StringToLongLong(argv[1], &enable) != REDISMODULE_OK) { | ||
| 36 | RedisModule_ReplyWithError(ctx, "ERR enable value"); | ||
| 37 | return REDISMODULE_OK; | ||
| 38 | } | ||
| 39 | replicateModuleCommand = (enable != 0); | ||
| 40 | |||
| 41 | /* Set the key name and value to replicate. */ | ||
| 42 | if (moduleCommandKeyName) RedisModule_FreeString(ctx, moduleCommandKeyName); | ||
| 43 | if (moduleCommandKeyVal) RedisModule_FreeString(ctx, moduleCommandKeyVal); | ||
| 44 | moduleCommandKeyName = RedisModule_CreateStringFromString(ctx, argv[2]); | ||
| 45 | moduleCommandKeyVal = RedisModule_CreateStringFromString(ctx, argv[3]); | ||
| 46 | |||
| 47 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 48 | return REDISMODULE_OK; | ||
| 49 | } | ||
| 50 | |||
| 51 | int lpush_and_replicate_crossslot_command(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 52 | if (argc != 3) return RedisModule_WrongArity(ctx); | ||
| 53 | |||
| 54 | /* LPUSH */ | ||
| 55 | RedisModuleCallReply *rep = RedisModule_Call(ctx, "LPUSH", "!ss", argv[1], argv[2]); | ||
| 56 | RedisModule_Assert(RedisModule_CallReplyType(rep) != REDISMODULE_REPLY_ERROR); | ||
| 57 | RedisModule_FreeCallReply(rep); | ||
| 58 | |||
| 59 | /* Replicate cross slot command */ | ||
| 60 | int ret = RedisModule_Replicate(ctx, "MSET", "cccccc", "key1", "val1", "key2", "val2", "key3", "val3"); | ||
| 61 | RedisModule_Assert(ret == REDISMODULE_OK); | ||
| 62 | |||
| 63 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 64 | return REDISMODULE_OK; | ||
| 65 | } | ||
| 66 | |||
| 67 | int testClusterGetLocalSlotRanges(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 68 | REDISMODULE_NOT_USED(argv); | ||
| 69 | REDISMODULE_NOT_USED(argc); | ||
| 70 | |||
| 71 | static int use_auto_memory = 0; | ||
| 72 | use_auto_memory = !use_auto_memory; | ||
| 73 | |||
| 74 | RedisModuleSlotRangeArray *slots; | ||
| 75 | if (use_auto_memory) { | ||
| 76 | RedisModule_AutoMemory(ctx); | ||
| 77 | slots = RedisModule_ClusterGetLocalSlotRanges(ctx); | ||
| 78 | } else { | ||
| 79 | slots = RedisModule_ClusterGetLocalSlotRanges(NULL); | ||
| 80 | } | ||
| 81 | |||
| 82 | RedisModule_ReplyWithArray(ctx, slots->num_ranges); | ||
| 83 | for (int i = 0; i < slots->num_ranges; i++) { | ||
| 84 | RedisModule_ReplyWithArray(ctx, 2); | ||
| 85 | RedisModule_ReplyWithLongLong(ctx, slots->ranges[i].start); | ||
| 86 | RedisModule_ReplyWithLongLong(ctx, slots->ranges[i].end); | ||
| 87 | } | ||
| 88 | if (!use_auto_memory) | ||
| 89 | RedisModule_ClusterFreeSlotRanges(NULL, slots); | ||
| 90 | return REDISMODULE_OK; | ||
| 91 | } | ||
| 92 | |||
| 93 | /* Helper function to check if a slot range array contains a given slot. */ | ||
| 94 | int slotRangeArrayContains(RedisModuleSlotRangeArray *sra, unsigned int slot) { | ||
| 95 | for (int i = 0; i < sra->num_ranges; i++) | ||
| 96 | if (sra->ranges[i].start <= slot && sra->ranges[i].end >= slot) | ||
| 97 | return 1; | ||
| 98 | return 0; | ||
| 99 | } | ||
| 100 | |||
| 101 | /* Sanity check. */ | ||
| 102 | int sanity(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 103 | REDISMODULE_NOT_USED(argv); | ||
| 104 | REDISMODULE_NOT_USED(argc); | ||
| 105 | |||
| 106 | RedisModule_Assert(RedisModule_ClusterCanAccessKeysInSlot(-1) == 0); | ||
| 107 | RedisModule_Assert(RedisModule_ClusterCanAccessKeysInSlot(16384) == 0); | ||
| 108 | RedisModule_Assert(RedisModule_ClusterCanAccessKeysInSlot(100000) == 0); | ||
| 109 | |||
| 110 | /* Call with invalid args. */ | ||
| 111 | errno = 0; | ||
| 112 | RedisModule_Assert(RedisModule_ClusterPropagateForSlotMigration(NULL, NULL, NULL) == REDISMODULE_ERR); | ||
| 113 | RedisModule_Assert(errno == EINVAL); | ||
| 114 | |||
| 115 | /* Call with invalid args. */ | ||
| 116 | errno = 0; | ||
| 117 | RedisModule_Assert(RedisModule_ClusterPropagateForSlotMigration(ctx, NULL, NULL) == REDISMODULE_ERR); | ||
| 118 | RedisModule_Assert(errno == EINVAL); | ||
| 119 | |||
| 120 | /* Call with invalid args. */ | ||
| 121 | errno = 0; | ||
| 122 | RedisModule_Assert(RedisModule_ClusterPropagateForSlotMigration(NULL, "asm.keyless_cmd", "") == REDISMODULE_ERR); | ||
| 123 | RedisModule_Assert(errno == EINVAL); | ||
| 124 | |||
| 125 | /* Call outside of slot migration. */ | ||
| 126 | errno = 0; | ||
| 127 | RedisModule_Assert(RedisModule_ClusterPropagateForSlotMigration(ctx, "asm.keyless_cmd", "") == REDISMODULE_ERR); | ||
| 128 | RedisModule_Assert(errno == EBADF); | ||
| 129 | |||
| 130 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 131 | return REDISMODULE_OK; | ||
| 132 | } | ||
| 133 | |||
| 134 | /* Command to test RM_ClusterCanAccessKeysInSlot(). */ | ||
| 135 | int testClusterCanAccessKeysInSlot(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 136 | REDISMODULE_NOT_USED(argc); | ||
| 137 | long long slot = 0; | ||
| 138 | |||
| 139 | if (RedisModule_StringToLongLong(argv[1],&slot) != REDISMODULE_OK) { | ||
| 140 | return RedisModule_ReplyWithError(ctx,"ERR invalid slot"); | ||
| 141 | } | ||
| 142 | RedisModule_ReplyWithLongLong(ctx, RedisModule_ClusterCanAccessKeysInSlot(slot)); | ||
| 143 | return REDISMODULE_OK; | ||
| 144 | } | ||
| 145 | |||
| 146 | /* Generate a string representation of the info struct and subevent. | ||
| 147 | e.g. 'sub: cluster-slot-migration-import-started, task_id: aeBd..., slots: 0-100,200-300' */ | ||
| 148 | const char *clusterAsmInfoToString(RedisModuleClusterSlotMigrationInfo *info, uint64_t sub) { | ||
| 149 | char buf[1024] = {0}; | ||
| 150 | |||
| 151 | if (sub == REDISMODULE_SUBEVENT_CLUSTER_SLOT_MIGRATION_IMPORT_STARTED) | ||
| 152 | snprintf(buf, sizeof(buf), "sub: cluster-slot-migration-import-started, "); | ||
| 153 | else if (sub == REDISMODULE_SUBEVENT_CLUSTER_SLOT_MIGRATION_IMPORT_FAILED) | ||
| 154 | snprintf(buf, sizeof(buf), "sub: cluster-slot-migration-import-failed, "); | ||
| 155 | else if (sub == REDISMODULE_SUBEVENT_CLUSTER_SLOT_MIGRATION_IMPORT_COMPLETED) | ||
| 156 | snprintf(buf, sizeof(buf), "sub: cluster-slot-migration-import-completed, "); | ||
| 157 | else if (sub == REDISMODULE_SUBEVENT_CLUSTER_SLOT_MIGRATION_MIGRATE_STARTED) | ||
| 158 | snprintf(buf, sizeof(buf), "sub: cluster-slot-migration-migrate-started, "); | ||
| 159 | else if (sub == REDISMODULE_SUBEVENT_CLUSTER_SLOT_MIGRATION_MIGRATE_FAILED) | ||
| 160 | snprintf(buf, sizeof(buf), "sub: cluster-slot-migration-migrate-failed, "); | ||
| 161 | else if (sub == REDISMODULE_SUBEVENT_CLUSTER_SLOT_MIGRATION_MIGRATE_COMPLETED) | ||
| 162 | snprintf(buf, sizeof(buf), "sub: cluster-slot-migration-migrate-completed, "); | ||
| 163 | else { | ||
| 164 | RedisModule_Assert(0); | ||
| 165 | } | ||
| 166 | snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), "source_node_id:%.40s, destination_node_id:%.40s, ", | ||
| 167 | info->source_node_id, info->destination_node_id); | ||
| 168 | snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), "task_id:%s, slots:", info->task_id); | ||
| 169 | for (int i = 0; i < info->slots->num_ranges; i++) { | ||
| 170 | RedisModuleSlotRange *sr = &info->slots->ranges[i]; | ||
| 171 | snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), "%d-%d", sr->start, sr->end); | ||
| 172 | if (i != info->slots->num_ranges - 1) | ||
| 173 | snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), ","); | ||
| 174 | } | ||
| 175 | return RedisModule_Strdup(buf); | ||
| 176 | } | ||
| 177 | |||
| 178 | /* Generate a string representation of the info struct and subevent. | ||
| 179 | e.g. 'sub: cluster-slot-migration-trim-started, task_id: aeBd..., slots:0-100,200-300' */ | ||
| 180 | const char *clusterTrimInfoToString(RedisModuleClusterSlotMigrationTrimInfo *info, uint64_t sub) { | ||
| 181 | RedisModule_Assert(info); | ||
| 182 | char buf[1024] = {0}; | ||
| 183 | |||
| 184 | if (sub == REDISMODULE_SUBEVENT_CLUSTER_SLOT_MIGRATION_TRIM_BACKGROUND) | ||
| 185 | snprintf(buf, sizeof(buf), "sub: cluster-slot-migration-trim-background, "); | ||
| 186 | else if (sub == REDISMODULE_SUBEVENT_CLUSTER_SLOT_MIGRATION_TRIM_STARTED) | ||
| 187 | snprintf(buf, sizeof(buf), "sub: cluster-slot-migration-trim-started, "); | ||
| 188 | else if (sub == REDISMODULE_SUBEVENT_CLUSTER_SLOT_MIGRATION_TRIM_COMPLETED) | ||
| 189 | snprintf(buf, sizeof(buf), "sub: cluster-slot-migration-trim-completed, "); | ||
| 190 | else { | ||
| 191 | RedisModule_Assert(0); | ||
| 192 | } | ||
| 193 | snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), "slots:"); | ||
| 194 | for (int i = 0; i < info->slots->num_ranges; i++) { | ||
| 195 | RedisModuleSlotRange *sr = &info->slots->ranges[i]; | ||
| 196 | snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), "%d-%d", sr->start, sr->end); | ||
| 197 | if (i != info->slots->num_ranges - 1) | ||
| 198 | snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), ","); | ||
| 199 | } | ||
| 200 | return RedisModule_Strdup(buf); | ||
| 201 | } | ||
| 202 | |||
| 203 | static void testReplicatingOutsideSlotRange(RedisModuleCtx *ctx, RedisModuleClusterSlotMigrationInfo *info) { | ||
| 204 | int slot = 0; | ||
| 205 | while (slot >= 0 && slot <= 16383) { | ||
| 206 | if (!slotRangeArrayContains(info->slots, slot)) { | ||
| 207 | break; | ||
| 208 | } | ||
| 209 | slot++; | ||
| 210 | } | ||
| 211 | char buf[128] = {0}; | ||
| 212 | const char *prefix = RedisModule_ClusterCanonicalKeyNameInSlot(slot); | ||
| 213 | snprintf(buf, sizeof(buf), "{%s}%s", prefix, "modulekey"); | ||
| 214 | errno = 0; | ||
| 215 | int ret = RedisModule_ClusterPropagateForSlotMigration(ctx, "SET", "cc", buf, "value"); | ||
| 216 | RedisModule_Assert(ret == REDISMODULE_ERR); | ||
| 217 | RedisModule_Assert(errno == ERANGE); | ||
| 218 | } | ||
| 219 | |||
| 220 | static void testReplicatingCrossslotCommand(RedisModuleCtx *ctx) { | ||
| 221 | errno = 0; | ||
| 222 | int ret = RedisModule_ClusterPropagateForSlotMigration(ctx, "MSET", "cccccc", "key1", "val1", "key2", "val2", "key3", "val3"); | ||
| 223 | RedisModule_Assert(ret == REDISMODULE_ERR); | ||
| 224 | RedisModule_Assert(errno == ENOTSUP); | ||
| 225 | } | ||
| 226 | |||
| 227 | static void testReplicatingUnknownCommand(RedisModuleCtx *ctx) { | ||
| 228 | errno = 0; | ||
| 229 | int ret = RedisModule_ClusterPropagateForSlotMigration(ctx, "unknowncommand", ""); | ||
| 230 | RedisModule_Assert(ret == REDISMODULE_ERR); | ||
| 231 | RedisModule_Assert(errno == ENOENT); | ||
| 232 | } | ||
| 233 | |||
| 234 | static void testNonFatalScenarios(RedisModuleCtx *ctx, RedisModuleClusterSlotMigrationInfo *info) { | ||
| 235 | testReplicatingOutsideSlotRange(ctx, info); | ||
| 236 | testReplicatingCrossslotCommand(ctx); | ||
| 237 | testReplicatingUnknownCommand(ctx); | ||
| 238 | } | ||
| 239 | |||
| 240 | int disableTrimCmd(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 241 | REDISMODULE_NOT_USED(argv); | ||
| 242 | REDISMODULE_NOT_USED(argc); | ||
| 243 | disableTrimFlag = 1; | ||
| 244 | /* Only disable when MIGRATE_COMPLETED for simulating recommended usage. */ | ||
| 245 | // RedisModule_ClusterDisableTrim(ctx) | ||
| 246 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 247 | return REDISMODULE_OK; | ||
| 248 | } | ||
| 249 | |||
| 250 | int enableTrimCmd(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 251 | REDISMODULE_NOT_USED(argv); | ||
| 252 | REDISMODULE_NOT_USED(argc); | ||
| 253 | disableTrimFlag = 0; | ||
| 254 | RedisModule_Assert(RedisModule_ClusterEnableTrim(ctx) == REDISMODULE_OK); | ||
| 255 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 256 | return REDISMODULE_OK; | ||
| 257 | } | ||
| 258 | |||
| 259 | int trimInProgressCmd(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 260 | REDISMODULE_NOT_USED(argv); | ||
| 261 | REDISMODULE_NOT_USED(argc); | ||
| 262 | uint64_t flags = RedisModule_GetContextFlags(ctx); | ||
| 263 | RedisModule_ReplyWithLongLong(ctx, !!(flags & REDISMODULE_CTX_FLAGS_TRIM_IN_PROGRESS)); | ||
| 264 | return REDISMODULE_OK; | ||
| 265 | } | ||
| 266 | |||
| 267 | void clusterEventCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data) { | ||
| 268 | REDISMODULE_NOT_USED(ctx); | ||
| 269 | int ret; | ||
| 270 | |||
| 271 | RedisModule_Assert(RedisModule_IsSubEventSupported(e, sub)); | ||
| 272 | |||
| 273 | if (e.id == REDISMODULE_EVENT_CLUSTER_SLOT_MIGRATION) { | ||
| 274 | RedisModuleClusterSlotMigrationInfo *info = data; | ||
| 275 | |||
| 276 | if (sub == REDISMODULE_SUBEVENT_CLUSTER_SLOT_MIGRATION_MIGRATE_MODULE_PROPAGATE) { | ||
| 277 | /* Test some non-fatal scenarios. */ | ||
| 278 | testNonFatalScenarios(ctx, info); | ||
| 279 | |||
| 280 | if (replicateModuleCommand == 0) return; | ||
| 281 | |||
| 282 | /* Replicate a keyless command. */ | ||
| 283 | ret = RedisModule_ClusterPropagateForSlotMigration(ctx, "asm.keyless_cmd", ""); | ||
| 284 | RedisModule_Assert(ret == REDISMODULE_OK); | ||
| 285 | |||
| 286 | /* Propagate configured key and value. */ | ||
| 287 | ret = RedisModule_ClusterPropagateForSlotMigration(ctx, "SET", "ss", moduleCommandKeyName, moduleCommandKeyVal); | ||
| 288 | RedisModule_Assert(ret == REDISMODULE_OK); | ||
| 289 | } else { | ||
| 290 | /* Log the event. */ | ||
| 291 | if (numClusterEvents >= MAX_EVENTS) return; | ||
| 292 | clusterEventLog[numClusterEvents++] = clusterAsmInfoToString(info, sub); | ||
| 293 | |||
| 294 | if (sub == REDISMODULE_SUBEVENT_CLUSTER_SLOT_MIGRATION_MIGRATE_COMPLETED) { | ||
| 295 | /* If users ask to disable trim, we disable trim. */ | ||
| 296 | if (disableTrimFlag) { | ||
| 297 | RedisModule_Assert(RedisModule_ClusterDisableTrim(ctx) == REDISMODULE_OK); | ||
| 298 | } | ||
| 299 | } | ||
| 300 | } | ||
| 301 | } | ||
| 302 | } | ||
| 303 | |||
| 304 | int getPendingTrimKeyCmd(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 305 | if (argc != 2) { | ||
| 306 | RedisModule_ReplyWithError(ctx, "ERR wrong number of arguments"); | ||
| 307 | return REDISMODULE_ERR; | ||
| 308 | } | ||
| 309 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], | ||
| 310 | REDISMODULE_READ | REDISMODULE_OPEN_KEY_ACCESS_TRIMMED); | ||
| 311 | if (!key) { | ||
| 312 | RedisModule_ReplyWithNull(ctx); | ||
| 313 | return REDISMODULE_OK; | ||
| 314 | } | ||
| 315 | if (RedisModule_KeyType(key) != REDISMODULE_KEYTYPE_STRING) { | ||
| 316 | RedisModule_ReplyWithError(ctx, "key is not a string"); | ||
| 317 | return REDISMODULE_ERR; | ||
| 318 | } | ||
| 319 | size_t len; | ||
| 320 | const char *value = RedisModule_StringDMA(key, &len, 0); | ||
| 321 | RedisModule_ReplyWithStringBuffer(ctx, value, len); | ||
| 322 | RedisModule_CloseKey(key); | ||
| 323 | return REDISMODULE_OK; | ||
| 324 | } | ||
| 325 | |||
| 326 | void clusterTrimEventCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data) { | ||
| 327 | REDISMODULE_NOT_USED(ctx); | ||
| 328 | |||
| 329 | RedisModule_Assert(RedisModule_IsSubEventSupported(e, sub)); | ||
| 330 | |||
| 331 | if (e.id == REDISMODULE_EVENT_CLUSTER_SLOT_MIGRATION_TRIM) { | ||
| 332 | /* Log the event. */ | ||
| 333 | if (numClusterTrimEvents >= MAX_EVENTS) return; | ||
| 334 | RedisModuleClusterSlotMigrationTrimInfo *info = data; | ||
| 335 | clusterTrimEventLog[numClusterTrimEvents++] = clusterTrimInfoToString(info, sub); | ||
| 336 | } | ||
| 337 | } | ||
| 338 | |||
| 339 | static int keyspaceNotificationTrimmedCallback(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key) { | ||
| 340 | REDISMODULE_NOT_USED(ctx); | ||
| 341 | |||
| 342 | RedisModule_Assert(type == REDISMODULE_NOTIFY_KEY_TRIMMED); | ||
| 343 | RedisModule_Assert(strcmp(event, "key_trimmed") == 0); | ||
| 344 | |||
| 345 | if (numClusterTrimEvents >= MAX_EVENTS) return REDISMODULE_OK; | ||
| 346 | |||
| 347 | /* Log the trimmed key event. */ | ||
| 348 | size_t len; | ||
| 349 | const char *key_str = RedisModule_StringPtrLen(key, &len); | ||
| 350 | |||
| 351 | char buf[1024] = {0}; | ||
| 352 | snprintf(buf, sizeof(buf), "keyspace: key_trimmed, key: %s", key_str); | ||
| 353 | |||
| 354 | clusterTrimEventLog[numClusterTrimEvents++] = RedisModule_Strdup(buf); | ||
| 355 | return REDISMODULE_OK; | ||
| 356 | } | ||
| 357 | |||
| 358 | /* ASM.PARENT SET key value (just proxy to Redis SET) */ | ||
| 359 | static int asmParentSet(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 360 | if (argc != 4) return RedisModule_WrongArity(ctx); | ||
| 361 | RedisModuleCallReply *reply = RedisModule_Call(ctx, "SET", "ss", argv[2], argv[3]); | ||
| 362 | if (!reply) return RedisModule_ReplyWithError(ctx, "ERR internal"); | ||
| 363 | RedisModule_ReplyWithCallReply(ctx, reply); | ||
| 364 | RedisModule_FreeCallReply(reply); | ||
| 365 | RedisModule_ReplicateVerbatim(ctx); | ||
| 366 | return REDISMODULE_OK; | ||
| 367 | } | ||
| 368 | |||
| 369 | /* Clear both the cluster and trim event logs. */ | ||
| 370 | int clearEventLog(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 371 | REDISMODULE_NOT_USED(argv); | ||
| 372 | REDISMODULE_NOT_USED(argc); | ||
| 373 | |||
| 374 | for (int i = 0; i < numClusterEvents; i++) | ||
| 375 | RedisModule_Free((void *)clusterEventLog[i]); | ||
| 376 | numClusterEvents = 0; | ||
| 377 | |||
| 378 | for (int i = 0; i < numClusterTrimEvents; i++) | ||
| 379 | RedisModule_Free((void *)clusterTrimEventLog[i]); | ||
| 380 | numClusterTrimEvents = 0; | ||
| 381 | |||
| 382 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 383 | return REDISMODULE_OK; | ||
| 384 | } | ||
| 385 | |||
| 386 | /* Reply with the cluster event log. */ | ||
| 387 | int getClusterEventLog(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 388 | REDISMODULE_NOT_USED(ctx); | ||
| 389 | REDISMODULE_NOT_USED(argv); | ||
| 390 | REDISMODULE_NOT_USED(argc); | ||
| 391 | |||
| 392 | RedisModule_ReplyWithArray(ctx, numClusterEvents); | ||
| 393 | for (int i = 0; i < numClusterEvents; i++) | ||
| 394 | RedisModule_ReplyWithStringBuffer(ctx, clusterEventLog[i], strlen(clusterEventLog[i])); | ||
| 395 | return REDISMODULE_OK; | ||
| 396 | } | ||
| 397 | |||
| 398 | /* Reply with the cluster trim event log. */ | ||
| 399 | int getClusterTrimEventLog(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 400 | REDISMODULE_NOT_USED(ctx); | ||
| 401 | REDISMODULE_NOT_USED(argv); | ||
| 402 | REDISMODULE_NOT_USED(argc); | ||
| 403 | |||
| 404 | RedisModule_ReplyWithArray(ctx, numClusterTrimEvents); | ||
| 405 | for (int i = 0; i < numClusterTrimEvents; i++) | ||
| 406 | RedisModule_ReplyWithStringBuffer(ctx, clusterTrimEventLog[i], strlen(clusterTrimEventLog[i])); | ||
| 407 | return REDISMODULE_OK; | ||
| 408 | } | ||
| 409 | |||
| 410 | /* A keyless command to test module command replication. */ | ||
| 411 | int moduledata = 0; | ||
| 412 | int keylessCmd(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 413 | REDISMODULE_NOT_USED(ctx); | ||
| 414 | REDISMODULE_NOT_USED(argv); | ||
| 415 | REDISMODULE_NOT_USED(argc); | ||
| 416 | moduledata++; | ||
| 417 | RedisModule_ReplyWithLongLong(ctx, moduledata); | ||
| 418 | return REDISMODULE_OK; | ||
| 419 | } | ||
| 420 | int readkeylessCmdVal(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 421 | REDISMODULE_NOT_USED(ctx); | ||
| 422 | REDISMODULE_NOT_USED(argv); | ||
| 423 | REDISMODULE_NOT_USED(argc); | ||
| 424 | RedisModule_ReplyWithLongLong(ctx, moduledata); | ||
| 425 | return REDISMODULE_OK; | ||
| 426 | } | ||
| 427 | |||
| 428 | int subscribeTrimmedEvent(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 429 | REDISMODULE_NOT_USED(ctx); | ||
| 430 | if (argc != 2) | ||
| 431 | return RedisModule_WrongArity(ctx); | ||
| 432 | |||
| 433 | long long subscribe = 0; | ||
| 434 | if (RedisModule_StringToLongLong(argv[1], &subscribe) != REDISMODULE_OK) { | ||
| 435 | RedisModule_ReplyWithError(ctx, "ERR subscribe value"); | ||
| 436 | return REDISMODULE_OK; | ||
| 437 | } | ||
| 438 | |||
| 439 | if (subscribe) { | ||
| 440 | /* Unsubscribe first to avoid duplicate subscription. */ | ||
| 441 | RedisModule_UnsubscribeFromKeyspaceEvents(ctx, REDISMODULE_NOTIFY_KEY_TRIMMED, keyspaceNotificationTrimmedCallback); | ||
| 442 | int ret = RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_KEY_TRIMMED, keyspaceNotificationTrimmedCallback); | ||
| 443 | RedisModule_Assert(ret == REDISMODULE_OK); | ||
| 444 | } else { | ||
| 445 | int ret = RedisModule_UnsubscribeFromKeyspaceEvents(ctx, REDISMODULE_NOTIFY_KEY_TRIMMED, keyspaceNotificationTrimmedCallback); | ||
| 446 | RedisModule_Assert(ret == REDISMODULE_OK); | ||
| 447 | } | ||
| 448 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 449 | return REDISMODULE_OK; | ||
| 450 | } | ||
| 451 | |||
| 452 | void keyEventCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data) { | ||
| 453 | REDISMODULE_NOT_USED(ctx); | ||
| 454 | REDISMODULE_NOT_USED(e); | ||
| 455 | |||
| 456 | if (sub == REDISMODULE_SUBEVENT_KEY_DELETED) { | ||
| 457 | RedisModuleKeyInfoV1 *ei = data; | ||
| 458 | RedisModuleKey *kp = ei->key; | ||
| 459 | RedisModuleString *key = (RedisModuleString *) RedisModule_GetKeyNameFromModuleKey(kp); | ||
| 460 | size_t keylen; | ||
| 461 | const char *keyname = RedisModule_StringPtrLen(key, &keylen); | ||
| 462 | |||
| 463 | /* Verify value can be read. It will be used to verify key's value can | ||
| 464 | * be read in a trim callback. */ | ||
| 465 | size_t valuelen = 0; | ||
| 466 | const char *value = ""; | ||
| 467 | RedisModuleKey *mk = RedisModule_OpenKey(ctx, key, REDISMODULE_READ); | ||
| 468 | if (RedisModule_KeyType(mk) == REDISMODULE_KEYTYPE_STRING) { | ||
| 469 | value = RedisModule_StringDMA(mk, &valuelen, 0); | ||
| 470 | } | ||
| 471 | RedisModule_CloseKey(mk); | ||
| 472 | |||
| 473 | char buf[1024] = {0}; | ||
| 474 | snprintf(buf, sizeof(buf), "keyevent: key: %.*s, value: %.*s", (int) keylen, keyname, (int)valuelen, value); | ||
| 475 | |||
| 476 | if (lastDeletedKeyLog) RedisModule_Free((void *)lastDeletedKeyLog); | ||
| 477 | lastDeletedKeyLog = RedisModule_Strdup(buf); | ||
| 478 | } | ||
| 479 | } | ||
| 480 | |||
| 481 | int getLastDeletedKey(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 482 | REDISMODULE_NOT_USED(ctx); | ||
| 483 | REDISMODULE_NOT_USED(argv); | ||
| 484 | REDISMODULE_NOT_USED(argc); | ||
| 485 | |||
| 486 | if (lastDeletedKeyLog) { | ||
| 487 | RedisModule_ReplyWithStringBuffer(ctx, lastDeletedKeyLog, strlen(lastDeletedKeyLog)); | ||
| 488 | } else { | ||
| 489 | RedisModule_ReplyWithNull(ctx); | ||
| 490 | } | ||
| 491 | return REDISMODULE_OK; | ||
| 492 | } | ||
| 493 | |||
| 494 | int asmGetCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 495 | REDISMODULE_NOT_USED(ctx); | ||
| 496 | |||
| 497 | if (argc != 2) return RedisModule_WrongArity(ctx); | ||
| 498 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ); | ||
| 499 | if (key == NULL) { | ||
| 500 | RedisModule_ReplyWithNull(ctx); | ||
| 501 | return REDISMODULE_OK; | ||
| 502 | } | ||
| 503 | |||
| 504 | RedisModule_Assert(RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_STRING); | ||
| 505 | size_t len; | ||
| 506 | const char *value = RedisModule_StringDMA(key, &len, 0); | ||
| 507 | RedisModule_ReplyWithStringBuffer(ctx, value, len); | ||
| 508 | RedisModule_CloseKey(key); | ||
| 509 | |||
| 510 | return REDISMODULE_OK; | ||
| 511 | } | ||
| 512 | |||
| 513 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 514 | REDISMODULE_NOT_USED(argv); | ||
| 515 | REDISMODULE_NOT_USED(argc); | ||
| 516 | |||
| 517 | if (RedisModule_Init(ctx, "asm", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) | ||
| 518 | return REDISMODULE_ERR; | ||
| 519 | |||
| 520 | if (RedisModule_CreateCommand(ctx, "asm.cluster_can_access_keys_in_slot", testClusterCanAccessKeysInSlot, "", 0, 0, 0) == REDISMODULE_ERR) | ||
| 521 | return REDISMODULE_ERR; | ||
| 522 | |||
| 523 | if (RedisModule_CreateCommand(ctx, "asm.clear_event_log", clearEventLog, "", 0, 0, 0) == REDISMODULE_ERR) | ||
| 524 | return REDISMODULE_ERR; | ||
| 525 | |||
| 526 | if (RedisModule_CreateCommand(ctx, "asm.get_cluster_event_log", getClusterEventLog, "", 0, 0, 0) == REDISMODULE_ERR) | ||
| 527 | return REDISMODULE_ERR; | ||
| 528 | |||
| 529 | if (RedisModule_CreateCommand(ctx, "asm.get_cluster_trim_event_log", getClusterTrimEventLog, "", 0, 0, 0) == REDISMODULE_ERR) | ||
| 530 | return REDISMODULE_ERR; | ||
| 531 | |||
| 532 | if (RedisModule_CreateCommand(ctx, "asm.keyless_cmd", keylessCmd, "write", 0, 0, 0) == REDISMODULE_ERR) | ||
| 533 | return REDISMODULE_ERR; | ||
| 534 | |||
| 535 | if (RedisModule_CreateCommand(ctx, "asm.disable_trim", disableTrimCmd, "", 0, 0, 0) == REDISMODULE_ERR) | ||
| 536 | return REDISMODULE_ERR; | ||
| 537 | |||
| 538 | if (RedisModule_CreateCommand(ctx, "asm.enable_trim", enableTrimCmd, "", 0, 0, 0) == REDISMODULE_ERR) | ||
| 539 | return REDISMODULE_ERR; | ||
| 540 | |||
| 541 | if (RedisModule_CreateCommand(ctx, "asm.read_pending_trim_key", getPendingTrimKeyCmd, "readonly", 0, 0, 0) == REDISMODULE_ERR) | ||
| 542 | return REDISMODULE_ERR; | ||
| 543 | |||
| 544 | if (RedisModule_CreateCommand(ctx, "asm.trim_in_progress", trimInProgressCmd, "", 0, 0, 0) == REDISMODULE_ERR) | ||
| 545 | return REDISMODULE_ERR; | ||
| 546 | |||
| 547 | if (RedisModule_CreateCommand(ctx, "asm.read_keyless_cmd_val", readkeylessCmdVal, "", 0, 0, 0) == REDISMODULE_ERR) | ||
| 548 | return REDISMODULE_ERR; | ||
| 549 | |||
| 550 | if (RedisModule_CreateCommand(ctx, "asm.sanity", sanity, "", 0, 0, 0) == REDISMODULE_ERR) | ||
| 551 | return REDISMODULE_ERR; | ||
| 552 | |||
| 553 | if (RedisModule_CreateCommand(ctx, "asm.subscribe_trimmed_event", subscribeTrimmedEvent, "", 0, 0, 0) == REDISMODULE_ERR) | ||
| 554 | return REDISMODULE_ERR; | ||
| 555 | |||
| 556 | if (RedisModule_CreateCommand(ctx, "asm.replicate_module_command", replicate_module_command, "", 0, 0, 0) == REDISMODULE_ERR) | ||
| 557 | return REDISMODULE_ERR; | ||
| 558 | |||
| 559 | if (RedisModule_CreateCommand(ctx, "asm.lpush_replicate_crossslot_command", lpush_and_replicate_crossslot_command, "write", 0, 0, 0) == REDISMODULE_ERR) | ||
| 560 | return REDISMODULE_ERR; | ||
| 561 | |||
| 562 | if (RedisModule_CreateCommand(ctx, "asm.cluster_get_local_slot_ranges", testClusterGetLocalSlotRanges, "", 0, 0, 0) == REDISMODULE_ERR) | ||
| 563 | return REDISMODULE_ERR; | ||
| 564 | |||
| 565 | if (RedisModule_CreateCommand(ctx, "asm.get_last_deleted_key", getLastDeletedKey, "", 0, 0, 0) == REDISMODULE_ERR) | ||
| 566 | return REDISMODULE_ERR; | ||
| 567 | |||
| 568 | if (RedisModule_CreateCommand(ctx, "asm.get", asmGetCommand, "", 0, 0, 0) == REDISMODULE_ERR) | ||
| 569 | return REDISMODULE_ERR; | ||
| 570 | |||
| 571 | if (RedisModule_CreateCommand(ctx, "asm.parent", NULL, "", 0, 0, 0) == REDISMODULE_ERR) | ||
| 572 | return REDISMODULE_ERR; | ||
| 573 | |||
| 574 | RedisModuleCommand *parent = RedisModule_GetCommand(ctx, "asm.parent"); | ||
| 575 | if (!parent) return REDISMODULE_ERR; | ||
| 576 | |||
| 577 | /* Subcommand: ASM.PARENT SET (write) */ | ||
| 578 | if (RedisModule_CreateSubcommand(parent, "set", asmParentSet, "write fast", 2, 2, 1) == REDISMODULE_ERR) | ||
| 579 | return REDISMODULE_ERR; | ||
| 580 | |||
| 581 | if (RedisModule_SubscribeToServerEvent(ctx, RedisModuleEvent_ClusterSlotMigration, clusterEventCallback) == REDISMODULE_ERR) | ||
| 582 | return REDISMODULE_ERR; | ||
| 583 | |||
| 584 | if (RedisModule_SubscribeToServerEvent(ctx, RedisModuleEvent_ClusterSlotMigrationTrim, clusterTrimEventCallback) == REDISMODULE_ERR) | ||
| 585 | return REDISMODULE_ERR; | ||
| 586 | |||
| 587 | if (RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_KEY_TRIMMED, keyspaceNotificationTrimmedCallback) == REDISMODULE_ERR) | ||
| 588 | return REDISMODULE_ERR; | ||
| 589 | |||
| 590 | if (RedisModule_SubscribeToServerEvent(ctx, RedisModuleEvent_Key, keyEventCallback) == REDISMODULE_ERR) | ||
| 591 | return REDISMODULE_ERR; | ||
| 592 | |||
| 593 | return REDISMODULE_OK; | ||
| 594 | } | ||
diff --git a/examples/redis-unstable/tests/modules/auth.c b/examples/redis-unstable/tests/modules/auth.c new file mode 100644 index 0000000..cc2378e --- /dev/null +++ b/examples/redis-unstable/tests/modules/auth.c | |||
| @@ -0,0 +1,286 @@ | |||
| 1 | /* define macros for having usleep */ | ||
| 2 | #define _BSD_SOURCE | ||
| 3 | #define _DEFAULT_SOURCE | ||
| 4 | |||
| 5 | #include "redismodule.h" | ||
| 6 | |||
| 7 | #include <string.h> | ||
| 8 | #include <unistd.h> | ||
| 9 | #include <pthread.h> | ||
| 10 | |||
| 11 | #define UNUSED(V) ((void) V) | ||
| 12 | |||
| 13 | // A simple global user | ||
| 14 | static RedisModuleUser *global = NULL; | ||
| 15 | static long long client_change_delta = 0; | ||
| 16 | static pthread_t tid; | ||
| 17 | |||
| 18 | void UserChangedCallback(uint64_t client_id, void *privdata) { | ||
| 19 | REDISMODULE_NOT_USED(privdata); | ||
| 20 | REDISMODULE_NOT_USED(client_id); | ||
| 21 | client_change_delta++; | ||
| 22 | } | ||
| 23 | |||
| 24 | int Auth_CreateModuleUser(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 25 | REDISMODULE_NOT_USED(argv); | ||
| 26 | REDISMODULE_NOT_USED(argc); | ||
| 27 | |||
| 28 | if (global) { | ||
| 29 | RedisModule_FreeModuleUser(global); | ||
| 30 | } | ||
| 31 | |||
| 32 | global = RedisModule_CreateModuleUser("global"); | ||
| 33 | RedisModule_SetModuleUserACL(global, "allcommands"); | ||
| 34 | RedisModule_SetModuleUserACL(global, "allkeys"); | ||
| 35 | RedisModule_SetModuleUserACL(global, "on"); | ||
| 36 | |||
| 37 | return RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 38 | } | ||
| 39 | |||
| 40 | int Auth_AuthModuleUser(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 41 | REDISMODULE_NOT_USED(argv); | ||
| 42 | REDISMODULE_NOT_USED(argc); | ||
| 43 | uint64_t client_id; | ||
| 44 | RedisModule_AuthenticateClientWithUser(ctx, global, UserChangedCallback, NULL, &client_id); | ||
| 45 | |||
| 46 | return RedisModule_ReplyWithLongLong(ctx, (uint64_t) client_id); | ||
| 47 | } | ||
| 48 | |||
| 49 | int Auth_AuthRealUser(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 50 | if (argc != 2) return RedisModule_WrongArity(ctx); | ||
| 51 | |||
| 52 | size_t length; | ||
| 53 | uint64_t client_id; | ||
| 54 | |||
| 55 | RedisModuleString *user_string = argv[1]; | ||
| 56 | const char *name = RedisModule_StringPtrLen(user_string, &length); | ||
| 57 | |||
| 58 | if (RedisModule_AuthenticateClientWithACLUser(ctx, name, length, | ||
| 59 | UserChangedCallback, NULL, &client_id) == REDISMODULE_ERR) { | ||
| 60 | return RedisModule_ReplyWithError(ctx, "Invalid user"); | ||
| 61 | } | ||
| 62 | |||
| 63 | return RedisModule_ReplyWithLongLong(ctx, (uint64_t) client_id); | ||
| 64 | } | ||
| 65 | |||
| 66 | /* This command redacts every other arguments and returns OK */ | ||
| 67 | int Auth_RedactedAPI(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 68 | REDISMODULE_NOT_USED(argv); | ||
| 69 | for(int i = argc - 1; i > 0; i -= 2) { | ||
| 70 | int result = RedisModule_RedactClientCommandArgument(ctx, i); | ||
| 71 | RedisModule_Assert(result == REDISMODULE_OK); | ||
| 72 | } | ||
| 73 | return RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 74 | } | ||
| 75 | |||
| 76 | int Auth_ChangeCount(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 77 | REDISMODULE_NOT_USED(argv); | ||
| 78 | REDISMODULE_NOT_USED(argc); | ||
| 79 | long long result = client_change_delta; | ||
| 80 | client_change_delta = 0; | ||
| 81 | return RedisModule_ReplyWithLongLong(ctx, result); | ||
| 82 | } | ||
| 83 | |||
| 84 | /* The Module functionality below validates that module authentication callbacks can be registered | ||
| 85 | * to support both non-blocking and blocking module based authentication. */ | ||
| 86 | |||
| 87 | /* Non Blocking Module Auth callback / implementation. */ | ||
| 88 | int auth_cb(RedisModuleCtx *ctx, RedisModuleString *username, RedisModuleString *password, RedisModuleString **err) { | ||
| 89 | const char *user = RedisModule_StringPtrLen(username, NULL); | ||
| 90 | const char *pwd = RedisModule_StringPtrLen(password, NULL); | ||
| 91 | if (!strcmp(user,"foo") && !strcmp(pwd,"allow")) { | ||
| 92 | RedisModule_AuthenticateClientWithACLUser(ctx, "foo", 3, NULL, NULL, NULL); | ||
| 93 | return REDISMODULE_AUTH_HANDLED; | ||
| 94 | } | ||
| 95 | else if (!strcmp(user,"foo") && !strcmp(pwd,"deny")) { | ||
| 96 | RedisModuleString *log = RedisModule_CreateString(ctx, "Module Auth", 11); | ||
| 97 | RedisModule_ACLAddLogEntryByUserName(ctx, username, log, REDISMODULE_ACL_LOG_AUTH); | ||
| 98 | RedisModule_FreeString(ctx, log); | ||
| 99 | const char *err_msg = "Auth denied by Misc Module."; | ||
| 100 | *err = RedisModule_CreateString(ctx, err_msg, strlen(err_msg)); | ||
| 101 | return REDISMODULE_AUTH_HANDLED; | ||
| 102 | } | ||
| 103 | return REDISMODULE_AUTH_NOT_HANDLED; | ||
| 104 | } | ||
| 105 | |||
| 106 | int test_rm_register_auth_cb(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 107 | REDISMODULE_NOT_USED(argv); | ||
| 108 | REDISMODULE_NOT_USED(argc); | ||
| 109 | RedisModule_RegisterAuthCallback(ctx, auth_cb); | ||
| 110 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 111 | return REDISMODULE_OK; | ||
| 112 | } | ||
| 113 | |||
| 114 | /* | ||
| 115 | * The thread entry point that actually executes the blocking part of the AUTH command. | ||
| 116 | * This function sleeps for 0.5 seconds and then unblocks the client which will later call | ||
| 117 | * `AuthBlock_Reply`. | ||
| 118 | * `arg` is expected to contain the RedisModuleBlockedClient, username, and password. | ||
| 119 | */ | ||
| 120 | void *AuthBlock_ThreadMain(void *arg) { | ||
| 121 | usleep(500000); | ||
| 122 | void **targ = arg; | ||
| 123 | RedisModuleBlockedClient *bc = targ[0]; | ||
| 124 | int result = 2; | ||
| 125 | const char *user = RedisModule_StringPtrLen(targ[1], NULL); | ||
| 126 | const char *pwd = RedisModule_StringPtrLen(targ[2], NULL); | ||
| 127 | if (!strcmp(user,"foo") && !strcmp(pwd,"block_allow")) { | ||
| 128 | result = 1; | ||
| 129 | } | ||
| 130 | else if (!strcmp(user,"foo") && !strcmp(pwd,"block_deny")) { | ||
| 131 | result = 0; | ||
| 132 | } | ||
| 133 | else if (!strcmp(user,"foo") && !strcmp(pwd,"block_abort")) { | ||
| 134 | RedisModule_BlockedClientMeasureTimeEnd(bc); | ||
| 135 | RedisModule_AbortBlock(bc); | ||
| 136 | goto cleanup; | ||
| 137 | } | ||
| 138 | /* Provide the result to the blocking reply cb. */ | ||
| 139 | void **replyarg = RedisModule_Alloc(sizeof(void*)); | ||
| 140 | replyarg[0] = (void *) (uintptr_t) result; | ||
| 141 | RedisModule_BlockedClientMeasureTimeEnd(bc); | ||
| 142 | RedisModule_UnblockClient(bc, replyarg); | ||
| 143 | cleanup: | ||
| 144 | /* Free the username and password and thread / arg data. */ | ||
| 145 | RedisModule_FreeString(NULL, targ[1]); | ||
| 146 | RedisModule_FreeString(NULL, targ[2]); | ||
| 147 | RedisModule_Free(targ); | ||
| 148 | return NULL; | ||
| 149 | } | ||
| 150 | |||
| 151 | /* | ||
| 152 | * Reply callback for a blocking AUTH command. This is called when the client is unblocked. | ||
| 153 | */ | ||
| 154 | int AuthBlock_Reply(RedisModuleCtx *ctx, RedisModuleString *username, RedisModuleString *password, RedisModuleString **err) { | ||
| 155 | REDISMODULE_NOT_USED(password); | ||
| 156 | void **targ = RedisModule_GetBlockedClientPrivateData(ctx); | ||
| 157 | int result = (uintptr_t) targ[0]; | ||
| 158 | size_t userlen = 0; | ||
| 159 | const char *user = RedisModule_StringPtrLen(username, &userlen); | ||
| 160 | /* Handle the success case by authenticating. */ | ||
| 161 | if (result == 1) { | ||
| 162 | RedisModule_AuthenticateClientWithACLUser(ctx, user, userlen, NULL, NULL, NULL); | ||
| 163 | return REDISMODULE_AUTH_HANDLED; | ||
| 164 | } | ||
| 165 | /* Handle the Error case by denying auth */ | ||
| 166 | else if (result == 0) { | ||
| 167 | RedisModuleString *log = RedisModule_CreateString(ctx, "Module Auth", 11); | ||
| 168 | RedisModule_ACLAddLogEntryByUserName(ctx, username, log, REDISMODULE_ACL_LOG_AUTH); | ||
| 169 | RedisModule_FreeString(ctx, log); | ||
| 170 | const char *err_msg = "Auth denied by Misc Module."; | ||
| 171 | *err = RedisModule_CreateString(ctx, err_msg, strlen(err_msg)); | ||
| 172 | return REDISMODULE_AUTH_HANDLED; | ||
| 173 | } | ||
| 174 | /* "Skip" Authentication */ | ||
| 175 | return REDISMODULE_AUTH_NOT_HANDLED; | ||
| 176 | } | ||
| 177 | |||
| 178 | /* Private data freeing callback for Module Auth. */ | ||
| 179 | void AuthBlock_FreeData(RedisModuleCtx *ctx, void *privdata) { | ||
| 180 | REDISMODULE_NOT_USED(ctx); | ||
| 181 | RedisModule_Free(privdata); | ||
| 182 | } | ||
| 183 | |||
| 184 | /* Callback triggered when the engine attempts module auth | ||
| 185 | * Return code here is one of the following: Auth succeeded, Auth denied, | ||
| 186 | * Auth not handled, Auth blocked. | ||
| 187 | * The Module can have auth succeed / denied here itself, but this is an example | ||
| 188 | * of blocking module auth. | ||
| 189 | */ | ||
| 190 | int blocking_auth_cb(RedisModuleCtx *ctx, RedisModuleString *username, RedisModuleString *password, RedisModuleString **err) { | ||
| 191 | REDISMODULE_NOT_USED(username); | ||
| 192 | REDISMODULE_NOT_USED(password); | ||
| 193 | REDISMODULE_NOT_USED(err); | ||
| 194 | /* Block the client from the Module. */ | ||
| 195 | RedisModuleBlockedClient *bc = RedisModule_BlockClientOnAuth(ctx, AuthBlock_Reply, AuthBlock_FreeData); | ||
| 196 | int ctx_flags = RedisModule_GetContextFlags(ctx); | ||
| 197 | if (ctx_flags & REDISMODULE_CTX_FLAGS_MULTI || ctx_flags & REDISMODULE_CTX_FLAGS_LUA) { | ||
| 198 | /* Clean up by using RedisModule_UnblockClient since we attempted blocking the client. */ | ||
| 199 | RedisModule_UnblockClient(bc, NULL); | ||
| 200 | return REDISMODULE_AUTH_HANDLED; | ||
| 201 | } | ||
| 202 | |||
| 203 | /* Another blocking auth cb may have spawned a thread, we'll just wait for it | ||
| 204 | * to finish here */ | ||
| 205 | if (tid) pthread_join(tid, NULL); | ||
| 206 | |||
| 207 | RedisModule_BlockedClientMeasureTimeStart(bc); | ||
| 208 | |||
| 209 | /* Allocate memory for information needed. */ | ||
| 210 | void **targ = RedisModule_Alloc(sizeof(void*)*3); | ||
| 211 | targ[0] = bc; | ||
| 212 | targ[1] = RedisModule_CreateStringFromString(NULL, username); | ||
| 213 | targ[2] = RedisModule_CreateStringFromString(NULL, password); | ||
| 214 | |||
| 215 | /* Create bg thread and pass the blockedclient, username and password to it. */ | ||
| 216 | if (pthread_create(&tid, NULL, AuthBlock_ThreadMain, targ) != 0) { | ||
| 217 | RedisModule_AbortBlock(bc); | ||
| 218 | |||
| 219 | /* These are freed in AuthBlock_ThreadMain but since we failed to spawn | ||
| 220 | * the thread need to free them here. */ | ||
| 221 | RedisModule_FreeString(NULL, targ[1]); | ||
| 222 | RedisModule_FreeString(NULL, targ[2]); | ||
| 223 | RedisModule_Free(targ); | ||
| 224 | } | ||
| 225 | |||
| 226 | return REDISMODULE_AUTH_HANDLED; | ||
| 227 | } | ||
| 228 | |||
| 229 | int test_rm_register_blocking_auth_cb(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 230 | REDISMODULE_NOT_USED(argv); | ||
| 231 | REDISMODULE_NOT_USED(argc); | ||
| 232 | RedisModule_RegisterAuthCallback(ctx, blocking_auth_cb); | ||
| 233 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 234 | return REDISMODULE_OK; | ||
| 235 | } | ||
| 236 | |||
| 237 | /* This function must be present on each Redis module. It is used in order to | ||
| 238 | * register the commands into the Redis server. */ | ||
| 239 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 240 | REDISMODULE_NOT_USED(argv); | ||
| 241 | REDISMODULE_NOT_USED(argc); | ||
| 242 | |||
| 243 | if (RedisModule_Init(ctx,"testacl",1,REDISMODULE_APIVER_1) | ||
| 244 | == REDISMODULE_ERR) return REDISMODULE_ERR; | ||
| 245 | |||
| 246 | if (RedisModule_CreateCommand(ctx,"auth.authrealuser", | ||
| 247 | Auth_AuthRealUser,"no-auth",0,0,0) == REDISMODULE_ERR) | ||
| 248 | return REDISMODULE_ERR; | ||
| 249 | |||
| 250 | if (RedisModule_CreateCommand(ctx,"auth.createmoduleuser", | ||
| 251 | Auth_CreateModuleUser,"",0,0,0) == REDISMODULE_ERR) | ||
| 252 | return REDISMODULE_ERR; | ||
| 253 | |||
| 254 | if (RedisModule_CreateCommand(ctx,"auth.authmoduleuser", | ||
| 255 | Auth_AuthModuleUser,"no-auth",0,0,0) == REDISMODULE_ERR) | ||
| 256 | return REDISMODULE_ERR; | ||
| 257 | |||
| 258 | if (RedisModule_CreateCommand(ctx,"auth.changecount", | ||
| 259 | Auth_ChangeCount,"",0,0,0) == REDISMODULE_ERR) | ||
| 260 | return REDISMODULE_ERR; | ||
| 261 | |||
| 262 | if (RedisModule_CreateCommand(ctx,"auth.redact", | ||
| 263 | Auth_RedactedAPI,"",0,0,0) == REDISMODULE_ERR) | ||
| 264 | return REDISMODULE_ERR; | ||
| 265 | |||
| 266 | if (RedisModule_CreateCommand(ctx,"testmoduleone.rm_register_auth_cb", | ||
| 267 | test_rm_register_auth_cb,"",0,0,0) == REDISMODULE_ERR) | ||
| 268 | return REDISMODULE_ERR; | ||
| 269 | |||
| 270 | if (RedisModule_CreateCommand(ctx,"testmoduleone.rm_register_blocking_auth_cb", | ||
| 271 | test_rm_register_blocking_auth_cb,"",0,0,0) == REDISMODULE_ERR) | ||
| 272 | return REDISMODULE_ERR; | ||
| 273 | |||
| 274 | return REDISMODULE_OK; | ||
| 275 | } | ||
| 276 | |||
| 277 | int RedisModule_OnUnload(RedisModuleCtx *ctx) { | ||
| 278 | UNUSED(ctx); | ||
| 279 | |||
| 280 | if (tid) pthread_join(tid, NULL); | ||
| 281 | |||
| 282 | if (global) | ||
| 283 | RedisModule_FreeModuleUser(global); | ||
| 284 | |||
| 285 | return REDISMODULE_OK; | ||
| 286 | } | ||
diff --git a/examples/redis-unstable/tests/modules/basics.c b/examples/redis-unstable/tests/modules/basics.c new file mode 100644 index 0000000..85c6196 --- /dev/null +++ b/examples/redis-unstable/tests/modules/basics.c | |||
| @@ -0,0 +1,1051 @@ | |||
| 1 | /* Module designed to test the Redis modules subsystem. | ||
| 2 | * | ||
| 3 | * ----------------------------------------------------------------------------- | ||
| 4 | * | ||
| 5 | * Copyright (c) 2016-Present, Redis Ltd. | ||
| 6 | * All rights reserved. | ||
| 7 | * | ||
| 8 | * Licensed under your choice of (a) the Redis Source Available License 2.0 | ||
| 9 | * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the | ||
| 10 | * GNU Affero General Public License v3 (AGPLv3). | ||
| 11 | */ | ||
| 12 | |||
| 13 | #include "redismodule.h" | ||
| 14 | #include <string.h> | ||
| 15 | #include <stdlib.h> | ||
| 16 | |||
| 17 | /* --------------------------------- Helpers -------------------------------- */ | ||
| 18 | |||
| 19 | /* Return true if the reply and the C null term string matches. */ | ||
| 20 | int TestMatchReply(RedisModuleCallReply *reply, char *str) { | ||
| 21 | RedisModuleString *mystr; | ||
| 22 | mystr = RedisModule_CreateStringFromCallReply(reply); | ||
| 23 | if (!mystr) return 0; | ||
| 24 | const char *ptr = RedisModule_StringPtrLen(mystr,NULL); | ||
| 25 | return strcmp(ptr,str) == 0; | ||
| 26 | } | ||
| 27 | |||
| 28 | /* ------------------------------- Test units ------------------------------- */ | ||
| 29 | |||
| 30 | /* TEST.CALL -- Test Call() API. */ | ||
| 31 | int TestCall(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 32 | REDISMODULE_NOT_USED(argv); | ||
| 33 | REDISMODULE_NOT_USED(argc); | ||
| 34 | |||
| 35 | RedisModule_AutoMemory(ctx); | ||
| 36 | RedisModuleCallReply *reply; | ||
| 37 | |||
| 38 | RedisModule_Call(ctx,"DEL","c","mylist"); | ||
| 39 | RedisModuleString *mystr = RedisModule_CreateString(ctx,"foo",3); | ||
| 40 | RedisModule_Call(ctx,"RPUSH","csl","mylist",mystr,(long long)1234); | ||
| 41 | reply = RedisModule_Call(ctx,"LRANGE","ccc","mylist","0","-1"); | ||
| 42 | long long items = RedisModule_CallReplyLength(reply); | ||
| 43 | if (items != 2) goto fail; | ||
| 44 | |||
| 45 | RedisModuleCallReply *item0, *item1; | ||
| 46 | |||
| 47 | item0 = RedisModule_CallReplyArrayElement(reply,0); | ||
| 48 | item1 = RedisModule_CallReplyArrayElement(reply,1); | ||
| 49 | if (!TestMatchReply(item0,"foo")) goto fail; | ||
| 50 | if (!TestMatchReply(item1,"1234")) goto fail; | ||
| 51 | |||
| 52 | RedisModule_ReplyWithSimpleString(ctx,"OK"); | ||
| 53 | return REDISMODULE_OK; | ||
| 54 | |||
| 55 | fail: | ||
| 56 | RedisModule_ReplyWithSimpleString(ctx,"ERR"); | ||
| 57 | return REDISMODULE_OK; | ||
| 58 | } | ||
| 59 | |||
| 60 | int TestCallResp3Attribute(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 61 | REDISMODULE_NOT_USED(argv); | ||
| 62 | REDISMODULE_NOT_USED(argc); | ||
| 63 | |||
| 64 | RedisModule_AutoMemory(ctx); | ||
| 65 | RedisModuleCallReply *reply; | ||
| 66 | |||
| 67 | reply = RedisModule_Call(ctx,"DEBUG","3cc" ,"PROTOCOL", "attrib"); /* 3 stands for resp 3 reply */ | ||
| 68 | if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_STRING) goto fail; | ||
| 69 | |||
| 70 | /* make sure we can not reply to resp2 client with resp3 (it might be a string but it contains attribute) */ | ||
| 71 | if (RedisModule_ReplyWithCallReply(ctx, reply) != REDISMODULE_ERR) goto fail; | ||
| 72 | |||
| 73 | if (!TestMatchReply(reply,"Some real reply following the attribute")) goto fail; | ||
| 74 | |||
| 75 | reply = RedisModule_CallReplyAttribute(reply); | ||
| 76 | if (!reply || RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_ATTRIBUTE) goto fail; | ||
| 77 | /* make sure we can not reply to resp2 client with resp3 attribute */ | ||
| 78 | if (RedisModule_ReplyWithCallReply(ctx, reply) != REDISMODULE_ERR) goto fail; | ||
| 79 | if (RedisModule_CallReplyLength(reply) != 1) goto fail; | ||
| 80 | |||
| 81 | RedisModuleCallReply *key, *val; | ||
| 82 | if (RedisModule_CallReplyAttributeElement(reply,0,&key,&val) != REDISMODULE_OK) goto fail; | ||
| 83 | if (!TestMatchReply(key,"key-popularity")) goto fail; | ||
| 84 | if (RedisModule_CallReplyType(val) != REDISMODULE_REPLY_ARRAY) goto fail; | ||
| 85 | if (RedisModule_CallReplyLength(val) != 2) goto fail; | ||
| 86 | if (!TestMatchReply(RedisModule_CallReplyArrayElement(val, 0),"key:123")) goto fail; | ||
| 87 | if (!TestMatchReply(RedisModule_CallReplyArrayElement(val, 1),"90")) goto fail; | ||
| 88 | |||
| 89 | RedisModule_ReplyWithSimpleString(ctx,"OK"); | ||
| 90 | return REDISMODULE_OK; | ||
| 91 | |||
| 92 | fail: | ||
| 93 | RedisModule_ReplyWithSimpleString(ctx,"ERR"); | ||
| 94 | return REDISMODULE_OK; | ||
| 95 | } | ||
| 96 | |||
| 97 | int TestGetResp(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 98 | REDISMODULE_NOT_USED(argv); | ||
| 99 | REDISMODULE_NOT_USED(argc); | ||
| 100 | |||
| 101 | int flags = RedisModule_GetContextFlags(ctx); | ||
| 102 | |||
| 103 | if (flags & REDISMODULE_CTX_FLAGS_RESP3) { | ||
| 104 | RedisModule_ReplyWithLongLong(ctx, 3); | ||
| 105 | } else { | ||
| 106 | RedisModule_ReplyWithLongLong(ctx, 2); | ||
| 107 | } | ||
| 108 | |||
| 109 | return REDISMODULE_OK; | ||
| 110 | } | ||
| 111 | |||
| 112 | int TestCallRespAutoMode(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 113 | REDISMODULE_NOT_USED(argv); | ||
| 114 | REDISMODULE_NOT_USED(argc); | ||
| 115 | |||
| 116 | RedisModule_AutoMemory(ctx); | ||
| 117 | RedisModuleCallReply *reply; | ||
| 118 | |||
| 119 | RedisModule_Call(ctx,"DEL","c","myhash"); | ||
| 120 | RedisModule_Call(ctx,"HSET","ccccc","myhash", "f1", "v1", "f2", "v2"); | ||
| 121 | /* 0 stands for auto mode, we will get the reply in the same format as the client */ | ||
| 122 | reply = RedisModule_Call(ctx,"HGETALL","0c" ,"myhash"); | ||
| 123 | RedisModule_ReplyWithCallReply(ctx, reply); | ||
| 124 | return REDISMODULE_OK; | ||
| 125 | } | ||
| 126 | |||
| 127 | int TestCallResp3Map(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 128 | REDISMODULE_NOT_USED(argv); | ||
| 129 | REDISMODULE_NOT_USED(argc); | ||
| 130 | |||
| 131 | RedisModule_AutoMemory(ctx); | ||
| 132 | RedisModuleCallReply *reply; | ||
| 133 | |||
| 134 | RedisModule_Call(ctx,"DEL","c","myhash"); | ||
| 135 | RedisModule_Call(ctx,"HSET","ccccc","myhash", "f1", "v1", "f2", "v2"); | ||
| 136 | reply = RedisModule_Call(ctx,"HGETALL","3c" ,"myhash"); /* 3 stands for resp 3 reply */ | ||
| 137 | if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_MAP) goto fail; | ||
| 138 | |||
| 139 | /* make sure we can not reply to resp2 client with resp3 map */ | ||
| 140 | if (RedisModule_ReplyWithCallReply(ctx, reply) != REDISMODULE_ERR) goto fail; | ||
| 141 | |||
| 142 | long long items = RedisModule_CallReplyLength(reply); | ||
| 143 | if (items != 2) goto fail; | ||
| 144 | |||
| 145 | RedisModuleCallReply *key0, *key1; | ||
| 146 | RedisModuleCallReply *val0, *val1; | ||
| 147 | if (RedisModule_CallReplyMapElement(reply,0,&key0,&val0) != REDISMODULE_OK) goto fail; | ||
| 148 | if (RedisModule_CallReplyMapElement(reply,1,&key1,&val1) != REDISMODULE_OK) goto fail; | ||
| 149 | if (!TestMatchReply(key0,"f1")) goto fail; | ||
| 150 | if (!TestMatchReply(key1,"f2")) goto fail; | ||
| 151 | if (!TestMatchReply(val0,"v1")) goto fail; | ||
| 152 | if (!TestMatchReply(val1,"v2")) goto fail; | ||
| 153 | |||
| 154 | RedisModule_ReplyWithSimpleString(ctx,"OK"); | ||
| 155 | return REDISMODULE_OK; | ||
| 156 | |||
| 157 | fail: | ||
| 158 | RedisModule_ReplyWithSimpleString(ctx,"ERR"); | ||
| 159 | return REDISMODULE_OK; | ||
| 160 | } | ||
| 161 | |||
| 162 | int TestCallResp3Bool(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 163 | REDISMODULE_NOT_USED(argv); | ||
| 164 | REDISMODULE_NOT_USED(argc); | ||
| 165 | |||
| 166 | RedisModule_AutoMemory(ctx); | ||
| 167 | RedisModuleCallReply *reply; | ||
| 168 | |||
| 169 | reply = RedisModule_Call(ctx,"DEBUG","3cc" ,"PROTOCOL", "true"); /* 3 stands for resp 3 reply */ | ||
| 170 | if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_BOOL) goto fail; | ||
| 171 | /* make sure we can not reply to resp2 client with resp3 bool */ | ||
| 172 | if (RedisModule_ReplyWithCallReply(ctx, reply) != REDISMODULE_ERR) goto fail; | ||
| 173 | |||
| 174 | if (!RedisModule_CallReplyBool(reply)) goto fail; | ||
| 175 | reply = RedisModule_Call(ctx,"DEBUG","3cc" ,"PROTOCOL", "false"); /* 3 stands for resp 3 reply */ | ||
| 176 | if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_BOOL) goto fail; | ||
| 177 | if (RedisModule_CallReplyBool(reply)) goto fail; | ||
| 178 | |||
| 179 | RedisModule_ReplyWithSimpleString(ctx,"OK"); | ||
| 180 | return REDISMODULE_OK; | ||
| 181 | |||
| 182 | fail: | ||
| 183 | RedisModule_ReplyWithSimpleString(ctx,"ERR"); | ||
| 184 | return REDISMODULE_OK; | ||
| 185 | } | ||
| 186 | |||
| 187 | int TestCallResp3Null(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 188 | REDISMODULE_NOT_USED(argv); | ||
| 189 | REDISMODULE_NOT_USED(argc); | ||
| 190 | |||
| 191 | RedisModule_AutoMemory(ctx); | ||
| 192 | RedisModuleCallReply *reply; | ||
| 193 | |||
| 194 | reply = RedisModule_Call(ctx,"DEBUG","3cc" ,"PROTOCOL", "null"); /* 3 stands for resp 3 reply */ | ||
| 195 | if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_NULL) goto fail; | ||
| 196 | |||
| 197 | /* make sure we can not reply to resp2 client with resp3 null */ | ||
| 198 | if (RedisModule_ReplyWithCallReply(ctx, reply) != REDISMODULE_ERR) goto fail; | ||
| 199 | |||
| 200 | RedisModule_ReplyWithSimpleString(ctx,"OK"); | ||
| 201 | return REDISMODULE_OK; | ||
| 202 | |||
| 203 | fail: | ||
| 204 | RedisModule_ReplyWithSimpleString(ctx,"ERR"); | ||
| 205 | return REDISMODULE_OK; | ||
| 206 | } | ||
| 207 | |||
| 208 | int TestCallReplyWithNestedReply(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 209 | REDISMODULE_NOT_USED(argv); | ||
| 210 | REDISMODULE_NOT_USED(argc); | ||
| 211 | |||
| 212 | RedisModule_AutoMemory(ctx); | ||
| 213 | RedisModuleCallReply *reply; | ||
| 214 | |||
| 215 | RedisModule_Call(ctx,"DEL","c","mylist"); | ||
| 216 | RedisModule_Call(ctx,"RPUSH","ccl","mylist","test",(long long)1234); | ||
| 217 | reply = RedisModule_Call(ctx,"LRANGE","ccc","mylist","0","-1"); | ||
| 218 | if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_ARRAY) goto fail; | ||
| 219 | if (RedisModule_CallReplyLength(reply) < 1) goto fail; | ||
| 220 | RedisModuleCallReply *nestedReply = RedisModule_CallReplyArrayElement(reply, 0); | ||
| 221 | |||
| 222 | RedisModule_ReplyWithCallReply(ctx,nestedReply); | ||
| 223 | return REDISMODULE_OK; | ||
| 224 | |||
| 225 | fail: | ||
| 226 | RedisModule_ReplyWithSimpleString(ctx,"ERR"); | ||
| 227 | return REDISMODULE_OK; | ||
| 228 | } | ||
| 229 | |||
| 230 | int TestCallReplyWithArrayReply(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 231 | REDISMODULE_NOT_USED(argv); | ||
| 232 | REDISMODULE_NOT_USED(argc); | ||
| 233 | |||
| 234 | RedisModule_AutoMemory(ctx); | ||
| 235 | RedisModuleCallReply *reply; | ||
| 236 | |||
| 237 | RedisModule_Call(ctx,"DEL","c","mylist"); | ||
| 238 | RedisModule_Call(ctx,"RPUSH","ccl","mylist","test",(long long)1234); | ||
| 239 | reply = RedisModule_Call(ctx,"LRANGE","ccc","mylist","0","-1"); | ||
| 240 | if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_ARRAY) goto fail; | ||
| 241 | |||
| 242 | RedisModule_ReplyWithCallReply(ctx,reply); | ||
| 243 | return REDISMODULE_OK; | ||
| 244 | |||
| 245 | fail: | ||
| 246 | RedisModule_ReplyWithSimpleString(ctx,"ERR"); | ||
| 247 | return REDISMODULE_OK; | ||
| 248 | } | ||
| 249 | |||
| 250 | int TestCallResp3Double(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 251 | REDISMODULE_NOT_USED(argv); | ||
| 252 | REDISMODULE_NOT_USED(argc); | ||
| 253 | |||
| 254 | RedisModule_AutoMemory(ctx); | ||
| 255 | RedisModuleCallReply *reply; | ||
| 256 | |||
| 257 | reply = RedisModule_Call(ctx,"DEBUG","3cc" ,"PROTOCOL", "double"); /* 3 stands for resp 3 reply */ | ||
| 258 | if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_DOUBLE) goto fail; | ||
| 259 | |||
| 260 | /* make sure we can not reply to resp2 client with resp3 double*/ | ||
| 261 | if (RedisModule_ReplyWithCallReply(ctx, reply) != REDISMODULE_ERR) goto fail; | ||
| 262 | |||
| 263 | double d = RedisModule_CallReplyDouble(reply); | ||
| 264 | /* we compare strings, since comparing doubles directly can fail in various architectures, e.g. 32bit */ | ||
| 265 | char got[30], expected[30]; | ||
| 266 | snprintf(got, sizeof(got), "%.17g", d); | ||
| 267 | snprintf(expected, sizeof(expected), "%.17g", 3.141); | ||
| 268 | if (strcmp(got, expected) != 0) goto fail; | ||
| 269 | RedisModule_ReplyWithSimpleString(ctx,"OK"); | ||
| 270 | return REDISMODULE_OK; | ||
| 271 | |||
| 272 | fail: | ||
| 273 | RedisModule_ReplyWithSimpleString(ctx,"ERR"); | ||
| 274 | return REDISMODULE_OK; | ||
| 275 | } | ||
| 276 | |||
| 277 | int TestCallResp3BigNumber(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 278 | REDISMODULE_NOT_USED(argv); | ||
| 279 | REDISMODULE_NOT_USED(argc); | ||
| 280 | |||
| 281 | RedisModule_AutoMemory(ctx); | ||
| 282 | RedisModuleCallReply *reply; | ||
| 283 | |||
| 284 | reply = RedisModule_Call(ctx,"DEBUG","3cc" ,"PROTOCOL", "bignum"); /* 3 stands for resp 3 reply */ | ||
| 285 | if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_BIG_NUMBER) goto fail; | ||
| 286 | |||
| 287 | /* make sure we can not reply to resp2 client with resp3 big number */ | ||
| 288 | if (RedisModule_ReplyWithCallReply(ctx, reply) != REDISMODULE_ERR) goto fail; | ||
| 289 | |||
| 290 | size_t len; | ||
| 291 | const char* big_num = RedisModule_CallReplyBigNumber(reply, &len); | ||
| 292 | RedisModule_ReplyWithStringBuffer(ctx,big_num,len); | ||
| 293 | return REDISMODULE_OK; | ||
| 294 | |||
| 295 | fail: | ||
| 296 | RedisModule_ReplyWithSimpleString(ctx,"ERR"); | ||
| 297 | return REDISMODULE_OK; | ||
| 298 | } | ||
| 299 | |||
| 300 | int TestCallResp3Verbatim(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 301 | REDISMODULE_NOT_USED(argv); | ||
| 302 | REDISMODULE_NOT_USED(argc); | ||
| 303 | |||
| 304 | RedisModule_AutoMemory(ctx); | ||
| 305 | RedisModuleCallReply *reply; | ||
| 306 | |||
| 307 | reply = RedisModule_Call(ctx,"DEBUG","3cc" ,"PROTOCOL", "verbatim"); /* 3 stands for resp 3 reply */ | ||
| 308 | if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_VERBATIM_STRING) goto fail; | ||
| 309 | |||
| 310 | /* make sure we can not reply to resp2 client with resp3 verbatim string */ | ||
| 311 | if (RedisModule_ReplyWithCallReply(ctx, reply) != REDISMODULE_ERR) goto fail; | ||
| 312 | |||
| 313 | const char* format; | ||
| 314 | size_t len; | ||
| 315 | const char* str = RedisModule_CallReplyVerbatim(reply, &len, &format); | ||
| 316 | RedisModuleString *s = RedisModule_CreateStringPrintf(ctx, "%.*s:%.*s", 3, format, (int)len, str); | ||
| 317 | RedisModule_ReplyWithString(ctx,s); | ||
| 318 | return REDISMODULE_OK; | ||
| 319 | |||
| 320 | fail: | ||
| 321 | RedisModule_ReplyWithSimpleString(ctx,"ERR"); | ||
| 322 | return REDISMODULE_OK; | ||
| 323 | } | ||
| 324 | |||
| 325 | int TestCallResp3Set(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 326 | REDISMODULE_NOT_USED(argv); | ||
| 327 | REDISMODULE_NOT_USED(argc); | ||
| 328 | |||
| 329 | RedisModule_AutoMemory(ctx); | ||
| 330 | RedisModuleCallReply *reply; | ||
| 331 | |||
| 332 | RedisModule_Call(ctx,"DEL","c","myset"); | ||
| 333 | RedisModule_Call(ctx,"sadd","ccc","myset", "v1", "v2"); | ||
| 334 | reply = RedisModule_Call(ctx,"smembers","3c" ,"myset"); // N stands for resp 3 reply | ||
| 335 | if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_SET) goto fail; | ||
| 336 | |||
| 337 | /* make sure we can not reply to resp2 client with resp3 set */ | ||
| 338 | if (RedisModule_ReplyWithCallReply(ctx, reply) != REDISMODULE_ERR) goto fail; | ||
| 339 | |||
| 340 | long long items = RedisModule_CallReplyLength(reply); | ||
| 341 | if (items != 2) goto fail; | ||
| 342 | |||
| 343 | RedisModuleCallReply *val0, *val1; | ||
| 344 | |||
| 345 | val0 = RedisModule_CallReplySetElement(reply,0); | ||
| 346 | val1 = RedisModule_CallReplySetElement(reply,1); | ||
| 347 | |||
| 348 | /* | ||
| 349 | * The order of elements on sets are not promised so we just | ||
| 350 | * veridy that the reply matches one of the elements. | ||
| 351 | */ | ||
| 352 | if (!TestMatchReply(val0,"v1") && !TestMatchReply(val0,"v2")) goto fail; | ||
| 353 | if (!TestMatchReply(val1,"v1") && !TestMatchReply(val1,"v2")) goto fail; | ||
| 354 | |||
| 355 | RedisModule_ReplyWithSimpleString(ctx,"OK"); | ||
| 356 | return REDISMODULE_OK; | ||
| 357 | |||
| 358 | fail: | ||
| 359 | RedisModule_ReplyWithSimpleString(ctx,"ERR"); | ||
| 360 | return REDISMODULE_OK; | ||
| 361 | } | ||
| 362 | |||
| 363 | /* TEST.STRING.APPEND -- Test appending to an existing string object. */ | ||
| 364 | int TestStringAppend(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 365 | REDISMODULE_NOT_USED(argv); | ||
| 366 | REDISMODULE_NOT_USED(argc); | ||
| 367 | |||
| 368 | RedisModuleString *s = RedisModule_CreateString(ctx,"foo",3); | ||
| 369 | RedisModule_StringAppendBuffer(ctx,s,"bar",3); | ||
| 370 | RedisModule_ReplyWithString(ctx,s); | ||
| 371 | RedisModule_FreeString(ctx,s); | ||
| 372 | return REDISMODULE_OK; | ||
| 373 | } | ||
| 374 | |||
| 375 | /* TEST.STRING.APPEND.AM -- Test append with retain when auto memory is on. */ | ||
| 376 | int TestStringAppendAM(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 377 | REDISMODULE_NOT_USED(argv); | ||
| 378 | REDISMODULE_NOT_USED(argc); | ||
| 379 | |||
| 380 | RedisModule_AutoMemory(ctx); | ||
| 381 | RedisModuleString *s = RedisModule_CreateString(ctx,"foo",3); | ||
| 382 | RedisModule_RetainString(ctx,s); | ||
| 383 | RedisModule_TrimStringAllocation(s); /* Mostly NOP, but exercises the API function */ | ||
| 384 | RedisModule_StringAppendBuffer(ctx,s,"bar",3); | ||
| 385 | RedisModule_ReplyWithString(ctx,s); | ||
| 386 | RedisModule_FreeString(ctx,s); | ||
| 387 | return REDISMODULE_OK; | ||
| 388 | } | ||
| 389 | |||
| 390 | /* TEST.STRING.TRIM -- Test we trim a string with free space. */ | ||
| 391 | int TestTrimString(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 392 | REDISMODULE_NOT_USED(argv); | ||
| 393 | REDISMODULE_NOT_USED(argc); | ||
| 394 | RedisModuleString *s = RedisModule_CreateString(ctx,"foo",3); | ||
| 395 | char *tmp = RedisModule_Alloc(1024); | ||
| 396 | RedisModule_StringAppendBuffer(ctx,s,tmp,1024); | ||
| 397 | size_t string_len = RedisModule_MallocSizeString(s); | ||
| 398 | RedisModule_TrimStringAllocation(s); | ||
| 399 | size_t len_after_trim = RedisModule_MallocSizeString(s); | ||
| 400 | |||
| 401 | /* Determine if using jemalloc memory allocator. */ | ||
| 402 | RedisModuleServerInfoData *info = RedisModule_GetServerInfo(ctx, "memory"); | ||
| 403 | const char *field = RedisModule_ServerInfoGetFieldC(info, "mem_allocator"); | ||
| 404 | int use_jemalloc = !strncmp(field, "jemalloc", 8); | ||
| 405 | |||
| 406 | /* Jemalloc will reallocate `s` from 2k to 1k after RedisModule_TrimStringAllocation(), | ||
| 407 | * but non-jemalloc memory allocators may keep the old size. */ | ||
| 408 | if ((use_jemalloc && len_after_trim < string_len) || | ||
| 409 | (!use_jemalloc && len_after_trim <= string_len)) | ||
| 410 | { | ||
| 411 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 412 | } else { | ||
| 413 | RedisModule_ReplyWithError(ctx, "String was not trimmed as expected."); | ||
| 414 | } | ||
| 415 | RedisModule_FreeServerInfo(ctx, info); | ||
| 416 | RedisModule_Free(tmp); | ||
| 417 | RedisModule_FreeString(ctx,s); | ||
| 418 | return REDISMODULE_OK; | ||
| 419 | } | ||
| 420 | |||
| 421 | /* TEST.STRING.PRINTF -- Test string formatting. */ | ||
| 422 | int TestStringPrintf(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 423 | RedisModule_AutoMemory(ctx); | ||
| 424 | if (argc < 3) { | ||
| 425 | return RedisModule_WrongArity(ctx); | ||
| 426 | } | ||
| 427 | RedisModuleString *s = RedisModule_CreateStringPrintf(ctx, | ||
| 428 | "Got %d args. argv[1]: %s, argv[2]: %s", | ||
| 429 | argc, | ||
| 430 | RedisModule_StringPtrLen(argv[1], NULL), | ||
| 431 | RedisModule_StringPtrLen(argv[2], NULL) | ||
| 432 | ); | ||
| 433 | |||
| 434 | RedisModule_ReplyWithString(ctx,s); | ||
| 435 | |||
| 436 | return REDISMODULE_OK; | ||
| 437 | } | ||
| 438 | |||
| 439 | int failTest(RedisModuleCtx *ctx, const char *msg) { | ||
| 440 | RedisModule_ReplyWithError(ctx, msg); | ||
| 441 | return REDISMODULE_ERR; | ||
| 442 | } | ||
| 443 | |||
| 444 | int TestUnlink(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 445 | RedisModule_AutoMemory(ctx); | ||
| 446 | REDISMODULE_NOT_USED(argv); | ||
| 447 | REDISMODULE_NOT_USED(argc); | ||
| 448 | |||
| 449 | RedisModuleKey *k = RedisModule_OpenKey(ctx, RedisModule_CreateStringPrintf(ctx, "unlinked"), REDISMODULE_WRITE | REDISMODULE_READ); | ||
| 450 | if (!k) return failTest(ctx, "Could not create key"); | ||
| 451 | |||
| 452 | if (REDISMODULE_ERR == RedisModule_StringSet(k, RedisModule_CreateStringPrintf(ctx, "Foobar"))) { | ||
| 453 | return failTest(ctx, "Could not set string value"); | ||
| 454 | } | ||
| 455 | |||
| 456 | RedisModuleCallReply *rep = RedisModule_Call(ctx, "EXISTS", "c", "unlinked"); | ||
| 457 | if (!rep || RedisModule_CallReplyInteger(rep) != 1) { | ||
| 458 | return failTest(ctx, "Key does not exist before unlink"); | ||
| 459 | } | ||
| 460 | |||
| 461 | if (REDISMODULE_ERR == RedisModule_UnlinkKey(k)) { | ||
| 462 | return failTest(ctx, "Could not unlink key"); | ||
| 463 | } | ||
| 464 | |||
| 465 | rep = RedisModule_Call(ctx, "EXISTS", "c", "unlinked"); | ||
| 466 | if (!rep || RedisModule_CallReplyInteger(rep) != 0) { | ||
| 467 | return failTest(ctx, "Could not verify key to be unlinked"); | ||
| 468 | } | ||
| 469 | return RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 470 | } | ||
| 471 | |||
| 472 | int TestNestedCallReplyArrayElement(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 473 | RedisModule_AutoMemory(ctx); | ||
| 474 | REDISMODULE_NOT_USED(argv); | ||
| 475 | REDISMODULE_NOT_USED(argc); | ||
| 476 | |||
| 477 | RedisModuleString *expect_key = RedisModule_CreateString(ctx, "mykey", strlen("mykey")); | ||
| 478 | RedisModule_SelectDb(ctx, 1); | ||
| 479 | RedisModule_Call(ctx, "LPUSH", "sc", expect_key, "myvalue"); | ||
| 480 | |||
| 481 | RedisModuleCallReply *scan_reply = RedisModule_Call(ctx, "SCAN", "l", (long long)0); | ||
| 482 | RedisModule_Assert(scan_reply != NULL && RedisModule_CallReplyType(scan_reply) == REDISMODULE_REPLY_ARRAY); | ||
| 483 | RedisModule_Assert(RedisModule_CallReplyLength(scan_reply) == 2); | ||
| 484 | |||
| 485 | long long scan_cursor; | ||
| 486 | RedisModuleCallReply *cursor_reply = RedisModule_CallReplyArrayElement(scan_reply, 0); | ||
| 487 | RedisModule_Assert(RedisModule_CallReplyType(cursor_reply) == REDISMODULE_REPLY_STRING); | ||
| 488 | RedisModule_Assert(RedisModule_StringToLongLong(RedisModule_CreateStringFromCallReply(cursor_reply), &scan_cursor) == REDISMODULE_OK); | ||
| 489 | RedisModule_Assert(scan_cursor == 0); | ||
| 490 | |||
| 491 | RedisModuleCallReply *keys_reply = RedisModule_CallReplyArrayElement(scan_reply, 1); | ||
| 492 | RedisModule_Assert(RedisModule_CallReplyType(keys_reply) == REDISMODULE_REPLY_ARRAY); | ||
| 493 | RedisModule_Assert( RedisModule_CallReplyLength(keys_reply) == 1); | ||
| 494 | |||
| 495 | RedisModuleCallReply *key_reply = RedisModule_CallReplyArrayElement(keys_reply, 0); | ||
| 496 | RedisModule_Assert(RedisModule_CallReplyType(key_reply) == REDISMODULE_REPLY_STRING); | ||
| 497 | RedisModuleString *key = RedisModule_CreateStringFromCallReply(key_reply); | ||
| 498 | RedisModule_Assert(RedisModule_StringCompare(key, expect_key) == 0); | ||
| 499 | |||
| 500 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 501 | return REDISMODULE_OK; | ||
| 502 | } | ||
| 503 | |||
| 504 | /* TEST.STRING.TRUNCATE -- Test truncating an existing string object. */ | ||
| 505 | int TestStringTruncate(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 506 | RedisModule_AutoMemory(ctx); | ||
| 507 | REDISMODULE_NOT_USED(argv); | ||
| 508 | REDISMODULE_NOT_USED(argc); | ||
| 509 | |||
| 510 | RedisModule_Call(ctx, "SET", "cc", "foo", "abcde"); | ||
| 511 | RedisModuleKey *k = RedisModule_OpenKey(ctx, RedisModule_CreateStringPrintf(ctx, "foo"), REDISMODULE_READ | REDISMODULE_WRITE); | ||
| 512 | if (!k) return failTest(ctx, "Could not create key"); | ||
| 513 | |||
| 514 | size_t len = 0; | ||
| 515 | char* s; | ||
| 516 | |||
| 517 | /* expand from 5 to 8 and check null pad */ | ||
| 518 | if (REDISMODULE_ERR == RedisModule_StringTruncate(k, 8)) { | ||
| 519 | return failTest(ctx, "Could not truncate string value (8)"); | ||
| 520 | } | ||
| 521 | s = RedisModule_StringDMA(k, &len, REDISMODULE_READ); | ||
| 522 | if (!s) { | ||
| 523 | return failTest(ctx, "Failed to read truncated string (8)"); | ||
| 524 | } else if (len != 8) { | ||
| 525 | return failTest(ctx, "Failed to expand string value (8)"); | ||
| 526 | } else if (0 != strncmp(s, "abcde\0\0\0", 8)) { | ||
| 527 | return failTest(ctx, "Failed to null pad string value (8)"); | ||
| 528 | } | ||
| 529 | |||
| 530 | /* shrink from 8 to 4 */ | ||
| 531 | if (REDISMODULE_ERR == RedisModule_StringTruncate(k, 4)) { | ||
| 532 | return failTest(ctx, "Could not truncate string value (4)"); | ||
| 533 | } | ||
| 534 | s = RedisModule_StringDMA(k, &len, REDISMODULE_READ); | ||
| 535 | if (!s) { | ||
| 536 | return failTest(ctx, "Failed to read truncated string (4)"); | ||
| 537 | } else if (len != 4) { | ||
| 538 | return failTest(ctx, "Failed to shrink string value (4)"); | ||
| 539 | } else if (0 != strncmp(s, "abcd", 4)) { | ||
| 540 | return failTest(ctx, "Failed to truncate string value (4)"); | ||
| 541 | } | ||
| 542 | |||
| 543 | /* shrink to 0 */ | ||
| 544 | if (REDISMODULE_ERR == RedisModule_StringTruncate(k, 0)) { | ||
| 545 | return failTest(ctx, "Could not truncate string value (0)"); | ||
| 546 | } | ||
| 547 | s = RedisModule_StringDMA(k, &len, REDISMODULE_READ); | ||
| 548 | if (!s) { | ||
| 549 | return failTest(ctx, "Failed to read truncated string (0)"); | ||
| 550 | } else if (len != 0) { | ||
| 551 | return failTest(ctx, "Failed to shrink string value to (0)"); | ||
| 552 | } | ||
| 553 | |||
| 554 | return RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 555 | } | ||
| 556 | |||
| 557 | int NotifyCallback(RedisModuleCtx *ctx, int type, const char *event, | ||
| 558 | RedisModuleString *key) { | ||
| 559 | RedisModule_AutoMemory(ctx); | ||
| 560 | /* Increment a counter on the notifications: for each key notified we | ||
| 561 | * increment a counter */ | ||
| 562 | RedisModule_Log(ctx, "notice", "Got event type %d, event %s, key %s", type, | ||
| 563 | event, RedisModule_StringPtrLen(key, NULL)); | ||
| 564 | |||
| 565 | RedisModule_Call(ctx, "HINCRBY", "csc", "notifications", key, "1"); | ||
| 566 | return REDISMODULE_OK; | ||
| 567 | } | ||
| 568 | |||
| 569 | /* TEST.NOTIFICATIONS -- Test Keyspace Notifications. */ | ||
| 570 | int TestNotifications(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 571 | RedisModule_AutoMemory(ctx); | ||
| 572 | REDISMODULE_NOT_USED(argv); | ||
| 573 | REDISMODULE_NOT_USED(argc); | ||
| 574 | |||
| 575 | #define FAIL(msg, ...) \ | ||
| 576 | { \ | ||
| 577 | RedisModule_Log(ctx, "warning", "Failed NOTIFY Test. Reason: " #msg, ##__VA_ARGS__); \ | ||
| 578 | goto err; \ | ||
| 579 | } | ||
| 580 | RedisModule_Call(ctx, "FLUSHDB", ""); | ||
| 581 | |||
| 582 | RedisModule_Call(ctx, "SET", "cc", "foo", "bar"); | ||
| 583 | RedisModule_Call(ctx, "SET", "cc", "foo", "baz"); | ||
| 584 | RedisModule_Call(ctx, "SADD", "cc", "bar", "x"); | ||
| 585 | RedisModule_Call(ctx, "SADD", "cc", "bar", "y"); | ||
| 586 | |||
| 587 | RedisModule_Call(ctx, "HSET", "ccc", "baz", "x", "y"); | ||
| 588 | /* LPUSH should be ignored and not increment any counters */ | ||
| 589 | RedisModule_Call(ctx, "LPUSH", "cc", "l", "y"); | ||
| 590 | RedisModule_Call(ctx, "LPUSH", "cc", "l", "y"); | ||
| 591 | |||
| 592 | /* Miss some keys intentionally so we will get a "keymiss" notification. */ | ||
| 593 | RedisModule_Call(ctx, "GET", "c", "nosuchkey"); | ||
| 594 | RedisModule_Call(ctx, "SMEMBERS", "c", "nosuchkey"); | ||
| 595 | |||
| 596 | size_t sz; | ||
| 597 | const char *rep; | ||
| 598 | RedisModuleCallReply *r = RedisModule_Call(ctx, "HGET", "cc", "notifications", "foo"); | ||
| 599 | if (r == NULL || RedisModule_CallReplyType(r) != REDISMODULE_REPLY_STRING) { | ||
| 600 | FAIL("Wrong or no reply for foo"); | ||
| 601 | } else { | ||
| 602 | rep = RedisModule_CallReplyStringPtr(r, &sz); | ||
| 603 | if (sz != 1 || *rep != '2') { | ||
| 604 | FAIL("Got reply '%s'. expected '2'", RedisModule_CallReplyStringPtr(r, NULL)); | ||
| 605 | } | ||
| 606 | } | ||
| 607 | |||
| 608 | r = RedisModule_Call(ctx, "HGET", "cc", "notifications", "bar"); | ||
| 609 | if (r == NULL || RedisModule_CallReplyType(r) != REDISMODULE_REPLY_STRING) { | ||
| 610 | FAIL("Wrong or no reply for bar"); | ||
| 611 | } else { | ||
| 612 | rep = RedisModule_CallReplyStringPtr(r, &sz); | ||
| 613 | if (sz != 1 || *rep != '2') { | ||
| 614 | FAIL("Got reply '%s'. expected '2'", rep); | ||
| 615 | } | ||
| 616 | } | ||
| 617 | |||
| 618 | r = RedisModule_Call(ctx, "HGET", "cc", "notifications", "baz"); | ||
| 619 | if (r == NULL || RedisModule_CallReplyType(r) != REDISMODULE_REPLY_STRING) { | ||
| 620 | FAIL("Wrong or no reply for baz"); | ||
| 621 | } else { | ||
| 622 | rep = RedisModule_CallReplyStringPtr(r, &sz); | ||
| 623 | if (sz != 1 || *rep != '1') { | ||
| 624 | FAIL("Got reply '%.*s'. expected '1'", (int)sz, rep); | ||
| 625 | } | ||
| 626 | } | ||
| 627 | /* For l we expect nothing since we didn't subscribe to list events */ | ||
| 628 | r = RedisModule_Call(ctx, "HGET", "cc", "notifications", "l"); | ||
| 629 | if (r == NULL || RedisModule_CallReplyType(r) != REDISMODULE_REPLY_NULL) { | ||
| 630 | FAIL("Wrong reply for l"); | ||
| 631 | } | ||
| 632 | |||
| 633 | r = RedisModule_Call(ctx, "HGET", "cc", "notifications", "nosuchkey"); | ||
| 634 | if (r == NULL || RedisModule_CallReplyType(r) != REDISMODULE_REPLY_STRING) { | ||
| 635 | FAIL("Wrong or no reply for nosuchkey"); | ||
| 636 | } else { | ||
| 637 | rep = RedisModule_CallReplyStringPtr(r, &sz); | ||
| 638 | if (sz != 1 || *rep != '2') { | ||
| 639 | FAIL("Got reply '%.*s'. expected '2'", (int)sz, rep); | ||
| 640 | } | ||
| 641 | } | ||
| 642 | |||
| 643 | RedisModule_Call(ctx, "FLUSHDB", ""); | ||
| 644 | |||
| 645 | return RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 646 | err: | ||
| 647 | RedisModule_Call(ctx, "FLUSHDB", ""); | ||
| 648 | |||
| 649 | return RedisModule_ReplyWithSimpleString(ctx, "ERR"); | ||
| 650 | } | ||
| 651 | |||
| 652 | /* TEST.CTXFLAGS -- Test GetContextFlags. */ | ||
| 653 | int TestCtxFlags(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 654 | REDISMODULE_NOT_USED(argc); | ||
| 655 | REDISMODULE_NOT_USED(argv); | ||
| 656 | |||
| 657 | RedisModule_AutoMemory(ctx); | ||
| 658 | |||
| 659 | int ok = 1; | ||
| 660 | const char *errString = NULL; | ||
| 661 | #undef FAIL | ||
| 662 | #define FAIL(msg) \ | ||
| 663 | { \ | ||
| 664 | ok = 0; \ | ||
| 665 | errString = msg; \ | ||
| 666 | goto end; \ | ||
| 667 | } | ||
| 668 | |||
| 669 | int flags = RedisModule_GetContextFlags(ctx); | ||
| 670 | if (flags == 0) { | ||
| 671 | FAIL("Got no flags"); | ||
| 672 | } | ||
| 673 | |||
| 674 | if (flags & REDISMODULE_CTX_FLAGS_LUA) FAIL("Lua flag was set"); | ||
| 675 | if (flags & REDISMODULE_CTX_FLAGS_MULTI) FAIL("Multi flag was set"); | ||
| 676 | |||
| 677 | if (flags & REDISMODULE_CTX_FLAGS_AOF) FAIL("AOF Flag was set") | ||
| 678 | /* Enable AOF to test AOF flags */ | ||
| 679 | RedisModule_Call(ctx, "config", "ccc", "set", "appendonly", "yes"); | ||
| 680 | flags = RedisModule_GetContextFlags(ctx); | ||
| 681 | if (!(flags & REDISMODULE_CTX_FLAGS_AOF)) FAIL("AOF Flag not set after config set"); | ||
| 682 | |||
| 683 | /* Disable RDB saving and test the flag. */ | ||
| 684 | RedisModule_Call(ctx, "config", "ccc", "set", "save", ""); | ||
| 685 | flags = RedisModule_GetContextFlags(ctx); | ||
| 686 | if (flags & REDISMODULE_CTX_FLAGS_RDB) FAIL("RDB Flag was set"); | ||
| 687 | /* Enable RDB to test RDB flags */ | ||
| 688 | RedisModule_Call(ctx, "config", "ccc", "set", "save", "900 1"); | ||
| 689 | flags = RedisModule_GetContextFlags(ctx); | ||
| 690 | if (!(flags & REDISMODULE_CTX_FLAGS_RDB)) FAIL("RDB Flag was not set after config set"); | ||
| 691 | |||
| 692 | if (!(flags & REDISMODULE_CTX_FLAGS_MASTER)) FAIL("Master flag was not set"); | ||
| 693 | if (flags & REDISMODULE_CTX_FLAGS_SLAVE) FAIL("Slave flag was set"); | ||
| 694 | if (flags & REDISMODULE_CTX_FLAGS_READONLY) FAIL("Read-only flag was set"); | ||
| 695 | if (flags & REDISMODULE_CTX_FLAGS_CLUSTER) FAIL("Cluster flag was set"); | ||
| 696 | |||
| 697 | /* Disable maxmemory and test the flag. (it is implicitly set in 32bit builds. */ | ||
| 698 | RedisModule_Call(ctx, "config", "ccc", "set", "maxmemory", "0"); | ||
| 699 | flags = RedisModule_GetContextFlags(ctx); | ||
| 700 | if (flags & REDISMODULE_CTX_FLAGS_MAXMEMORY) FAIL("Maxmemory flag was set"); | ||
| 701 | |||
| 702 | /* Enable maxmemory and test the flag. */ | ||
| 703 | RedisModule_Call(ctx, "config", "ccc", "set", "maxmemory", "100000000"); | ||
| 704 | flags = RedisModule_GetContextFlags(ctx); | ||
| 705 | if (!(flags & REDISMODULE_CTX_FLAGS_MAXMEMORY)) | ||
| 706 | FAIL("Maxmemory flag was not set after config set"); | ||
| 707 | |||
| 708 | if (flags & REDISMODULE_CTX_FLAGS_EVICT) FAIL("Eviction flag was set"); | ||
| 709 | RedisModule_Call(ctx, "config", "ccc", "set", "maxmemory-policy", "allkeys-lru"); | ||
| 710 | flags = RedisModule_GetContextFlags(ctx); | ||
| 711 | if (!(flags & REDISMODULE_CTX_FLAGS_EVICT)) FAIL("Eviction flag was not set after config set"); | ||
| 712 | |||
| 713 | end: | ||
| 714 | /* Revert config changes */ | ||
| 715 | RedisModule_Call(ctx, "config", "ccc", "set", "appendonly", "no"); | ||
| 716 | RedisModule_Call(ctx, "config", "ccc", "set", "save", ""); | ||
| 717 | RedisModule_Call(ctx, "config", "ccc", "set", "maxmemory", "0"); | ||
| 718 | RedisModule_Call(ctx, "config", "ccc", "set", "maxmemory-policy", "noeviction"); | ||
| 719 | |||
| 720 | if (!ok) { | ||
| 721 | RedisModule_Log(ctx, "warning", "Failed CTXFLAGS Test. Reason: %s", errString); | ||
| 722 | return RedisModule_ReplyWithSimpleString(ctx, "ERR"); | ||
| 723 | } | ||
| 724 | |||
| 725 | return RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 726 | } | ||
| 727 | |||
| 728 | /* ----------------------------- Test framework ----------------------------- */ | ||
| 729 | |||
| 730 | /* Return 1 if the reply matches the specified string, otherwise log errors | ||
| 731 | * in the server log and return 0. */ | ||
| 732 | int TestAssertErrorReply(RedisModuleCtx *ctx, RedisModuleCallReply *reply, char *str, size_t len) { | ||
| 733 | RedisModuleString *mystr, *expected; | ||
| 734 | if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_ERROR) { | ||
| 735 | return 0; | ||
| 736 | } | ||
| 737 | |||
| 738 | mystr = RedisModule_CreateStringFromCallReply(reply); | ||
| 739 | expected = RedisModule_CreateString(ctx,str,len); | ||
| 740 | if (RedisModule_StringCompare(mystr,expected) != 0) { | ||
| 741 | const char *mystr_ptr = RedisModule_StringPtrLen(mystr,NULL); | ||
| 742 | const char *expected_ptr = RedisModule_StringPtrLen(expected,NULL); | ||
| 743 | RedisModule_Log(ctx,"warning", | ||
| 744 | "Unexpected Error reply reply '%s' (instead of '%s')", | ||
| 745 | mystr_ptr, expected_ptr); | ||
| 746 | return 0; | ||
| 747 | } | ||
| 748 | return 1; | ||
| 749 | } | ||
| 750 | |||
| 751 | int TestAssertStringReply(RedisModuleCtx *ctx, RedisModuleCallReply *reply, char *str, size_t len) { | ||
| 752 | RedisModuleString *mystr, *expected; | ||
| 753 | |||
| 754 | if (RedisModule_CallReplyType(reply) == REDISMODULE_REPLY_ERROR) { | ||
| 755 | RedisModule_Log(ctx,"warning","Test error reply: %s", | ||
| 756 | RedisModule_CallReplyStringPtr(reply, NULL)); | ||
| 757 | return 0; | ||
| 758 | } else if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_STRING) { | ||
| 759 | RedisModule_Log(ctx,"warning","Unexpected reply type %d", | ||
| 760 | RedisModule_CallReplyType(reply)); | ||
| 761 | return 0; | ||
| 762 | } | ||
| 763 | mystr = RedisModule_CreateStringFromCallReply(reply); | ||
| 764 | expected = RedisModule_CreateString(ctx,str,len); | ||
| 765 | if (RedisModule_StringCompare(mystr,expected) != 0) { | ||
| 766 | const char *mystr_ptr = RedisModule_StringPtrLen(mystr,NULL); | ||
| 767 | const char *expected_ptr = RedisModule_StringPtrLen(expected,NULL); | ||
| 768 | RedisModule_Log(ctx,"warning", | ||
| 769 | "Unexpected string reply '%s' (instead of '%s')", | ||
| 770 | mystr_ptr, expected_ptr); | ||
| 771 | return 0; | ||
| 772 | } | ||
| 773 | return 1; | ||
| 774 | } | ||
| 775 | |||
| 776 | /* Return 1 if the reply matches the specified integer, otherwise log errors | ||
| 777 | * in the server log and return 0. */ | ||
| 778 | int TestAssertIntegerReply(RedisModuleCtx *ctx, RedisModuleCallReply *reply, long long expected) { | ||
| 779 | if (RedisModule_CallReplyType(reply) == REDISMODULE_REPLY_ERROR) { | ||
| 780 | RedisModule_Log(ctx,"warning","Test error reply: %s", | ||
| 781 | RedisModule_CallReplyStringPtr(reply, NULL)); | ||
| 782 | return 0; | ||
| 783 | } else if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_INTEGER) { | ||
| 784 | RedisModule_Log(ctx,"warning","Unexpected reply type %d", | ||
| 785 | RedisModule_CallReplyType(reply)); | ||
| 786 | return 0; | ||
| 787 | } | ||
| 788 | long long val = RedisModule_CallReplyInteger(reply); | ||
| 789 | if (val != expected) { | ||
| 790 | RedisModule_Log(ctx,"warning", | ||
| 791 | "Unexpected integer reply '%lld' (instead of '%lld')", | ||
| 792 | val, expected); | ||
| 793 | return 0; | ||
| 794 | } | ||
| 795 | return 1; | ||
| 796 | } | ||
| 797 | |||
| 798 | /* Replies "yes", "no" otherwise if the context may execute debug commands */ | ||
| 799 | int TestCanDebug(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 800 | REDISMODULE_NOT_USED(argv); | ||
| 801 | REDISMODULE_NOT_USED(argc); | ||
| 802 | int flags = RedisModule_GetContextFlags(ctx); | ||
| 803 | int allFlags = RedisModule_GetContextFlagsAll(); | ||
| 804 | if ((allFlags & REDISMODULE_CTX_FLAGS_DEBUG_ENABLED) && | ||
| 805 | (flags & REDISMODULE_CTX_FLAGS_DEBUG_ENABLED)) { | ||
| 806 | RedisModule_ReplyWithSimpleString(ctx, "yes"); | ||
| 807 | } else { | ||
| 808 | RedisModule_ReplyWithSimpleString(ctx, "no"); | ||
| 809 | } | ||
| 810 | return REDISMODULE_OK; | ||
| 811 | } | ||
| 812 | |||
| 813 | #define T(name,...) \ | ||
| 814 | do { \ | ||
| 815 | RedisModule_Log(ctx,"warning","Testing %s", name); \ | ||
| 816 | reply = RedisModule_Call(ctx,name,__VA_ARGS__); \ | ||
| 817 | } while (0) | ||
| 818 | |||
| 819 | /* TEST.BASICS -- Run all the tests. | ||
| 820 | * Note: it is useful to run these tests from the module rather than TCL | ||
| 821 | * since it's easier to check the reply types like that make a distinction | ||
| 822 | * between 0 and "0", etc. */ | ||
| 823 | int TestBasics(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 824 | REDISMODULE_NOT_USED(argv); | ||
| 825 | REDISMODULE_NOT_USED(argc); | ||
| 826 | |||
| 827 | RedisModule_AutoMemory(ctx); | ||
| 828 | RedisModuleCallReply *reply; | ||
| 829 | |||
| 830 | /* Make sure the DB is empty before to proceed. */ | ||
| 831 | T("dbsize",""); | ||
| 832 | if (!TestAssertIntegerReply(ctx,reply,0)) goto fail; | ||
| 833 | |||
| 834 | T("ping",""); | ||
| 835 | if (!TestAssertStringReply(ctx,reply,"PONG",4)) goto fail; | ||
| 836 | |||
| 837 | T("test.call",""); | ||
| 838 | if (!TestAssertStringReply(ctx,reply,"OK",2)) goto fail; | ||
| 839 | |||
| 840 | T("test.callresp3map",""); | ||
| 841 | if (!TestAssertStringReply(ctx,reply,"OK",2)) goto fail; | ||
| 842 | |||
| 843 | T("test.callresp3set",""); | ||
| 844 | if (!TestAssertStringReply(ctx,reply,"OK",2)) goto fail; | ||
| 845 | |||
| 846 | T("test.callresp3double",""); | ||
| 847 | if (!TestAssertStringReply(ctx,reply,"OK",2)) goto fail; | ||
| 848 | |||
| 849 | T("test.callresp3bool",""); | ||
| 850 | if (!TestAssertStringReply(ctx,reply,"OK",2)) goto fail; | ||
| 851 | |||
| 852 | T("test.callresp3null",""); | ||
| 853 | if (!TestAssertStringReply(ctx,reply,"OK",2)) goto fail; | ||
| 854 | |||
| 855 | T("test.callreplywithnestedreply",""); | ||
| 856 | if (!TestAssertStringReply(ctx,reply,"test",4)) goto fail; | ||
| 857 | |||
| 858 | T("test.callreplywithbignumberreply",""); | ||
| 859 | if (!TestAssertStringReply(ctx,reply,"1234567999999999999999999999999999999",37)) goto fail; | ||
| 860 | |||
| 861 | T("test.callreplywithverbatimstringreply",""); | ||
| 862 | if (!TestAssertStringReply(ctx,reply,"txt:This is a verbatim\nstring",29)) goto fail; | ||
| 863 | |||
| 864 | T("test.ctxflags",""); | ||
| 865 | if (!TestAssertStringReply(ctx,reply,"OK",2)) goto fail; | ||
| 866 | |||
| 867 | T("test.string.append",""); | ||
| 868 | if (!TestAssertStringReply(ctx,reply,"foobar",6)) goto fail; | ||
| 869 | |||
| 870 | T("test.string.truncate",""); | ||
| 871 | if (!TestAssertStringReply(ctx,reply,"OK",2)) goto fail; | ||
| 872 | |||
| 873 | T("test.unlink",""); | ||
| 874 | if (!TestAssertStringReply(ctx,reply,"OK",2)) goto fail; | ||
| 875 | |||
| 876 | T("test.nestedcallreplyarray",""); | ||
| 877 | if (!TestAssertStringReply(ctx,reply,"OK",2)) goto fail; | ||
| 878 | |||
| 879 | T("test.string.append.am",""); | ||
| 880 | if (!TestAssertStringReply(ctx,reply,"foobar",6)) goto fail; | ||
| 881 | |||
| 882 | T("test.string.trim",""); | ||
| 883 | if (!TestAssertStringReply(ctx,reply,"OK",2)) goto fail; | ||
| 884 | |||
| 885 | T("test.string.printf", "cc", "foo", "bar"); | ||
| 886 | if (!TestAssertStringReply(ctx,reply,"Got 3 args. argv[1]: foo, argv[2]: bar",38)) goto fail; | ||
| 887 | |||
| 888 | T("test.notify", ""); | ||
| 889 | if (!TestAssertStringReply(ctx,reply,"OK",2)) goto fail; | ||
| 890 | |||
| 891 | T("test.callreplywitharrayreply", ""); | ||
| 892 | if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_ARRAY) goto fail; | ||
| 893 | if (RedisModule_CallReplyLength(reply) != 2) goto fail; | ||
| 894 | if (!TestAssertStringReply(ctx,RedisModule_CallReplyArrayElement(reply, 0),"test",4)) goto fail; | ||
| 895 | if (!TestAssertStringReply(ctx,RedisModule_CallReplyArrayElement(reply, 1),"1234",4)) goto fail; | ||
| 896 | |||
| 897 | T("foo", "E"); | ||
| 898 | if (!TestAssertErrorReply(ctx,reply,"ERR unknown command 'foo'",25)) goto fail; | ||
| 899 | |||
| 900 | T("set", "Ec", "x"); | ||
| 901 | if (!TestAssertErrorReply(ctx,reply,"ERR wrong number of arguments for 'set' command",47)) goto fail; | ||
| 902 | |||
| 903 | T("shutdown", "SE"); | ||
| 904 | if (!TestAssertErrorReply(ctx,reply,"ERR command 'shutdown' is not allowed on script mode",52)) goto fail; | ||
| 905 | |||
| 906 | T("set", "WEcc", "x", "1"); | ||
| 907 | if (!TestAssertErrorReply(ctx,reply,"ERR Write command 'set' was called while write is not allowed.",62)) goto fail; | ||
| 908 | |||
| 909 | RedisModule_ReplyWithSimpleString(ctx,"ALL TESTS PASSED"); | ||
| 910 | return REDISMODULE_OK; | ||
| 911 | |||
| 912 | fail: | ||
| 913 | RedisModule_ReplyWithSimpleString(ctx, | ||
| 914 | "SOME TEST DID NOT PASS! Check server logs"); | ||
| 915 | return REDISMODULE_OK; | ||
| 916 | } | ||
| 917 | |||
| 918 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 919 | REDISMODULE_NOT_USED(argv); | ||
| 920 | REDISMODULE_NOT_USED(argc); | ||
| 921 | |||
| 922 | if (RedisModule_Init(ctx,"test",1,REDISMODULE_APIVER_1) | ||
| 923 | == REDISMODULE_ERR) return REDISMODULE_ERR; | ||
| 924 | |||
| 925 | /* Perform RM_Call inside the RedisModule_OnLoad | ||
| 926 | * to verify that it works as expected without crashing. | ||
| 927 | * The tests will verify it on different configurations | ||
| 928 | * options (cluster/no cluster). A simple ping command | ||
| 929 | * is enough for this test. */ | ||
| 930 | RedisModuleCallReply *reply = RedisModule_Call(ctx, "ping", ""); | ||
| 931 | if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_STRING) { | ||
| 932 | RedisModule_FreeCallReply(reply); | ||
| 933 | return REDISMODULE_ERR; | ||
| 934 | } | ||
| 935 | size_t len; | ||
| 936 | const char *reply_str = RedisModule_CallReplyStringPtr(reply, &len); | ||
| 937 | if (len != 4) { | ||
| 938 | RedisModule_FreeCallReply(reply); | ||
| 939 | return REDISMODULE_ERR; | ||
| 940 | } | ||
| 941 | if (memcmp(reply_str, "PONG", 4) != 0) { | ||
| 942 | RedisModule_FreeCallReply(reply); | ||
| 943 | return REDISMODULE_ERR; | ||
| 944 | } | ||
| 945 | RedisModule_FreeCallReply(reply); | ||
| 946 | |||
| 947 | if (RedisModule_CreateCommand(ctx,"test.call", | ||
| 948 | TestCall,"write deny-oom",1,1,1) == REDISMODULE_ERR) | ||
| 949 | return REDISMODULE_ERR; | ||
| 950 | |||
| 951 | if (RedisModule_CreateCommand(ctx,"test.callresp3map", | ||
| 952 | TestCallResp3Map,"write deny-oom",1,1,1) == REDISMODULE_ERR) | ||
| 953 | return REDISMODULE_ERR; | ||
| 954 | |||
| 955 | if (RedisModule_CreateCommand(ctx,"test.callresp3attribute", | ||
| 956 | TestCallResp3Attribute,"write deny-oom",1,1,1) == REDISMODULE_ERR) | ||
| 957 | return REDISMODULE_ERR; | ||
| 958 | |||
| 959 | if (RedisModule_CreateCommand(ctx,"test.callresp3set", | ||
| 960 | TestCallResp3Set,"write deny-oom",1,1,1) == REDISMODULE_ERR) | ||
| 961 | return REDISMODULE_ERR; | ||
| 962 | |||
| 963 | if (RedisModule_CreateCommand(ctx,"test.callresp3double", | ||
| 964 | TestCallResp3Double,"write deny-oom",1,1,1) == REDISMODULE_ERR) | ||
| 965 | return REDISMODULE_ERR; | ||
| 966 | |||
| 967 | if (RedisModule_CreateCommand(ctx,"test.callresp3bool", | ||
| 968 | TestCallResp3Bool,"write deny-oom",1,1,1) == REDISMODULE_ERR) | ||
| 969 | return REDISMODULE_ERR; | ||
| 970 | |||
| 971 | if (RedisModule_CreateCommand(ctx,"test.callresp3null", | ||
| 972 | TestCallResp3Null,"write deny-oom",1,1,1) == REDISMODULE_ERR) | ||
| 973 | return REDISMODULE_ERR; | ||
| 974 | |||
| 975 | if (RedisModule_CreateCommand(ctx,"test.callreplywitharrayreply", | ||
| 976 | TestCallReplyWithArrayReply,"write deny-oom",1,1,1) == REDISMODULE_ERR) | ||
| 977 | return REDISMODULE_ERR; | ||
| 978 | |||
| 979 | if (RedisModule_CreateCommand(ctx,"test.callreplywithnestedreply", | ||
| 980 | TestCallReplyWithNestedReply,"write deny-oom",1,1,1) == REDISMODULE_ERR) | ||
| 981 | return REDISMODULE_ERR; | ||
| 982 | |||
| 983 | if (RedisModule_CreateCommand(ctx,"test.callreplywithbignumberreply", | ||
| 984 | TestCallResp3BigNumber,"write deny-oom",1,1,1) == REDISMODULE_ERR) | ||
| 985 | return REDISMODULE_ERR; | ||
| 986 | |||
| 987 | if (RedisModule_CreateCommand(ctx,"test.callreplywithverbatimstringreply", | ||
| 988 | TestCallResp3Verbatim,"write deny-oom",1,1,1) == REDISMODULE_ERR) | ||
| 989 | return REDISMODULE_ERR; | ||
| 990 | |||
| 991 | if (RedisModule_CreateCommand(ctx,"test.string.append", | ||
| 992 | TestStringAppend,"write deny-oom",1,1,1) == REDISMODULE_ERR) | ||
| 993 | return REDISMODULE_ERR; | ||
| 994 | |||
| 995 | if (RedisModule_CreateCommand(ctx,"test.string.trim", | ||
| 996 | TestTrimString,"write deny-oom",1,1,1) == REDISMODULE_ERR) | ||
| 997 | return REDISMODULE_ERR; | ||
| 998 | |||
| 999 | if (RedisModule_CreateCommand(ctx,"test.string.append.am", | ||
| 1000 | TestStringAppendAM,"write deny-oom",1,1,1) == REDISMODULE_ERR) | ||
| 1001 | return REDISMODULE_ERR; | ||
| 1002 | |||
| 1003 | if (RedisModule_CreateCommand(ctx,"test.string.truncate", | ||
| 1004 | TestStringTruncate,"write deny-oom",1,1,1) == REDISMODULE_ERR) | ||
| 1005 | return REDISMODULE_ERR; | ||
| 1006 | |||
| 1007 | if (RedisModule_CreateCommand(ctx,"test.string.printf", | ||
| 1008 | TestStringPrintf,"write deny-oom",1,1,1) == REDISMODULE_ERR) | ||
| 1009 | return REDISMODULE_ERR; | ||
| 1010 | |||
| 1011 | if (RedisModule_CreateCommand(ctx,"test.ctxflags", | ||
| 1012 | TestCtxFlags,"readonly",1,1,1) == REDISMODULE_ERR) | ||
| 1013 | return REDISMODULE_ERR; | ||
| 1014 | |||
| 1015 | if (RedisModule_CreateCommand(ctx,"test.unlink", | ||
| 1016 | TestUnlink,"write deny-oom",1,1,1) == REDISMODULE_ERR) | ||
| 1017 | return REDISMODULE_ERR; | ||
| 1018 | |||
| 1019 | if (RedisModule_CreateCommand(ctx,"test.nestedcallreplyarray", | ||
| 1020 | TestNestedCallReplyArrayElement,"write deny-oom",1,1,1) == REDISMODULE_ERR) | ||
| 1021 | return REDISMODULE_ERR; | ||
| 1022 | |||
| 1023 | if (RedisModule_CreateCommand(ctx,"test.basics", | ||
| 1024 | TestBasics,"write",1,1,1) == REDISMODULE_ERR) | ||
| 1025 | return REDISMODULE_ERR; | ||
| 1026 | |||
| 1027 | /* the following commands are used by an external test and should not be added to TestBasics */ | ||
| 1028 | if (RedisModule_CreateCommand(ctx,"test.rmcallautomode", | ||
| 1029 | TestCallRespAutoMode,"write",1,1,1) == REDISMODULE_ERR) | ||
| 1030 | return REDISMODULE_ERR; | ||
| 1031 | |||
| 1032 | if (RedisModule_CreateCommand(ctx,"test.getresp", | ||
| 1033 | TestGetResp,"readonly",1,1,1) == REDISMODULE_ERR) | ||
| 1034 | return REDISMODULE_ERR; | ||
| 1035 | |||
| 1036 | if (RedisModule_CreateCommand(ctx,"test.candebug", | ||
| 1037 | TestCanDebug,"readonly",1,1,1) == REDISMODULE_ERR) | ||
| 1038 | return REDISMODULE_ERR; | ||
| 1039 | |||
| 1040 | RedisModule_SubscribeToKeyspaceEvents(ctx, | ||
| 1041 | REDISMODULE_NOTIFY_HASH | | ||
| 1042 | REDISMODULE_NOTIFY_SET | | ||
| 1043 | REDISMODULE_NOTIFY_STRING | | ||
| 1044 | REDISMODULE_NOTIFY_KEY_MISS, | ||
| 1045 | NotifyCallback); | ||
| 1046 | if (RedisModule_CreateCommand(ctx,"test.notify", | ||
| 1047 | TestNotifications,"write deny-oom",1,1,1) == REDISMODULE_ERR) | ||
| 1048 | return REDISMODULE_ERR; | ||
| 1049 | |||
| 1050 | return REDISMODULE_OK; | ||
| 1051 | } | ||
diff --git a/examples/redis-unstable/tests/modules/blockedclient.c b/examples/redis-unstable/tests/modules/blockedclient.c new file mode 100644 index 0000000..dc226ee --- /dev/null +++ b/examples/redis-unstable/tests/modules/blockedclient.c | |||
| @@ -0,0 +1,723 @@ | |||
| 1 | /* define macros for having usleep */ | ||
| 2 | #define _BSD_SOURCE | ||
| 3 | #define _DEFAULT_SOURCE | ||
| 4 | #include <unistd.h> | ||
| 5 | |||
| 6 | #include "redismodule.h" | ||
| 7 | #include <assert.h> | ||
| 8 | #include <stdio.h> | ||
| 9 | #include <pthread.h> | ||
| 10 | #include <strings.h> | ||
| 11 | |||
| 12 | #define UNUSED(V) ((void) V) | ||
| 13 | |||
| 14 | /* used to test processing events during slow bg operation */ | ||
| 15 | static volatile int g_slow_bg_operation = 0; | ||
| 16 | static volatile int g_is_in_slow_bg_operation = 0; | ||
| 17 | |||
| 18 | void *sub_worker(void *arg) { | ||
| 19 | // Get Redis module context | ||
| 20 | RedisModuleCtx *ctx = (RedisModuleCtx *)arg; | ||
| 21 | |||
| 22 | // Try acquiring GIL | ||
| 23 | int res = RedisModule_ThreadSafeContextTryLock(ctx); | ||
| 24 | |||
| 25 | // GIL is already taken by the calling thread expecting to fail. | ||
| 26 | assert(res != REDISMODULE_OK); | ||
| 27 | |||
| 28 | return NULL; | ||
| 29 | } | ||
| 30 | |||
| 31 | void *worker(void *arg) { | ||
| 32 | // Retrieve blocked client | ||
| 33 | RedisModuleBlockedClient *bc = (RedisModuleBlockedClient *)arg; | ||
| 34 | |||
| 35 | // Get Redis module context | ||
| 36 | RedisModuleCtx *ctx = RedisModule_GetThreadSafeContext(bc); | ||
| 37 | |||
| 38 | // Acquire GIL | ||
| 39 | RedisModule_ThreadSafeContextLock(ctx); | ||
| 40 | |||
| 41 | // Create another thread which will try to acquire the GIL | ||
| 42 | pthread_t tid; | ||
| 43 | int res = pthread_create(&tid, NULL, sub_worker, ctx); | ||
| 44 | assert(res == 0); | ||
| 45 | |||
| 46 | // Wait for thread | ||
| 47 | pthread_join(tid, NULL); | ||
| 48 | |||
| 49 | // Release GIL | ||
| 50 | RedisModule_ThreadSafeContextUnlock(ctx); | ||
| 51 | |||
| 52 | // Reply to client | ||
| 53 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 54 | |||
| 55 | // Unblock client | ||
| 56 | RedisModule_UnblockClient(bc, NULL); | ||
| 57 | |||
| 58 | // Free the Redis module context | ||
| 59 | RedisModule_FreeThreadSafeContext(ctx); | ||
| 60 | |||
| 61 | return NULL; | ||
| 62 | } | ||
| 63 | |||
| 64 | int acquire_gil(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 65 | { | ||
| 66 | UNUSED(argv); | ||
| 67 | UNUSED(argc); | ||
| 68 | |||
| 69 | int flags = RedisModule_GetContextFlags(ctx); | ||
| 70 | int allFlags = RedisModule_GetContextFlagsAll(); | ||
| 71 | if ((allFlags & REDISMODULE_CTX_FLAGS_MULTI) && | ||
| 72 | (flags & REDISMODULE_CTX_FLAGS_MULTI)) { | ||
| 73 | RedisModule_ReplyWithSimpleString(ctx, "Blocked client is not supported inside multi"); | ||
| 74 | return REDISMODULE_OK; | ||
| 75 | } | ||
| 76 | |||
| 77 | if ((allFlags & REDISMODULE_CTX_FLAGS_DENY_BLOCKING) && | ||
| 78 | (flags & REDISMODULE_CTX_FLAGS_DENY_BLOCKING)) { | ||
| 79 | RedisModule_ReplyWithSimpleString(ctx, "Blocked client is not allowed"); | ||
| 80 | return REDISMODULE_OK; | ||
| 81 | } | ||
| 82 | |||
| 83 | /* This command handler tries to acquire the GIL twice | ||
| 84 | * once in the worker thread using "RedisModule_ThreadSafeContextLock" | ||
| 85 | * second in the sub-worker thread | ||
| 86 | * using "RedisModule_ThreadSafeContextTryLock" | ||
| 87 | * as the GIL is already locked. */ | ||
| 88 | RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx, NULL, NULL, NULL, 0); | ||
| 89 | |||
| 90 | pthread_t tid; | ||
| 91 | int res = pthread_create(&tid, NULL, worker, bc); | ||
| 92 | assert(res == 0); | ||
| 93 | pthread_detach(tid); | ||
| 94 | |||
| 95 | return REDISMODULE_OK; | ||
| 96 | } | ||
| 97 | |||
| 98 | typedef struct { | ||
| 99 | RedisModuleString **argv; | ||
| 100 | int argc; | ||
| 101 | RedisModuleBlockedClient *bc; | ||
| 102 | } bg_call_data; | ||
| 103 | |||
| 104 | void *bg_call_worker(void *arg) { | ||
| 105 | bg_call_data *bg = arg; | ||
| 106 | RedisModuleBlockedClient *bc = bg->bc; | ||
| 107 | |||
| 108 | // Get Redis module context | ||
| 109 | RedisModuleCtx *ctx = RedisModule_GetThreadSafeContext(bg->bc); | ||
| 110 | |||
| 111 | // Acquire GIL | ||
| 112 | RedisModule_ThreadSafeContextLock(ctx); | ||
| 113 | |||
| 114 | // Test slow operation yielding | ||
| 115 | if (g_slow_bg_operation) { | ||
| 116 | g_is_in_slow_bg_operation = 1; | ||
| 117 | while (g_slow_bg_operation) { | ||
| 118 | RedisModule_Yield(ctx, REDISMODULE_YIELD_FLAG_CLIENTS, "Slow module operation"); | ||
| 119 | usleep(1000); | ||
| 120 | } | ||
| 121 | g_is_in_slow_bg_operation = 0; | ||
| 122 | } | ||
| 123 | |||
| 124 | // Call the command | ||
| 125 | const char *module_cmd = RedisModule_StringPtrLen(bg->argv[0], NULL); | ||
| 126 | int cmd_pos = 1; | ||
| 127 | RedisModuleString *format_redis_str = RedisModule_CreateString(NULL, "v", 1); | ||
| 128 | if (!strcasecmp(module_cmd, "do_bg_rm_call_format")) { | ||
| 129 | cmd_pos = 2; | ||
| 130 | size_t format_len; | ||
| 131 | const char *format = RedisModule_StringPtrLen(bg->argv[1], &format_len); | ||
| 132 | RedisModule_StringAppendBuffer(NULL, format_redis_str, format, format_len); | ||
| 133 | RedisModule_StringAppendBuffer(NULL, format_redis_str, "E", 1); | ||
| 134 | } | ||
| 135 | const char *format = RedisModule_StringPtrLen(format_redis_str, NULL); | ||
| 136 | const char *cmd = RedisModule_StringPtrLen(bg->argv[cmd_pos], NULL); | ||
| 137 | RedisModuleCallReply *rep = RedisModule_Call(ctx, cmd, format, bg->argv + cmd_pos + 1, (size_t)bg->argc - cmd_pos - 1); | ||
| 138 | RedisModule_FreeString(NULL, format_redis_str); | ||
| 139 | |||
| 140 | /* Free the arguments within GIL to prevent simultaneous freeing in main thread. */ | ||
| 141 | for (int i=0; i<bg->argc; i++) | ||
| 142 | RedisModule_FreeString(ctx, bg->argv[i]); | ||
| 143 | RedisModule_Free(bg->argv); | ||
| 144 | RedisModule_Free(bg); | ||
| 145 | |||
| 146 | // Release GIL | ||
| 147 | RedisModule_ThreadSafeContextUnlock(ctx); | ||
| 148 | |||
| 149 | // Reply to client | ||
| 150 | if (!rep) { | ||
| 151 | RedisModule_ReplyWithError(ctx, "NULL reply returned"); | ||
| 152 | } else { | ||
| 153 | RedisModule_ReplyWithCallReply(ctx, rep); | ||
| 154 | RedisModule_FreeCallReply(rep); | ||
| 155 | } | ||
| 156 | |||
| 157 | // Unblock client | ||
| 158 | RedisModule_UnblockClient(bc, NULL); | ||
| 159 | |||
| 160 | // Free the Redis module context | ||
| 161 | RedisModule_FreeThreadSafeContext(ctx); | ||
| 162 | |||
| 163 | return NULL; | ||
| 164 | } | ||
| 165 | |||
| 166 | int do_bg_rm_call(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 167 | { | ||
| 168 | UNUSED(argv); | ||
| 169 | UNUSED(argc); | ||
| 170 | |||
| 171 | /* Make sure we're not trying to block a client when we shouldn't */ | ||
| 172 | int flags = RedisModule_GetContextFlags(ctx); | ||
| 173 | int allFlags = RedisModule_GetContextFlagsAll(); | ||
| 174 | if ((allFlags & REDISMODULE_CTX_FLAGS_MULTI) && | ||
| 175 | (flags & REDISMODULE_CTX_FLAGS_MULTI)) { | ||
| 176 | RedisModule_ReplyWithSimpleString(ctx, "Blocked client is not supported inside multi"); | ||
| 177 | return REDISMODULE_OK; | ||
| 178 | } | ||
| 179 | if ((allFlags & REDISMODULE_CTX_FLAGS_DENY_BLOCKING) && | ||
| 180 | (flags & REDISMODULE_CTX_FLAGS_DENY_BLOCKING)) { | ||
| 181 | RedisModule_ReplyWithSimpleString(ctx, "Blocked client is not allowed"); | ||
| 182 | return REDISMODULE_OK; | ||
| 183 | } | ||
| 184 | |||
| 185 | /* Make a copy of the arguments and pass them to the thread. */ | ||
| 186 | bg_call_data *bg = RedisModule_Alloc(sizeof(bg_call_data)); | ||
| 187 | bg->argv = RedisModule_Alloc(sizeof(RedisModuleString*)*argc); | ||
| 188 | bg->argc = argc; | ||
| 189 | for (int i=0; i<argc; i++) | ||
| 190 | bg->argv[i] = RedisModule_HoldString(ctx, argv[i]); | ||
| 191 | |||
| 192 | /* Block the client */ | ||
| 193 | bg->bc = RedisModule_BlockClient(ctx, NULL, NULL, NULL, 0); | ||
| 194 | |||
| 195 | /* Start a thread to handle the request */ | ||
| 196 | pthread_t tid; | ||
| 197 | int res = pthread_create(&tid, NULL, bg_call_worker, bg); | ||
| 198 | assert(res == 0); | ||
| 199 | pthread_detach(tid); | ||
| 200 | |||
| 201 | return REDISMODULE_OK; | ||
| 202 | } | ||
| 203 | |||
| 204 | int do_rm_call(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){ | ||
| 205 | UNUSED(argv); | ||
| 206 | UNUSED(argc); | ||
| 207 | |||
| 208 | if(argc < 2){ | ||
| 209 | return RedisModule_WrongArity(ctx); | ||
| 210 | } | ||
| 211 | |||
| 212 | const char* cmd = RedisModule_StringPtrLen(argv[1], NULL); | ||
| 213 | |||
| 214 | RedisModuleCallReply* rep = RedisModule_Call(ctx, cmd, "Ev", argv + 2, (size_t)argc - 2); | ||
| 215 | if(!rep){ | ||
| 216 | RedisModule_ReplyWithError(ctx, "NULL reply returned"); | ||
| 217 | }else{ | ||
| 218 | RedisModule_ReplyWithCallReply(ctx, rep); | ||
| 219 | RedisModule_FreeCallReply(rep); | ||
| 220 | } | ||
| 221 | |||
| 222 | return REDISMODULE_OK; | ||
| 223 | } | ||
| 224 | |||
| 225 | static void rm_call_async_send_reply(RedisModuleCtx *ctx, RedisModuleCallReply *reply) { | ||
| 226 | RedisModule_ReplyWithCallReply(ctx, reply); | ||
| 227 | RedisModule_FreeCallReply(reply); | ||
| 228 | } | ||
| 229 | |||
| 230 | /* Called when the command that was blocked on 'RM_Call' gets unblocked | ||
| 231 | * and send the reply to the blocked client. */ | ||
| 232 | static void rm_call_async_on_unblocked(RedisModuleCtx *ctx, RedisModuleCallReply *reply, void *private_data) { | ||
| 233 | UNUSED(ctx); | ||
| 234 | RedisModuleBlockedClient *bc = private_data; | ||
| 235 | RedisModuleCtx *bctx = RedisModule_GetThreadSafeContext(bc); | ||
| 236 | rm_call_async_send_reply(bctx, reply); | ||
| 237 | RedisModule_FreeThreadSafeContext(bctx); | ||
| 238 | RedisModule_UnblockClient(bc, RedisModule_BlockClientGetPrivateData(bc)); | ||
| 239 | } | ||
| 240 | |||
| 241 | int do_rm_call_async_fire_and_forget(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){ | ||
| 242 | UNUSED(argv); | ||
| 243 | UNUSED(argc); | ||
| 244 | |||
| 245 | if(argc < 2){ | ||
| 246 | return RedisModule_WrongArity(ctx); | ||
| 247 | } | ||
| 248 | const char* cmd = RedisModule_StringPtrLen(argv[1], NULL); | ||
| 249 | |||
| 250 | RedisModuleCallReply* rep = RedisModule_Call(ctx, cmd, "!KEv", argv + 2, (size_t)argc - 2); | ||
| 251 | |||
| 252 | if(RedisModule_CallReplyType(rep) != REDISMODULE_REPLY_PROMISE) { | ||
| 253 | RedisModule_ReplyWithCallReply(ctx, rep); | ||
| 254 | } else { | ||
| 255 | RedisModule_ReplyWithSimpleString(ctx, "Blocked"); | ||
| 256 | } | ||
| 257 | RedisModule_FreeCallReply(rep); | ||
| 258 | |||
| 259 | return REDISMODULE_OK; | ||
| 260 | } | ||
| 261 | |||
| 262 | static void do_rm_call_async_free_pd(RedisModuleCtx * ctx, void *pd) { | ||
| 263 | UNUSED(ctx); | ||
| 264 | RedisModule_FreeCallReply(pd); | ||
| 265 | } | ||
| 266 | |||
| 267 | static void do_rm_call_async_disconnect(RedisModuleCtx *ctx, struct RedisModuleBlockedClient *bc) { | ||
| 268 | UNUSED(ctx); | ||
| 269 | RedisModuleCallReply* rep = RedisModule_BlockClientGetPrivateData(bc); | ||
| 270 | RedisModule_CallReplyPromiseAbort(rep, NULL); | ||
| 271 | RedisModule_FreeCallReply(rep); | ||
| 272 | RedisModule_AbortBlock(bc); | ||
| 273 | } | ||
| 274 | |||
| 275 | /* | ||
| 276 | * Callback for do_rm_call_async / do_rm_call_async_script_mode | ||
| 277 | * Gets the command to invoke as the first argument to the command and runs it, | ||
| 278 | * passing the rest of the arguments to the command invocation. | ||
| 279 | * If the command got blocked, blocks the client and unblock it when the command gets unblocked, | ||
| 280 | * this allows check the K (allow blocking) argument to RM_Call. | ||
| 281 | */ | ||
| 282 | int do_rm_call_async(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){ | ||
| 283 | UNUSED(argv); | ||
| 284 | UNUSED(argc); | ||
| 285 | |||
| 286 | if(argc < 2){ | ||
| 287 | return RedisModule_WrongArity(ctx); | ||
| 288 | } | ||
| 289 | |||
| 290 | size_t format_len = 0; | ||
| 291 | char format[6] = {0}; | ||
| 292 | |||
| 293 | if (!(RedisModule_GetContextFlags(ctx) & REDISMODULE_CTX_FLAGS_DENY_BLOCKING)) { | ||
| 294 | /* We are allowed to block the client so we can allow RM_Call to also block us */ | ||
| 295 | format[format_len++] = 'K'; | ||
| 296 | } | ||
| 297 | |||
| 298 | const char* invoked_cmd = RedisModule_StringPtrLen(argv[0], NULL); | ||
| 299 | if (strcasecmp(invoked_cmd, "do_rm_call_async_script_mode") == 0) { | ||
| 300 | format[format_len++] = 'S'; | ||
| 301 | } | ||
| 302 | |||
| 303 | format[format_len++] = 'E'; | ||
| 304 | format[format_len++] = 'v'; | ||
| 305 | if (strcasecmp(invoked_cmd, "do_rm_call_async_no_replicate") != 0) { | ||
| 306 | /* Notice, without the '!' flag we will have inconsistency between master and replica. | ||
| 307 | * This is used only to check '!' flag correctness on blocked commands. */ | ||
| 308 | format[format_len++] = '!'; | ||
| 309 | } | ||
| 310 | |||
| 311 | const char* cmd = RedisModule_StringPtrLen(argv[1], NULL); | ||
| 312 | |||
| 313 | RedisModuleCallReply* rep = RedisModule_Call(ctx, cmd, format, argv + 2, (size_t)argc - 2); | ||
| 314 | |||
| 315 | if(RedisModule_CallReplyType(rep) != REDISMODULE_REPLY_PROMISE) { | ||
| 316 | rm_call_async_send_reply(ctx, rep); | ||
| 317 | } else { | ||
| 318 | RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx, NULL, NULL, do_rm_call_async_free_pd, 0); | ||
| 319 | RedisModule_SetDisconnectCallback(bc, do_rm_call_async_disconnect); | ||
| 320 | RedisModule_BlockClientSetPrivateData(bc, rep); | ||
| 321 | RedisModule_CallReplyPromiseSetUnblockHandler(rep, rm_call_async_on_unblocked, bc); | ||
| 322 | } | ||
| 323 | |||
| 324 | return REDISMODULE_OK; | ||
| 325 | } | ||
| 326 | |||
| 327 | typedef struct ThreadedAsyncRMCallCtx{ | ||
| 328 | RedisModuleBlockedClient *bc; | ||
| 329 | RedisModuleCallReply *reply; | ||
| 330 | } ThreadedAsyncRMCallCtx; | ||
| 331 | |||
| 332 | void *send_async_reply(void *arg) { | ||
| 333 | ThreadedAsyncRMCallCtx *ta_rm_call_ctx = arg; | ||
| 334 | rm_call_async_on_unblocked(NULL, ta_rm_call_ctx->reply, ta_rm_call_ctx->bc); | ||
| 335 | RedisModule_Free(ta_rm_call_ctx); | ||
| 336 | return NULL; | ||
| 337 | } | ||
| 338 | |||
| 339 | /* Called when the command that was blocked on 'RM_Call' gets unblocked | ||
| 340 | * and schedule a thread to send the reply to the blocked client. */ | ||
| 341 | static void rm_call_async_reply_on_thread(RedisModuleCtx *ctx, RedisModuleCallReply *reply, void *private_data) { | ||
| 342 | UNUSED(ctx); | ||
| 343 | ThreadedAsyncRMCallCtx *ta_rm_call_ctx = RedisModule_Alloc(sizeof(*ta_rm_call_ctx)); | ||
| 344 | ta_rm_call_ctx->bc = private_data; | ||
| 345 | ta_rm_call_ctx->reply = reply; | ||
| 346 | pthread_t tid; | ||
| 347 | int res = pthread_create(&tid, NULL, send_async_reply, ta_rm_call_ctx); | ||
| 348 | assert(res == 0); | ||
| 349 | pthread_detach(tid); | ||
| 350 | } | ||
| 351 | |||
| 352 | /* | ||
| 353 | * Callback for do_rm_call_async_on_thread. | ||
| 354 | * Gets the command to invoke as the first argument to the command and runs it, | ||
| 355 | * passing the rest of the arguments to the command invocation. | ||
| 356 | * If the command got blocked, blocks the client and unblock on a background thread. | ||
| 357 | * this allows check the K (allow blocking) argument to RM_Call, and make sure that the reply | ||
| 358 | * that passes to unblock handler is owned by the handler and are not attached to any | ||
| 359 | * context that might be freed after the callback ends. | ||
| 360 | */ | ||
| 361 | int do_rm_call_async_on_thread(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){ | ||
| 362 | UNUSED(argv); | ||
| 363 | UNUSED(argc); | ||
| 364 | |||
| 365 | if(argc < 2){ | ||
| 366 | return RedisModule_WrongArity(ctx); | ||
| 367 | } | ||
| 368 | |||
| 369 | const char* cmd = RedisModule_StringPtrLen(argv[1], NULL); | ||
| 370 | |||
| 371 | RedisModuleCallReply* rep = RedisModule_Call(ctx, cmd, "KEv", argv + 2, (size_t)argc - 2); | ||
| 372 | |||
| 373 | if(RedisModule_CallReplyType(rep) != REDISMODULE_REPLY_PROMISE) { | ||
| 374 | rm_call_async_send_reply(ctx, rep); | ||
| 375 | } else { | ||
| 376 | RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx, NULL, NULL, NULL, 0); | ||
| 377 | RedisModule_CallReplyPromiseSetUnblockHandler(rep, rm_call_async_reply_on_thread, bc); | ||
| 378 | RedisModule_FreeCallReply(rep); | ||
| 379 | } | ||
| 380 | |||
| 381 | return REDISMODULE_OK; | ||
| 382 | } | ||
| 383 | |||
| 384 | /* Private data for wait_and_do_rm_call_async that holds information about: | ||
| 385 | * 1. the block client, to unblock when done. | ||
| 386 | * 2. the arguments, contains the command to run using RM_Call */ | ||
| 387 | typedef struct WaitAndDoRMCallCtx { | ||
| 388 | RedisModuleBlockedClient *bc; | ||
| 389 | RedisModuleString **argv; | ||
| 390 | int argc; | ||
| 391 | } WaitAndDoRMCallCtx; | ||
| 392 | |||
| 393 | /* | ||
| 394 | * This callback will be called when the 'wait' command invoke on 'wait_and_do_rm_call_async' will finish. | ||
| 395 | * This callback will continue the execution flow just like 'do_rm_call_async' command. | ||
| 396 | */ | ||
| 397 | static void wait_and_do_rm_call_async_on_unblocked(RedisModuleCtx *ctx, RedisModuleCallReply *reply, void *private_data) { | ||
| 398 | WaitAndDoRMCallCtx *wctx = private_data; | ||
| 399 | if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_INTEGER) { | ||
| 400 | goto done; | ||
| 401 | } | ||
| 402 | |||
| 403 | if (RedisModule_CallReplyInteger(reply) != 1) { | ||
| 404 | goto done; | ||
| 405 | } | ||
| 406 | |||
| 407 | RedisModule_FreeCallReply(reply); | ||
| 408 | reply = NULL; | ||
| 409 | |||
| 410 | const char* cmd = RedisModule_StringPtrLen(wctx->argv[0], NULL); | ||
| 411 | reply = RedisModule_Call(ctx, cmd, "!EKv", wctx->argv + 1, (size_t)wctx->argc - 1); | ||
| 412 | |||
| 413 | done: | ||
| 414 | if(RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_PROMISE) { | ||
| 415 | RedisModuleCtx *bctx = RedisModule_GetThreadSafeContext(wctx->bc); | ||
| 416 | rm_call_async_send_reply(bctx, reply); | ||
| 417 | RedisModule_FreeThreadSafeContext(bctx); | ||
| 418 | RedisModule_UnblockClient(wctx->bc, NULL); | ||
| 419 | } else { | ||
| 420 | RedisModule_CallReplyPromiseSetUnblockHandler(reply, rm_call_async_on_unblocked, wctx->bc); | ||
| 421 | RedisModule_FreeCallReply(reply); | ||
| 422 | } | ||
| 423 | for (int i = 0 ; i < wctx->argc ; ++i) { | ||
| 424 | RedisModule_FreeString(NULL, wctx->argv[i]); | ||
| 425 | } | ||
| 426 | RedisModule_Free(wctx->argv); | ||
| 427 | RedisModule_Free(wctx); | ||
| 428 | } | ||
| 429 | |||
| 430 | /* | ||
| 431 | * Callback for wait_and_do_rm_call | ||
| 432 | * Gets the command to invoke as the first argument, runs 'wait' | ||
| 433 | * command (using the K flag to RM_Call). Once the wait finished, runs the | ||
| 434 | * command that was given (just like 'do_rm_call_async'). | ||
| 435 | */ | ||
| 436 | int wait_and_do_rm_call_async(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 437 | UNUSED(argv); | ||
| 438 | UNUSED(argc); | ||
| 439 | |||
| 440 | if(argc < 2){ | ||
| 441 | return RedisModule_WrongArity(ctx); | ||
| 442 | } | ||
| 443 | |||
| 444 | int flags = RedisModule_GetContextFlags(ctx); | ||
| 445 | if (flags & REDISMODULE_CTX_FLAGS_DENY_BLOCKING) { | ||
| 446 | return RedisModule_ReplyWithError(ctx, "Err can not run wait, blocking is not allowed."); | ||
| 447 | } | ||
| 448 | |||
| 449 | RedisModuleCallReply* rep = RedisModule_Call(ctx, "wait", "!EKcc", "1", "0"); | ||
| 450 | if(RedisModule_CallReplyType(rep) != REDISMODULE_REPLY_PROMISE) { | ||
| 451 | rm_call_async_send_reply(ctx, rep); | ||
| 452 | } else { | ||
| 453 | RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx, NULL, NULL, NULL, 0); | ||
| 454 | WaitAndDoRMCallCtx *wctx = RedisModule_Alloc(sizeof(*wctx)); | ||
| 455 | *wctx = (WaitAndDoRMCallCtx){ | ||
| 456 | .bc = bc, | ||
| 457 | .argv = RedisModule_Alloc((argc - 1) * sizeof(RedisModuleString*)), | ||
| 458 | .argc = argc - 1, | ||
| 459 | }; | ||
| 460 | |||
| 461 | for (int i = 1 ; i < argc ; ++i) { | ||
| 462 | wctx->argv[i - 1] = RedisModule_HoldString(NULL, argv[i]); | ||
| 463 | } | ||
| 464 | RedisModule_CallReplyPromiseSetUnblockHandler(rep, wait_and_do_rm_call_async_on_unblocked, wctx); | ||
| 465 | RedisModule_FreeCallReply(rep); | ||
| 466 | } | ||
| 467 | |||
| 468 | return REDISMODULE_OK; | ||
| 469 | } | ||
| 470 | |||
| 471 | static void blpop_and_set_multiple_keys_on_unblocked(RedisModuleCtx *ctx, RedisModuleCallReply *reply, void *private_data) { | ||
| 472 | /* ignore the reply */ | ||
| 473 | RedisModule_FreeCallReply(reply); | ||
| 474 | WaitAndDoRMCallCtx *wctx = private_data; | ||
| 475 | for (int i = 0 ; i < wctx->argc ; i += 2) { | ||
| 476 | RedisModuleCallReply* rep = RedisModule_Call(ctx, "set", "!ss", wctx->argv[i], wctx->argv[i + 1]); | ||
| 477 | RedisModule_FreeCallReply(rep); | ||
| 478 | } | ||
| 479 | |||
| 480 | RedisModuleCtx *bctx = RedisModule_GetThreadSafeContext(wctx->bc); | ||
| 481 | RedisModule_ReplyWithSimpleString(bctx, "OK"); | ||
| 482 | RedisModule_FreeThreadSafeContext(bctx); | ||
| 483 | RedisModule_UnblockClient(wctx->bc, NULL); | ||
| 484 | |||
| 485 | for (int i = 0 ; i < wctx->argc ; ++i) { | ||
| 486 | RedisModule_FreeString(NULL, wctx->argv[i]); | ||
| 487 | } | ||
| 488 | RedisModule_Free(wctx->argv); | ||
| 489 | RedisModule_Free(wctx); | ||
| 490 | |||
| 491 | } | ||
| 492 | |||
| 493 | /* | ||
| 494 | * Performs a blpop command on a given list and when unblocked set multiple string keys. | ||
| 495 | * This command allows checking that the unblock callback is performed as a unit | ||
| 496 | * and its effect are replicated to the replica and AOF wrapped with multi exec. | ||
| 497 | */ | ||
| 498 | int blpop_and_set_multiple_keys(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 499 | UNUSED(argv); | ||
| 500 | UNUSED(argc); | ||
| 501 | |||
| 502 | if(argc < 2 || argc % 2 != 0){ | ||
| 503 | return RedisModule_WrongArity(ctx); | ||
| 504 | } | ||
| 505 | |||
| 506 | int flags = RedisModule_GetContextFlags(ctx); | ||
| 507 | if (flags & REDISMODULE_CTX_FLAGS_DENY_BLOCKING) { | ||
| 508 | return RedisModule_ReplyWithError(ctx, "Err can not run wait, blocking is not allowed."); | ||
| 509 | } | ||
| 510 | |||
| 511 | RedisModuleCallReply* rep = RedisModule_Call(ctx, "blpop", "!EKsc", argv[1], "0"); | ||
| 512 | if(RedisModule_CallReplyType(rep) != REDISMODULE_REPLY_PROMISE) { | ||
| 513 | rm_call_async_send_reply(ctx, rep); | ||
| 514 | } else { | ||
| 515 | RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx, NULL, NULL, NULL, 0); | ||
| 516 | WaitAndDoRMCallCtx *wctx = RedisModule_Alloc(sizeof(*wctx)); | ||
| 517 | *wctx = (WaitAndDoRMCallCtx){ | ||
| 518 | .bc = bc, | ||
| 519 | .argv = RedisModule_Alloc((argc - 2) * sizeof(RedisModuleString*)), | ||
| 520 | .argc = argc - 2, | ||
| 521 | }; | ||
| 522 | |||
| 523 | for (int i = 0 ; i < argc - 2 ; ++i) { | ||
| 524 | wctx->argv[i] = RedisModule_HoldString(NULL, argv[i + 2]); | ||
| 525 | } | ||
| 526 | RedisModule_CallReplyPromiseSetUnblockHandler(rep, blpop_and_set_multiple_keys_on_unblocked, wctx); | ||
| 527 | RedisModule_FreeCallReply(rep); | ||
| 528 | } | ||
| 529 | |||
| 530 | return REDISMODULE_OK; | ||
| 531 | } | ||
| 532 | |||
| 533 | /* simulate a blocked client replying to a thread safe context without creating a thread */ | ||
| 534 | int do_fake_bg_true(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 535 | UNUSED(argv); | ||
| 536 | UNUSED(argc); | ||
| 537 | |||
| 538 | RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx, NULL, NULL, NULL, 0); | ||
| 539 | RedisModuleCtx *bctx = RedisModule_GetThreadSafeContext(bc); | ||
| 540 | |||
| 541 | RedisModule_ReplyWithBool(bctx, 1); | ||
| 542 | |||
| 543 | RedisModule_FreeThreadSafeContext(bctx); | ||
| 544 | RedisModule_UnblockClient(bc, NULL); | ||
| 545 | |||
| 546 | return REDISMODULE_OK; | ||
| 547 | } | ||
| 548 | |||
| 549 | |||
| 550 | /* this flag is used to work with busy commands, that might take a while | ||
| 551 | * and ability to stop the busy work with a different command*/ | ||
| 552 | static volatile int abort_flag = 0; | ||
| 553 | |||
| 554 | int slow_fg_command(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 555 | if (argc != 2) { | ||
| 556 | RedisModule_WrongArity(ctx); | ||
| 557 | return REDISMODULE_OK; | ||
| 558 | } | ||
| 559 | long long block_time = 0; | ||
| 560 | if (RedisModule_StringToLongLong(argv[1], &block_time) != REDISMODULE_OK) { | ||
| 561 | RedisModule_ReplyWithError(ctx, "Invalid integer value"); | ||
| 562 | return REDISMODULE_OK; | ||
| 563 | } | ||
| 564 | |||
| 565 | uint64_t start_time = RedisModule_MonotonicMicroseconds(); | ||
| 566 | /* when not blocking indefinitely, we don't process client commands in this test. */ | ||
| 567 | int yield_flags = block_time? REDISMODULE_YIELD_FLAG_NONE: REDISMODULE_YIELD_FLAG_CLIENTS; | ||
| 568 | while (!abort_flag) { | ||
| 569 | RedisModule_Yield(ctx, yield_flags, "Slow module operation"); | ||
| 570 | usleep(1000); | ||
| 571 | if (block_time && RedisModule_MonotonicMicroseconds() - start_time > (uint64_t)block_time) | ||
| 572 | break; | ||
| 573 | } | ||
| 574 | |||
| 575 | abort_flag = 0; | ||
| 576 | RedisModule_ReplyWithLongLong(ctx, 1); | ||
| 577 | return REDISMODULE_OK; | ||
| 578 | } | ||
| 579 | |||
| 580 | int stop_slow_fg_command(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 581 | REDISMODULE_NOT_USED(argv); | ||
| 582 | REDISMODULE_NOT_USED(argc); | ||
| 583 | abort_flag = 1; | ||
| 584 | RedisModule_ReplyWithLongLong(ctx, 1); | ||
| 585 | return REDISMODULE_OK; | ||
| 586 | } | ||
| 587 | |||
| 588 | /* used to enable or disable slow operation in do_bg_rm_call */ | ||
| 589 | static int set_slow_bg_operation(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 590 | if (argc != 2) { | ||
| 591 | RedisModule_WrongArity(ctx); | ||
| 592 | return REDISMODULE_OK; | ||
| 593 | } | ||
| 594 | long long ll; | ||
| 595 | if (RedisModule_StringToLongLong(argv[1], &ll) != REDISMODULE_OK) { | ||
| 596 | RedisModule_ReplyWithError(ctx, "Invalid integer value"); | ||
| 597 | return REDISMODULE_OK; | ||
| 598 | } | ||
| 599 | g_slow_bg_operation = ll; | ||
| 600 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 601 | return REDISMODULE_OK; | ||
| 602 | } | ||
| 603 | |||
| 604 | /* used to test if we reached the slow operation in do_bg_rm_call */ | ||
| 605 | static int is_in_slow_bg_operation(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 606 | UNUSED(argv); | ||
| 607 | if (argc != 1) { | ||
| 608 | RedisModule_WrongArity(ctx); | ||
| 609 | return REDISMODULE_OK; | ||
| 610 | } | ||
| 611 | |||
| 612 | RedisModule_ReplyWithLongLong(ctx, g_is_in_slow_bg_operation); | ||
| 613 | return REDISMODULE_OK; | ||
| 614 | } | ||
| 615 | |||
| 616 | static void timer_callback(RedisModuleCtx *ctx, void *data) | ||
| 617 | { | ||
| 618 | UNUSED(ctx); | ||
| 619 | |||
| 620 | RedisModuleBlockedClient *bc = data; | ||
| 621 | |||
| 622 | // Get Redis module context | ||
| 623 | RedisModuleCtx *reply_ctx = RedisModule_GetThreadSafeContext(bc); | ||
| 624 | |||
| 625 | // Reply to client | ||
| 626 | RedisModule_ReplyWithSimpleString(reply_ctx, "OK"); | ||
| 627 | |||
| 628 | // Unblock client | ||
| 629 | RedisModule_UnblockClient(bc, NULL); | ||
| 630 | |||
| 631 | // Free the Redis module context | ||
| 632 | RedisModule_FreeThreadSafeContext(reply_ctx); | ||
| 633 | } | ||
| 634 | |||
| 635 | /* unblock_by_timer <period_ms> <timeout_ms> | ||
| 636 | * period_ms is the period of the timer. | ||
| 637 | * timeout_ms is the blocking timeout. */ | ||
| 638 | int unblock_by_timer(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 639 | { | ||
| 640 | if (argc != 3) | ||
| 641 | return RedisModule_WrongArity(ctx); | ||
| 642 | |||
| 643 | long long period; | ||
| 644 | long long timeout; | ||
| 645 | if (RedisModule_StringToLongLong(argv[1],&period) != REDISMODULE_OK) | ||
| 646 | return RedisModule_ReplyWithError(ctx,"ERR invalid period"); | ||
| 647 | if (RedisModule_StringToLongLong(argv[2],&timeout) != REDISMODULE_OK) { | ||
| 648 | return RedisModule_ReplyWithError(ctx,"ERR invalid timeout"); | ||
| 649 | } | ||
| 650 | |||
| 651 | RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx, NULL, NULL, NULL, timeout); | ||
| 652 | RedisModule_CreateTimer(ctx, period, timer_callback, bc); | ||
| 653 | return REDISMODULE_OK; | ||
| 654 | } | ||
| 655 | |||
| 656 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 657 | REDISMODULE_NOT_USED(argv); | ||
| 658 | REDISMODULE_NOT_USED(argc); | ||
| 659 | |||
| 660 | if (RedisModule_Init(ctx, "blockedclient", 1, REDISMODULE_APIVER_1)== REDISMODULE_ERR) | ||
| 661 | return REDISMODULE_ERR; | ||
| 662 | |||
| 663 | if (RedisModule_CreateCommand(ctx, "acquire_gil", acquire_gil, "", 0, 0, 0) == REDISMODULE_ERR) | ||
| 664 | return REDISMODULE_ERR; | ||
| 665 | |||
| 666 | if (RedisModule_CreateCommand(ctx, "do_rm_call", do_rm_call, | ||
| 667 | "write", 0, 0, 0) == REDISMODULE_ERR) | ||
| 668 | return REDISMODULE_ERR; | ||
| 669 | |||
| 670 | if (RedisModule_CreateCommand(ctx, "do_rm_call_async", do_rm_call_async, | ||
| 671 | "write", 0, 0, 0) == REDISMODULE_ERR) | ||
| 672 | return REDISMODULE_ERR; | ||
| 673 | |||
| 674 | if (RedisModule_CreateCommand(ctx, "do_rm_call_async_on_thread", do_rm_call_async_on_thread, | ||
| 675 | "write", 0, 0, 0) == REDISMODULE_ERR) | ||
| 676 | return REDISMODULE_ERR; | ||
| 677 | |||
| 678 | if (RedisModule_CreateCommand(ctx, "do_rm_call_async_script_mode", do_rm_call_async, | ||
| 679 | "write", 0, 0, 0) == REDISMODULE_ERR) | ||
| 680 | return REDISMODULE_ERR; | ||
| 681 | |||
| 682 | if (RedisModule_CreateCommand(ctx, "do_rm_call_async_no_replicate", do_rm_call_async, | ||
| 683 | "write", 0, 0, 0) == REDISMODULE_ERR) | ||
| 684 | return REDISMODULE_ERR; | ||
| 685 | |||
| 686 | if (RedisModule_CreateCommand(ctx, "do_rm_call_fire_and_forget", do_rm_call_async_fire_and_forget, | ||
| 687 | "write", 0, 0, 0) == REDISMODULE_ERR) | ||
| 688 | return REDISMODULE_ERR; | ||
| 689 | |||
| 690 | if (RedisModule_CreateCommand(ctx, "wait_and_do_rm_call", wait_and_do_rm_call_async, | ||
| 691 | "write", 0, 0, 0) == REDISMODULE_ERR) | ||
| 692 | return REDISMODULE_ERR; | ||
| 693 | |||
| 694 | if (RedisModule_CreateCommand(ctx, "blpop_and_set_multiple_keys", blpop_and_set_multiple_keys, | ||
| 695 | "write", 0, 0, 0) == REDISMODULE_ERR) | ||
| 696 | return REDISMODULE_ERR; | ||
| 697 | |||
| 698 | if (RedisModule_CreateCommand(ctx, "do_bg_rm_call", do_bg_rm_call, "", 0, 0, 0) == REDISMODULE_ERR) | ||
| 699 | return REDISMODULE_ERR; | ||
| 700 | |||
| 701 | if (RedisModule_CreateCommand(ctx, "do_bg_rm_call_format", do_bg_rm_call, "", 0, 0, 0) == REDISMODULE_ERR) | ||
| 702 | return REDISMODULE_ERR; | ||
| 703 | |||
| 704 | if (RedisModule_CreateCommand(ctx, "do_fake_bg_true", do_fake_bg_true, "", 0, 0, 0) == REDISMODULE_ERR) | ||
| 705 | return REDISMODULE_ERR; | ||
| 706 | |||
| 707 | if (RedisModule_CreateCommand(ctx, "slow_fg_command", slow_fg_command,"", 0, 0, 0) == REDISMODULE_ERR) | ||
| 708 | return REDISMODULE_ERR; | ||
| 709 | |||
| 710 | if (RedisModule_CreateCommand(ctx, "stop_slow_fg_command", stop_slow_fg_command,"allow-busy", 0, 0, 0) == REDISMODULE_ERR) | ||
| 711 | return REDISMODULE_ERR; | ||
| 712 | |||
| 713 | if (RedisModule_CreateCommand(ctx, "set_slow_bg_operation", set_slow_bg_operation, "allow-busy", 0, 0, 0) == REDISMODULE_ERR) | ||
| 714 | return REDISMODULE_ERR; | ||
| 715 | |||
| 716 | if (RedisModule_CreateCommand(ctx, "is_in_slow_bg_operation", is_in_slow_bg_operation, "allow-busy", 0, 0, 0) == REDISMODULE_ERR) | ||
| 717 | return REDISMODULE_ERR; | ||
| 718 | |||
| 719 | if (RedisModule_CreateCommand(ctx, "unblock_by_timer", unblock_by_timer, "", 0, 0, 0) == REDISMODULE_ERR) | ||
| 720 | return REDISMODULE_ERR; | ||
| 721 | |||
| 722 | return REDISMODULE_OK; | ||
| 723 | } | ||
diff --git a/examples/redis-unstable/tests/modules/blockonbackground.c b/examples/redis-unstable/tests/modules/blockonbackground.c new file mode 100644 index 0000000..7aeb011 --- /dev/null +++ b/examples/redis-unstable/tests/modules/blockonbackground.c | |||
| @@ -0,0 +1,333 @@ | |||
| 1 | #define _XOPEN_SOURCE 700 | ||
| 2 | #include "redismodule.h" | ||
| 3 | #include <stdio.h> | ||
| 4 | #include <stdlib.h> | ||
| 5 | #include <pthread.h> | ||
| 6 | #include <time.h> | ||
| 7 | |||
| 8 | #define UNUSED(x) (void)(x) | ||
| 9 | |||
| 10 | typedef struct { | ||
| 11 | /* Mutex for protecting RedisModule_BlockedClientMeasureTime*() API from race | ||
| 12 | * conditions due to timeout callback triggered in the main thread. */ | ||
| 13 | pthread_mutex_t measuretime_mutex; | ||
| 14 | int measuretime_completed; /* Indicates that time measure has ended and will not continue further */ | ||
| 15 | int myint; /* Used for replying */ | ||
| 16 | } BlockPrivdata; | ||
| 17 | |||
| 18 | void blockClientPrivdataInit(RedisModuleBlockedClient *bc) { | ||
| 19 | BlockPrivdata *block_privdata = RedisModule_Calloc(1, sizeof(*block_privdata)); | ||
| 20 | block_privdata->measuretime_mutex = (pthread_mutex_t)PTHREAD_MUTEX_INITIALIZER; | ||
| 21 | RedisModule_BlockClientSetPrivateData(bc, block_privdata); | ||
| 22 | } | ||
| 23 | |||
| 24 | void blockClientMeasureTimeStart(RedisModuleBlockedClient *bc, BlockPrivdata *block_privdata) { | ||
| 25 | pthread_mutex_lock(&block_privdata->measuretime_mutex); | ||
| 26 | RedisModule_BlockedClientMeasureTimeStart(bc); | ||
| 27 | pthread_mutex_unlock(&block_privdata->measuretime_mutex); | ||
| 28 | } | ||
| 29 | |||
| 30 | void blockClientMeasureTimeEnd(RedisModuleBlockedClient *bc, BlockPrivdata *block_privdata, int completed) { | ||
| 31 | pthread_mutex_lock(&block_privdata->measuretime_mutex); | ||
| 32 | if (!block_privdata->measuretime_completed) { | ||
| 33 | RedisModule_BlockedClientMeasureTimeEnd(bc); | ||
| 34 | if (completed) block_privdata->measuretime_completed = 1; | ||
| 35 | } | ||
| 36 | pthread_mutex_unlock(&block_privdata->measuretime_mutex); | ||
| 37 | } | ||
| 38 | |||
| 39 | /* Reply callback for blocking command BLOCK.DEBUG */ | ||
| 40 | int HelloBlock_Reply(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 41 | UNUSED(argv); | ||
| 42 | UNUSED(argc); | ||
| 43 | BlockPrivdata *block_privdata = RedisModule_GetBlockedClientPrivateData(ctx); | ||
| 44 | return RedisModule_ReplyWithLongLong(ctx,block_privdata->myint); | ||
| 45 | } | ||
| 46 | |||
| 47 | /* Timeout callback for blocking command BLOCK.DEBUG */ | ||
| 48 | int HelloBlock_Timeout(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 49 | UNUSED(argv); | ||
| 50 | UNUSED(argc); | ||
| 51 | RedisModuleBlockedClient *bc = RedisModule_GetBlockedClientHandle(ctx); | ||
| 52 | BlockPrivdata *block_privdata = RedisModule_GetBlockedClientPrivateData(ctx); | ||
| 53 | blockClientMeasureTimeEnd(bc, block_privdata, 1); | ||
| 54 | return RedisModule_ReplyWithSimpleString(ctx,"Request timedout"); | ||
| 55 | } | ||
| 56 | |||
| 57 | /* Private data freeing callback for BLOCK.DEBUG command. */ | ||
| 58 | void HelloBlock_FreeData(RedisModuleCtx *ctx, void *privdata) { | ||
| 59 | UNUSED(ctx); | ||
| 60 | BlockPrivdata *block_privdata = privdata; | ||
| 61 | pthread_mutex_destroy(&block_privdata->measuretime_mutex); | ||
| 62 | RedisModule_Free(privdata); | ||
| 63 | } | ||
| 64 | |||
| 65 | /* Private data freeing callback for BLOCK.BLOCK command. */ | ||
| 66 | void HelloBlock_FreeStringData(RedisModuleCtx *ctx, void *privdata) { | ||
| 67 | RedisModule_FreeString(ctx, (RedisModuleString*)privdata); | ||
| 68 | } | ||
| 69 | |||
| 70 | /* The thread entry point that actually executes the blocking part | ||
| 71 | * of the command BLOCK.DEBUG. */ | ||
| 72 | void *BlockDebug_ThreadMain(void *arg) { | ||
| 73 | void **targ = arg; | ||
| 74 | RedisModuleBlockedClient *bc = targ[0]; | ||
| 75 | long long delay = (unsigned long)targ[1]; | ||
| 76 | long long enable_time_track = (unsigned long)targ[2]; | ||
| 77 | BlockPrivdata *block_privdata = RedisModule_BlockClientGetPrivateData(bc); | ||
| 78 | |||
| 79 | if (enable_time_track) | ||
| 80 | blockClientMeasureTimeStart(bc, block_privdata); | ||
| 81 | RedisModule_Free(targ); | ||
| 82 | |||
| 83 | struct timespec ts; | ||
| 84 | ts.tv_sec = delay / 1000; | ||
| 85 | ts.tv_nsec = (delay % 1000) * 1000000; | ||
| 86 | nanosleep(&ts, NULL); | ||
| 87 | if (enable_time_track) | ||
| 88 | blockClientMeasureTimeEnd(bc, block_privdata, 0); | ||
| 89 | block_privdata->myint = rand(); | ||
| 90 | RedisModule_UnblockClient(bc,block_privdata); | ||
| 91 | return NULL; | ||
| 92 | } | ||
| 93 | |||
| 94 | /* The thread entry point that actually executes the blocking part | ||
| 95 | * of the command BLOCK.DOUBLE_DEBUG. */ | ||
| 96 | void *DoubleBlock_ThreadMain(void *arg) { | ||
| 97 | void **targ = arg; | ||
| 98 | RedisModuleBlockedClient *bc = targ[0]; | ||
| 99 | long long delay = (unsigned long)targ[1]; | ||
| 100 | BlockPrivdata *block_privdata = RedisModule_BlockClientGetPrivateData(bc); | ||
| 101 | blockClientMeasureTimeStart(bc, block_privdata); | ||
| 102 | RedisModule_Free(targ); | ||
| 103 | struct timespec ts; | ||
| 104 | ts.tv_sec = delay / 1000; | ||
| 105 | ts.tv_nsec = (delay % 1000) * 1000000; | ||
| 106 | nanosleep(&ts, NULL); | ||
| 107 | blockClientMeasureTimeEnd(bc, block_privdata, 0); | ||
| 108 | /* call again RedisModule_BlockedClientMeasureTimeStart() and | ||
| 109 | * RedisModule_BlockedClientMeasureTimeEnd and ensure that the | ||
| 110 | * total execution time is 2x the delay. */ | ||
| 111 | blockClientMeasureTimeStart(bc, block_privdata); | ||
| 112 | nanosleep(&ts, NULL); | ||
| 113 | blockClientMeasureTimeEnd(bc, block_privdata, 0); | ||
| 114 | block_privdata->myint = rand(); | ||
| 115 | RedisModule_UnblockClient(bc,block_privdata); | ||
| 116 | return NULL; | ||
| 117 | } | ||
| 118 | |||
| 119 | void HelloBlock_Disconnected(RedisModuleCtx *ctx, RedisModuleBlockedClient *bc) { | ||
| 120 | RedisModule_Log(ctx,"warning","Blocked client %p disconnected!", | ||
| 121 | (void*)bc); | ||
| 122 | } | ||
| 123 | |||
| 124 | /* BLOCK.DEBUG <delay_ms> <timeout_ms> -- Block for <count> milliseconds, then reply with | ||
| 125 | * a random number. Timeout is the command timeout, so that you can test | ||
| 126 | * what happens when the delay is greater than the timeout. */ | ||
| 127 | int HelloBlock_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 128 | if (argc != 3) return RedisModule_WrongArity(ctx); | ||
| 129 | long long delay; | ||
| 130 | long long timeout; | ||
| 131 | |||
| 132 | if (RedisModule_StringToLongLong(argv[1],&delay) != REDISMODULE_OK) { | ||
| 133 | return RedisModule_ReplyWithError(ctx,"ERR invalid count"); | ||
| 134 | } | ||
| 135 | |||
| 136 | if (RedisModule_StringToLongLong(argv[2],&timeout) != REDISMODULE_OK) { | ||
| 137 | return RedisModule_ReplyWithError(ctx,"ERR invalid count"); | ||
| 138 | } | ||
| 139 | |||
| 140 | pthread_t tid; | ||
| 141 | RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx,HelloBlock_Reply,HelloBlock_Timeout,HelloBlock_FreeData,timeout); | ||
| 142 | blockClientPrivdataInit(bc); | ||
| 143 | |||
| 144 | /* Here we set a disconnection handler, however since this module will | ||
| 145 | * block in sleep() in a thread, there is not much we can do in the | ||
| 146 | * callback, so this is just to show you the API. */ | ||
| 147 | RedisModule_SetDisconnectCallback(bc,HelloBlock_Disconnected); | ||
| 148 | |||
| 149 | /* Now that we setup a blocking client, we need to pass the control | ||
| 150 | * to the thread. However we need to pass arguments to the thread: | ||
| 151 | * the delay and a reference to the blocked client handle. */ | ||
| 152 | void **targ = RedisModule_Alloc(sizeof(void*)*3); | ||
| 153 | targ[0] = bc; | ||
| 154 | targ[1] = (void*)(unsigned long) delay; | ||
| 155 | // pass 1 as flag to enable time tracking | ||
| 156 | targ[2] = (void*)(unsigned long) 1; | ||
| 157 | |||
| 158 | if (pthread_create(&tid,NULL,BlockDebug_ThreadMain,targ) != 0) { | ||
| 159 | RedisModule_AbortBlock(bc); | ||
| 160 | return RedisModule_ReplyWithError(ctx,"-ERR Can't start thread"); | ||
| 161 | } | ||
| 162 | pthread_detach(tid); | ||
| 163 | return REDISMODULE_OK; | ||
| 164 | } | ||
| 165 | |||
| 166 | /* BLOCK.DEBUG_NOTRACKING <delay_ms> <timeout_ms> -- Block for <count> milliseconds, then reply with | ||
| 167 | * a random number. Timeout is the command timeout, so that you can test | ||
| 168 | * what happens when the delay is greater than the timeout. | ||
| 169 | * this command does not track background time so the background time should no appear in stats*/ | ||
| 170 | int HelloBlockNoTracking_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 171 | if (argc != 3) return RedisModule_WrongArity(ctx); | ||
| 172 | long long delay; | ||
| 173 | long long timeout; | ||
| 174 | |||
| 175 | if (RedisModule_StringToLongLong(argv[1],&delay) != REDISMODULE_OK) { | ||
| 176 | return RedisModule_ReplyWithError(ctx,"ERR invalid count"); | ||
| 177 | } | ||
| 178 | |||
| 179 | if (RedisModule_StringToLongLong(argv[2],&timeout) != REDISMODULE_OK) { | ||
| 180 | return RedisModule_ReplyWithError(ctx,"ERR invalid count"); | ||
| 181 | } | ||
| 182 | |||
| 183 | pthread_t tid; | ||
| 184 | RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx,HelloBlock_Reply,HelloBlock_Timeout,HelloBlock_FreeData,timeout); | ||
| 185 | blockClientPrivdataInit(bc); | ||
| 186 | |||
| 187 | /* Here we set a disconnection handler, however since this module will | ||
| 188 | * block in sleep() in a thread, there is not much we can do in the | ||
| 189 | * callback, so this is just to show you the API. */ | ||
| 190 | RedisModule_SetDisconnectCallback(bc,HelloBlock_Disconnected); | ||
| 191 | |||
| 192 | /* Now that we setup a blocking client, we need to pass the control | ||
| 193 | * to the thread. However we need to pass arguments to the thread: | ||
| 194 | * the delay and a reference to the blocked client handle. */ | ||
| 195 | void **targ = RedisModule_Alloc(sizeof(void*)*3); | ||
| 196 | targ[0] = bc; | ||
| 197 | targ[1] = (void*)(unsigned long) delay; | ||
| 198 | // pass 0 as flag to enable time tracking | ||
| 199 | targ[2] = (void*)(unsigned long) 0; | ||
| 200 | |||
| 201 | if (pthread_create(&tid,NULL,BlockDebug_ThreadMain,targ) != 0) { | ||
| 202 | RedisModule_AbortBlock(bc); | ||
| 203 | return RedisModule_ReplyWithError(ctx,"-ERR Can't start thread"); | ||
| 204 | } | ||
| 205 | pthread_detach(tid); | ||
| 206 | return REDISMODULE_OK; | ||
| 207 | } | ||
| 208 | |||
| 209 | /* BLOCK.DOUBLE_DEBUG <delay_ms> -- Block for 2 x <count> milliseconds, | ||
| 210 | * then reply with a random number. | ||
| 211 | * This command is used to test multiple calls to RedisModule_BlockedClientMeasureTimeStart() | ||
| 212 | * and RedisModule_BlockedClientMeasureTimeEnd() within the same execution. */ | ||
| 213 | int HelloDoubleBlock_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 214 | if (argc != 2) return RedisModule_WrongArity(ctx); | ||
| 215 | long long delay; | ||
| 216 | |||
| 217 | if (RedisModule_StringToLongLong(argv[1],&delay) != REDISMODULE_OK) { | ||
| 218 | return RedisModule_ReplyWithError(ctx,"ERR invalid count"); | ||
| 219 | } | ||
| 220 | |||
| 221 | pthread_t tid; | ||
| 222 | RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx,HelloBlock_Reply,HelloBlock_Timeout,HelloBlock_FreeData,0); | ||
| 223 | blockClientPrivdataInit(bc); | ||
| 224 | |||
| 225 | /* Now that we setup a blocking client, we need to pass the control | ||
| 226 | * to the thread. However we need to pass arguments to the thread: | ||
| 227 | * the delay and a reference to the blocked client handle. */ | ||
| 228 | void **targ = RedisModule_Alloc(sizeof(void*)*2); | ||
| 229 | targ[0] = bc; | ||
| 230 | targ[1] = (void*)(unsigned long) delay; | ||
| 231 | |||
| 232 | if (pthread_create(&tid,NULL,DoubleBlock_ThreadMain,targ) != 0) { | ||
| 233 | RedisModule_AbortBlock(bc); | ||
| 234 | return RedisModule_ReplyWithError(ctx,"-ERR Can't start thread"); | ||
| 235 | } | ||
| 236 | pthread_detach(tid); | ||
| 237 | return REDISMODULE_OK; | ||
| 238 | } | ||
| 239 | |||
| 240 | RedisModuleBlockedClient *blocked_client = NULL; | ||
| 241 | |||
| 242 | /* BLOCK.BLOCK [TIMEOUT] -- Blocks the current client until released | ||
| 243 | * or TIMEOUT seconds. If TIMEOUT is zero, no timeout function is | ||
| 244 | * registered. | ||
| 245 | */ | ||
| 246 | int Block_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 247 | if (RedisModule_IsBlockedReplyRequest(ctx)) { | ||
| 248 | RedisModuleString *r = RedisModule_GetBlockedClientPrivateData(ctx); | ||
| 249 | return RedisModule_ReplyWithString(ctx, r); | ||
| 250 | } else if (RedisModule_IsBlockedTimeoutRequest(ctx)) { | ||
| 251 | RedisModule_UnblockClient(blocked_client, NULL); /* Must be called to avoid leaks. */ | ||
| 252 | blocked_client = NULL; | ||
| 253 | return RedisModule_ReplyWithSimpleString(ctx, "Timed out"); | ||
| 254 | } | ||
| 255 | |||
| 256 | if (argc != 2) return RedisModule_WrongArity(ctx); | ||
| 257 | long long timeout; | ||
| 258 | |||
| 259 | if (RedisModule_StringToLongLong(argv[1], &timeout) != REDISMODULE_OK) { | ||
| 260 | return RedisModule_ReplyWithError(ctx, "ERR invalid timeout"); | ||
| 261 | } | ||
| 262 | if (blocked_client) { | ||
| 263 | return RedisModule_ReplyWithError(ctx, "ERR another client already blocked"); | ||
| 264 | } | ||
| 265 | |||
| 266 | /* Block client. We use this function as both a reply and optional timeout | ||
| 267 | * callback and differentiate the different code flows above. | ||
| 268 | */ | ||
| 269 | blocked_client = RedisModule_BlockClient(ctx, Block_RedisCommand, | ||
| 270 | timeout > 0 ? Block_RedisCommand : NULL, HelloBlock_FreeStringData, timeout); | ||
| 271 | return REDISMODULE_OK; | ||
| 272 | } | ||
| 273 | |||
| 274 | /* BLOCK.IS_BLOCKED -- Returns 1 if we have a blocked client, or 0 otherwise. | ||
| 275 | */ | ||
| 276 | int IsBlocked_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 277 | UNUSED(argv); | ||
| 278 | UNUSED(argc); | ||
| 279 | RedisModule_ReplyWithLongLong(ctx, blocked_client ? 1 : 0); | ||
| 280 | return REDISMODULE_OK; | ||
| 281 | } | ||
| 282 | |||
| 283 | /* BLOCK.RELEASE [reply] -- Releases the blocked client and produce the specified reply. | ||
| 284 | */ | ||
| 285 | int Release_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 286 | if (argc != 2) return RedisModule_WrongArity(ctx); | ||
| 287 | if (!blocked_client) { | ||
| 288 | return RedisModule_ReplyWithError(ctx, "ERR No blocked client"); | ||
| 289 | } | ||
| 290 | |||
| 291 | RedisModuleString *replystr = argv[1]; | ||
| 292 | RedisModule_RetainString(ctx, replystr); | ||
| 293 | RedisModule_UnblockClient(blocked_client, replystr); | ||
| 294 | blocked_client = NULL; | ||
| 295 | |||
| 296 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 297 | |||
| 298 | return REDISMODULE_OK; | ||
| 299 | } | ||
| 300 | |||
| 301 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 302 | UNUSED(argv); | ||
| 303 | UNUSED(argc); | ||
| 304 | |||
| 305 | if (RedisModule_Init(ctx,"block",1,REDISMODULE_APIVER_1) | ||
| 306 | == REDISMODULE_ERR) return REDISMODULE_ERR; | ||
| 307 | |||
| 308 | if (RedisModule_CreateCommand(ctx,"block.debug", | ||
| 309 | HelloBlock_RedisCommand,"",0,0,0) == REDISMODULE_ERR) | ||
| 310 | return REDISMODULE_ERR; | ||
| 311 | |||
| 312 | if (RedisModule_CreateCommand(ctx,"block.double_debug", | ||
| 313 | HelloDoubleBlock_RedisCommand,"",0,0,0) == REDISMODULE_ERR) | ||
| 314 | return REDISMODULE_ERR; | ||
| 315 | |||
| 316 | if (RedisModule_CreateCommand(ctx,"block.debug_no_track", | ||
| 317 | HelloBlockNoTracking_RedisCommand,"",0,0,0) == REDISMODULE_ERR) | ||
| 318 | return REDISMODULE_ERR; | ||
| 319 | |||
| 320 | if (RedisModule_CreateCommand(ctx, "block.block", | ||
| 321 | Block_RedisCommand, "", 0, 0, 0) == REDISMODULE_ERR) | ||
| 322 | return REDISMODULE_ERR; | ||
| 323 | |||
| 324 | if (RedisModule_CreateCommand(ctx,"block.is_blocked", | ||
| 325 | IsBlocked_RedisCommand,"",0,0,0) == REDISMODULE_ERR) | ||
| 326 | return REDISMODULE_ERR; | ||
| 327 | |||
| 328 | if (RedisModule_CreateCommand(ctx,"block.release", | ||
| 329 | Release_RedisCommand,"",0,0,0) == REDISMODULE_ERR) | ||
| 330 | return REDISMODULE_ERR; | ||
| 331 | |||
| 332 | return REDISMODULE_OK; | ||
| 333 | } | ||
diff --git a/examples/redis-unstable/tests/modules/blockonkeys.c b/examples/redis-unstable/tests/modules/blockonkeys.c new file mode 100644 index 0000000..94bb361 --- /dev/null +++ b/examples/redis-unstable/tests/modules/blockonkeys.c | |||
| @@ -0,0 +1,645 @@ | |||
| 1 | #include "redismodule.h" | ||
| 2 | |||
| 3 | #include <string.h> | ||
| 4 | #include <strings.h> | ||
| 5 | #include <assert.h> | ||
| 6 | #include <unistd.h> | ||
| 7 | |||
| 8 | #define UNUSED(V) ((void) V) | ||
| 9 | |||
| 10 | #define LIST_SIZE 1024 | ||
| 11 | |||
| 12 | /* The FSL (Fixed-Size List) data type is a low-budget imitation of the | ||
| 13 | * native Redis list, in order to test list-like commands implemented | ||
| 14 | * by a module. | ||
| 15 | * Examples: FSL.PUSH, FSL.BPOP, etc. */ | ||
| 16 | |||
| 17 | typedef struct { | ||
| 18 | long long list[LIST_SIZE]; | ||
| 19 | long long length; | ||
| 20 | } fsl_t; /* Fixed-size list */ | ||
| 21 | |||
| 22 | static RedisModuleType *fsltype = NULL; | ||
| 23 | |||
| 24 | fsl_t *fsl_type_create(void) { | ||
| 25 | fsl_t *o; | ||
| 26 | o = RedisModule_Alloc(sizeof(*o)); | ||
| 27 | o->length = 0; | ||
| 28 | return o; | ||
| 29 | } | ||
| 30 | |||
| 31 | void fsl_type_free(fsl_t *o) { | ||
| 32 | RedisModule_Free(o); | ||
| 33 | } | ||
| 34 | |||
| 35 | /* ========================== "fsltype" type methods ======================= */ | ||
| 36 | |||
| 37 | void *fsl_rdb_load(RedisModuleIO *rdb, int encver) { | ||
| 38 | if (encver != 0) { | ||
| 39 | return NULL; | ||
| 40 | } | ||
| 41 | fsl_t *fsl = fsl_type_create(); | ||
| 42 | fsl->length = RedisModule_LoadUnsigned(rdb); | ||
| 43 | for (long long i = 0; i < fsl->length; i++) | ||
| 44 | fsl->list[i] = RedisModule_LoadSigned(rdb); | ||
| 45 | return fsl; | ||
| 46 | } | ||
| 47 | |||
| 48 | void fsl_rdb_save(RedisModuleIO *rdb, void *value) { | ||
| 49 | fsl_t *fsl = value; | ||
| 50 | RedisModule_SaveUnsigned(rdb,fsl->length); | ||
| 51 | for (long long i = 0; i < fsl->length; i++) | ||
| 52 | RedisModule_SaveSigned(rdb, fsl->list[i]); | ||
| 53 | } | ||
| 54 | |||
| 55 | void fsl_aofrw(RedisModuleIO *aof, RedisModuleString *key, void *value) { | ||
| 56 | fsl_t *fsl = value; | ||
| 57 | for (long long i = 0; i < fsl->length; i++) | ||
| 58 | RedisModule_EmitAOF(aof, "FSL.PUSH","sl", key, fsl->list[i]); | ||
| 59 | } | ||
| 60 | |||
| 61 | void fsl_free(void *value) { | ||
| 62 | fsl_type_free(value); | ||
| 63 | } | ||
| 64 | |||
| 65 | /* ========================== helper methods ======================= */ | ||
| 66 | |||
| 67 | /* Wrapper to the boilerplate code of opening a key, checking its type, etc. | ||
| 68 | * Returns 0 if `keyname` exists in the dataset, but it's of the wrong type (i.e. not FSL) */ | ||
| 69 | int get_fsl(RedisModuleCtx *ctx, RedisModuleString *keyname, int mode, int create, fsl_t **fsl, int reply_on_failure) { | ||
| 70 | *fsl = NULL; | ||
| 71 | RedisModuleKey *key = RedisModule_OpenKey(ctx, keyname, mode); | ||
| 72 | |||
| 73 | if (RedisModule_KeyType(key) != REDISMODULE_KEYTYPE_EMPTY) { | ||
| 74 | /* Key exists */ | ||
| 75 | if (RedisModule_ModuleTypeGetType(key) != fsltype) { | ||
| 76 | /* Key is not FSL */ | ||
| 77 | RedisModule_CloseKey(key); | ||
| 78 | if (reply_on_failure) | ||
| 79 | RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE); | ||
| 80 | RedisModuleCallReply *reply = RedisModule_Call(ctx, "INCR", "c", "fsl_wrong_type"); | ||
| 81 | RedisModule_FreeCallReply(reply); | ||
| 82 | return 0; | ||
| 83 | } | ||
| 84 | |||
| 85 | *fsl = RedisModule_ModuleTypeGetValue(key); | ||
| 86 | if (*fsl && !(*fsl)->length && mode & REDISMODULE_WRITE) { | ||
| 87 | /* Key exists, but it's logically empty */ | ||
| 88 | if (create) { | ||
| 89 | create = 0; /* No need to create, key exists in its basic state */ | ||
| 90 | } else { | ||
| 91 | RedisModule_DeleteKey(key); | ||
| 92 | *fsl = NULL; | ||
| 93 | } | ||
| 94 | } else { | ||
| 95 | /* Key exists, and has elements in it - no need to create anything */ | ||
| 96 | create = 0; | ||
| 97 | } | ||
| 98 | } | ||
| 99 | |||
| 100 | if (create) { | ||
| 101 | *fsl = fsl_type_create(); | ||
| 102 | RedisModule_ModuleTypeSetValue(key, fsltype, *fsl); | ||
| 103 | } | ||
| 104 | |||
| 105 | RedisModule_CloseKey(key); | ||
| 106 | return 1; | ||
| 107 | } | ||
| 108 | |||
| 109 | /* ========================== commands ======================= */ | ||
| 110 | |||
| 111 | /* FSL.PUSH <key> <int> - Push an integer to the fixed-size list (to the right). | ||
| 112 | * It must be greater than the element in the head of the list. */ | ||
| 113 | int fsl_push(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 114 | if (argc != 3) | ||
| 115 | return RedisModule_WrongArity(ctx); | ||
| 116 | |||
| 117 | long long ele; | ||
| 118 | if (RedisModule_StringToLongLong(argv[2],&ele) != REDISMODULE_OK) | ||
| 119 | return RedisModule_ReplyWithError(ctx,"ERR invalid integer"); | ||
| 120 | |||
| 121 | fsl_t *fsl; | ||
| 122 | if (!get_fsl(ctx, argv[1], REDISMODULE_WRITE, 1, &fsl, 1)) | ||
| 123 | return REDISMODULE_OK; | ||
| 124 | |||
| 125 | if (fsl->length == LIST_SIZE) | ||
| 126 | return RedisModule_ReplyWithError(ctx,"ERR list is full"); | ||
| 127 | |||
| 128 | if (fsl->length != 0 && fsl->list[fsl->length-1] >= ele) | ||
| 129 | return RedisModule_ReplyWithError(ctx,"ERR new element has to be greater than the head element"); | ||
| 130 | |||
| 131 | fsl->list[fsl->length++] = ele; | ||
| 132 | RedisModule_SignalKeyAsReady(ctx, argv[1]); | ||
| 133 | |||
| 134 | RedisModule_ReplicateVerbatim(ctx); | ||
| 135 | |||
| 136 | return RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 137 | } | ||
| 138 | |||
| 139 | typedef struct { | ||
| 140 | RedisModuleString *keyname; | ||
| 141 | long long ele; | ||
| 142 | } timer_data_t; | ||
| 143 | |||
| 144 | static void timer_callback(RedisModuleCtx *ctx, void *data) | ||
| 145 | { | ||
| 146 | timer_data_t *td = data; | ||
| 147 | |||
| 148 | fsl_t *fsl; | ||
| 149 | if (!get_fsl(ctx, td->keyname, REDISMODULE_WRITE, 1, &fsl, 1)) | ||
| 150 | return; | ||
| 151 | |||
| 152 | if (fsl->length == LIST_SIZE) | ||
| 153 | return; /* list is full */ | ||
| 154 | |||
| 155 | if (fsl->length != 0 && fsl->list[fsl->length-1] >= td->ele) | ||
| 156 | return; /* new element has to be greater than the head element */ | ||
| 157 | |||
| 158 | fsl->list[fsl->length++] = td->ele; | ||
| 159 | RedisModule_SignalKeyAsReady(ctx, td->keyname); | ||
| 160 | |||
| 161 | RedisModule_Replicate(ctx, "FSL.PUSH", "sl", td->keyname, td->ele); | ||
| 162 | |||
| 163 | RedisModule_FreeString(ctx, td->keyname); | ||
| 164 | RedisModule_Free(td); | ||
| 165 | } | ||
| 166 | |||
| 167 | /* FSL.PUSHTIMER <key> <int> <period-in-ms> - Push the number 9000 to the fixed-size list (to the right). | ||
| 168 | * It must be greater than the element in the head of the list. */ | ||
| 169 | int fsl_pushtimer(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 170 | { | ||
| 171 | if (argc != 4) | ||
| 172 | return RedisModule_WrongArity(ctx); | ||
| 173 | |||
| 174 | long long ele; | ||
| 175 | if (RedisModule_StringToLongLong(argv[2],&ele) != REDISMODULE_OK) | ||
| 176 | return RedisModule_ReplyWithError(ctx,"ERR invalid integer"); | ||
| 177 | |||
| 178 | long long period; | ||
| 179 | if (RedisModule_StringToLongLong(argv[3],&period) != REDISMODULE_OK) | ||
| 180 | return RedisModule_ReplyWithError(ctx,"ERR invalid period"); | ||
| 181 | |||
| 182 | fsl_t *fsl; | ||
| 183 | if (!get_fsl(ctx, argv[1], REDISMODULE_WRITE, 1, &fsl, 1)) | ||
| 184 | return REDISMODULE_OK; | ||
| 185 | |||
| 186 | if (fsl->length == LIST_SIZE) | ||
| 187 | return RedisModule_ReplyWithError(ctx,"ERR list is full"); | ||
| 188 | |||
| 189 | timer_data_t *td = RedisModule_Alloc(sizeof(*td)); | ||
| 190 | td->keyname = argv[1]; | ||
| 191 | RedisModule_RetainString(ctx, td->keyname); | ||
| 192 | td->ele = ele; | ||
| 193 | |||
| 194 | RedisModuleTimerID id = RedisModule_CreateTimer(ctx, period, timer_callback, td); | ||
| 195 | RedisModule_ReplyWithLongLong(ctx, id); | ||
| 196 | |||
| 197 | return REDISMODULE_OK; | ||
| 198 | } | ||
| 199 | |||
| 200 | int bpop_reply_callback(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 201 | REDISMODULE_NOT_USED(argv); | ||
| 202 | REDISMODULE_NOT_USED(argc); | ||
| 203 | RedisModuleString *keyname = RedisModule_GetBlockedClientReadyKey(ctx); | ||
| 204 | |||
| 205 | fsl_t *fsl; | ||
| 206 | if (!get_fsl(ctx, keyname, REDISMODULE_WRITE, 0, &fsl, 0) || !fsl) | ||
| 207 | return REDISMODULE_ERR; | ||
| 208 | |||
| 209 | RedisModule_Assert(fsl->length); | ||
| 210 | RedisModule_ReplyWithLongLong(ctx, fsl->list[--fsl->length]); | ||
| 211 | |||
| 212 | /* I'm lazy so i'll replicate a potentially blocking command, it shouldn't block in this flow. */ | ||
| 213 | RedisModule_ReplicateVerbatim(ctx); | ||
| 214 | return REDISMODULE_OK; | ||
| 215 | } | ||
| 216 | |||
| 217 | int bpop_timeout_callback(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 218 | REDISMODULE_NOT_USED(argv); | ||
| 219 | REDISMODULE_NOT_USED(argc); | ||
| 220 | return RedisModule_ReplyWithSimpleString(ctx, "Request timedout"); | ||
| 221 | } | ||
| 222 | |||
| 223 | /* FSL.BPOP <key> <timeout> [NO_TO_CB]- Block clients until list has two or more elements. | ||
| 224 | * When that happens, unblock client and pop the last two elements (from the right). */ | ||
| 225 | int fsl_bpop(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 226 | if (argc < 3) | ||
| 227 | return RedisModule_WrongArity(ctx); | ||
| 228 | |||
| 229 | long long timeout; | ||
| 230 | if (RedisModule_StringToLongLong(argv[2],&timeout) != REDISMODULE_OK || timeout < 0) | ||
| 231 | return RedisModule_ReplyWithError(ctx,"ERR invalid timeout"); | ||
| 232 | |||
| 233 | int to_cb = 1; | ||
| 234 | if (argc == 4) { | ||
| 235 | if (strcasecmp("NO_TO_CB", RedisModule_StringPtrLen(argv[3], NULL))) | ||
| 236 | return RedisModule_ReplyWithError(ctx,"ERR invalid argument"); | ||
| 237 | to_cb = 0; | ||
| 238 | } | ||
| 239 | |||
| 240 | fsl_t *fsl; | ||
| 241 | if (!get_fsl(ctx, argv[1], REDISMODULE_WRITE, 0, &fsl, 1)) | ||
| 242 | return REDISMODULE_OK; | ||
| 243 | |||
| 244 | if (!fsl) { | ||
| 245 | RedisModule_BlockClientOnKeys(ctx, bpop_reply_callback, to_cb ? bpop_timeout_callback : NULL, | ||
| 246 | NULL, timeout, &argv[1], 1, NULL); | ||
| 247 | } else { | ||
| 248 | RedisModule_Assert(fsl->length); | ||
| 249 | RedisModule_ReplyWithLongLong(ctx, fsl->list[--fsl->length]); | ||
| 250 | /* I'm lazy so i'll replicate a potentially blocking command, it shouldn't block in this flow. */ | ||
| 251 | RedisModule_ReplicateVerbatim(ctx); | ||
| 252 | } | ||
| 253 | |||
| 254 | return REDISMODULE_OK; | ||
| 255 | } | ||
| 256 | |||
| 257 | int bpopgt_reply_callback(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 258 | REDISMODULE_NOT_USED(argv); | ||
| 259 | REDISMODULE_NOT_USED(argc); | ||
| 260 | RedisModuleString *keyname = RedisModule_GetBlockedClientReadyKey(ctx); | ||
| 261 | long long *pgt = RedisModule_GetBlockedClientPrivateData(ctx); | ||
| 262 | |||
| 263 | fsl_t *fsl; | ||
| 264 | if (!get_fsl(ctx, keyname, REDISMODULE_WRITE, 0, &fsl, 0) || !fsl) | ||
| 265 | return RedisModule_ReplyWithError(ctx,"UNBLOCKED key no longer exists"); | ||
| 266 | |||
| 267 | if (fsl->list[fsl->length-1] <= *pgt) | ||
| 268 | return REDISMODULE_ERR; | ||
| 269 | |||
| 270 | RedisModule_Assert(fsl->length); | ||
| 271 | RedisModule_ReplyWithLongLong(ctx, fsl->list[--fsl->length]); | ||
| 272 | /* I'm lazy so i'll replicate a potentially blocking command, it shouldn't block in this flow. */ | ||
| 273 | RedisModule_ReplicateVerbatim(ctx); | ||
| 274 | return REDISMODULE_OK; | ||
| 275 | } | ||
| 276 | |||
| 277 | int bpopgt_timeout_callback(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 278 | REDISMODULE_NOT_USED(argv); | ||
| 279 | REDISMODULE_NOT_USED(argc); | ||
| 280 | return RedisModule_ReplyWithSimpleString(ctx, "Request timedout"); | ||
| 281 | } | ||
| 282 | |||
| 283 | void bpopgt_free_privdata(RedisModuleCtx *ctx, void *privdata) { | ||
| 284 | REDISMODULE_NOT_USED(ctx); | ||
| 285 | RedisModule_Free(privdata); | ||
| 286 | } | ||
| 287 | |||
| 288 | /* FSL.BPOPGT <key> <gt> <timeout> - Block clients until list has an element greater than <gt>. | ||
| 289 | * When that happens, unblock client and pop the last element (from the right). */ | ||
| 290 | int fsl_bpopgt(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 291 | if (argc != 4) | ||
| 292 | return RedisModule_WrongArity(ctx); | ||
| 293 | |||
| 294 | long long gt; | ||
| 295 | if (RedisModule_StringToLongLong(argv[2],>) != REDISMODULE_OK) | ||
| 296 | return RedisModule_ReplyWithError(ctx,"ERR invalid integer"); | ||
| 297 | |||
| 298 | long long timeout; | ||
| 299 | if (RedisModule_StringToLongLong(argv[3],&timeout) != REDISMODULE_OK || timeout < 0) | ||
| 300 | return RedisModule_ReplyWithError(ctx,"ERR invalid timeout"); | ||
| 301 | |||
| 302 | fsl_t *fsl; | ||
| 303 | if (!get_fsl(ctx, argv[1], REDISMODULE_WRITE, 0, &fsl, 1)) | ||
| 304 | return REDISMODULE_OK; | ||
| 305 | |||
| 306 | if (!fsl) | ||
| 307 | return RedisModule_ReplyWithError(ctx,"ERR key must exist"); | ||
| 308 | |||
| 309 | if (fsl->list[fsl->length-1] <= gt) { | ||
| 310 | /* We use malloc so the tests in blockedonkeys.tcl can check for memory leaks */ | ||
| 311 | long long *pgt = RedisModule_Alloc(sizeof(long long)); | ||
| 312 | *pgt = gt; | ||
| 313 | RedisModule_BlockClientOnKeysWithFlags( | ||
| 314 | ctx, bpopgt_reply_callback, bpopgt_timeout_callback, | ||
| 315 | bpopgt_free_privdata, timeout, &argv[1], 1, pgt, | ||
| 316 | REDISMODULE_BLOCK_UNBLOCK_DELETED); | ||
| 317 | } else { | ||
| 318 | RedisModule_Assert(fsl->length); | ||
| 319 | RedisModule_ReplyWithLongLong(ctx, fsl->list[--fsl->length]); | ||
| 320 | /* I'm lazy so i'll replicate a potentially blocking command, it shouldn't block in this flow. */ | ||
| 321 | RedisModule_ReplicateVerbatim(ctx); | ||
| 322 | } | ||
| 323 | |||
| 324 | return REDISMODULE_OK; | ||
| 325 | } | ||
| 326 | |||
| 327 | int bpoppush_reply_callback(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 328 | REDISMODULE_NOT_USED(argv); | ||
| 329 | REDISMODULE_NOT_USED(argc); | ||
| 330 | RedisModuleString *src_keyname = RedisModule_GetBlockedClientReadyKey(ctx); | ||
| 331 | RedisModuleString *dst_keyname = RedisModule_GetBlockedClientPrivateData(ctx); | ||
| 332 | |||
| 333 | fsl_t *src; | ||
| 334 | if (!get_fsl(ctx, src_keyname, REDISMODULE_WRITE, 0, &src, 0) || !src) | ||
| 335 | return REDISMODULE_ERR; | ||
| 336 | |||
| 337 | fsl_t *dst; | ||
| 338 | if (!get_fsl(ctx, dst_keyname, REDISMODULE_WRITE, 1, &dst, 0) || !dst) | ||
| 339 | return REDISMODULE_ERR; | ||
| 340 | |||
| 341 | RedisModule_Assert(src->length); | ||
| 342 | long long ele = src->list[--src->length]; | ||
| 343 | dst->list[dst->length++] = ele; | ||
| 344 | RedisModule_SignalKeyAsReady(ctx, dst_keyname); | ||
| 345 | /* I'm lazy so i'll replicate a potentially blocking command, it shouldn't block in this flow. */ | ||
| 346 | RedisModule_ReplicateVerbatim(ctx); | ||
| 347 | return RedisModule_ReplyWithLongLong(ctx, ele); | ||
| 348 | } | ||
| 349 | |||
| 350 | int bpoppush_timeout_callback(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 351 | REDISMODULE_NOT_USED(argv); | ||
| 352 | REDISMODULE_NOT_USED(argc); | ||
| 353 | return RedisModule_ReplyWithSimpleString(ctx, "Request timedout"); | ||
| 354 | } | ||
| 355 | |||
| 356 | void bpoppush_free_privdata(RedisModuleCtx *ctx, void *privdata) { | ||
| 357 | RedisModule_FreeString(ctx, privdata); | ||
| 358 | } | ||
| 359 | |||
| 360 | /* FSL.BPOPPUSH <src> <dst> <timeout> - Block clients until <src> has an element. | ||
| 361 | * When that happens, unblock client, pop the last element from <src> and push it to <dst> | ||
| 362 | * (from the right). */ | ||
| 363 | int fsl_bpoppush(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 364 | if (argc != 4) | ||
| 365 | return RedisModule_WrongArity(ctx); | ||
| 366 | |||
| 367 | long long timeout; | ||
| 368 | if (RedisModule_StringToLongLong(argv[3],&timeout) != REDISMODULE_OK || timeout < 0) | ||
| 369 | return RedisModule_ReplyWithError(ctx,"ERR invalid timeout"); | ||
| 370 | |||
| 371 | fsl_t *src; | ||
| 372 | if (!get_fsl(ctx, argv[1], REDISMODULE_WRITE, 0, &src, 1)) | ||
| 373 | return REDISMODULE_OK; | ||
| 374 | |||
| 375 | if (!src) { | ||
| 376 | /* Retain string for reply callback */ | ||
| 377 | RedisModule_RetainString(ctx, argv[2]); | ||
| 378 | /* Key is empty, we must block */ | ||
| 379 | RedisModule_BlockClientOnKeys(ctx, bpoppush_reply_callback, bpoppush_timeout_callback, | ||
| 380 | bpoppush_free_privdata, timeout, &argv[1], 1, argv[2]); | ||
| 381 | } else { | ||
| 382 | fsl_t *dst; | ||
| 383 | if (!get_fsl(ctx, argv[2], REDISMODULE_WRITE, 1, &dst, 1)) | ||
| 384 | return REDISMODULE_OK; | ||
| 385 | |||
| 386 | RedisModule_Assert(src->length); | ||
| 387 | long long ele = src->list[--src->length]; | ||
| 388 | dst->list[dst->length++] = ele; | ||
| 389 | RedisModule_SignalKeyAsReady(ctx, argv[2]); | ||
| 390 | RedisModule_ReplyWithLongLong(ctx, ele); | ||
| 391 | /* I'm lazy so i'll replicate a potentially blocking command, it shouldn't block in this flow. */ | ||
| 392 | RedisModule_ReplicateVerbatim(ctx); | ||
| 393 | } | ||
| 394 | |||
| 395 | return REDISMODULE_OK; | ||
| 396 | } | ||
| 397 | |||
| 398 | /* FSL.GETALL <key> - Reply with an array containing all elements. */ | ||
| 399 | int fsl_getall(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 400 | if (argc != 2) | ||
| 401 | return RedisModule_WrongArity(ctx); | ||
| 402 | |||
| 403 | fsl_t *fsl; | ||
| 404 | if (!get_fsl(ctx, argv[1], REDISMODULE_READ, 0, &fsl, 1)) | ||
| 405 | return REDISMODULE_OK; | ||
| 406 | |||
| 407 | if (!fsl) | ||
| 408 | return RedisModule_ReplyWithArray(ctx, 0); | ||
| 409 | |||
| 410 | RedisModule_ReplyWithArray(ctx, fsl->length); | ||
| 411 | for (int i = 0; i < fsl->length; i++) | ||
| 412 | RedisModule_ReplyWithLongLong(ctx, fsl->list[i]); | ||
| 413 | return REDISMODULE_OK; | ||
| 414 | } | ||
| 415 | |||
| 416 | /* Callback for blockonkeys_popall */ | ||
| 417 | int blockonkeys_popall_reply_callback(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 418 | REDISMODULE_NOT_USED(argc); | ||
| 419 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE); | ||
| 420 | if (RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_LIST) { | ||
| 421 | RedisModuleString *elem; | ||
| 422 | long len = 0; | ||
| 423 | RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_ARRAY_LEN); | ||
| 424 | while ((elem = RedisModule_ListPop(key, REDISMODULE_LIST_HEAD)) != NULL) { | ||
| 425 | len++; | ||
| 426 | RedisModule_ReplyWithString(ctx, elem); | ||
| 427 | RedisModule_FreeString(ctx, elem); | ||
| 428 | } | ||
| 429 | /* I'm lazy so i'll replicate a potentially blocking command, it shouldn't block in this flow. */ | ||
| 430 | RedisModule_ReplicateVerbatim(ctx); | ||
| 431 | RedisModule_ReplySetArrayLength(ctx, len); | ||
| 432 | } else { | ||
| 433 | RedisModule_ReplyWithError(ctx, "ERR Not a list"); | ||
| 434 | } | ||
| 435 | RedisModule_CloseKey(key); | ||
| 436 | return REDISMODULE_OK; | ||
| 437 | } | ||
| 438 | |||
| 439 | int blockonkeys_popall_timeout_callback(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 440 | REDISMODULE_NOT_USED(argv); | ||
| 441 | REDISMODULE_NOT_USED(argc); | ||
| 442 | return RedisModule_ReplyWithError(ctx, "ERR Timeout"); | ||
| 443 | } | ||
| 444 | |||
| 445 | /* BLOCKONKEYS.POPALL key | ||
| 446 | * | ||
| 447 | * Blocks on an empty key for up to 3 seconds. When unblocked by a list | ||
| 448 | * operation like LPUSH, all the elements are popped and returned. Fails with an | ||
| 449 | * error on timeout. */ | ||
| 450 | int blockonkeys_popall(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 451 | if (argc != 2) | ||
| 452 | return RedisModule_WrongArity(ctx); | ||
| 453 | |||
| 454 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ); | ||
| 455 | if (RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_EMPTY) { | ||
| 456 | RedisModule_BlockClientOnKeys(ctx, blockonkeys_popall_reply_callback, | ||
| 457 | blockonkeys_popall_timeout_callback, | ||
| 458 | NULL, 3000, &argv[1], 1, NULL); | ||
| 459 | } else { | ||
| 460 | RedisModule_ReplyWithError(ctx, "ERR Key not empty"); | ||
| 461 | } | ||
| 462 | RedisModule_CloseKey(key); | ||
| 463 | return REDISMODULE_OK; | ||
| 464 | } | ||
| 465 | |||
| 466 | /* BLOCKONKEYS.LPUSH key val [val ..] | ||
| 467 | * BLOCKONKEYS.LPUSH_UNBLOCK key val [val ..] | ||
| 468 | * | ||
| 469 | * A module equivalent of LPUSH. If the name LPUSH_UNBLOCK is used, | ||
| 470 | * RM_SignalKeyAsReady() is also called. */ | ||
| 471 | int blockonkeys_lpush(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 472 | if (argc < 3) | ||
| 473 | return RedisModule_WrongArity(ctx); | ||
| 474 | |||
| 475 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE); | ||
| 476 | if (RedisModule_KeyType(key) != REDISMODULE_KEYTYPE_EMPTY && | ||
| 477 | RedisModule_KeyType(key) != REDISMODULE_KEYTYPE_LIST) { | ||
| 478 | RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE); | ||
| 479 | } else { | ||
| 480 | for (int i = 2; i < argc; i++) { | ||
| 481 | if (RedisModule_ListPush(key, REDISMODULE_LIST_HEAD, | ||
| 482 | argv[i]) != REDISMODULE_OK) { | ||
| 483 | RedisModule_CloseKey(key); | ||
| 484 | return RedisModule_ReplyWithError(ctx, "ERR Push failed"); | ||
| 485 | } | ||
| 486 | } | ||
| 487 | } | ||
| 488 | RedisModule_CloseKey(key); | ||
| 489 | |||
| 490 | /* signal key as ready if the command is lpush_unblock */ | ||
| 491 | size_t len; | ||
| 492 | const char *str = RedisModule_StringPtrLen(argv[0], &len); | ||
| 493 | if (!strncasecmp(str, "blockonkeys.lpush_unblock", len)) { | ||
| 494 | RedisModule_SignalKeyAsReady(ctx, argv[1]); | ||
| 495 | } | ||
| 496 | RedisModule_ReplicateVerbatim(ctx); | ||
| 497 | return RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 498 | } | ||
| 499 | |||
| 500 | /* Callback for the BLOCKONKEYS.BLPOPN command */ | ||
| 501 | int blockonkeys_blpopn_reply_callback(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 502 | REDISMODULE_NOT_USED(argc); | ||
| 503 | long long n; | ||
| 504 | RedisModule_StringToLongLong(argv[2], &n); | ||
| 505 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE); | ||
| 506 | int result; | ||
| 507 | if (RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_LIST && | ||
| 508 | RedisModule_ValueLength(key) >= (size_t)n) { | ||
| 509 | RedisModule_ReplyWithArray(ctx, n); | ||
| 510 | for (long i = 0; i < n; i++) { | ||
| 511 | RedisModuleString *elem = RedisModule_ListPop(key, REDISMODULE_LIST_HEAD); | ||
| 512 | RedisModule_ReplyWithString(ctx, elem); | ||
| 513 | RedisModule_FreeString(ctx, elem); | ||
| 514 | } | ||
| 515 | /* I'm lazy so i'll replicate a potentially blocking command, it shouldn't block in this flow. */ | ||
| 516 | RedisModule_ReplicateVerbatim(ctx); | ||
| 517 | result = REDISMODULE_OK; | ||
| 518 | } else if (RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_LIST || | ||
| 519 | RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_EMPTY) { | ||
| 520 | const char *module_cmd = RedisModule_StringPtrLen(argv[0], NULL); | ||
| 521 | if (!strcasecmp(module_cmd, "blockonkeys.blpopn_or_unblock")) | ||
| 522 | RedisModule_UnblockClient(RedisModule_GetBlockedClientHandle(ctx), NULL); | ||
| 523 | |||
| 524 | /* continue blocking */ | ||
| 525 | result = REDISMODULE_ERR; | ||
| 526 | } else { | ||
| 527 | result = RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE); | ||
| 528 | } | ||
| 529 | RedisModule_CloseKey(key); | ||
| 530 | return result; | ||
| 531 | } | ||
| 532 | |||
| 533 | int blockonkeys_blpopn_timeout_callback(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 534 | REDISMODULE_NOT_USED(argv); | ||
| 535 | REDISMODULE_NOT_USED(argc); | ||
| 536 | return RedisModule_ReplyWithError(ctx, "ERR Timeout"); | ||
| 537 | } | ||
| 538 | |||
| 539 | int blockonkeys_blpopn_abort_callback(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 540 | REDISMODULE_NOT_USED(argv); | ||
| 541 | REDISMODULE_NOT_USED(argc); | ||
| 542 | return RedisModule_ReplyWithSimpleString(ctx, "Action aborted"); | ||
| 543 | } | ||
| 544 | |||
| 545 | /* BLOCKONKEYS.BLPOPN key N | ||
| 546 | * | ||
| 547 | * Blocks until key has N elements and then pops them or fails after 3 seconds. | ||
| 548 | */ | ||
| 549 | int blockonkeys_blpopn(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 550 | if (argc < 3) return RedisModule_WrongArity(ctx); | ||
| 551 | |||
| 552 | long long n, timeout = 3000LL; | ||
| 553 | if (RedisModule_StringToLongLong(argv[2], &n) != REDISMODULE_OK) { | ||
| 554 | return RedisModule_ReplyWithError(ctx, "ERR Invalid N"); | ||
| 555 | } | ||
| 556 | |||
| 557 | if (argc > 3 ) { | ||
| 558 | if (RedisModule_StringToLongLong(argv[3], &timeout) != REDISMODULE_OK) { | ||
| 559 | return RedisModule_ReplyWithError(ctx, "ERR Invalid timeout value"); | ||
| 560 | } | ||
| 561 | } | ||
| 562 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE); | ||
| 563 | int keytype = RedisModule_KeyType(key); | ||
| 564 | if (keytype != REDISMODULE_KEYTYPE_EMPTY && | ||
| 565 | keytype != REDISMODULE_KEYTYPE_LIST) { | ||
| 566 | RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE); | ||
| 567 | } else if (keytype == REDISMODULE_KEYTYPE_LIST && | ||
| 568 | RedisModule_ValueLength(key) >= (size_t)n) { | ||
| 569 | RedisModule_ReplyWithArray(ctx, n); | ||
| 570 | for (long i = 0; i < n; i++) { | ||
| 571 | RedisModuleString *elem = RedisModule_ListPop(key, REDISMODULE_LIST_HEAD); | ||
| 572 | RedisModule_ReplyWithString(ctx, elem); | ||
| 573 | RedisModule_FreeString(ctx, elem); | ||
| 574 | } | ||
| 575 | /* I'm lazy so i'll replicate a potentially blocking command, it shouldn't block in this flow. */ | ||
| 576 | RedisModule_ReplicateVerbatim(ctx); | ||
| 577 | } else { | ||
| 578 | RedisModule_BlockClientOnKeys(ctx, blockonkeys_blpopn_reply_callback, | ||
| 579 | timeout ? blockonkeys_blpopn_timeout_callback : blockonkeys_blpopn_abort_callback, | ||
| 580 | NULL, timeout, &argv[1], 1, NULL); | ||
| 581 | } | ||
| 582 | RedisModule_CloseKey(key); | ||
| 583 | return REDISMODULE_OK; | ||
| 584 | } | ||
| 585 | |||
| 586 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 587 | REDISMODULE_NOT_USED(argv); | ||
| 588 | REDISMODULE_NOT_USED(argc); | ||
| 589 | |||
| 590 | if (RedisModule_Init(ctx, "blockonkeys", 1, REDISMODULE_APIVER_1)== REDISMODULE_ERR) | ||
| 591 | return REDISMODULE_ERR; | ||
| 592 | |||
| 593 | RedisModuleTypeMethods tm = { | ||
| 594 | .version = REDISMODULE_TYPE_METHOD_VERSION, | ||
| 595 | .rdb_load = fsl_rdb_load, | ||
| 596 | .rdb_save = fsl_rdb_save, | ||
| 597 | .aof_rewrite = fsl_aofrw, | ||
| 598 | .mem_usage = NULL, | ||
| 599 | .free = fsl_free, | ||
| 600 | .digest = NULL, | ||
| 601 | }; | ||
| 602 | |||
| 603 | fsltype = RedisModule_CreateDataType(ctx, "fsltype_t", 0, &tm); | ||
| 604 | if (fsltype == NULL) | ||
| 605 | return REDISMODULE_ERR; | ||
| 606 | |||
| 607 | if (RedisModule_CreateCommand(ctx,"fsl.push",fsl_push,"write",1,1,1) == REDISMODULE_ERR) | ||
| 608 | return REDISMODULE_ERR; | ||
| 609 | |||
| 610 | if (RedisModule_CreateCommand(ctx,"fsl.pushtimer",fsl_pushtimer,"write",1,1,1) == REDISMODULE_ERR) | ||
| 611 | return REDISMODULE_ERR; | ||
| 612 | |||
| 613 | if (RedisModule_CreateCommand(ctx,"fsl.bpop",fsl_bpop,"write",1,1,1) == REDISMODULE_ERR) | ||
| 614 | return REDISMODULE_ERR; | ||
| 615 | |||
| 616 | if (RedisModule_CreateCommand(ctx,"fsl.bpopgt",fsl_bpopgt,"write",1,1,1) == REDISMODULE_ERR) | ||
| 617 | return REDISMODULE_ERR; | ||
| 618 | |||
| 619 | if (RedisModule_CreateCommand(ctx,"fsl.bpoppush",fsl_bpoppush,"write",1,2,1) == REDISMODULE_ERR) | ||
| 620 | return REDISMODULE_ERR; | ||
| 621 | |||
| 622 | if (RedisModule_CreateCommand(ctx,"fsl.getall",fsl_getall,"",1,1,1) == REDISMODULE_ERR) | ||
| 623 | return REDISMODULE_ERR; | ||
| 624 | |||
| 625 | if (RedisModule_CreateCommand(ctx, "blockonkeys.popall", blockonkeys_popall, | ||
| 626 | "write", 1, 1, 1) == REDISMODULE_ERR) | ||
| 627 | return REDISMODULE_ERR; | ||
| 628 | |||
| 629 | if (RedisModule_CreateCommand(ctx, "blockonkeys.lpush", blockonkeys_lpush, | ||
| 630 | "write", 1, 1, 1) == REDISMODULE_ERR) | ||
| 631 | return REDISMODULE_ERR; | ||
| 632 | |||
| 633 | if (RedisModule_CreateCommand(ctx, "blockonkeys.lpush_unblock", blockonkeys_lpush, | ||
| 634 | "write", 1, 1, 1) == REDISMODULE_ERR) | ||
| 635 | return REDISMODULE_ERR; | ||
| 636 | |||
| 637 | if (RedisModule_CreateCommand(ctx, "blockonkeys.blpopn", blockonkeys_blpopn, | ||
| 638 | "write", 1, 1, 1) == REDISMODULE_ERR) | ||
| 639 | return REDISMODULE_ERR; | ||
| 640 | |||
| 641 | if (RedisModule_CreateCommand(ctx, "blockonkeys.blpopn_or_unblock", blockonkeys_blpopn, | ||
| 642 | "write", 1, 1, 1) == REDISMODULE_ERR) | ||
| 643 | return REDISMODULE_ERR; | ||
| 644 | return REDISMODULE_OK; | ||
| 645 | } | ||
diff --git a/examples/redis-unstable/tests/modules/cmdintrospection.c b/examples/redis-unstable/tests/modules/cmdintrospection.c new file mode 100644 index 0000000..48208ff --- /dev/null +++ b/examples/redis-unstable/tests/modules/cmdintrospection.c | |||
| @@ -0,0 +1,226 @@ | |||
| 1 | #include "redismodule.h" | ||
| 2 | |||
| 3 | #define UNUSED(V) ((void) V) | ||
| 4 | |||
| 5 | int cmd_xadd(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 6 | UNUSED(argv); | ||
| 7 | UNUSED(argc); | ||
| 8 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 9 | return REDISMODULE_OK; | ||
| 10 | } | ||
| 11 | |||
| 12 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 13 | REDISMODULE_NOT_USED(argv); | ||
| 14 | REDISMODULE_NOT_USED(argc); | ||
| 15 | if (RedisModule_Init(ctx, "cmdintrospection", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) | ||
| 16 | return REDISMODULE_ERR; | ||
| 17 | |||
| 18 | if (RedisModule_CreateCommand(ctx,"cmdintrospection.xadd",cmd_xadd,"write deny-oom random fast",0,0,0) == REDISMODULE_ERR) | ||
| 19 | return REDISMODULE_ERR; | ||
| 20 | |||
| 21 | RedisModuleCommand *xadd = RedisModule_GetCommand(ctx,"cmdintrospection.xadd"); | ||
| 22 | |||
| 23 | RedisModuleCommandInfo info = { | ||
| 24 | .version = REDISMODULE_COMMAND_INFO_VERSION, | ||
| 25 | .arity = -5, | ||
| 26 | .summary = "Appends a new message to a stream. Creates the key if it doesn't exist.", | ||
| 27 | .since = "5.0.0", | ||
| 28 | .complexity = "O(1) when adding a new entry, O(N) when trimming where N being the number of entries evicted.", | ||
| 29 | .tips = "nondeterministic_output", | ||
| 30 | .history = (RedisModuleCommandHistoryEntry[]){ | ||
| 31 | /* NOTE: All versions specified should be the module's versions, not | ||
| 32 | * Redis'! We use Redis versions in this example for the purpose of | ||
| 33 | * testing (comparing the output with the output of the vanilla | ||
| 34 | * XADD). */ | ||
| 35 | {"6.2.0", "Added the `NOMKSTREAM` option, `MINID` trimming strategy and the `LIMIT` option."}, | ||
| 36 | {"7.0.0", "Added support for the `<ms>-*` explicit ID form."}, | ||
| 37 | {"8.2.0", "Added the `KEEPREF`, `DELREF` and `ACKED` options."}, | ||
| 38 | {0} | ||
| 39 | }, | ||
| 40 | .key_specs = (RedisModuleCommandKeySpec[]){ | ||
| 41 | { | ||
| 42 | .notes = "UPDATE instead of INSERT because of the optional trimming feature", | ||
| 43 | .flags = REDISMODULE_CMD_KEY_RW | REDISMODULE_CMD_KEY_UPDATE, | ||
| 44 | .begin_search_type = REDISMODULE_KSPEC_BS_INDEX, | ||
| 45 | .bs.index.pos = 1, | ||
| 46 | .find_keys_type = REDISMODULE_KSPEC_FK_RANGE, | ||
| 47 | .fk.range = {0,1,0} | ||
| 48 | }, | ||
| 49 | {0} | ||
| 50 | }, | ||
| 51 | .args = (RedisModuleCommandArg[]){ | ||
| 52 | { | ||
| 53 | .name = "key", | ||
| 54 | .type = REDISMODULE_ARG_TYPE_KEY, | ||
| 55 | .key_spec_index = 0 | ||
| 56 | }, | ||
| 57 | { | ||
| 58 | .name = "nomkstream", | ||
| 59 | .type = REDISMODULE_ARG_TYPE_PURE_TOKEN, | ||
| 60 | .token = "NOMKSTREAM", | ||
| 61 | .since = "6.2.0", | ||
| 62 | .flags = REDISMODULE_CMD_ARG_OPTIONAL | ||
| 63 | }, | ||
| 64 | { | ||
| 65 | .name = "condition", | ||
| 66 | .type = REDISMODULE_ARG_TYPE_ONEOF, | ||
| 67 | .flags = REDISMODULE_CMD_ARG_OPTIONAL, | ||
| 68 | .subargs = (RedisModuleCommandArg[]){ | ||
| 69 | { | ||
| 70 | .name = "keepref", | ||
| 71 | .type = REDISMODULE_ARG_TYPE_PURE_TOKEN, | ||
| 72 | .token = "KEEPREF" | ||
| 73 | }, | ||
| 74 | { | ||
| 75 | .name = "delref", | ||
| 76 | .type = REDISMODULE_ARG_TYPE_PURE_TOKEN, | ||
| 77 | .token = "DELREF" | ||
| 78 | }, | ||
| 79 | { | ||
| 80 | .name = "acked", | ||
| 81 | .type = REDISMODULE_ARG_TYPE_PURE_TOKEN, | ||
| 82 | .token = "ACKED" | ||
| 83 | }, | ||
| 84 | {0} | ||
| 85 | } | ||
| 86 | }, | ||
| 87 | { | ||
| 88 | .name = "idmp", | ||
| 89 | .type = REDISMODULE_ARG_TYPE_ONEOF, | ||
| 90 | .flags = REDISMODULE_CMD_ARG_OPTIONAL, | ||
| 91 | .subargs = (RedisModuleCommandArg[]){ | ||
| 92 | { | ||
| 93 | .name = "idmpauto-with-pid", | ||
| 94 | .type = REDISMODULE_ARG_TYPE_BLOCK, | ||
| 95 | .subargs = (RedisModuleCommandArg[]){ | ||
| 96 | { | ||
| 97 | .name = "idmpauto-token", | ||
| 98 | .type = REDISMODULE_ARG_TYPE_PURE_TOKEN, | ||
| 99 | .token = "IDMPAUTO" | ||
| 100 | }, | ||
| 101 | { | ||
| 102 | .name = "pid", | ||
| 103 | .type = REDISMODULE_ARG_TYPE_STRING, | ||
| 104 | }, | ||
| 105 | {0} | ||
| 106 | } | ||
| 107 | }, | ||
| 108 | { | ||
| 109 | .name = "idmp-with-pid-iid", | ||
| 110 | .type = REDISMODULE_ARG_TYPE_BLOCK, | ||
| 111 | .subargs = (RedisModuleCommandArg[]){ | ||
| 112 | { | ||
| 113 | .name = "idmp-token", | ||
| 114 | .type = REDISMODULE_ARG_TYPE_PURE_TOKEN, | ||
| 115 | .token = "IDMP" | ||
| 116 | }, | ||
| 117 | { | ||
| 118 | .name = "pid", | ||
| 119 | .type = REDISMODULE_ARG_TYPE_STRING, | ||
| 120 | }, | ||
| 121 | { | ||
| 122 | .name = "iid", | ||
| 123 | .type = REDISMODULE_ARG_TYPE_STRING, | ||
| 124 | }, | ||
| 125 | {0} | ||
| 126 | } | ||
| 127 | }, | ||
| 128 | {0} | ||
| 129 | } | ||
| 130 | }, | ||
| 131 | { | ||
| 132 | .name = "trim", | ||
| 133 | .type = REDISMODULE_ARG_TYPE_BLOCK, | ||
| 134 | .flags = REDISMODULE_CMD_ARG_OPTIONAL, | ||
| 135 | .subargs = (RedisModuleCommandArg[]){ | ||
| 136 | { | ||
| 137 | .name = "strategy", | ||
| 138 | .type = REDISMODULE_ARG_TYPE_ONEOF, | ||
| 139 | .subargs = (RedisModuleCommandArg[]){ | ||
| 140 | { | ||
| 141 | .name = "maxlen", | ||
| 142 | .type = REDISMODULE_ARG_TYPE_PURE_TOKEN, | ||
| 143 | .token = "MAXLEN", | ||
| 144 | }, | ||
| 145 | { | ||
| 146 | .name = "minid", | ||
| 147 | .type = REDISMODULE_ARG_TYPE_PURE_TOKEN, | ||
| 148 | .token = "MINID", | ||
| 149 | .since = "6.2.0", | ||
| 150 | }, | ||
| 151 | {0} | ||
| 152 | } | ||
| 153 | }, | ||
| 154 | { | ||
| 155 | .name = "operator", | ||
| 156 | .type = REDISMODULE_ARG_TYPE_ONEOF, | ||
| 157 | .flags = REDISMODULE_CMD_ARG_OPTIONAL, | ||
| 158 | .subargs = (RedisModuleCommandArg[]){ | ||
| 159 | { | ||
| 160 | .name = "equal", | ||
| 161 | .type = REDISMODULE_ARG_TYPE_PURE_TOKEN, | ||
| 162 | .token = "=" | ||
| 163 | }, | ||
| 164 | { | ||
| 165 | .name = "approximately", | ||
| 166 | .type = REDISMODULE_ARG_TYPE_PURE_TOKEN, | ||
| 167 | .token = "~" | ||
| 168 | }, | ||
| 169 | {0} | ||
| 170 | } | ||
| 171 | }, | ||
| 172 | { | ||
| 173 | .name = "threshold", | ||
| 174 | .type = REDISMODULE_ARG_TYPE_STRING, | ||
| 175 | .display_text = "threshold" /* Just for coverage, doesn't have a visible effect */ | ||
| 176 | }, | ||
| 177 | { | ||
| 178 | .name = "count", | ||
| 179 | .type = REDISMODULE_ARG_TYPE_INTEGER, | ||
| 180 | .token = "LIMIT", | ||
| 181 | .since = "6.2.0", | ||
| 182 | .flags = REDISMODULE_CMD_ARG_OPTIONAL | ||
| 183 | }, | ||
| 184 | {0} | ||
| 185 | } | ||
| 186 | }, | ||
| 187 | { | ||
| 188 | .name = "id-selector", | ||
| 189 | .type = REDISMODULE_ARG_TYPE_ONEOF, | ||
| 190 | .subargs = (RedisModuleCommandArg[]){ | ||
| 191 | { | ||
| 192 | .name = "auto-id", | ||
| 193 | .type = REDISMODULE_ARG_TYPE_PURE_TOKEN, | ||
| 194 | .token = "*" | ||
| 195 | }, | ||
| 196 | { | ||
| 197 | .name = "id", | ||
| 198 | .type = REDISMODULE_ARG_TYPE_STRING, | ||
| 199 | }, | ||
| 200 | {0} | ||
| 201 | } | ||
| 202 | }, | ||
| 203 | { | ||
| 204 | .name = "data", | ||
| 205 | .type = REDISMODULE_ARG_TYPE_BLOCK, | ||
| 206 | .flags = REDISMODULE_CMD_ARG_MULTIPLE, | ||
| 207 | .subargs = (RedisModuleCommandArg[]){ | ||
| 208 | { | ||
| 209 | .name = "field", | ||
| 210 | .type = REDISMODULE_ARG_TYPE_STRING, | ||
| 211 | }, | ||
| 212 | { | ||
| 213 | .name = "value", | ||
| 214 | .type = REDISMODULE_ARG_TYPE_STRING, | ||
| 215 | }, | ||
| 216 | {0} | ||
| 217 | } | ||
| 218 | }, | ||
| 219 | {0} | ||
| 220 | } | ||
| 221 | }; | ||
| 222 | if (RedisModule_SetCommandInfo(xadd, &info) == REDISMODULE_ERR) | ||
| 223 | return REDISMODULE_ERR; | ||
| 224 | |||
| 225 | return REDISMODULE_OK; | ||
| 226 | } | ||
diff --git a/examples/redis-unstable/tests/modules/commandfilter.c b/examples/redis-unstable/tests/modules/commandfilter.c new file mode 100644 index 0000000..333b263 --- /dev/null +++ b/examples/redis-unstable/tests/modules/commandfilter.c | |||
| @@ -0,0 +1,251 @@ | |||
| 1 | #include "redismodule.h" | ||
| 2 | |||
| 3 | #include <string.h> | ||
| 4 | #include <strings.h> | ||
| 5 | |||
| 6 | static RedisModuleString *log_key_name; | ||
| 7 | |||
| 8 | static const char log_command_name[] = "commandfilter.log"; | ||
| 9 | static const char ping_command_name[] = "commandfilter.ping"; | ||
| 10 | static const char retained_command_name[] = "commandfilter.retained"; | ||
| 11 | static const char unregister_command_name[] = "commandfilter.unregister"; | ||
| 12 | static const char unfiltered_clientid_name[] = "unfilter_clientid"; | ||
| 13 | static int in_log_command = 0; | ||
| 14 | |||
| 15 | unsigned long long unfiltered_clientid = 0; | ||
| 16 | |||
| 17 | static RedisModuleCommandFilter *filter, *filter1; | ||
| 18 | static RedisModuleString *retained; | ||
| 19 | |||
| 20 | int CommandFilter_UnregisterCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 21 | { | ||
| 22 | (void) argc; | ||
| 23 | (void) argv; | ||
| 24 | |||
| 25 | RedisModule_ReplyWithLongLong(ctx, | ||
| 26 | RedisModule_UnregisterCommandFilter(ctx, filter)); | ||
| 27 | |||
| 28 | return REDISMODULE_OK; | ||
| 29 | } | ||
| 30 | |||
| 31 | int CommandFilter_PingCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 32 | { | ||
| 33 | (void) argc; | ||
| 34 | (void) argv; | ||
| 35 | |||
| 36 | RedisModuleCallReply *reply = RedisModule_Call(ctx, "ping", "c", "@log"); | ||
| 37 | if (reply) { | ||
| 38 | RedisModule_ReplyWithCallReply(ctx, reply); | ||
| 39 | RedisModule_FreeCallReply(reply); | ||
| 40 | } else { | ||
| 41 | RedisModule_ReplyWithSimpleString(ctx, "Unknown command or invalid arguments"); | ||
| 42 | } | ||
| 43 | |||
| 44 | return REDISMODULE_OK; | ||
| 45 | } | ||
| 46 | |||
| 47 | int CommandFilter_Retained(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 48 | { | ||
| 49 | (void) argc; | ||
| 50 | (void) argv; | ||
| 51 | |||
| 52 | if (retained) { | ||
| 53 | RedisModule_ReplyWithString(ctx, retained); | ||
| 54 | } else { | ||
| 55 | RedisModule_ReplyWithNull(ctx); | ||
| 56 | } | ||
| 57 | |||
| 58 | return REDISMODULE_OK; | ||
| 59 | } | ||
| 60 | |||
| 61 | int CommandFilter_LogCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 62 | { | ||
| 63 | RedisModuleString *s = RedisModule_CreateString(ctx, "", 0); | ||
| 64 | |||
| 65 | int i; | ||
| 66 | for (i = 1; i < argc; i++) { | ||
| 67 | size_t arglen; | ||
| 68 | const char *arg = RedisModule_StringPtrLen(argv[i], &arglen); | ||
| 69 | |||
| 70 | if (i > 1) RedisModule_StringAppendBuffer(ctx, s, " ", 1); | ||
| 71 | RedisModule_StringAppendBuffer(ctx, s, arg, arglen); | ||
| 72 | } | ||
| 73 | |||
| 74 | RedisModuleKey *log = RedisModule_OpenKey(ctx, log_key_name, REDISMODULE_WRITE|REDISMODULE_READ); | ||
| 75 | RedisModule_ListPush(log, REDISMODULE_LIST_HEAD, s); | ||
| 76 | RedisModule_CloseKey(log); | ||
| 77 | RedisModule_FreeString(ctx, s); | ||
| 78 | |||
| 79 | in_log_command = 1; | ||
| 80 | |||
| 81 | size_t cmdlen; | ||
| 82 | const char *cmdname = RedisModule_StringPtrLen(argv[1], &cmdlen); | ||
| 83 | RedisModuleCallReply *reply = RedisModule_Call(ctx, cmdname, "v", &argv[2], (size_t)argc - 2); | ||
| 84 | if (reply) { | ||
| 85 | RedisModule_ReplyWithCallReply(ctx, reply); | ||
| 86 | RedisModule_FreeCallReply(reply); | ||
| 87 | } else { | ||
| 88 | RedisModule_ReplyWithSimpleString(ctx, "Unknown command or invalid arguments"); | ||
| 89 | } | ||
| 90 | |||
| 91 | in_log_command = 0; | ||
| 92 | |||
| 93 | return REDISMODULE_OK; | ||
| 94 | } | ||
| 95 | |||
| 96 | int CommandFilter_UnfilteredClientId(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 97 | { | ||
| 98 | if (argc < 2) | ||
| 99 | return RedisModule_WrongArity(ctx); | ||
| 100 | |||
| 101 | long long id; | ||
| 102 | if (RedisModule_StringToLongLong(argv[1], &id) != REDISMODULE_OK) { | ||
| 103 | RedisModule_ReplyWithError(ctx, "invalid client id"); | ||
| 104 | return REDISMODULE_OK; | ||
| 105 | } | ||
| 106 | if (id < 0) { | ||
| 107 | RedisModule_ReplyWithError(ctx, "invalid client id"); | ||
| 108 | return REDISMODULE_OK; | ||
| 109 | } | ||
| 110 | |||
| 111 | unfiltered_clientid = id; | ||
| 112 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 113 | return REDISMODULE_OK; | ||
| 114 | } | ||
| 115 | |||
| 116 | /* Filter to protect against Bug #11894 reappearing | ||
| 117 | * | ||
| 118 | * ensures that the filter is only run the first time through, and not on reprocessing | ||
| 119 | */ | ||
| 120 | void CommandFilter_BlmoveSwap(RedisModuleCommandFilterCtx *filter) | ||
| 121 | { | ||
| 122 | if (RedisModule_CommandFilterArgsCount(filter) != 6) | ||
| 123 | return; | ||
| 124 | |||
| 125 | RedisModuleString *arg = RedisModule_CommandFilterArgGet(filter, 0); | ||
| 126 | size_t arg_len; | ||
| 127 | const char *arg_str = RedisModule_StringPtrLen(arg, &arg_len); | ||
| 128 | |||
| 129 | if (arg_len != 6 || strncmp(arg_str, "blmove", 6)) | ||
| 130 | return; | ||
| 131 | |||
| 132 | /* | ||
| 133 | * Swapping directional args (right/left) from source and destination. | ||
| 134 | * need to hold here, can't push into the ArgReplace func, as it will cause other to freed -> use after free | ||
| 135 | */ | ||
| 136 | RedisModuleString *dir1 = RedisModule_HoldString(NULL, RedisModule_CommandFilterArgGet(filter, 3)); | ||
| 137 | RedisModuleString *dir2 = RedisModule_HoldString(NULL, RedisModule_CommandFilterArgGet(filter, 4)); | ||
| 138 | RedisModule_CommandFilterArgReplace(filter, 3, dir2); | ||
| 139 | RedisModule_CommandFilterArgReplace(filter, 4, dir1); | ||
| 140 | } | ||
| 141 | |||
| 142 | void CommandFilter_CommandFilter(RedisModuleCommandFilterCtx *filter) | ||
| 143 | { | ||
| 144 | unsigned long long id = RedisModule_CommandFilterGetClientId(filter); | ||
| 145 | if (id == unfiltered_clientid) return; | ||
| 146 | |||
| 147 | if (in_log_command) return; /* don't process our own RM_Call() from CommandFilter_LogCommand() */ | ||
| 148 | |||
| 149 | /* Fun manipulations: | ||
| 150 | * - Remove @delme | ||
| 151 | * - Replace @replaceme | ||
| 152 | * - Append @insertbefore or @insertafter | ||
| 153 | * - Prefix with Log command if @log encountered | ||
| 154 | */ | ||
| 155 | int log = 0; | ||
| 156 | int pos = 0; | ||
| 157 | while (pos < RedisModule_CommandFilterArgsCount(filter)) { | ||
| 158 | const RedisModuleString *arg = RedisModule_CommandFilterArgGet(filter, pos); | ||
| 159 | size_t arg_len; | ||
| 160 | const char *arg_str = RedisModule_StringPtrLen(arg, &arg_len); | ||
| 161 | |||
| 162 | if (arg_len == 6 && !memcmp(arg_str, "@delme", 6)) { | ||
| 163 | RedisModule_CommandFilterArgDelete(filter, pos); | ||
| 164 | continue; | ||
| 165 | } | ||
| 166 | if (arg_len == 10 && !memcmp(arg_str, "@replaceme", 10)) { | ||
| 167 | RedisModule_CommandFilterArgReplace(filter, pos, | ||
| 168 | RedisModule_CreateString(NULL, "--replaced--", 12)); | ||
| 169 | } else if (arg_len == 13 && !memcmp(arg_str, "@insertbefore", 13)) { | ||
| 170 | RedisModule_CommandFilterArgInsert(filter, pos, | ||
| 171 | RedisModule_CreateString(NULL, "--inserted-before--", 19)); | ||
| 172 | pos++; | ||
| 173 | } else if (arg_len == 12 && !memcmp(arg_str, "@insertafter", 12)) { | ||
| 174 | RedisModule_CommandFilterArgInsert(filter, pos + 1, | ||
| 175 | RedisModule_CreateString(NULL, "--inserted-after--", 18)); | ||
| 176 | pos++; | ||
| 177 | } else if (arg_len == 7 && !memcmp(arg_str, "@retain", 7)) { | ||
| 178 | if (retained) RedisModule_FreeString(NULL, retained); | ||
| 179 | retained = RedisModule_CommandFilterArgGet(filter, pos + 1); | ||
| 180 | RedisModule_RetainString(NULL, retained); | ||
| 181 | pos++; | ||
| 182 | } else if (arg_len == 4 && !memcmp(arg_str, "@log", 4)) { | ||
| 183 | log = 1; | ||
| 184 | } | ||
| 185 | pos++; | ||
| 186 | } | ||
| 187 | |||
| 188 | if (log) RedisModule_CommandFilterArgInsert(filter, 0, | ||
| 189 | RedisModule_CreateString(NULL, log_command_name, sizeof(log_command_name)-1)); | ||
| 190 | } | ||
| 191 | |||
| 192 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 193 | if (RedisModule_Init(ctx,"commandfilter",1,REDISMODULE_APIVER_1) | ||
| 194 | == REDISMODULE_ERR) return REDISMODULE_ERR; | ||
| 195 | |||
| 196 | if (argc != 2 && argc != 3) { | ||
| 197 | RedisModule_Log(ctx, "warning", "Log key name not specified"); | ||
| 198 | return REDISMODULE_ERR; | ||
| 199 | } | ||
| 200 | |||
| 201 | long long noself = 0; | ||
| 202 | log_key_name = RedisModule_CreateStringFromString(ctx, argv[0]); | ||
| 203 | RedisModule_StringToLongLong(argv[1], &noself); | ||
| 204 | retained = NULL; | ||
| 205 | |||
| 206 | if (RedisModule_CreateCommand(ctx,log_command_name, | ||
| 207 | CommandFilter_LogCommand,"write deny-oom",1,1,1) == REDISMODULE_ERR) | ||
| 208 | return REDISMODULE_ERR; | ||
| 209 | |||
| 210 | if (RedisModule_CreateCommand(ctx,ping_command_name, | ||
| 211 | CommandFilter_PingCommand,"deny-oom",1,1,1) == REDISMODULE_ERR) | ||
| 212 | return REDISMODULE_ERR; | ||
| 213 | |||
| 214 | if (RedisModule_CreateCommand(ctx,retained_command_name, | ||
| 215 | CommandFilter_Retained,"readonly",1,1,1) == REDISMODULE_ERR) | ||
| 216 | return REDISMODULE_ERR; | ||
| 217 | |||
| 218 | if (RedisModule_CreateCommand(ctx,unregister_command_name, | ||
| 219 | CommandFilter_UnregisterCommand,"write deny-oom",1,1,1) == REDISMODULE_ERR) | ||
| 220 | return REDISMODULE_ERR; | ||
| 221 | |||
| 222 | if (RedisModule_CreateCommand(ctx, unfiltered_clientid_name, | ||
| 223 | CommandFilter_UnfilteredClientId, "admin", 1,1,1) == REDISMODULE_ERR) | ||
| 224 | return REDISMODULE_ERR; | ||
| 225 | |||
| 226 | if ((filter = RedisModule_RegisterCommandFilter(ctx, CommandFilter_CommandFilter, | ||
| 227 | noself ? REDISMODULE_CMDFILTER_NOSELF : 0)) | ||
| 228 | == NULL) return REDISMODULE_ERR; | ||
| 229 | |||
| 230 | if ((filter1 = RedisModule_RegisterCommandFilter(ctx, CommandFilter_BlmoveSwap, 0)) == NULL) | ||
| 231 | return REDISMODULE_ERR; | ||
| 232 | |||
| 233 | if (argc == 3) { | ||
| 234 | const char *ptr = RedisModule_StringPtrLen(argv[2], NULL); | ||
| 235 | if (!strcasecmp(ptr, "noload")) { | ||
| 236 | /* This is a hint that we return ERR at the last moment of OnLoad. */ | ||
| 237 | RedisModule_FreeString(ctx, log_key_name); | ||
| 238 | if (retained) RedisModule_FreeString(NULL, retained); | ||
| 239 | return REDISMODULE_ERR; | ||
| 240 | } | ||
| 241 | } | ||
| 242 | |||
| 243 | return REDISMODULE_OK; | ||
| 244 | } | ||
| 245 | |||
| 246 | int RedisModule_OnUnload(RedisModuleCtx *ctx) { | ||
| 247 | RedisModule_FreeString(ctx, log_key_name); | ||
| 248 | if (retained) RedisModule_FreeString(NULL, retained); | ||
| 249 | |||
| 250 | return REDISMODULE_OK; | ||
| 251 | } | ||
diff --git a/examples/redis-unstable/tests/modules/configaccess.c b/examples/redis-unstable/tests/modules/configaccess.c new file mode 100644 index 0000000..7bc8a48 --- /dev/null +++ b/examples/redis-unstable/tests/modules/configaccess.c | |||
| @@ -0,0 +1,353 @@ | |||
| 1 | #include "redismodule.h" | ||
| 2 | #include <assert.h> | ||
| 3 | #include <string.h> | ||
| 4 | |||
| 5 | /* See moduleconfigs.c for registering module configs. We need to register some | ||
| 6 | * module configs with our module in order to test the interaction between | ||
| 7 | * module configs and the RM_Get/Set*Config APIs. */ | ||
| 8 | int configaccess_bool; | ||
| 9 | |||
| 10 | int getBoolConfigCommand(const char *name, void *privdata) { | ||
| 11 | REDISMODULE_NOT_USED(name); | ||
| 12 | return (*(int *)privdata); | ||
| 13 | } | ||
| 14 | |||
| 15 | int setBoolConfigCommand(const char *name, int new, void *privdata, RedisModuleString **err) { | ||
| 16 | REDISMODULE_NOT_USED(name); | ||
| 17 | REDISMODULE_NOT_USED(err); | ||
| 18 | *(int *)privdata = new; | ||
| 19 | return REDISMODULE_OK; | ||
| 20 | } | ||
| 21 | |||
| 22 | /* Test command for RM_GetConfigType */ | ||
| 23 | int TestGetConfigType_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 24 | if (argc != 2) { | ||
| 25 | return RedisModule_WrongArity(ctx); | ||
| 26 | } | ||
| 27 | |||
| 28 | size_t len; | ||
| 29 | const char *config_name = RedisModule_StringPtrLen(argv[1], &len); | ||
| 30 | |||
| 31 | RedisModuleConfigType type; | ||
| 32 | int res = RedisModule_ConfigGetType(config_name, &type); | ||
| 33 | if (res == REDISMODULE_ERR) { | ||
| 34 | RedisModule_ReplyWithError(ctx, "ERR Config does not exist"); | ||
| 35 | return REDISMODULE_ERR; | ||
| 36 | } | ||
| 37 | |||
| 38 | const char *type_str; | ||
| 39 | switch (type) { | ||
| 40 | case REDISMODULE_CONFIG_TYPE_BOOL: | ||
| 41 | type_str = "bool"; | ||
| 42 | break; | ||
| 43 | case REDISMODULE_CONFIG_TYPE_NUMERIC: | ||
| 44 | type_str = "numeric"; | ||
| 45 | break; | ||
| 46 | case REDISMODULE_CONFIG_TYPE_STRING: | ||
| 47 | type_str = "string"; | ||
| 48 | break; | ||
| 49 | case REDISMODULE_CONFIG_TYPE_ENUM: | ||
| 50 | type_str = "enum"; | ||
| 51 | break; | ||
| 52 | default: | ||
| 53 | assert(0); | ||
| 54 | break; | ||
| 55 | } | ||
| 56 | |||
| 57 | RedisModule_ReplyWithSimpleString(ctx, type_str); | ||
| 58 | return REDISMODULE_OK; | ||
| 59 | } | ||
| 60 | |||
| 61 | /* Test command for config iteration */ | ||
| 62 | int TestConfigIteration_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 63 | REDISMODULE_NOT_USED(argv); | ||
| 64 | |||
| 65 | if (argc > 2) { | ||
| 66 | return RedisModule_WrongArity(ctx); | ||
| 67 | } | ||
| 68 | |||
| 69 | const char *pattern = NULL; | ||
| 70 | if (argc == 2) { | ||
| 71 | pattern = RedisModule_StringPtrLen(argv[1], NULL); | ||
| 72 | } | ||
| 73 | |||
| 74 | RedisModuleConfigIterator *iter = RedisModule_ConfigIteratorCreate(ctx, pattern); | ||
| 75 | if (!iter) { | ||
| 76 | RedisModule_ReplyWithError(ctx, "ERR Failed to get config iterator"); | ||
| 77 | return REDISMODULE_ERR; | ||
| 78 | } | ||
| 79 | |||
| 80 | /* Start array reply for the configs */ | ||
| 81 | RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_ARRAY_LEN); | ||
| 82 | |||
| 83 | /* Iterate through the dictionary */ | ||
| 84 | const char *config_name = NULL; | ||
| 85 | long count = 0; | ||
| 86 | while ((config_name = RedisModule_ConfigIteratorNext(iter)) != NULL) { | ||
| 87 | RedisModuleString *value = NULL; | ||
| 88 | RedisModule_ConfigGet(ctx, config_name, &value); | ||
| 89 | |||
| 90 | RedisModule_ReplyWithArray(ctx, 2); | ||
| 91 | RedisModule_ReplyWithStringBuffer(ctx, config_name, strlen(config_name)); | ||
| 92 | RedisModule_ReplyWithString(ctx, value); | ||
| 93 | |||
| 94 | RedisModule_FreeString(ctx, value); | ||
| 95 | ++count; | ||
| 96 | } | ||
| 97 | RedisModule_ReplySetArrayLength(ctx, count); | ||
| 98 | |||
| 99 | /* Free the iterator */ | ||
| 100 | RedisModule_ConfigIteratorRelease(ctx, iter); | ||
| 101 | |||
| 102 | return REDISMODULE_OK; | ||
| 103 | } | ||
| 104 | |||
| 105 | /* Test command for RM_GetBoolConfig */ | ||
| 106 | int TestGetBoolConfig_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 107 | if (argc != 2) { | ||
| 108 | return RedisModule_WrongArity(ctx); | ||
| 109 | } | ||
| 110 | |||
| 111 | size_t len; | ||
| 112 | const char *config_name = RedisModule_StringPtrLen(argv[1], &len); | ||
| 113 | |||
| 114 | int value; | ||
| 115 | if (RedisModule_ConfigGetBool(ctx, config_name, &value) == REDISMODULE_ERR) { | ||
| 116 | RedisModule_ReplyWithError(ctx, "ERR Failed to get bool config"); | ||
| 117 | return REDISMODULE_ERR; | ||
| 118 | } | ||
| 119 | |||
| 120 | RedisModule_ReplyWithLongLong(ctx, value); | ||
| 121 | return REDISMODULE_OK; | ||
| 122 | } | ||
| 123 | |||
| 124 | /* Test command for RM_GetNumericConfig */ | ||
| 125 | int TestGetNumericConfig_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 126 | if (argc != 2) { | ||
| 127 | return RedisModule_WrongArity(ctx); | ||
| 128 | } | ||
| 129 | |||
| 130 | size_t len; | ||
| 131 | const char *config_name = RedisModule_StringPtrLen(argv[1], &len); | ||
| 132 | |||
| 133 | long long value; | ||
| 134 | if (RedisModule_ConfigGetNumeric(ctx, config_name, &value) == REDISMODULE_ERR) { | ||
| 135 | RedisModule_ReplyWithError(ctx, "ERR Failed to get numeric config"); | ||
| 136 | return REDISMODULE_ERR; | ||
| 137 | } | ||
| 138 | |||
| 139 | RedisModule_ReplyWithLongLong(ctx, value); | ||
| 140 | return REDISMODULE_OK; | ||
| 141 | } | ||
| 142 | |||
| 143 | /* Test command for RM_GetConfig */ | ||
| 144 | int TestGetConfig_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 145 | if (argc != 2) { | ||
| 146 | return RedisModule_WrongArity(ctx); | ||
| 147 | } | ||
| 148 | |||
| 149 | size_t len; | ||
| 150 | const char *config_name = RedisModule_StringPtrLen(argv[1], &len); | ||
| 151 | |||
| 152 | RedisModuleString *value; | ||
| 153 | if (RedisModule_ConfigGet(ctx, config_name, &value) == REDISMODULE_ERR) { | ||
| 154 | RedisModule_ReplyWithError(ctx, "ERR Failed to get string config"); | ||
| 155 | return REDISMODULE_ERR; | ||
| 156 | } | ||
| 157 | |||
| 158 | RedisModule_ReplyWithString(ctx, value); | ||
| 159 | RedisModule_FreeString(ctx,value); | ||
| 160 | return REDISMODULE_OK; | ||
| 161 | } | ||
| 162 | |||
| 163 | /* Test command for RM_GetEnumConfig */ | ||
| 164 | int TestGetEnumConfig_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 165 | if (argc != 2) { | ||
| 166 | return RedisModule_WrongArity(ctx); | ||
| 167 | } | ||
| 168 | |||
| 169 | size_t len; | ||
| 170 | const char *config_name = RedisModule_StringPtrLen(argv[1], &len); | ||
| 171 | |||
| 172 | RedisModuleString *value; | ||
| 173 | if (RedisModule_ConfigGetEnum(ctx, config_name, &value) == REDISMODULE_ERR) { | ||
| 174 | RedisModule_ReplyWithError(ctx, "ERR Failed to get enum name config"); | ||
| 175 | return REDISMODULE_ERR; | ||
| 176 | } | ||
| 177 | |||
| 178 | RedisModule_ReplyWithString(ctx, value); | ||
| 179 | RedisModule_Free(value); | ||
| 180 | return REDISMODULE_OK; | ||
| 181 | } | ||
| 182 | |||
| 183 | /* Test command for RM_SetBoolConfig */ | ||
| 184 | int TestSetBoolConfig_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 185 | if (argc != 3) { | ||
| 186 | return RedisModule_WrongArity(ctx); | ||
| 187 | } | ||
| 188 | |||
| 189 | size_t name_len, value_len; | ||
| 190 | const char *config_name = RedisModule_StringPtrLen(argv[1], &name_len); | ||
| 191 | const char *config_value = RedisModule_StringPtrLen(argv[2], &value_len); | ||
| 192 | |||
| 193 | int bool_value; | ||
| 194 | if (!strcasecmp(config_value, "yes")) { | ||
| 195 | bool_value = 1; | ||
| 196 | } else if (!strcasecmp(config_value, "no")) { | ||
| 197 | bool_value = 0; | ||
| 198 | } else { | ||
| 199 | bool_value = -1; | ||
| 200 | } | ||
| 201 | |||
| 202 | RedisModuleString *error = NULL; | ||
| 203 | int result = RedisModule_ConfigSetBool(ctx, config_name, bool_value, &error); | ||
| 204 | if (result == REDISMODULE_ERR) { | ||
| 205 | RedisModule_ReplyWithErrorFormat(ctx, "ERR Failed to set bool config %s: %s", config_name, RedisModule_StringPtrLen(error, NULL)); | ||
| 206 | RedisModule_FreeString(ctx, error); | ||
| 207 | return REDISMODULE_ERR; | ||
| 208 | } | ||
| 209 | |||
| 210 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 211 | return REDISMODULE_OK; | ||
| 212 | } | ||
| 213 | |||
| 214 | /* Test command for RM_SetNumericConfig */ | ||
| 215 | int TestSetNumericConfig_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 216 | if (argc != 3) { | ||
| 217 | return RedisModule_WrongArity(ctx); | ||
| 218 | } | ||
| 219 | |||
| 220 | size_t name_len; | ||
| 221 | const char *config_name = RedisModule_StringPtrLen(argv[1], &name_len); | ||
| 222 | |||
| 223 | long long value; | ||
| 224 | const char *value_str= RedisModule_StringPtrLen(argv[2], NULL); | ||
| 225 | if (value_str[0] == '-') { | ||
| 226 | if (RedisModule_StringToLongLong(argv[2], &value) != REDISMODULE_OK) { | ||
| 227 | RedisModule_ReplyWithError(ctx, "ERR Invalid numeric value"); | ||
| 228 | return REDISMODULE_ERR; | ||
| 229 | } | ||
| 230 | } else { | ||
| 231 | if (RedisModule_StringToULongLong(argv[2], (unsigned long long*)&value) != REDISMODULE_OK) { | ||
| 232 | RedisModule_ReplyWithError(ctx, "ERR Invalid numeric value"); | ||
| 233 | return REDISMODULE_ERR; | ||
| 234 | } | ||
| 235 | } | ||
| 236 | |||
| 237 | RedisModuleString *error = NULL; | ||
| 238 | int result = RedisModule_ConfigSetNumeric(ctx, config_name, value, &error); | ||
| 239 | if (result == REDISMODULE_OK) { | ||
| 240 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 241 | } else { | ||
| 242 | RedisModule_ReplyWithErrorFormat(ctx, "ERR Failed to set numeric config %s: %s", config_name, RedisModule_StringPtrLen(error, NULL)); | ||
| 243 | RedisModule_FreeString(ctx, error); | ||
| 244 | return REDISMODULE_ERR; | ||
| 245 | } | ||
| 246 | |||
| 247 | return REDISMODULE_OK; | ||
| 248 | } | ||
| 249 | |||
| 250 | /* Test command for RM_SetConfig */ | ||
| 251 | int TestSetConfig_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 252 | if (argc != 3) { | ||
| 253 | return RedisModule_WrongArity(ctx); | ||
| 254 | } | ||
| 255 | |||
| 256 | size_t name_len; | ||
| 257 | const char *config_name = RedisModule_StringPtrLen(argv[1], &name_len); | ||
| 258 | |||
| 259 | RedisModuleString *error = NULL; | ||
| 260 | int result = RedisModule_ConfigSet(ctx, config_name, argv[2], &error); | ||
| 261 | if (result == REDISMODULE_OK) { | ||
| 262 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 263 | } else { | ||
| 264 | RedisModule_ReplyWithErrorFormat(ctx, "ERR Failed to set string config %s: %s", config_name, RedisModule_StringPtrLen(error, NULL)); | ||
| 265 | RedisModule_FreeString(ctx, error); | ||
| 266 | return REDISMODULE_ERR; | ||
| 267 | } | ||
| 268 | |||
| 269 | return REDISMODULE_OK; | ||
| 270 | } | ||
| 271 | |||
| 272 | /* Test command for RM_SetEnumConfig with name */ | ||
| 273 | int TestSetEnumConfig_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 274 | if (argc < 3) { | ||
| 275 | return RedisModule_WrongArity(ctx); | ||
| 276 | } | ||
| 277 | |||
| 278 | const char *config_name = RedisModule_StringPtrLen(argv[1], NULL); | ||
| 279 | |||
| 280 | RedisModuleString *error = NULL; | ||
| 281 | int result = RedisModule_ConfigSetEnum(ctx, config_name, argv[2], &error); | ||
| 282 | |||
| 283 | if (result == REDISMODULE_OK) { | ||
| 284 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 285 | } else { | ||
| 286 | RedisModule_ReplyWithErrorFormat(ctx, "ERR Failed to set enum config %s: %s", config_name, RedisModule_StringPtrLen(error, NULL)); | ||
| 287 | RedisModule_FreeString(ctx, error); | ||
| 288 | return REDISMODULE_ERR; | ||
| 289 | } | ||
| 290 | |||
| 291 | return REDISMODULE_OK; | ||
| 292 | } | ||
| 293 | |||
| 294 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 295 | REDISMODULE_NOT_USED(argv); | ||
| 296 | REDISMODULE_NOT_USED(argc); | ||
| 297 | |||
| 298 | if (RedisModule_Init(ctx, "configaccess", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) | ||
| 299 | return REDISMODULE_ERR; | ||
| 300 | |||
| 301 | if (RedisModule_CreateCommand(ctx, "configaccess.getconfigs", | ||
| 302 | TestConfigIteration_RedisCommand, "readonly", 0, 0, 0) == REDISMODULE_ERR) | ||
| 303 | return REDISMODULE_ERR; | ||
| 304 | |||
| 305 | if (RedisModule_CreateCommand(ctx, "configaccess.getbool", | ||
| 306 | TestGetBoolConfig_RedisCommand, "readonly", 0, 0, 0) == REDISMODULE_ERR) | ||
| 307 | return REDISMODULE_ERR; | ||
| 308 | |||
| 309 | if (RedisModule_CreateCommand(ctx, "configaccess.getnumeric", | ||
| 310 | TestGetNumericConfig_RedisCommand, "readonly", 0, 0, 0) == REDISMODULE_ERR) | ||
| 311 | return REDISMODULE_ERR; | ||
| 312 | |||
| 313 | if (RedisModule_CreateCommand(ctx, "configaccess.get", | ||
| 314 | TestGetConfig_RedisCommand, "readonly", 0, 0, 0) == REDISMODULE_ERR) | ||
| 315 | return REDISMODULE_ERR; | ||
| 316 | |||
| 317 | if (RedisModule_CreateCommand(ctx, "configaccess.getenum", | ||
| 318 | TestGetEnumConfig_RedisCommand, "readonly", 0, 0, 0) == REDISMODULE_ERR) | ||
| 319 | return REDISMODULE_ERR; | ||
| 320 | |||
| 321 | if (RedisModule_CreateCommand(ctx, "configaccess.setbool", | ||
| 322 | TestSetBoolConfig_RedisCommand, "write", 0, 0, 0) == REDISMODULE_ERR) | ||
| 323 | return REDISMODULE_ERR; | ||
| 324 | |||
| 325 | if (RedisModule_CreateCommand(ctx, "configaccess.setnumeric", | ||
| 326 | TestSetNumericConfig_RedisCommand, "write", 0, 0, 0) == REDISMODULE_ERR) | ||
| 327 | return REDISMODULE_ERR; | ||
| 328 | |||
| 329 | if (RedisModule_CreateCommand(ctx, "configaccess.set", | ||
| 330 | TestSetConfig_RedisCommand, "write", 0, 0, 0) == REDISMODULE_ERR) | ||
| 331 | return REDISMODULE_ERR; | ||
| 332 | |||
| 333 | if (RedisModule_CreateCommand(ctx, "configaccess.setenum", | ||
| 334 | TestSetEnumConfig_RedisCommand, "write", 0, 0, 0) == REDISMODULE_ERR) | ||
| 335 | return REDISMODULE_ERR; | ||
| 336 | |||
| 337 | if (RedisModule_CreateCommand(ctx, "configaccess.getconfigtype", TestGetConfigType_RedisCommand, "readonly", 0, 0, 0) == REDISMODULE_ERR) | ||
| 338 | return REDISMODULE_ERR; | ||
| 339 | |||
| 340 | if (RedisModule_RegisterBoolConfig(ctx, "bool", 1, REDISMODULE_CONFIG_DEFAULT, | ||
| 341 | getBoolConfigCommand, setBoolConfigCommand, NULL, &configaccess_bool) == REDISMODULE_ERR) { | ||
| 342 | RedisModule_Log(ctx, "warning", "Failed to register configaccess_bool"); | ||
| 343 | return REDISMODULE_ERR; | ||
| 344 | } | ||
| 345 | |||
| 346 | RedisModule_Log(ctx, "debug", "Loading configaccess module configuration"); | ||
| 347 | if (RedisModule_LoadConfigs(ctx) == REDISMODULE_ERR) { | ||
| 348 | RedisModule_Log(ctx, "warning", "Failed to load configaccess module configuration"); | ||
| 349 | return REDISMODULE_ERR; | ||
| 350 | } | ||
| 351 | |||
| 352 | return REDISMODULE_OK; | ||
| 353 | } | ||
diff --git a/examples/redis-unstable/tests/modules/crash.c b/examples/redis-unstable/tests/modules/crash.c new file mode 100644 index 0000000..c5063d0 --- /dev/null +++ b/examples/redis-unstable/tests/modules/crash.c | |||
| @@ -0,0 +1,311 @@ | |||
| 1 | #include "redismodule.h" | ||
| 2 | |||
| 3 | #include <strings.h> | ||
| 4 | #include <sys/mman.h> | ||
| 5 | |||
| 6 | #define UNUSED(V) ((void) V) | ||
| 7 | |||
| 8 | void assertCrash(RedisModuleInfoCtx *ctx, int for_crash_report) { | ||
| 9 | UNUSED(ctx); | ||
| 10 | UNUSED(for_crash_report); | ||
| 11 | RedisModule_Assert(0); | ||
| 12 | } | ||
| 13 | |||
| 14 | void segfaultCrash(RedisModuleInfoCtx *ctx, int for_crash_report) { | ||
| 15 | UNUSED(ctx); | ||
| 16 | UNUSED(for_crash_report); | ||
| 17 | /* Compiler gives warnings about writing to a random address | ||
| 18 | * e.g "*((char*)-1) = 'x';". As a workaround, we map a read-only area | ||
| 19 | * and try to write there to trigger segmentation fault. */ | ||
| 20 | char *p = mmap(NULL, 4096, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); | ||
| 21 | *p = 'x'; | ||
| 22 | } | ||
| 23 | |||
| 24 | int cmd_crash(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 25 | UNUSED(ctx); | ||
| 26 | UNUSED(argv); | ||
| 27 | UNUSED(argc); | ||
| 28 | |||
| 29 | RedisModule_Assert(0); | ||
| 30 | return REDISMODULE_OK; | ||
| 31 | } | ||
| 32 | |||
| 33 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 34 | REDISMODULE_NOT_USED(argv); | ||
| 35 | REDISMODULE_NOT_USED(argc); | ||
| 36 | if (RedisModule_Init(ctx,"modulecrash",1,REDISMODULE_APIVER_1) | ||
| 37 | == REDISMODULE_ERR) return REDISMODULE_ERR; | ||
| 38 | |||
| 39 | if (argc >= 1) { | ||
| 40 | if (!strcasecmp(RedisModule_StringPtrLen(argv[0], NULL), "segfault")) { | ||
| 41 | if (RedisModule_RegisterInfoFunc(ctx, segfaultCrash) == REDISMODULE_ERR) return REDISMODULE_ERR; | ||
| 42 | } else if (!strcasecmp(RedisModule_StringPtrLen(argv[0], NULL),"assert")) { | ||
| 43 | if (RedisModule_RegisterInfoFunc(ctx, assertCrash) == REDISMODULE_ERR) return REDISMODULE_ERR; | ||
| 44 | } | ||
| 45 | } | ||
| 46 | |||
| 47 | /* Create modulecrash.xadd command which is similar to xadd command. | ||
| 48 | * It will crash in the command handler to verify we print command tokens | ||
| 49 | * when hide-user-data-from-log config is enabled */ | ||
| 50 | RedisModuleCommandInfo info = { | ||
| 51 | .version = REDISMODULE_COMMAND_INFO_VERSION, | ||
| 52 | .arity = -5, | ||
| 53 | .key_specs = (RedisModuleCommandKeySpec[]){ | ||
| 54 | { | ||
| 55 | .notes = "UPDATE instead of INSERT because of the optional trimming feature", | ||
| 56 | .flags = REDISMODULE_CMD_KEY_RW | REDISMODULE_CMD_KEY_UPDATE, | ||
| 57 | .begin_search_type = REDISMODULE_KSPEC_BS_INDEX, | ||
| 58 | .bs.index.pos = 1, | ||
| 59 | .find_keys_type = REDISMODULE_KSPEC_FK_RANGE, | ||
| 60 | .fk.range = {0,1,0} | ||
| 61 | }, | ||
| 62 | {0} | ||
| 63 | }, | ||
| 64 | .args = (RedisModuleCommandArg[]){ | ||
| 65 | { | ||
| 66 | .name = "key", | ||
| 67 | .type = REDISMODULE_ARG_TYPE_KEY, | ||
| 68 | .key_spec_index = 0 | ||
| 69 | }, | ||
| 70 | { | ||
| 71 | .name = "nomkstream", | ||
| 72 | .type = REDISMODULE_ARG_TYPE_PURE_TOKEN, | ||
| 73 | .token = "NOMKSTREAM", | ||
| 74 | .since = "6.2.0", | ||
| 75 | .flags = REDISMODULE_CMD_ARG_OPTIONAL | ||
| 76 | }, | ||
| 77 | { | ||
| 78 | .name = "trim", | ||
| 79 | .type = REDISMODULE_ARG_TYPE_BLOCK, | ||
| 80 | .flags = REDISMODULE_CMD_ARG_OPTIONAL, | ||
| 81 | .subargs = (RedisModuleCommandArg[]){ | ||
| 82 | { | ||
| 83 | .name = "strategy", | ||
| 84 | .type = REDISMODULE_ARG_TYPE_ONEOF, | ||
| 85 | .subargs = (RedisModuleCommandArg[]){ | ||
| 86 | { | ||
| 87 | .name = "maxlen", | ||
| 88 | .type = REDISMODULE_ARG_TYPE_PURE_TOKEN, | ||
| 89 | .token = "MAXLEN", | ||
| 90 | }, | ||
| 91 | { | ||
| 92 | .name = "minid", | ||
| 93 | .type = REDISMODULE_ARG_TYPE_PURE_TOKEN, | ||
| 94 | .token = "MINID", | ||
| 95 | .since = "6.2.0", | ||
| 96 | }, | ||
| 97 | {0} | ||
| 98 | } | ||
| 99 | }, | ||
| 100 | { | ||
| 101 | .name = "operator", | ||
| 102 | .type = REDISMODULE_ARG_TYPE_ONEOF, | ||
| 103 | .flags = REDISMODULE_CMD_ARG_OPTIONAL, | ||
| 104 | .subargs = (RedisModuleCommandArg[]){ | ||
| 105 | { | ||
| 106 | .name = "equal", | ||
| 107 | .type = REDISMODULE_ARG_TYPE_PURE_TOKEN, | ||
| 108 | .token = "=" | ||
| 109 | }, | ||
| 110 | { | ||
| 111 | .name = "approximately", | ||
| 112 | .type = REDISMODULE_ARG_TYPE_PURE_TOKEN, | ||
| 113 | .token = "~" | ||
| 114 | }, | ||
| 115 | {0} | ||
| 116 | } | ||
| 117 | }, | ||
| 118 | { | ||
| 119 | .name = "threshold", | ||
| 120 | .type = REDISMODULE_ARG_TYPE_STRING, | ||
| 121 | .display_text = "threshold" /* Just for coverage, doesn't have a visible effect */ | ||
| 122 | }, | ||
| 123 | { | ||
| 124 | .name = "count", | ||
| 125 | .type = REDISMODULE_ARG_TYPE_INTEGER, | ||
| 126 | .token = "LIMIT", | ||
| 127 | .since = "6.2.0", | ||
| 128 | .flags = REDISMODULE_CMD_ARG_OPTIONAL | ||
| 129 | }, | ||
| 130 | {0} | ||
| 131 | } | ||
| 132 | }, | ||
| 133 | { | ||
| 134 | .name = "id-selector", | ||
| 135 | .type = REDISMODULE_ARG_TYPE_ONEOF, | ||
| 136 | .subargs = (RedisModuleCommandArg[]){ | ||
| 137 | { | ||
| 138 | .name = "auto-id", | ||
| 139 | .type = REDISMODULE_ARG_TYPE_PURE_TOKEN, | ||
| 140 | .token = "*" | ||
| 141 | }, | ||
| 142 | { | ||
| 143 | .name = "id", | ||
| 144 | .type = REDISMODULE_ARG_TYPE_STRING, | ||
| 145 | }, | ||
| 146 | {0} | ||
| 147 | } | ||
| 148 | }, | ||
| 149 | { | ||
| 150 | .name = "data", | ||
| 151 | .type = REDISMODULE_ARG_TYPE_BLOCK, | ||
| 152 | .flags = REDISMODULE_CMD_ARG_MULTIPLE, | ||
| 153 | .subargs = (RedisModuleCommandArg[]){ | ||
| 154 | { | ||
| 155 | .name = "field", | ||
| 156 | .type = REDISMODULE_ARG_TYPE_STRING, | ||
| 157 | }, | ||
| 158 | { | ||
| 159 | .name = "value", | ||
| 160 | .type = REDISMODULE_ARG_TYPE_STRING, | ||
| 161 | }, | ||
| 162 | {0} | ||
| 163 | } | ||
| 164 | }, | ||
| 165 | {0} | ||
| 166 | } | ||
| 167 | }; | ||
| 168 | |||
| 169 | RedisModuleCommand *cmd; | ||
| 170 | |||
| 171 | if (RedisModule_CreateCommand(ctx,"modulecrash.xadd", cmd_crash,"write deny-oom random fast",0,0,0) == REDISMODULE_ERR) | ||
| 172 | return REDISMODULE_ERR; | ||
| 173 | cmd = RedisModule_GetCommand(ctx,"modulecrash.xadd"); | ||
| 174 | if (RedisModule_SetCommandInfo(cmd, &info) == REDISMODULE_ERR) | ||
| 175 | return REDISMODULE_ERR; | ||
| 176 | |||
| 177 | /* Create a subcommand: modulecrash.parent sub | ||
| 178 | * It will crash in the command handler to verify we print subcommand name | ||
| 179 | * when hide-user-data-from-log config is enabled */ | ||
| 180 | RedisModuleCommandInfo subcommand_info = { | ||
| 181 | .version = REDISMODULE_COMMAND_INFO_VERSION, | ||
| 182 | .arity = -5, | ||
| 183 | .key_specs = (RedisModuleCommandKeySpec[]){ | ||
| 184 | { | ||
| 185 | .flags = REDISMODULE_CMD_KEY_RW | REDISMODULE_CMD_KEY_UPDATE, | ||
| 186 | .begin_search_type = REDISMODULE_KSPEC_BS_INDEX, | ||
| 187 | .bs.index.pos = 1, | ||
| 188 | .find_keys_type = REDISMODULE_KSPEC_FK_RANGE, | ||
| 189 | .fk.range = {0,1,0} | ||
| 190 | }, | ||
| 191 | {0} | ||
| 192 | }, | ||
| 193 | .args = (RedisModuleCommandArg[]){ | ||
| 194 | { | ||
| 195 | .name = "key", | ||
| 196 | .type = REDISMODULE_ARG_TYPE_KEY, | ||
| 197 | .key_spec_index = 0 | ||
| 198 | }, | ||
| 199 | { | ||
| 200 | .name = "token", | ||
| 201 | .type = REDISMODULE_ARG_TYPE_PURE_TOKEN, | ||
| 202 | .token = "TOKEN", | ||
| 203 | .flags = REDISMODULE_CMD_ARG_OPTIONAL | ||
| 204 | }, | ||
| 205 | { | ||
| 206 | .name = "data", | ||
| 207 | .type = REDISMODULE_ARG_TYPE_BLOCK, | ||
| 208 | .subargs = (RedisModuleCommandArg[]){ | ||
| 209 | { | ||
| 210 | .name = "field", | ||
| 211 | .type = REDISMODULE_ARG_TYPE_STRING, | ||
| 212 | }, | ||
| 213 | { | ||
| 214 | .name = "value", | ||
| 215 | .type = REDISMODULE_ARG_TYPE_STRING, | ||
| 216 | }, | ||
| 217 | {0} | ||
| 218 | } | ||
| 219 | }, | ||
| 220 | {0} | ||
| 221 | } | ||
| 222 | }; | ||
| 223 | |||
| 224 | if (RedisModule_CreateCommand(ctx,"modulecrash.parent",NULL,"",0,0,0) == REDISMODULE_ERR) | ||
| 225 | return REDISMODULE_ERR; | ||
| 226 | |||
| 227 | RedisModuleCommand *parent = RedisModule_GetCommand(ctx,"modulecrash.parent"); | ||
| 228 | |||
| 229 | if (RedisModule_CreateSubcommand(parent,"subcmd",cmd_crash,"",0,0,0) == REDISMODULE_ERR) | ||
| 230 | return REDISMODULE_ERR; | ||
| 231 | |||
| 232 | cmd = RedisModule_GetCommand(ctx,"modulecrash.parent|subcmd"); | ||
| 233 | if (RedisModule_SetCommandInfo(cmd, &subcommand_info) == REDISMODULE_ERR) | ||
| 234 | return REDISMODULE_ERR; | ||
| 235 | |||
| 236 | /* Create modulecrash.zunion command which is similar to zunion command. | ||
| 237 | * It will crash in the command handler to verify we print command tokens | ||
| 238 | * when hide-user-data-from-log config is enabled */ | ||
| 239 | RedisModuleCommandInfo zunioninfo = { | ||
| 240 | .version = REDISMODULE_COMMAND_INFO_VERSION, | ||
| 241 | .arity = -5, | ||
| 242 | .key_specs = (RedisModuleCommandKeySpec[]){ | ||
| 243 | { | ||
| 244 | .flags = REDISMODULE_CMD_KEY_RO, | ||
| 245 | .begin_search_type = REDISMODULE_KSPEC_BS_INDEX, | ||
| 246 | .bs.index.pos = 1, | ||
| 247 | .find_keys_type = REDISMODULE_KSPEC_FK_KEYNUM, | ||
| 248 | .fk.keynum = {0,1,1} | ||
| 249 | }, | ||
| 250 | {0} | ||
| 251 | }, | ||
| 252 | .args = (RedisModuleCommandArg[]){ | ||
| 253 | { | ||
| 254 | .name = "numkeys", | ||
| 255 | .type = REDISMODULE_ARG_TYPE_INTEGER, | ||
| 256 | }, | ||
| 257 | { | ||
| 258 | .name = "key", | ||
| 259 | .type = REDISMODULE_ARG_TYPE_KEY, | ||
| 260 | .key_spec_index = 0, | ||
| 261 | .flags = REDISMODULE_CMD_ARG_MULTIPLE | ||
| 262 | }, | ||
| 263 | { | ||
| 264 | .name = "weights", | ||
| 265 | .type = REDISMODULE_ARG_TYPE_INTEGER, | ||
| 266 | .token = "WEIGHTS", | ||
| 267 | .flags = REDISMODULE_CMD_ARG_OPTIONAL | REDISMODULE_CMD_ARG_MULTIPLE | ||
| 268 | }, | ||
| 269 | { | ||
| 270 | .name = "aggregate", | ||
| 271 | .type = REDISMODULE_ARG_TYPE_ONEOF, | ||
| 272 | .token = "AGGREGATE", | ||
| 273 | .flags = REDISMODULE_CMD_ARG_OPTIONAL, | ||
| 274 | .subargs = (RedisModuleCommandArg[]){ | ||
| 275 | { | ||
| 276 | .name = "sum", | ||
| 277 | .type = REDISMODULE_ARG_TYPE_PURE_TOKEN, | ||
| 278 | .token = "sum" | ||
| 279 | }, | ||
| 280 | { | ||
| 281 | .name = "min", | ||
| 282 | .type = REDISMODULE_ARG_TYPE_PURE_TOKEN, | ||
| 283 | .token = "min" | ||
| 284 | }, | ||
| 285 | { | ||
| 286 | .name = "max", | ||
| 287 | .type = REDISMODULE_ARG_TYPE_PURE_TOKEN, | ||
| 288 | .token = "max" | ||
| 289 | }, | ||
| 290 | {0} | ||
| 291 | } | ||
| 292 | }, | ||
| 293 | { | ||
| 294 | .name = "withscores", | ||
| 295 | .type = REDISMODULE_ARG_TYPE_PURE_TOKEN, | ||
| 296 | .token = "WITHSCORES", | ||
| 297 | .flags = REDISMODULE_CMD_ARG_OPTIONAL | ||
| 298 | }, | ||
| 299 | {0} | ||
| 300 | } | ||
| 301 | }; | ||
| 302 | |||
| 303 | if (RedisModule_CreateCommand(ctx,"modulecrash.zunion", cmd_crash,"readonly",0,0,0) == REDISMODULE_ERR) | ||
| 304 | return REDISMODULE_ERR; | ||
| 305 | cmd = RedisModule_GetCommand(ctx,"modulecrash.zunion"); | ||
| 306 | if (RedisModule_SetCommandInfo(cmd, &zunioninfo) == REDISMODULE_ERR) | ||
| 307 | return REDISMODULE_ERR; | ||
| 308 | |||
| 309 | |||
| 310 | return REDISMODULE_OK; | ||
| 311 | } | ||
diff --git a/examples/redis-unstable/tests/modules/datatype.c b/examples/redis-unstable/tests/modules/datatype.c new file mode 100644 index 0000000..05cf233 --- /dev/null +++ b/examples/redis-unstable/tests/modules/datatype.c | |||
| @@ -0,0 +1,323 @@ | |||
| 1 | /* This module current tests a small subset but should be extended in the future | ||
| 2 | * for general ModuleDataType coverage. | ||
| 3 | */ | ||
| 4 | |||
| 5 | /* define macros for having usleep */ | ||
| 6 | #define _BSD_SOURCE | ||
| 7 | #define _DEFAULT_SOURCE | ||
| 8 | #include <unistd.h> | ||
| 9 | |||
| 10 | #include "redismodule.h" | ||
| 11 | |||
| 12 | static RedisModuleType *datatype = NULL; | ||
| 13 | static int load_encver = 0; | ||
| 14 | |||
| 15 | /* used to test processing events during slow loading */ | ||
| 16 | static volatile int slow_loading = 0; | ||
| 17 | static volatile int is_in_slow_loading = 0; | ||
| 18 | |||
| 19 | #define DATATYPE_ENC_VER 1 | ||
| 20 | |||
| 21 | typedef struct { | ||
| 22 | long long intval; | ||
| 23 | RedisModuleString *strval; | ||
| 24 | } DataType; | ||
| 25 | |||
| 26 | static void *datatype_load(RedisModuleIO *io, int encver) { | ||
| 27 | load_encver = encver; | ||
| 28 | int intval = RedisModule_LoadSigned(io); | ||
| 29 | if (RedisModule_IsIOError(io)) return NULL; | ||
| 30 | |||
| 31 | RedisModuleString *strval = RedisModule_LoadString(io); | ||
| 32 | if (RedisModule_IsIOError(io)) return NULL; | ||
| 33 | |||
| 34 | DataType *dt = (DataType *) RedisModule_Alloc(sizeof(DataType)); | ||
| 35 | dt->intval = intval; | ||
| 36 | dt->strval = strval; | ||
| 37 | |||
| 38 | if (slow_loading) { | ||
| 39 | RedisModuleCtx *ctx = RedisModule_GetContextFromIO(io); | ||
| 40 | is_in_slow_loading = 1; | ||
| 41 | while (slow_loading) { | ||
| 42 | RedisModule_Yield(ctx, REDISMODULE_YIELD_FLAG_CLIENTS, "Slow module operation"); | ||
| 43 | usleep(1000); | ||
| 44 | } | ||
| 45 | is_in_slow_loading = 0; | ||
| 46 | } | ||
| 47 | |||
| 48 | return dt; | ||
| 49 | } | ||
| 50 | |||
| 51 | static void datatype_save(RedisModuleIO *io, void *value) { | ||
| 52 | DataType *dt = (DataType *) value; | ||
| 53 | RedisModule_SaveSigned(io, dt->intval); | ||
| 54 | RedisModule_SaveString(io, dt->strval); | ||
| 55 | } | ||
| 56 | |||
| 57 | static void datatype_free(void *value) { | ||
| 58 | if (value) { | ||
| 59 | DataType *dt = (DataType *) value; | ||
| 60 | |||
| 61 | if (dt->strval) RedisModule_FreeString(NULL, dt->strval); | ||
| 62 | RedisModule_Free(dt); | ||
| 63 | } | ||
| 64 | } | ||
| 65 | |||
| 66 | static void *datatype_copy(RedisModuleString *fromkey, RedisModuleString *tokey, const void *value) { | ||
| 67 | const DataType *old = value; | ||
| 68 | |||
| 69 | /* Answers to ultimate questions cannot be copied! */ | ||
| 70 | if (old->intval == 42) | ||
| 71 | return NULL; | ||
| 72 | |||
| 73 | DataType *new = (DataType *) RedisModule_Alloc(sizeof(DataType)); | ||
| 74 | |||
| 75 | new->intval = old->intval; | ||
| 76 | new->strval = RedisModule_CreateStringFromString(NULL, old->strval); | ||
| 77 | |||
| 78 | /* Breaking the rules here! We return a copy that also includes traces | ||
| 79 | * of fromkey/tokey to confirm we get what we expect. | ||
| 80 | */ | ||
| 81 | size_t len; | ||
| 82 | const char *str = RedisModule_StringPtrLen(fromkey, &len); | ||
| 83 | RedisModule_StringAppendBuffer(NULL, new->strval, "/", 1); | ||
| 84 | RedisModule_StringAppendBuffer(NULL, new->strval, str, len); | ||
| 85 | RedisModule_StringAppendBuffer(NULL, new->strval, "/", 1); | ||
| 86 | str = RedisModule_StringPtrLen(tokey, &len); | ||
| 87 | RedisModule_StringAppendBuffer(NULL, new->strval, str, len); | ||
| 88 | |||
| 89 | return new; | ||
| 90 | } | ||
| 91 | |||
| 92 | static int datatype_set(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 93 | if (argc != 4) { | ||
| 94 | RedisModule_WrongArity(ctx); | ||
| 95 | return REDISMODULE_OK; | ||
| 96 | } | ||
| 97 | |||
| 98 | long long intval; | ||
| 99 | |||
| 100 | if (RedisModule_StringToLongLong(argv[2], &intval) != REDISMODULE_OK) { | ||
| 101 | RedisModule_ReplyWithError(ctx, "Invalid integer value"); | ||
| 102 | return REDISMODULE_OK; | ||
| 103 | } | ||
| 104 | |||
| 105 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE); | ||
| 106 | DataType *dt = RedisModule_Calloc(sizeof(DataType), 1); | ||
| 107 | dt->intval = intval; | ||
| 108 | dt->strval = argv[3]; | ||
| 109 | RedisModule_RetainString(ctx, dt->strval); | ||
| 110 | |||
| 111 | RedisModule_ModuleTypeSetValue(key, datatype, dt); | ||
| 112 | RedisModule_CloseKey(key); | ||
| 113 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 114 | |||
| 115 | return REDISMODULE_OK; | ||
| 116 | } | ||
| 117 | |||
| 118 | static int datatype_restore(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 119 | if (argc != 4) { | ||
| 120 | RedisModule_WrongArity(ctx); | ||
| 121 | return REDISMODULE_OK; | ||
| 122 | } | ||
| 123 | |||
| 124 | long long encver; | ||
| 125 | if (RedisModule_StringToLongLong(argv[3], &encver) != REDISMODULE_OK) { | ||
| 126 | RedisModule_ReplyWithError(ctx, "Invalid integer value"); | ||
| 127 | return REDISMODULE_OK; | ||
| 128 | } | ||
| 129 | |||
| 130 | DataType *dt = RedisModule_LoadDataTypeFromStringEncver(argv[2], datatype, encver); | ||
| 131 | if (!dt) { | ||
| 132 | RedisModule_ReplyWithError(ctx, "Invalid data"); | ||
| 133 | return REDISMODULE_OK; | ||
| 134 | } | ||
| 135 | |||
| 136 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE); | ||
| 137 | RedisModule_ModuleTypeSetValue(key, datatype, dt); | ||
| 138 | RedisModule_CloseKey(key); | ||
| 139 | RedisModule_ReplyWithLongLong(ctx, load_encver); | ||
| 140 | |||
| 141 | return REDISMODULE_OK; | ||
| 142 | } | ||
| 143 | |||
| 144 | static int datatype_get(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 145 | if (argc != 2) { | ||
| 146 | RedisModule_WrongArity(ctx); | ||
| 147 | return REDISMODULE_OK; | ||
| 148 | } | ||
| 149 | |||
| 150 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ); | ||
| 151 | DataType *dt = RedisModule_ModuleTypeGetValue(key); | ||
| 152 | RedisModule_CloseKey(key); | ||
| 153 | |||
| 154 | if (!dt) { | ||
| 155 | RedisModule_ReplyWithNullArray(ctx); | ||
| 156 | } else { | ||
| 157 | RedisModule_ReplyWithArray(ctx, 2); | ||
| 158 | RedisModule_ReplyWithLongLong(ctx, dt->intval); | ||
| 159 | RedisModule_ReplyWithString(ctx, dt->strval); | ||
| 160 | } | ||
| 161 | return REDISMODULE_OK; | ||
| 162 | } | ||
| 163 | |||
| 164 | static int datatype_dump(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 165 | if (argc != 2) { | ||
| 166 | RedisModule_WrongArity(ctx); | ||
| 167 | return REDISMODULE_OK; | ||
| 168 | } | ||
| 169 | |||
| 170 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ); | ||
| 171 | DataType *dt = RedisModule_ModuleTypeGetValue(key); | ||
| 172 | RedisModule_CloseKey(key); | ||
| 173 | |||
| 174 | RedisModuleString *reply = RedisModule_SaveDataTypeToString(ctx, dt, datatype); | ||
| 175 | if (!reply) { | ||
| 176 | RedisModule_ReplyWithError(ctx, "Failed to save"); | ||
| 177 | return REDISMODULE_OK; | ||
| 178 | } | ||
| 179 | |||
| 180 | RedisModule_ReplyWithString(ctx, reply); | ||
| 181 | RedisModule_FreeString(ctx, reply); | ||
| 182 | return REDISMODULE_OK; | ||
| 183 | } | ||
| 184 | |||
| 185 | static int datatype_swap(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 186 | if (argc != 3) { | ||
| 187 | RedisModule_WrongArity(ctx); | ||
| 188 | return REDISMODULE_OK; | ||
| 189 | } | ||
| 190 | |||
| 191 | RedisModuleKey *a = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE); | ||
| 192 | RedisModuleKey *b = RedisModule_OpenKey(ctx, argv[2], REDISMODULE_WRITE); | ||
| 193 | void *val = RedisModule_ModuleTypeGetValue(a); | ||
| 194 | |||
| 195 | int error = (RedisModule_ModuleTypeReplaceValue(b, datatype, val, &val) == REDISMODULE_ERR || | ||
| 196 | RedisModule_ModuleTypeReplaceValue(a, datatype, val, NULL) == REDISMODULE_ERR); | ||
| 197 | if (!error) | ||
| 198 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 199 | else | ||
| 200 | RedisModule_ReplyWithError(ctx, "ERR failed"); | ||
| 201 | |||
| 202 | RedisModule_CloseKey(a); | ||
| 203 | RedisModule_CloseKey(b); | ||
| 204 | |||
| 205 | return REDISMODULE_OK; | ||
| 206 | } | ||
| 207 | |||
| 208 | /* used to enable or disable slow loading */ | ||
| 209 | static int datatype_slow_loading(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 210 | if (argc != 2) { | ||
| 211 | RedisModule_WrongArity(ctx); | ||
| 212 | return REDISMODULE_OK; | ||
| 213 | } | ||
| 214 | |||
| 215 | long long ll; | ||
| 216 | if (RedisModule_StringToLongLong(argv[1], &ll) != REDISMODULE_OK) { | ||
| 217 | RedisModule_ReplyWithError(ctx, "Invalid integer value"); | ||
| 218 | return REDISMODULE_OK; | ||
| 219 | } | ||
| 220 | slow_loading = ll; | ||
| 221 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 222 | return REDISMODULE_OK; | ||
| 223 | } | ||
| 224 | |||
| 225 | /* used to test if we reached the slow loading code */ | ||
| 226 | static int datatype_is_in_slow_loading(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 227 | REDISMODULE_NOT_USED(argv); | ||
| 228 | if (argc != 1) { | ||
| 229 | RedisModule_WrongArity(ctx); | ||
| 230 | return REDISMODULE_OK; | ||
| 231 | } | ||
| 232 | |||
| 233 | RedisModule_ReplyWithLongLong(ctx, is_in_slow_loading); | ||
| 234 | return REDISMODULE_OK; | ||
| 235 | } | ||
| 236 | |||
| 237 | int createDataTypeBlockCheck(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 238 | REDISMODULE_NOT_USED(argv); | ||
| 239 | REDISMODULE_NOT_USED(argc); | ||
| 240 | static RedisModuleType *datatype_outside_onload = NULL; | ||
| 241 | |||
| 242 | RedisModuleTypeMethods datatype_methods = { | ||
| 243 | .version = REDISMODULE_TYPE_METHOD_VERSION, | ||
| 244 | .rdb_load = datatype_load, | ||
| 245 | .rdb_save = datatype_save, | ||
| 246 | .free = datatype_free, | ||
| 247 | .copy = datatype_copy | ||
| 248 | }; | ||
| 249 | |||
| 250 | datatype_outside_onload = RedisModule_CreateDataType(ctx, "test_dt_outside_onload", 1, &datatype_methods); | ||
| 251 | |||
| 252 | /* This validates that it's not possible to create datatype outside OnLoad, | ||
| 253 | * thus returns an error if it succeeds. */ | ||
| 254 | if (datatype_outside_onload == NULL) { | ||
| 255 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 256 | } else { | ||
| 257 | RedisModule_ReplyWithError(ctx, "UNEXPECTEDOK"); | ||
| 258 | } | ||
| 259 | return REDISMODULE_OK; | ||
| 260 | } | ||
| 261 | |||
| 262 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 263 | REDISMODULE_NOT_USED(argv); | ||
| 264 | REDISMODULE_NOT_USED(argc); | ||
| 265 | |||
| 266 | if (RedisModule_Init(ctx,"datatype",DATATYPE_ENC_VER,REDISMODULE_APIVER_1) == REDISMODULE_ERR) | ||
| 267 | return REDISMODULE_ERR; | ||
| 268 | |||
| 269 | /* Creates a command which creates a datatype outside OnLoad() function. */ | ||
| 270 | if (RedisModule_CreateCommand(ctx,"block.create.datatype.outside.onload", createDataTypeBlockCheck, "write", 0, 0, 0) == REDISMODULE_ERR) | ||
| 271 | return REDISMODULE_ERR; | ||
| 272 | |||
| 273 | RedisModule_SetModuleOptions(ctx, REDISMODULE_OPTIONS_HANDLE_IO_ERRORS); | ||
| 274 | |||
| 275 | RedisModuleTypeMethods datatype_methods = { | ||
| 276 | .version = REDISMODULE_TYPE_METHOD_VERSION, | ||
| 277 | .rdb_load = datatype_load, | ||
| 278 | .rdb_save = datatype_save, | ||
| 279 | .free = datatype_free, | ||
| 280 | .copy = datatype_copy | ||
| 281 | }; | ||
| 282 | |||
| 283 | datatype = RedisModule_CreateDataType(ctx, "test___dt", 1, &datatype_methods); | ||
| 284 | if (datatype == NULL) | ||
| 285 | return REDISMODULE_ERR; | ||
| 286 | |||
| 287 | if (RedisModule_CreateCommand(ctx,"datatype.set", datatype_set, | ||
| 288 | "write deny-oom", 1, 1, 1) == REDISMODULE_ERR) | ||
| 289 | return REDISMODULE_ERR; | ||
| 290 | |||
| 291 | if (RedisModule_CreateCommand(ctx,"datatype.get", datatype_get,"",1,1,1) == REDISMODULE_ERR) | ||
| 292 | return REDISMODULE_ERR; | ||
| 293 | |||
| 294 | if (RedisModule_CreateCommand(ctx,"datatype.restore", datatype_restore, | ||
| 295 | "write deny-oom", 1, 1, 1) == REDISMODULE_ERR) | ||
| 296 | return REDISMODULE_ERR; | ||
| 297 | |||
| 298 | if (RedisModule_CreateCommand(ctx,"datatype.dump", datatype_dump,"",1,1,1) == REDISMODULE_ERR) | ||
| 299 | return REDISMODULE_ERR; | ||
| 300 | |||
| 301 | if (RedisModule_CreateCommand(ctx, "datatype.swap", datatype_swap, | ||
| 302 | "write", 1, 1, 1) == REDISMODULE_ERR) | ||
| 303 | return REDISMODULE_ERR; | ||
| 304 | |||
| 305 | if (RedisModule_CreateCommand(ctx, "datatype.slow_loading", datatype_slow_loading, | ||
| 306 | "allow-loading", 0, 0, 0) == REDISMODULE_ERR) | ||
| 307 | return REDISMODULE_ERR; | ||
| 308 | |||
| 309 | if (RedisModule_CreateCommand(ctx, "datatype.is_in_slow_loading", datatype_is_in_slow_loading, | ||
| 310 | "allow-loading", 0, 0, 0) == REDISMODULE_ERR) | ||
| 311 | return REDISMODULE_ERR; | ||
| 312 | |||
| 313 | return REDISMODULE_OK; | ||
| 314 | } | ||
| 315 | |||
| 316 | int RedisModule_OnUnload(RedisModuleCtx *ctx) { | ||
| 317 | REDISMODULE_NOT_USED(ctx); | ||
| 318 | if (datatype) { | ||
| 319 | RedisModule_Free(datatype); | ||
| 320 | datatype = NULL; | ||
| 321 | } | ||
| 322 | return REDISMODULE_OK; | ||
| 323 | } | ||
diff --git a/examples/redis-unstable/tests/modules/datatype2.c b/examples/redis-unstable/tests/modules/datatype2.c new file mode 100644 index 0000000..bc0dc3d --- /dev/null +++ b/examples/redis-unstable/tests/modules/datatype2.c | |||
| @@ -0,0 +1,739 @@ | |||
| 1 | /* This module is used to test a use case of a module that stores information | ||
| 2 | * about keys in global memory, and relies on the enhanced data type callbacks to | ||
| 3 | * get key name and dbid on various operations. | ||
| 4 | * | ||
| 5 | * it simulates a simple memory allocator. The smallest allocation unit of | ||
| 6 | * the allocator is a mem block with a size of 4KB. Multiple mem blocks are combined | ||
| 7 | * using a linked list. These linked lists are placed in a global dict named 'mem_pool'. | ||
| 8 | * Each db has a 'mem_pool'. You can use the 'mem.alloc' command to allocate a specified | ||
| 9 | * number of mem blocks, and use 'mem.free' to release the memory. Use 'mem.write', 'mem.read' | ||
| 10 | * to write and read the specified mem block (note that each mem block can only be written once). | ||
| 11 | * Use 'mem.usage' to get the memory usage under different dbs, and it will return the size | ||
| 12 | * mem blocks and used mem blocks under the db. | ||
| 13 | * The specific structure diagram is as follows: | ||
| 14 | * | ||
| 15 | * | ||
| 16 | * Global variables of the module: | ||
| 17 | * | ||
| 18 | * mem blocks link | ||
| 19 | * ┌─────┬─────┐ | ||
| 20 | * │ │ │ ┌───┐ ┌───┐ ┌───┐ | ||
| 21 | * │ k1 │ ───┼───►│4KB├───►│4KB├───►│4KB│ | ||
| 22 | * │ │ │ └───┘ └───┘ └───┘ | ||
| 23 | * ├─────┼─────┤ | ||
| 24 | * ┌───────┐ ┌────► │ │ │ ┌───┐ ┌───┐ | ||
| 25 | * │ │ │ │ k2 │ ───┼───►│4KB├───►│4KB│ | ||
| 26 | * │ db0 ├──────┘ │ │ │ └───┘ └───┘ | ||
| 27 | * │ │ ├─────┼─────┤ | ||
| 28 | * ├───────┤ │ │ │ ┌───┐ ┌───┐ ┌───┐ | ||
| 29 | * │ │ │ k3 │ ───┼───►│4KB├───►│4KB├───►│4KB│ | ||
| 30 | * │ db1 ├──►null │ │ │ └───┘ └───┘ └───┘ | ||
| 31 | * │ │ └─────┴─────┘ | ||
| 32 | * ├───────┤ dict | ||
| 33 | * │ │ | ||
| 34 | * │ db2 ├─────────┐ | ||
| 35 | * │ │ │ | ||
| 36 | * ├───────┤ │ ┌─────┬─────┐ | ||
| 37 | * │ │ │ │ │ │ ┌───┐ ┌───┐ ┌───┐ | ||
| 38 | * │ db3 ├──►null │ │ k1 │ ───┼───►│4KB├───►│4KB├───►│4KB│ | ||
| 39 | * │ │ │ │ │ │ └───┘ └───┘ └───┘ | ||
| 40 | * └───────┘ │ ├─────┼─────┤ | ||
| 41 | * mem_pool[MAX_DB] │ │ │ │ ┌───┐ ┌───┐ | ||
| 42 | * └──►│ k2 │ ───┼───►│4KB├───►│4KB│ | ||
| 43 | * │ │ │ └───┘ └───┘ | ||
| 44 | * └─────┴─────┘ | ||
| 45 | * dict | ||
| 46 | * | ||
| 47 | * | ||
| 48 | * Keys in redis database: | ||
| 49 | * | ||
| 50 | * ┌───────┐ | ||
| 51 | * │ size │ | ||
| 52 | * ┌───────────►│ used │ | ||
| 53 | * │ │ mask │ | ||
| 54 | * ┌─────┬─────┐ │ └───────┘ ┌───────┐ | ||
| 55 | * │ │ │ │ MemAllocObject │ size │ | ||
| 56 | * │ k1 │ ───┼─┘ ┌───────────►│ used │ | ||
| 57 | * │ │ │ │ │ mask │ | ||
| 58 | * ├─────┼─────┤ ┌───────┐ ┌─────┬─────┐ │ └───────┘ | ||
| 59 | * │ │ │ │ size │ │ │ │ │ MemAllocObject | ||
| 60 | * │ k2 │ ───┼─────────────►│ used │ │ k1 │ ───┼─┘ | ||
| 61 | * │ │ │ │ mask │ │ │ │ | ||
| 62 | * ├─────┼─────┤ └───────┘ ├─────┼─────┤ | ||
| 63 | * │ │ │ MemAllocObject │ │ │ | ||
| 64 | * │ k3 │ ───┼─┐ │ k2 │ ───┼─┐ | ||
| 65 | * │ │ │ │ │ │ │ │ | ||
| 66 | * └─────┴─────┘ │ ┌───────┐ └─────┴─────┘ │ ┌───────┐ | ||
| 67 | * redis db[0] │ │ size │ redis db[1] │ │ size │ | ||
| 68 | * └───────────►│ used │ └───────────►│ used │ | ||
| 69 | * │ mask │ │ mask │ | ||
| 70 | * └───────┘ └───────┘ | ||
| 71 | * MemAllocObject MemAllocObject | ||
| 72 | * | ||
| 73 | **/ | ||
| 74 | |||
| 75 | #include "redismodule.h" | ||
| 76 | #include <stdio.h> | ||
| 77 | #include <stdlib.h> | ||
| 78 | #include <ctype.h> | ||
| 79 | #include <string.h> | ||
| 80 | #include <stdint.h> | ||
| 81 | |||
| 82 | static RedisModuleType *MemAllocType; | ||
| 83 | |||
| 84 | #define MAX_DB 16 | ||
| 85 | RedisModuleDict *mem_pool[MAX_DB]; | ||
| 86 | typedef struct MemAllocObject { | ||
| 87 | long long size; | ||
| 88 | long long used; | ||
| 89 | uint64_t mask; | ||
| 90 | } MemAllocObject; | ||
| 91 | |||
| 92 | MemAllocObject *createMemAllocObject(void) { | ||
| 93 | MemAllocObject *o = RedisModule_Calloc(1, sizeof(*o)); | ||
| 94 | return o; | ||
| 95 | } | ||
| 96 | |||
| 97 | /*---------------------------- mem block apis ------------------------------------*/ | ||
| 98 | #define BLOCK_SIZE 4096 | ||
| 99 | struct MemBlock { | ||
| 100 | char block[BLOCK_SIZE]; | ||
| 101 | struct MemBlock *next; | ||
| 102 | }; | ||
| 103 | |||
| 104 | void MemBlockFree(struct MemBlock *head) { | ||
| 105 | if (head) { | ||
| 106 | struct MemBlock *block = head->next, *next; | ||
| 107 | RedisModule_Free(head); | ||
| 108 | while (block) { | ||
| 109 | next = block->next; | ||
| 110 | RedisModule_Free(block); | ||
| 111 | block = next; | ||
| 112 | } | ||
| 113 | } | ||
| 114 | } | ||
| 115 | struct MemBlock *MemBlockCreate(long long num) { | ||
| 116 | if (num <= 0) { | ||
| 117 | return NULL; | ||
| 118 | } | ||
| 119 | |||
| 120 | struct MemBlock *head = RedisModule_Calloc(1, sizeof(struct MemBlock)); | ||
| 121 | struct MemBlock *block = head; | ||
| 122 | while (--num) { | ||
| 123 | block->next = RedisModule_Calloc(1, sizeof(struct MemBlock)); | ||
| 124 | block = block->next; | ||
| 125 | } | ||
| 126 | |||
| 127 | return head; | ||
| 128 | } | ||
| 129 | |||
| 130 | long long MemBlockNum(const struct MemBlock *head) { | ||
| 131 | long long num = 0; | ||
| 132 | const struct MemBlock *block = head; | ||
| 133 | while (block) { | ||
| 134 | num++; | ||
| 135 | block = block->next; | ||
| 136 | } | ||
| 137 | |||
| 138 | return num; | ||
| 139 | } | ||
| 140 | |||
| 141 | size_t MemBlockWrite(struct MemBlock *head, long long block_index, const char *data, size_t size) { | ||
| 142 | size_t w_size = 0; | ||
| 143 | struct MemBlock *block = head; | ||
| 144 | while (block_index-- && block) { | ||
| 145 | block = block->next; | ||
| 146 | } | ||
| 147 | |||
| 148 | if (block) { | ||
| 149 | size = size > BLOCK_SIZE ? BLOCK_SIZE:size; | ||
| 150 | memcpy(block->block, data, size); | ||
| 151 | w_size += size; | ||
| 152 | } | ||
| 153 | |||
| 154 | return w_size; | ||
| 155 | } | ||
| 156 | |||
| 157 | int MemBlockRead(struct MemBlock *head, long long block_index, char *data, size_t size) { | ||
| 158 | size_t r_size = 0; | ||
| 159 | struct MemBlock *block = head; | ||
| 160 | while (block_index-- && block) { | ||
| 161 | block = block->next; | ||
| 162 | } | ||
| 163 | |||
| 164 | if (block) { | ||
| 165 | size = size > BLOCK_SIZE ? BLOCK_SIZE:size; | ||
| 166 | memcpy(data, block->block, size); | ||
| 167 | r_size += size; | ||
| 168 | } | ||
| 169 | |||
| 170 | return r_size; | ||
| 171 | } | ||
| 172 | |||
| 173 | void MemPoolFreeDb(RedisModuleCtx *ctx, int dbid) { | ||
| 174 | RedisModuleString *key; | ||
| 175 | void *tdata; | ||
| 176 | RedisModuleDictIter *iter = RedisModule_DictIteratorStartC(mem_pool[dbid], "^", NULL, 0); | ||
| 177 | while((key = RedisModule_DictNext(ctx, iter, &tdata)) != NULL) { | ||
| 178 | MemBlockFree((struct MemBlock *)tdata); | ||
| 179 | } | ||
| 180 | RedisModule_DictIteratorStop(iter); | ||
| 181 | RedisModule_FreeDict(NULL, mem_pool[dbid]); | ||
| 182 | mem_pool[dbid] = RedisModule_CreateDict(NULL); | ||
| 183 | } | ||
| 184 | |||
| 185 | struct MemBlock *MemBlockClone(const struct MemBlock *head) { | ||
| 186 | struct MemBlock *newhead = NULL; | ||
| 187 | if (head) { | ||
| 188 | newhead = RedisModule_Calloc(1, sizeof(struct MemBlock)); | ||
| 189 | memcpy(newhead->block, head->block, BLOCK_SIZE); | ||
| 190 | struct MemBlock *newblock = newhead; | ||
| 191 | const struct MemBlock *oldblock = head->next; | ||
| 192 | while (oldblock) { | ||
| 193 | newblock->next = RedisModule_Calloc(1, sizeof(struct MemBlock)); | ||
| 194 | newblock = newblock->next; | ||
| 195 | memcpy(newblock->block, oldblock->block, BLOCK_SIZE); | ||
| 196 | oldblock = oldblock->next; | ||
| 197 | } | ||
| 198 | } | ||
| 199 | |||
| 200 | return newhead; | ||
| 201 | } | ||
| 202 | |||
| 203 | /*---------------------------- event handler ------------------------------------*/ | ||
| 204 | void swapDbCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data) { | ||
| 205 | REDISMODULE_NOT_USED(ctx); | ||
| 206 | REDISMODULE_NOT_USED(e); | ||
| 207 | REDISMODULE_NOT_USED(sub); | ||
| 208 | |||
| 209 | RedisModuleSwapDbInfo *ei = data; | ||
| 210 | |||
| 211 | // swap | ||
| 212 | RedisModuleDict *tmp = mem_pool[ei->dbnum_first]; | ||
| 213 | mem_pool[ei->dbnum_first] = mem_pool[ei->dbnum_second]; | ||
| 214 | mem_pool[ei->dbnum_second] = tmp; | ||
| 215 | } | ||
| 216 | |||
| 217 | void flushdbCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data) { | ||
| 218 | REDISMODULE_NOT_USED(ctx); | ||
| 219 | REDISMODULE_NOT_USED(e); | ||
| 220 | int i; | ||
| 221 | RedisModuleFlushInfo *fi = data; | ||
| 222 | |||
| 223 | RedisModule_AutoMemory(ctx); | ||
| 224 | |||
| 225 | if (sub == REDISMODULE_SUBEVENT_FLUSHDB_START) { | ||
| 226 | if (fi->dbnum != -1) { | ||
| 227 | MemPoolFreeDb(ctx, fi->dbnum); | ||
| 228 | } else { | ||
| 229 | for (i = 0; i < MAX_DB; i++) { | ||
| 230 | MemPoolFreeDb(ctx, i); | ||
| 231 | } | ||
| 232 | } | ||
| 233 | } | ||
| 234 | } | ||
| 235 | |||
| 236 | /*---------------------------- command implementation ------------------------------------*/ | ||
| 237 | |||
| 238 | /* MEM.ALLOC key block_num */ | ||
| 239 | int MemAlloc_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 240 | RedisModule_AutoMemory(ctx); | ||
| 241 | |||
| 242 | if (argc != 3) { | ||
| 243 | return RedisModule_WrongArity(ctx); | ||
| 244 | } | ||
| 245 | |||
| 246 | long long block_num; | ||
| 247 | if ((RedisModule_StringToLongLong(argv[2], &block_num) != REDISMODULE_OK) || block_num <= 0) { | ||
| 248 | return RedisModule_ReplyWithError(ctx, "ERR invalid block_num: must be a value greater than 0"); | ||
| 249 | } | ||
| 250 | |||
| 251 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ | REDISMODULE_WRITE); | ||
| 252 | int type = RedisModule_KeyType(key); | ||
| 253 | if (type != REDISMODULE_KEYTYPE_EMPTY && RedisModule_ModuleTypeGetType(key) != MemAllocType) { | ||
| 254 | return RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE); | ||
| 255 | } | ||
| 256 | |||
| 257 | MemAllocObject *o; | ||
| 258 | if (type == REDISMODULE_KEYTYPE_EMPTY) { | ||
| 259 | o = createMemAllocObject(); | ||
| 260 | RedisModule_ModuleTypeSetValue(key, MemAllocType, o); | ||
| 261 | } else { | ||
| 262 | o = RedisModule_ModuleTypeGetValue(key); | ||
| 263 | } | ||
| 264 | |||
| 265 | struct MemBlock *mem = MemBlockCreate(block_num); | ||
| 266 | RedisModule_Assert(mem != NULL); | ||
| 267 | RedisModule_DictSet(mem_pool[RedisModule_GetSelectedDb(ctx)], argv[1], mem); | ||
| 268 | o->size = block_num; | ||
| 269 | o->used = 0; | ||
| 270 | o->mask = 0; | ||
| 271 | |||
| 272 | RedisModule_ReplyWithLongLong(ctx, block_num); | ||
| 273 | RedisModule_ReplicateVerbatim(ctx); | ||
| 274 | return REDISMODULE_OK; | ||
| 275 | } | ||
| 276 | |||
| 277 | /* MEM.FREE key */ | ||
| 278 | int MemFree_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 279 | RedisModule_AutoMemory(ctx); | ||
| 280 | |||
| 281 | if (argc != 2) { | ||
| 282 | return RedisModule_WrongArity(ctx); | ||
| 283 | } | ||
| 284 | |||
| 285 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ); | ||
| 286 | int type = RedisModule_KeyType(key); | ||
| 287 | if (type != REDISMODULE_KEYTYPE_EMPTY && RedisModule_ModuleTypeGetType(key) != MemAllocType) { | ||
| 288 | return RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE); | ||
| 289 | } | ||
| 290 | |||
| 291 | int ret = 0; | ||
| 292 | MemAllocObject *o; | ||
| 293 | if (type == REDISMODULE_KEYTYPE_EMPTY) { | ||
| 294 | RedisModule_ReplyWithLongLong(ctx, ret); | ||
| 295 | return REDISMODULE_OK; | ||
| 296 | } else { | ||
| 297 | o = RedisModule_ModuleTypeGetValue(key); | ||
| 298 | } | ||
| 299 | |||
| 300 | int nokey; | ||
| 301 | struct MemBlock *mem = (struct MemBlock *)RedisModule_DictGet(mem_pool[RedisModule_GetSelectedDb(ctx)], argv[1], &nokey); | ||
| 302 | if (!nokey && mem) { | ||
| 303 | RedisModule_DictDel(mem_pool[RedisModule_GetSelectedDb(ctx)], argv[1], NULL); | ||
| 304 | MemBlockFree(mem); | ||
| 305 | o->used = 0; | ||
| 306 | o->size = 0; | ||
| 307 | o->mask = 0; | ||
| 308 | ret = 1; | ||
| 309 | } | ||
| 310 | |||
| 311 | RedisModule_ReplyWithLongLong(ctx, ret); | ||
| 312 | RedisModule_ReplicateVerbatim(ctx); | ||
| 313 | return REDISMODULE_OK; | ||
| 314 | } | ||
| 315 | |||
| 316 | /* MEM.WRITE key block_index data */ | ||
| 317 | int MemWrite_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 318 | RedisModule_AutoMemory(ctx); | ||
| 319 | |||
| 320 | if (argc != 4) { | ||
| 321 | return RedisModule_WrongArity(ctx); | ||
| 322 | } | ||
| 323 | |||
| 324 | long long block_index; | ||
| 325 | if ((RedisModule_StringToLongLong(argv[2], &block_index) != REDISMODULE_OK) || block_index < 0) { | ||
| 326 | return RedisModule_ReplyWithError(ctx, "ERR invalid block_index: must be a value greater than 0"); | ||
| 327 | } | ||
| 328 | |||
| 329 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ | REDISMODULE_WRITE); | ||
| 330 | int type = RedisModule_KeyType(key); | ||
| 331 | if (type != REDISMODULE_KEYTYPE_EMPTY && RedisModule_ModuleTypeGetType(key) != MemAllocType) { | ||
| 332 | return RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE); | ||
| 333 | } | ||
| 334 | |||
| 335 | MemAllocObject *o; | ||
| 336 | if (type == REDISMODULE_KEYTYPE_EMPTY) { | ||
| 337 | return RedisModule_ReplyWithError(ctx, "ERR Memory has not been allocated"); | ||
| 338 | } else { | ||
| 339 | o = RedisModule_ModuleTypeGetValue(key); | ||
| 340 | } | ||
| 341 | |||
| 342 | if (o->mask & (1UL << block_index)) { | ||
| 343 | return RedisModule_ReplyWithError(ctx, "ERR block is busy"); | ||
| 344 | } | ||
| 345 | |||
| 346 | int ret = 0; | ||
| 347 | int nokey; | ||
| 348 | struct MemBlock *mem = (struct MemBlock *)RedisModule_DictGet(mem_pool[RedisModule_GetSelectedDb(ctx)], argv[1], &nokey); | ||
| 349 | if (!nokey && mem) { | ||
| 350 | size_t len; | ||
| 351 | const char *buf = RedisModule_StringPtrLen(argv[3], &len); | ||
| 352 | ret = MemBlockWrite(mem, block_index, buf, len); | ||
| 353 | o->mask |= (1UL << block_index); | ||
| 354 | o->used++; | ||
| 355 | } | ||
| 356 | |||
| 357 | RedisModule_ReplyWithLongLong(ctx, ret); | ||
| 358 | RedisModule_ReplicateVerbatim(ctx); | ||
| 359 | return REDISMODULE_OK; | ||
| 360 | } | ||
| 361 | |||
| 362 | /* MEM.READ key block_index */ | ||
| 363 | int MemRead_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 364 | RedisModule_AutoMemory(ctx); | ||
| 365 | |||
| 366 | if (argc != 3) { | ||
| 367 | return RedisModule_WrongArity(ctx); | ||
| 368 | } | ||
| 369 | |||
| 370 | long long block_index; | ||
| 371 | if ((RedisModule_StringToLongLong(argv[2], &block_index) != REDISMODULE_OK) || block_index < 0) { | ||
| 372 | return RedisModule_ReplyWithError(ctx, "ERR invalid block_index: must be a value greater than 0"); | ||
| 373 | } | ||
| 374 | |||
| 375 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ); | ||
| 376 | int type = RedisModule_KeyType(key); | ||
| 377 | if (type != REDISMODULE_KEYTYPE_EMPTY && RedisModule_ModuleTypeGetType(key) != MemAllocType) { | ||
| 378 | return RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE); | ||
| 379 | } | ||
| 380 | |||
| 381 | MemAllocObject *o; | ||
| 382 | if (type == REDISMODULE_KEYTYPE_EMPTY) { | ||
| 383 | return RedisModule_ReplyWithError(ctx, "ERR Memory has not been allocated"); | ||
| 384 | } else { | ||
| 385 | o = RedisModule_ModuleTypeGetValue(key); | ||
| 386 | } | ||
| 387 | |||
| 388 | if (!(o->mask & (1UL << block_index))) { | ||
| 389 | return RedisModule_ReplyWithNull(ctx); | ||
| 390 | } | ||
| 391 | |||
| 392 | int nokey; | ||
| 393 | struct MemBlock *mem = (struct MemBlock *)RedisModule_DictGet(mem_pool[RedisModule_GetSelectedDb(ctx)], argv[1], &nokey); | ||
| 394 | RedisModule_Assert(nokey == 0 && mem != NULL); | ||
| 395 | |||
| 396 | char buf[BLOCK_SIZE]; | ||
| 397 | MemBlockRead(mem, block_index, buf, sizeof(buf)); | ||
| 398 | |||
| 399 | /* Assuming that the contents are all c-style strings */ | ||
| 400 | RedisModule_ReplyWithStringBuffer(ctx, buf, strlen(buf)); | ||
| 401 | return REDISMODULE_OK; | ||
| 402 | } | ||
| 403 | |||
| 404 | /* MEM.USAGE dbid */ | ||
| 405 | int MemUsage_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 406 | RedisModule_AutoMemory(ctx); | ||
| 407 | |||
| 408 | if (argc != 2) { | ||
| 409 | return RedisModule_WrongArity(ctx); | ||
| 410 | } | ||
| 411 | |||
| 412 | long long dbid; | ||
| 413 | if ((RedisModule_StringToLongLong(argv[1], (long long *)&dbid) != REDISMODULE_OK)) { | ||
| 414 | return RedisModule_ReplyWithError(ctx, "ERR invalid value: must be a integer"); | ||
| 415 | } | ||
| 416 | |||
| 417 | if (dbid < 0 || dbid >= MAX_DB) { | ||
| 418 | return RedisModule_ReplyWithError(ctx, "ERR dbid out of range"); | ||
| 419 | } | ||
| 420 | |||
| 421 | |||
| 422 | long long size = 0, used = 0; | ||
| 423 | |||
| 424 | void *data; | ||
| 425 | RedisModuleString *key; | ||
| 426 | RedisModuleDictIter *iter = RedisModule_DictIteratorStartC(mem_pool[dbid], "^", NULL, 0); | ||
| 427 | while((key = RedisModule_DictNext(ctx, iter, &data)) != NULL) { | ||
| 428 | int dbbackup = RedisModule_GetSelectedDb(ctx); | ||
| 429 | RedisModule_SelectDb(ctx, dbid); | ||
| 430 | RedisModuleKey *openkey = RedisModule_OpenKey(ctx, key, REDISMODULE_READ); | ||
| 431 | int type = RedisModule_KeyType(openkey); | ||
| 432 | RedisModule_Assert(type != REDISMODULE_KEYTYPE_EMPTY && RedisModule_ModuleTypeGetType(openkey) == MemAllocType); | ||
| 433 | MemAllocObject *o = RedisModule_ModuleTypeGetValue(openkey); | ||
| 434 | used += o->used; | ||
| 435 | size += o->size; | ||
| 436 | RedisModule_CloseKey(openkey); | ||
| 437 | RedisModule_SelectDb(ctx, dbbackup); | ||
| 438 | } | ||
| 439 | RedisModule_DictIteratorStop(iter); | ||
| 440 | |||
| 441 | RedisModule_ReplyWithArray(ctx, 4); | ||
| 442 | RedisModule_ReplyWithSimpleString(ctx, "total"); | ||
| 443 | RedisModule_ReplyWithLongLong(ctx, size); | ||
| 444 | RedisModule_ReplyWithSimpleString(ctx, "used"); | ||
| 445 | RedisModule_ReplyWithLongLong(ctx, used); | ||
| 446 | return REDISMODULE_OK; | ||
| 447 | } | ||
| 448 | |||
| 449 | /* MEM.ALLOCANDWRITE key block_num block_index data block_index data ... */ | ||
| 450 | int MemAllocAndWrite_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 451 | RedisModule_AutoMemory(ctx); | ||
| 452 | |||
| 453 | if (argc < 3) { | ||
| 454 | return RedisModule_WrongArity(ctx); | ||
| 455 | } | ||
| 456 | |||
| 457 | long long block_num; | ||
| 458 | if ((RedisModule_StringToLongLong(argv[2], &block_num) != REDISMODULE_OK) || block_num <= 0) { | ||
| 459 | return RedisModule_ReplyWithError(ctx, "ERR invalid block_num: must be a value greater than 0"); | ||
| 460 | } | ||
| 461 | |||
| 462 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ | REDISMODULE_WRITE); | ||
| 463 | int type = RedisModule_KeyType(key); | ||
| 464 | if (type != REDISMODULE_KEYTYPE_EMPTY && RedisModule_ModuleTypeGetType(key) != MemAllocType) { | ||
| 465 | return RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE); | ||
| 466 | } | ||
| 467 | |||
| 468 | MemAllocObject *o; | ||
| 469 | if (type == REDISMODULE_KEYTYPE_EMPTY) { | ||
| 470 | o = createMemAllocObject(); | ||
| 471 | RedisModule_ModuleTypeSetValue(key, MemAllocType, o); | ||
| 472 | } else { | ||
| 473 | o = RedisModule_ModuleTypeGetValue(key); | ||
| 474 | } | ||
| 475 | |||
| 476 | struct MemBlock *mem = MemBlockCreate(block_num); | ||
| 477 | RedisModule_Assert(mem != NULL); | ||
| 478 | RedisModule_DictSet(mem_pool[RedisModule_GetSelectedDb(ctx)], argv[1], mem); | ||
| 479 | o->used = 0; | ||
| 480 | o->mask = 0; | ||
| 481 | o->size = block_num; | ||
| 482 | |||
| 483 | int i = 3; | ||
| 484 | long long block_index; | ||
| 485 | for (; i < argc; i++) { | ||
| 486 | /* Security is guaranteed internally, so no security check. */ | ||
| 487 | RedisModule_StringToLongLong(argv[i], &block_index); | ||
| 488 | size_t len; | ||
| 489 | const char * buf = RedisModule_StringPtrLen(argv[i + 1], &len); | ||
| 490 | MemBlockWrite(mem, block_index, buf, len); | ||
| 491 | o->used++; | ||
| 492 | o->mask |= (1UL << block_index); | ||
| 493 | } | ||
| 494 | |||
| 495 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 496 | RedisModule_ReplicateVerbatim(ctx); | ||
| 497 | return REDISMODULE_OK; | ||
| 498 | } | ||
| 499 | |||
| 500 | /*---------------------------- type callbacks ------------------------------------*/ | ||
| 501 | |||
| 502 | void *MemAllocRdbLoad(RedisModuleIO *rdb, int encver) { | ||
| 503 | if (encver != 0) { | ||
| 504 | return NULL; | ||
| 505 | } | ||
| 506 | |||
| 507 | MemAllocObject *o = createMemAllocObject(); | ||
| 508 | o->size = RedisModule_LoadSigned(rdb); | ||
| 509 | o->used = RedisModule_LoadSigned(rdb); | ||
| 510 | o->mask = RedisModule_LoadUnsigned(rdb); | ||
| 511 | |||
| 512 | const RedisModuleString *key = RedisModule_GetKeyNameFromIO(rdb); | ||
| 513 | int dbid = RedisModule_GetDbIdFromIO(rdb); | ||
| 514 | |||
| 515 | if (o->size) { | ||
| 516 | size_t size; | ||
| 517 | char *tmpbuf; | ||
| 518 | long long num = o->size; | ||
| 519 | struct MemBlock *head = RedisModule_Calloc(1, sizeof(struct MemBlock)); | ||
| 520 | tmpbuf = RedisModule_LoadStringBuffer(rdb, &size); | ||
| 521 | memcpy(head->block, tmpbuf, size > BLOCK_SIZE ? BLOCK_SIZE:size); | ||
| 522 | RedisModule_Free(tmpbuf); | ||
| 523 | struct MemBlock *block = head; | ||
| 524 | while (--num) { | ||
| 525 | block->next = RedisModule_Calloc(1, sizeof(struct MemBlock)); | ||
| 526 | block = block->next; | ||
| 527 | |||
| 528 | tmpbuf = RedisModule_LoadStringBuffer(rdb, &size); | ||
| 529 | memcpy(block->block, tmpbuf, size > BLOCK_SIZE ? BLOCK_SIZE:size); | ||
| 530 | RedisModule_Free(tmpbuf); | ||
| 531 | } | ||
| 532 | |||
| 533 | RedisModule_DictSet(mem_pool[dbid], (RedisModuleString *)key, head); | ||
| 534 | } | ||
| 535 | |||
| 536 | return o; | ||
| 537 | } | ||
| 538 | |||
| 539 | void MemAllocRdbSave(RedisModuleIO *rdb, void *value) { | ||
| 540 | MemAllocObject *o = value; | ||
| 541 | RedisModule_SaveSigned(rdb, o->size); | ||
| 542 | RedisModule_SaveSigned(rdb, o->used); | ||
| 543 | RedisModule_SaveUnsigned(rdb, o->mask); | ||
| 544 | |||
| 545 | const RedisModuleString *key = RedisModule_GetKeyNameFromIO(rdb); | ||
| 546 | int dbid = RedisModule_GetDbIdFromIO(rdb); | ||
| 547 | |||
| 548 | if (o->size) { | ||
| 549 | int nokey; | ||
| 550 | struct MemBlock *mem = (struct MemBlock *)RedisModule_DictGet(mem_pool[dbid], (RedisModuleString *)key, &nokey); | ||
| 551 | RedisModule_Assert(nokey == 0 && mem != NULL); | ||
| 552 | |||
| 553 | struct MemBlock *block = mem; | ||
| 554 | while (block) { | ||
| 555 | RedisModule_SaveStringBuffer(rdb, block->block, BLOCK_SIZE); | ||
| 556 | block = block->next; | ||
| 557 | } | ||
| 558 | } | ||
| 559 | } | ||
| 560 | |||
| 561 | void MemAllocAofRewrite(RedisModuleIO *aof, RedisModuleString *key, void *value) { | ||
| 562 | MemAllocObject *o = (MemAllocObject *)value; | ||
| 563 | if (o->size) { | ||
| 564 | int dbid = RedisModule_GetDbIdFromIO(aof); | ||
| 565 | int nokey; | ||
| 566 | size_t i = 0, j = 0; | ||
| 567 | struct MemBlock *mem = (struct MemBlock *)RedisModule_DictGet(mem_pool[dbid], (RedisModuleString *)key, &nokey); | ||
| 568 | RedisModule_Assert(nokey == 0 && mem != NULL); | ||
| 569 | size_t array_size = o->size * 2; | ||
| 570 | RedisModuleString ** string_array = RedisModule_Calloc(array_size, sizeof(RedisModuleString *)); | ||
| 571 | while (mem) { | ||
| 572 | string_array[i] = RedisModule_CreateStringFromLongLong(NULL, j); | ||
| 573 | string_array[i + 1] = RedisModule_CreateString(NULL, mem->block, BLOCK_SIZE); | ||
| 574 | mem = mem->next; | ||
| 575 | i += 2; | ||
| 576 | j++; | ||
| 577 | } | ||
| 578 | RedisModule_EmitAOF(aof, "mem.allocandwrite", "slv", key, o->size, string_array, array_size); | ||
| 579 | for (i = 0; i < array_size; i++) { | ||
| 580 | RedisModule_FreeString(NULL, string_array[i]); | ||
| 581 | } | ||
| 582 | RedisModule_Free(string_array); | ||
| 583 | } else { | ||
| 584 | RedisModule_EmitAOF(aof, "mem.allocandwrite", "sl", key, o->size); | ||
| 585 | } | ||
| 586 | } | ||
| 587 | |||
| 588 | void MemAllocFree(void *value) { | ||
| 589 | RedisModule_Free(value); | ||
| 590 | } | ||
| 591 | |||
| 592 | void MemAllocUnlink(RedisModuleString *key, const void *value) { | ||
| 593 | REDISMODULE_NOT_USED(key); | ||
| 594 | REDISMODULE_NOT_USED(value); | ||
| 595 | |||
| 596 | /* When unlink and unlink2 exist at the same time, we will only call unlink2. */ | ||
| 597 | RedisModule_Assert(0); | ||
| 598 | } | ||
| 599 | |||
| 600 | void MemAllocUnlink2(RedisModuleKeyOptCtx *ctx, const void *value) { | ||
| 601 | MemAllocObject *o = (MemAllocObject *)value; | ||
| 602 | |||
| 603 | const RedisModuleString *key = RedisModule_GetKeyNameFromOptCtx(ctx); | ||
| 604 | int dbid = RedisModule_GetDbIdFromOptCtx(ctx); | ||
| 605 | |||
| 606 | if (o->size) { | ||
| 607 | void *oldval; | ||
| 608 | RedisModule_DictDel(mem_pool[dbid], (RedisModuleString *)key, &oldval); | ||
| 609 | RedisModule_Assert(oldval != NULL); | ||
| 610 | MemBlockFree((struct MemBlock *)oldval); | ||
| 611 | } | ||
| 612 | } | ||
| 613 | |||
| 614 | void MemAllocDigest(RedisModuleDigest *md, void *value) { | ||
| 615 | MemAllocObject *o = (MemAllocObject *)value; | ||
| 616 | RedisModule_DigestAddLongLong(md, o->size); | ||
| 617 | RedisModule_DigestAddLongLong(md, o->used); | ||
| 618 | RedisModule_DigestAddLongLong(md, o->mask); | ||
| 619 | |||
| 620 | int dbid = RedisModule_GetDbIdFromDigest(md); | ||
| 621 | const RedisModuleString *key = RedisModule_GetKeyNameFromDigest(md); | ||
| 622 | |||
| 623 | if (o->size) { | ||
| 624 | int nokey; | ||
| 625 | struct MemBlock *mem = (struct MemBlock *)RedisModule_DictGet(mem_pool[dbid], (RedisModuleString *)key, &nokey); | ||
| 626 | RedisModule_Assert(nokey == 0 && mem != NULL); | ||
| 627 | |||
| 628 | struct MemBlock *block = mem; | ||
| 629 | while (block) { | ||
| 630 | RedisModule_DigestAddStringBuffer(md, (const char *)block->block, BLOCK_SIZE); | ||
| 631 | block = block->next; | ||
| 632 | } | ||
| 633 | } | ||
| 634 | } | ||
| 635 | |||
| 636 | void *MemAllocCopy2(RedisModuleKeyOptCtx *ctx, const void *value) { | ||
| 637 | const MemAllocObject *old = value; | ||
| 638 | MemAllocObject *new = createMemAllocObject(); | ||
| 639 | new->size = old->size; | ||
| 640 | new->used = old->used; | ||
| 641 | new->mask = old->mask; | ||
| 642 | |||
| 643 | int from_dbid = RedisModule_GetDbIdFromOptCtx(ctx); | ||
| 644 | int to_dbid = RedisModule_GetToDbIdFromOptCtx(ctx); | ||
| 645 | const RedisModuleString *fromkey = RedisModule_GetKeyNameFromOptCtx(ctx); | ||
| 646 | const RedisModuleString *tokey = RedisModule_GetToKeyNameFromOptCtx(ctx); | ||
| 647 | |||
| 648 | if (old->size) { | ||
| 649 | int nokey; | ||
| 650 | struct MemBlock *oldmem = (struct MemBlock *)RedisModule_DictGet(mem_pool[from_dbid], (RedisModuleString *)fromkey, &nokey); | ||
| 651 | RedisModule_Assert(nokey == 0 && oldmem != NULL); | ||
| 652 | struct MemBlock *newmem = MemBlockClone(oldmem); | ||
| 653 | RedisModule_Assert(newmem != NULL); | ||
| 654 | RedisModule_DictSet(mem_pool[to_dbid], (RedisModuleString *)tokey, newmem); | ||
| 655 | } | ||
| 656 | |||
| 657 | return new; | ||
| 658 | } | ||
| 659 | |||
| 660 | size_t MemAllocMemUsage2(RedisModuleKeyOptCtx *ctx, const void *value, size_t sample_size) { | ||
| 661 | REDISMODULE_NOT_USED(ctx); | ||
| 662 | REDISMODULE_NOT_USED(sample_size); | ||
| 663 | uint64_t size = 0; | ||
| 664 | MemAllocObject *o = (MemAllocObject *)value; | ||
| 665 | |||
| 666 | size += sizeof(*o); | ||
| 667 | size += o->size * sizeof(struct MemBlock); | ||
| 668 | |||
| 669 | return size; | ||
| 670 | } | ||
| 671 | |||
| 672 | size_t MemAllocMemFreeEffort2(RedisModuleKeyOptCtx *ctx, const void *value) { | ||
| 673 | REDISMODULE_NOT_USED(ctx); | ||
| 674 | MemAllocObject *o = (MemAllocObject *)value; | ||
| 675 | return o->size; | ||
| 676 | } | ||
| 677 | |||
| 678 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 679 | REDISMODULE_NOT_USED(argv); | ||
| 680 | REDISMODULE_NOT_USED(argc); | ||
| 681 | |||
| 682 | if (RedisModule_Init(ctx, "datatype2", 1,REDISMODULE_APIVER_1) == REDISMODULE_ERR) { | ||
| 683 | return REDISMODULE_ERR; | ||
| 684 | } | ||
| 685 | |||
| 686 | RedisModuleTypeMethods tm = { | ||
| 687 | .version = REDISMODULE_TYPE_METHOD_VERSION, | ||
| 688 | .rdb_load = MemAllocRdbLoad, | ||
| 689 | .rdb_save = MemAllocRdbSave, | ||
| 690 | .aof_rewrite = MemAllocAofRewrite, | ||
| 691 | .free = MemAllocFree, | ||
| 692 | .digest = MemAllocDigest, | ||
| 693 | .unlink = MemAllocUnlink, | ||
| 694 | // .defrag = MemAllocDefrag, // Tested in defragtest.c | ||
| 695 | .unlink2 = MemAllocUnlink2, | ||
| 696 | .copy2 = MemAllocCopy2, | ||
| 697 | .mem_usage2 = MemAllocMemUsage2, | ||
| 698 | .free_effort2 = MemAllocMemFreeEffort2, | ||
| 699 | }; | ||
| 700 | |||
| 701 | MemAllocType = RedisModule_CreateDataType(ctx, "mem_alloc", 0, &tm); | ||
| 702 | if (MemAllocType == NULL) { | ||
| 703 | return REDISMODULE_ERR; | ||
| 704 | } | ||
| 705 | |||
| 706 | if (RedisModule_CreateCommand(ctx, "mem.alloc", MemAlloc_RedisCommand, "write deny-oom", 1, 1, 1) == REDISMODULE_ERR) { | ||
| 707 | return REDISMODULE_ERR; | ||
| 708 | } | ||
| 709 | |||
| 710 | if (RedisModule_CreateCommand(ctx, "mem.free", MemFree_RedisCommand, "write deny-oom", 1, 1, 1) == REDISMODULE_ERR) { | ||
| 711 | return REDISMODULE_ERR; | ||
| 712 | } | ||
| 713 | |||
| 714 | if (RedisModule_CreateCommand(ctx, "mem.write", MemWrite_RedisCommand, "write deny-oom", 1, 1, 1) == REDISMODULE_ERR) { | ||
| 715 | return REDISMODULE_ERR; | ||
| 716 | } | ||
| 717 | |||
| 718 | if (RedisModule_CreateCommand(ctx, "mem.read", MemRead_RedisCommand, "readonly", 1, 1, 1) == REDISMODULE_ERR) { | ||
| 719 | return REDISMODULE_ERR; | ||
| 720 | } | ||
| 721 | |||
| 722 | if (RedisModule_CreateCommand(ctx, "mem.usage", MemUsage_RedisCommand, "readonly", 1, 1, 1) == REDISMODULE_ERR) { | ||
| 723 | return REDISMODULE_ERR; | ||
| 724 | } | ||
| 725 | |||
| 726 | /* used for internal aof rewrite */ | ||
| 727 | if (RedisModule_CreateCommand(ctx, "mem.allocandwrite", MemAllocAndWrite_RedisCommand, "write deny-oom", 1, 1, 1) == REDISMODULE_ERR) { | ||
| 728 | return REDISMODULE_ERR; | ||
| 729 | } | ||
| 730 | |||
| 731 | for(int i = 0; i < MAX_DB; i++){ | ||
| 732 | mem_pool[i] = RedisModule_CreateDict(NULL); | ||
| 733 | } | ||
| 734 | |||
| 735 | RedisModule_SubscribeToServerEvent(ctx, RedisModuleEvent_FlushDB, flushdbCallback); | ||
| 736 | RedisModule_SubscribeToServerEvent(ctx, RedisModuleEvent_SwapDB, swapDbCallback); | ||
| 737 | |||
| 738 | return REDISMODULE_OK; | ||
| 739 | } | ||
diff --git a/examples/redis-unstable/tests/modules/defragtest.c b/examples/redis-unstable/tests/modules/defragtest.c new file mode 100644 index 0000000..8b9e182 --- /dev/null +++ b/examples/redis-unstable/tests/modules/defragtest.c | |||
| @@ -0,0 +1,475 @@ | |||
| 1 | /* A module that implements defrag callback mechanisms. | ||
| 2 | */ | ||
| 3 | |||
| 4 | #include "redismodule.h" | ||
| 5 | #include <stdlib.h> | ||
| 6 | #include <string.h> | ||
| 7 | |||
| 8 | #define UNUSED(V) ((void) V) | ||
| 9 | |||
| 10 | static RedisModuleType *FragType; | ||
| 11 | |||
| 12 | struct FragObject { | ||
| 13 | unsigned long len; | ||
| 14 | void **values; | ||
| 15 | int maxstep; | ||
| 16 | }; | ||
| 17 | |||
| 18 | /* Make sure we get the expected cursor */ | ||
| 19 | unsigned long int last_set_cursor = 0; | ||
| 20 | |||
| 21 | unsigned long int datatype_attempts = 0; | ||
| 22 | unsigned long int datatype_defragged = 0; | ||
| 23 | unsigned long int datatype_raw_defragged = 0; | ||
| 24 | unsigned long int datatype_resumes = 0; | ||
| 25 | unsigned long int datatype_wrong_cursor = 0; | ||
| 26 | unsigned long int defrag_started = 0; | ||
| 27 | unsigned long int defrag_ended = 0; | ||
| 28 | unsigned long int global_strings_attempts = 0; | ||
| 29 | unsigned long int global_strings_defragged = 0; | ||
| 30 | unsigned long int global_dicts_resumes = 0; /* Number of dict defragmentation resumed from a previous break */ | ||
| 31 | unsigned long int global_subdicts_resumes = 0; /* Number of subdict defragmentation resumed from a previous break */ | ||
| 32 | unsigned long int global_dicts_attempts = 0; /* Number of attempts to defragment dictionary */ | ||
| 33 | unsigned long int global_dicts_defragged = 0; /* Number of dictionaries successfully defragmented */ | ||
| 34 | unsigned long int global_dicts_items_defragged = 0; /* Number of dictionaries items successfully defragmented */ | ||
| 35 | |||
| 36 | unsigned long global_strings_len = 0; | ||
| 37 | RedisModuleString **global_strings = NULL; | ||
| 38 | |||
| 39 | unsigned long global_dicts_len = 0; | ||
| 40 | RedisModuleDict **global_dicts = NULL; | ||
| 41 | |||
| 42 | static void createGlobalStrings(RedisModuleCtx *ctx, unsigned long count) | ||
| 43 | { | ||
| 44 | global_strings_len = count; | ||
| 45 | global_strings = RedisModule_Alloc(sizeof(RedisModuleString *) * count); | ||
| 46 | |||
| 47 | for (unsigned long i = 0; i < count; i++) { | ||
| 48 | global_strings[i] = RedisModule_CreateStringFromLongLong(ctx, i); | ||
| 49 | } | ||
| 50 | } | ||
| 51 | |||
| 52 | static int defragGlobalStrings(RedisModuleDefragCtx *ctx) | ||
| 53 | { | ||
| 54 | unsigned long cursor = 0; | ||
| 55 | RedisModule_DefragCursorGet(ctx, &cursor); | ||
| 56 | |||
| 57 | if (!global_strings_len) return 0; /* strings is empty. */ | ||
| 58 | RedisModule_Assert(cursor < global_strings_len); | ||
| 59 | for (; cursor < global_strings_len; cursor++) { | ||
| 60 | RedisModuleString *str = global_strings[cursor]; | ||
| 61 | if (!str) continue; | ||
| 62 | RedisModuleString *new = RedisModule_DefragRedisModuleString(ctx, str); | ||
| 63 | global_strings_attempts++; | ||
| 64 | if (new != NULL) { | ||
| 65 | global_strings[cursor] = new; | ||
| 66 | global_strings_defragged++; | ||
| 67 | } | ||
| 68 | |||
| 69 | if (RedisModule_DefragShouldStop(ctx)) { | ||
| 70 | RedisModule_DefragCursorSet(ctx, cursor); | ||
| 71 | return 1; | ||
| 72 | } | ||
| 73 | } | ||
| 74 | return 0; | ||
| 75 | } | ||
| 76 | |||
| 77 | static void createFragGlobalStrings(RedisModuleCtx *ctx) { | ||
| 78 | for (unsigned long i = 0; i < global_strings_len; i++) { | ||
| 79 | if (i % 2 == 1) { | ||
| 80 | RedisModule_FreeString(ctx, global_strings[i]); | ||
| 81 | global_strings[i] = NULL; | ||
| 82 | } | ||
| 83 | } | ||
| 84 | } | ||
| 85 | |||
| 86 | static void createGlobalDicts(RedisModuleCtx *ctx, unsigned long count) { | ||
| 87 | global_dicts_len = count; | ||
| 88 | global_dicts = RedisModule_Alloc(sizeof(RedisModuleDict *) * count); | ||
| 89 | |||
| 90 | /* Create some nested dictionaries: | ||
| 91 | * - Each main dict contains some subdicts. | ||
| 92 | * - Each sub-dict contains some strings. */ | ||
| 93 | for (unsigned long i = 0; i < count; i++) { | ||
| 94 | RedisModuleDict *dict = RedisModule_CreateDict(ctx); | ||
| 95 | for (unsigned long j = 0; j < 10; j++) { | ||
| 96 | /* Create sub dict. */ | ||
| 97 | RedisModuleDict *subdict = RedisModule_CreateDict(ctx); | ||
| 98 | for (unsigned long k = 0; k < 10; k++) { | ||
| 99 | RedisModuleString *str = RedisModule_CreateStringFromULongLong(ctx, k); | ||
| 100 | RedisModule_DictSet(subdict, str, str); | ||
| 101 | } | ||
| 102 | |||
| 103 | RedisModuleString *key = RedisModule_CreateStringFromULongLong(ctx, j); | ||
| 104 | RedisModule_DictSet(dict, key, subdict); | ||
| 105 | RedisModule_FreeString(ctx, key); | ||
| 106 | } | ||
| 107 | global_dicts[i] = dict; | ||
| 108 | } | ||
| 109 | } | ||
| 110 | |||
| 111 | static void freeFragGlobalSubDict(RedisModuleCtx *ctx, RedisModuleDict *subdict) { | ||
| 112 | char *key; | ||
| 113 | size_t keylen; | ||
| 114 | RedisModuleString *str; | ||
| 115 | RedisModuleDictIter *iter = RedisModule_DictIteratorStartC(subdict, "^", NULL, 0); | ||
| 116 | while ((key = RedisModule_DictNextC(iter, &keylen, (void**)&str))) { | ||
| 117 | RedisModule_FreeString(ctx, str); | ||
| 118 | } | ||
| 119 | RedisModule_FreeDict(ctx, subdict); | ||
| 120 | RedisModule_DictIteratorStop(iter); | ||
| 121 | } | ||
| 122 | |||
| 123 | static void createFragGlobalDicts(RedisModuleCtx *ctx) { | ||
| 124 | char *key; | ||
| 125 | size_t keylen; | ||
| 126 | RedisModuleDict *subdict; | ||
| 127 | |||
| 128 | for (unsigned long i = 0; i < global_dicts_len; i++) { | ||
| 129 | RedisModuleDict *dict = global_dicts[i]; | ||
| 130 | if (!dict) continue; | ||
| 131 | |||
| 132 | /* Handle dictionaries differently based on their index in global_dicts array: | ||
| 133 | * 1. For odd indices (i % 2 == 1): Remove the entire dictionary. | ||
| 134 | * 2. For even indices: Keep the dictionary but remove half of its items. */ | ||
| 135 | if (i % 2 == 1) { | ||
| 136 | RedisModuleDictIter *iter = RedisModule_DictIteratorStartC(dict, "^", NULL, 0); | ||
| 137 | while ((key = RedisModule_DictNextC(iter, &keylen, (void**)&subdict))) { | ||
| 138 | freeFragGlobalSubDict(ctx, subdict); | ||
| 139 | } | ||
| 140 | RedisModule_FreeDict(ctx, dict); | ||
| 141 | global_dicts[i] = NULL; | ||
| 142 | RedisModule_DictIteratorStop(iter); | ||
| 143 | } else { | ||
| 144 | int key_index = 0; | ||
| 145 | RedisModuleDictIter *iter = RedisModule_DictIteratorStartC(dict, "^", NULL, 0); | ||
| 146 | while ((key = RedisModule_DictNextC(iter, &keylen, (void**)&subdict))) { | ||
| 147 | if (key_index++ % 2 == 1) { | ||
| 148 | freeFragGlobalSubDict(ctx, subdict); | ||
| 149 | RedisModule_DictReplaceC(dict, key, keylen, NULL); | ||
| 150 | } | ||
| 151 | } | ||
| 152 | RedisModule_DictIteratorStop(iter); | ||
| 153 | } | ||
| 154 | } | ||
| 155 | } | ||
| 156 | |||
| 157 | static int defragGlobalSubDictValueCB(RedisModuleDefragCtx *ctx, void *data, unsigned char *key, size_t keylen, void **newptr) { | ||
| 158 | REDISMODULE_NOT_USED(key); | ||
| 159 | REDISMODULE_NOT_USED(keylen); | ||
| 160 | if (!data) return 0; | ||
| 161 | *newptr = RedisModule_DefragAlloc(ctx, data); | ||
| 162 | return 0; | ||
| 163 | } | ||
| 164 | |||
| 165 | static int defragGlobalDictValueCB(RedisModuleDefragCtx *ctx, void *data, unsigned char *key, size_t keylen, void **newptr) { | ||
| 166 | REDISMODULE_NOT_USED(key); | ||
| 167 | REDISMODULE_NOT_USED(keylen); | ||
| 168 | static RedisModuleString *seekTo = NULL; | ||
| 169 | RedisModuleDict *subdict = data; | ||
| 170 | if (!subdict) return 0; | ||
| 171 | if (seekTo != NULL) global_subdicts_resumes++; | ||
| 172 | |||
| 173 | *newptr = RedisModule_DefragRedisModuleDict(ctx, subdict, defragGlobalSubDictValueCB, &seekTo); | ||
| 174 | if (*newptr) global_dicts_items_defragged++; | ||
| 175 | /* Return 1 if seekTo is not NULL, indicating this node needs more defrag work. */ | ||
| 176 | return seekTo != NULL; | ||
| 177 | } | ||
| 178 | |||
| 179 | static int defragGlobalDicts(RedisModuleDefragCtx *ctx) { | ||
| 180 | static RedisModuleString *seekTo = NULL; | ||
| 181 | static unsigned long dict_index = 0; | ||
| 182 | unsigned long cursor = 0; | ||
| 183 | |||
| 184 | RedisModule_DefragCursorGet(ctx, &cursor); | ||
| 185 | if (cursor == 0) { /* Start a new defrag. */ | ||
| 186 | if (seekTo) { | ||
| 187 | RedisModule_FreeString(NULL, seekTo); | ||
| 188 | seekTo = NULL; | ||
| 189 | } | ||
| 190 | dict_index = 0; | ||
| 191 | } else { | ||
| 192 | global_dicts_resumes++; | ||
| 193 | } | ||
| 194 | |||
| 195 | if (!global_dicts_len) return 0; /* dicts is empty. */ | ||
| 196 | RedisModule_Assert(dict_index < global_dicts_len); | ||
| 197 | for (; dict_index < global_dicts_len; dict_index++) { | ||
| 198 | RedisModuleDict *dict = global_dicts[dict_index]; | ||
| 199 | if (!dict) continue; | ||
| 200 | RedisModuleDict *new = RedisModule_DefragRedisModuleDict(ctx, dict, defragGlobalDictValueCB, &seekTo); | ||
| 201 | global_dicts_attempts++; | ||
| 202 | if (new != NULL) { | ||
| 203 | global_dicts[dict_index] = new; | ||
| 204 | global_dicts_defragged++; | ||
| 205 | } | ||
| 206 | |||
| 207 | if (seekTo != NULL) { | ||
| 208 | /* Set cursor to 1 to indicate defragmentation is not finished. */ | ||
| 209 | RedisModule_DefragCursorSet(ctx, 1); | ||
| 210 | return 1; | ||
| 211 | } | ||
| 212 | } | ||
| 213 | |||
| 214 | /* Set cursor to 0 to indicate completion. */ | ||
| 215 | dict_index = 0; | ||
| 216 | RedisModule_DefragCursorSet(ctx, 0); | ||
| 217 | return 0; | ||
| 218 | } | ||
| 219 | |||
| 220 | typedef enum { DEFRAG_NOT_START, DEFRAG_STRING, DEFRAG_DICT } defrag_module_stage; | ||
| 221 | static int defragGlobal(RedisModuleDefragCtx *ctx) { | ||
| 222 | static defrag_module_stage stage = DEFRAG_NOT_START; | ||
| 223 | if (stage == DEFRAG_NOT_START) { | ||
| 224 | stage = DEFRAG_STRING; /* Start a new global defrag. */ | ||
| 225 | } | ||
| 226 | |||
| 227 | if (stage == DEFRAG_STRING) { | ||
| 228 | if (defragGlobalStrings(ctx) != 0) return 1; | ||
| 229 | stage = DEFRAG_DICT; | ||
| 230 | } | ||
| 231 | if (stage == DEFRAG_DICT) { | ||
| 232 | if (defragGlobalDicts(ctx) != 0) return 1; | ||
| 233 | stage = DEFRAG_NOT_START; | ||
| 234 | } | ||
| 235 | return 0; | ||
| 236 | } | ||
| 237 | |||
| 238 | static void defragStart(RedisModuleDefragCtx *ctx) { | ||
| 239 | REDISMODULE_NOT_USED(ctx); | ||
| 240 | defrag_started++; | ||
| 241 | } | ||
| 242 | |||
| 243 | static void defragEnd(RedisModuleDefragCtx *ctx) { | ||
| 244 | REDISMODULE_NOT_USED(ctx); | ||
| 245 | defrag_ended++; | ||
| 246 | } | ||
| 247 | |||
| 248 | static void FragInfo(RedisModuleInfoCtx *ctx, int for_crash_report) { | ||
| 249 | REDISMODULE_NOT_USED(for_crash_report); | ||
| 250 | |||
| 251 | RedisModule_InfoAddSection(ctx, "stats"); | ||
| 252 | RedisModule_InfoAddFieldLongLong(ctx, "datatype_attempts", datatype_attempts); | ||
| 253 | RedisModule_InfoAddFieldLongLong(ctx, "datatype_defragged", datatype_defragged); | ||
| 254 | RedisModule_InfoAddFieldLongLong(ctx, "datatype_raw_defragged", datatype_raw_defragged); | ||
| 255 | RedisModule_InfoAddFieldLongLong(ctx, "datatype_resumes", datatype_resumes); | ||
| 256 | RedisModule_InfoAddFieldLongLong(ctx, "datatype_wrong_cursor", datatype_wrong_cursor); | ||
| 257 | RedisModule_InfoAddFieldLongLong(ctx, "global_strings_attempts", global_strings_attempts); | ||
| 258 | RedisModule_InfoAddFieldLongLong(ctx, "global_strings_defragged", global_strings_defragged); | ||
| 259 | RedisModule_InfoAddFieldLongLong(ctx, "global_dicts_resumes", global_dicts_resumes); | ||
| 260 | RedisModule_InfoAddFieldLongLong(ctx, "global_subdicts_resumes", global_subdicts_resumes); | ||
| 261 | RedisModule_InfoAddFieldLongLong(ctx, "global_dicts_attempts", global_dicts_attempts); | ||
| 262 | RedisModule_InfoAddFieldLongLong(ctx, "global_dicts_defragged", global_dicts_defragged); | ||
| 263 | RedisModule_InfoAddFieldLongLong(ctx, "global_dicts_items_defragged", global_dicts_items_defragged); | ||
| 264 | RedisModule_InfoAddFieldLongLong(ctx, "defrag_started", defrag_started); | ||
| 265 | RedisModule_InfoAddFieldLongLong(ctx, "defrag_ended", defrag_ended); | ||
| 266 | } | ||
| 267 | |||
| 268 | struct FragObject *createFragObject(unsigned long len, unsigned long size, int maxstep) { | ||
| 269 | struct FragObject *o = RedisModule_Alloc(sizeof(*o)); | ||
| 270 | o->len = len; | ||
| 271 | o->values = RedisModule_Alloc(sizeof(RedisModuleString*) * len); | ||
| 272 | o->maxstep = maxstep; | ||
| 273 | |||
| 274 | for (unsigned long i = 0; i < len; i++) { | ||
| 275 | o->values[i] = RedisModule_Calloc(1, size); | ||
| 276 | } | ||
| 277 | |||
| 278 | return o; | ||
| 279 | } | ||
| 280 | |||
| 281 | /* FRAG.RESETSTATS */ | ||
| 282 | static int fragResetStatsCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 283 | REDISMODULE_NOT_USED(argv); | ||
| 284 | REDISMODULE_NOT_USED(argc); | ||
| 285 | |||
| 286 | datatype_attempts = 0; | ||
| 287 | datatype_defragged = 0; | ||
| 288 | datatype_raw_defragged = 0; | ||
| 289 | datatype_resumes = 0; | ||
| 290 | datatype_wrong_cursor = 0; | ||
| 291 | global_strings_attempts = 0; | ||
| 292 | global_strings_defragged = 0; | ||
| 293 | global_dicts_resumes = 0; | ||
| 294 | global_subdicts_resumes = 0; | ||
| 295 | global_dicts_attempts = 0; | ||
| 296 | global_dicts_defragged = 0; | ||
| 297 | global_dicts_items_defragged = 0; | ||
| 298 | defrag_started = 0; | ||
| 299 | defrag_ended = 0; | ||
| 300 | |||
| 301 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 302 | return REDISMODULE_OK; | ||
| 303 | } | ||
| 304 | |||
| 305 | /* FRAG.CREATE key len size maxstep */ | ||
| 306 | static int fragCreateCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 307 | if (argc != 5) | ||
| 308 | return RedisModule_WrongArity(ctx); | ||
| 309 | |||
| 310 | RedisModuleKey *key = RedisModule_OpenKey(ctx,argv[1], | ||
| 311 | REDISMODULE_READ|REDISMODULE_WRITE); | ||
| 312 | int type = RedisModule_KeyType(key); | ||
| 313 | if (type != REDISMODULE_KEYTYPE_EMPTY) | ||
| 314 | { | ||
| 315 | return RedisModule_ReplyWithError(ctx, "ERR key exists"); | ||
| 316 | } | ||
| 317 | |||
| 318 | long long len; | ||
| 319 | if ((RedisModule_StringToLongLong(argv[2], &len) != REDISMODULE_OK)) { | ||
| 320 | return RedisModule_ReplyWithError(ctx, "ERR invalid len"); | ||
| 321 | } | ||
| 322 | |||
| 323 | long long size; | ||
| 324 | if ((RedisModule_StringToLongLong(argv[3], &size) != REDISMODULE_OK)) { | ||
| 325 | return RedisModule_ReplyWithError(ctx, "ERR invalid size"); | ||
| 326 | } | ||
| 327 | |||
| 328 | long long maxstep; | ||
| 329 | if ((RedisModule_StringToLongLong(argv[4], &maxstep) != REDISMODULE_OK)) { | ||
| 330 | return RedisModule_ReplyWithError(ctx, "ERR invalid maxstep"); | ||
| 331 | } | ||
| 332 | |||
| 333 | struct FragObject *o = createFragObject(len, size, maxstep); | ||
| 334 | RedisModule_ModuleTypeSetValue(key, FragType, o); | ||
| 335 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 336 | RedisModule_CloseKey(key); | ||
| 337 | |||
| 338 | return REDISMODULE_OK; | ||
| 339 | } | ||
| 340 | |||
| 341 | /* FRAG.create_frag_global len */ | ||
| 342 | static int fragCreateGlobalCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 343 | UNUSED(argv); | ||
| 344 | if (argc != 2) | ||
| 345 | return RedisModule_WrongArity(ctx); | ||
| 346 | |||
| 347 | long long glen; | ||
| 348 | if ((RedisModule_StringToLongLong(argv[1], &glen) != REDISMODULE_OK)) { | ||
| 349 | return RedisModule_ReplyWithError(ctx, "ERR invalid len"); | ||
| 350 | } | ||
| 351 | |||
| 352 | createGlobalStrings(ctx, glen); | ||
| 353 | createGlobalDicts(ctx, glen); | ||
| 354 | createFragGlobalStrings(ctx); | ||
| 355 | createFragGlobalDicts(ctx); | ||
| 356 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 357 | return REDISMODULE_OK; | ||
| 358 | } | ||
| 359 | |||
| 360 | void FragFree(void *value) { | ||
| 361 | struct FragObject *o = value; | ||
| 362 | |||
| 363 | for (unsigned long i = 0; i < o->len; i++) | ||
| 364 | RedisModule_Free(o->values[i]); | ||
| 365 | RedisModule_Free(o->values); | ||
| 366 | RedisModule_Free(o); | ||
| 367 | } | ||
| 368 | |||
| 369 | size_t FragFreeEffort(RedisModuleString *key, const void *value) { | ||
| 370 | REDISMODULE_NOT_USED(key); | ||
| 371 | |||
| 372 | const struct FragObject *o = value; | ||
| 373 | return o->len; | ||
| 374 | } | ||
| 375 | |||
| 376 | int FragDefrag(RedisModuleDefragCtx *ctx, RedisModuleString *key, void **value) { | ||
| 377 | unsigned long i = 0; | ||
| 378 | int steps = 0; | ||
| 379 | |||
| 380 | int dbid = RedisModule_GetDbIdFromDefragCtx(ctx); | ||
| 381 | RedisModule_Assert(dbid != -1); | ||
| 382 | |||
| 383 | RedisModule_Log(NULL, "notice", "Defrag key: %s", RedisModule_StringPtrLen(key, NULL)); | ||
| 384 | |||
| 385 | /* Attempt to get cursor, validate it's what we're exepcting */ | ||
| 386 | if (RedisModule_DefragCursorGet(ctx, &i) == REDISMODULE_OK) { | ||
| 387 | if (i > 0) datatype_resumes++; | ||
| 388 | |||
| 389 | /* Validate we're expecting this cursor */ | ||
| 390 | if (i != last_set_cursor) datatype_wrong_cursor++; | ||
| 391 | } else { | ||
| 392 | if (last_set_cursor != 0) datatype_wrong_cursor++; | ||
| 393 | } | ||
| 394 | |||
| 395 | /* Attempt to defrag the object itself */ | ||
| 396 | datatype_attempts++; | ||
| 397 | struct FragObject *o = RedisModule_DefragAlloc(ctx, *value); | ||
| 398 | if (o == NULL) { | ||
| 399 | /* Not defragged */ | ||
| 400 | o = *value; | ||
| 401 | } else { | ||
| 402 | /* Defragged */ | ||
| 403 | *value = o; | ||
| 404 | datatype_defragged++; | ||
| 405 | } | ||
| 406 | |||
| 407 | /* Deep defrag now */ | ||
| 408 | for (; i < o->len; i++) { | ||
| 409 | datatype_attempts++; | ||
| 410 | void *new = RedisModule_DefragAlloc(ctx, o->values[i]); | ||
| 411 | if (new) { | ||
| 412 | o->values[i] = new; | ||
| 413 | datatype_defragged++; | ||
| 414 | } | ||
| 415 | |||
| 416 | if ((o->maxstep && ++steps > o->maxstep) || | ||
| 417 | ((i % 64 == 0) && RedisModule_DefragShouldStop(ctx))) | ||
| 418 | { | ||
| 419 | RedisModule_DefragCursorSet(ctx, i); | ||
| 420 | last_set_cursor = i; | ||
| 421 | return 1; | ||
| 422 | } | ||
| 423 | } | ||
| 424 | |||
| 425 | /* Defrag the values array itself using RedisModule_DefragAllocRaw | ||
| 426 | * and RedisModule_DefragFreeRaw for testing purposes. */ | ||
| 427 | void *new_values = RedisModule_DefragAllocRaw(ctx, o->len * sizeof(void*)); | ||
| 428 | memcpy(new_values, o->values, o->len * sizeof(void*)); | ||
| 429 | RedisModule_DefragFreeRaw(ctx, o->values); | ||
| 430 | o->values = new_values; | ||
| 431 | datatype_raw_defragged++; | ||
| 432 | |||
| 433 | last_set_cursor = 0; | ||
| 434 | return 0; | ||
| 435 | } | ||
| 436 | |||
| 437 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 438 | REDISMODULE_NOT_USED(argv); | ||
| 439 | REDISMODULE_NOT_USED(argc); | ||
| 440 | |||
| 441 | if (RedisModule_Init(ctx, "defragtest", 1, REDISMODULE_APIVER_1) | ||
| 442 | == REDISMODULE_ERR) return REDISMODULE_ERR; | ||
| 443 | |||
| 444 | if (RedisModule_GetTypeMethodVersion() < REDISMODULE_TYPE_METHOD_VERSION) { | ||
| 445 | return REDISMODULE_ERR; | ||
| 446 | } | ||
| 447 | |||
| 448 | RedisModuleTypeMethods tm = { | ||
| 449 | .version = REDISMODULE_TYPE_METHOD_VERSION, | ||
| 450 | .free = FragFree, | ||
| 451 | .free_effort = FragFreeEffort, | ||
| 452 | .defrag = FragDefrag | ||
| 453 | }; | ||
| 454 | |||
| 455 | FragType = RedisModule_CreateDataType(ctx, "frag_type", 0, &tm); | ||
| 456 | if (FragType == NULL) return REDISMODULE_ERR; | ||
| 457 | |||
| 458 | if (RedisModule_CreateCommand(ctx, "frag.create", | ||
| 459 | fragCreateCommand, "write deny-oom", 1, 1, 1) == REDISMODULE_ERR) | ||
| 460 | return REDISMODULE_ERR; | ||
| 461 | |||
| 462 | if (RedisModule_CreateCommand(ctx, "frag.create_frag_global", | ||
| 463 | fragCreateGlobalCommand, "write deny-oom", 1, 1, 1) == REDISMODULE_ERR) | ||
| 464 | return REDISMODULE_ERR; | ||
| 465 | |||
| 466 | if (RedisModule_CreateCommand(ctx, "frag.resetstats", | ||
| 467 | fragResetStatsCommand, "write deny-oom", 1, 1, 1) == REDISMODULE_ERR) | ||
| 468 | return REDISMODULE_ERR; | ||
| 469 | |||
| 470 | RedisModule_RegisterInfoFunc(ctx, FragInfo); | ||
| 471 | RedisModule_RegisterDefragFunc2(ctx, defragGlobal); | ||
| 472 | RedisModule_RegisterDefragCallbacks(ctx, defragStart, defragEnd); | ||
| 473 | |||
| 474 | return REDISMODULE_OK; | ||
| 475 | } | ||
diff --git a/examples/redis-unstable/tests/modules/eventloop.c b/examples/redis-unstable/tests/modules/eventloop.c new file mode 100644 index 0000000..c0cfdf0 --- /dev/null +++ b/examples/redis-unstable/tests/modules/eventloop.c | |||
| @@ -0,0 +1,276 @@ | |||
| 1 | /* This module contains four tests : | ||
| 2 | * 1- test.sanity : Basic tests for argument validation mostly. | ||
| 3 | * 2- test.sendbytes : Creates a pipe and registers its fds to the event loop, | ||
| 4 | * one end of the pipe for read events and the other end for | ||
| 5 | * the write events. On writable event, data is written. On | ||
| 6 | * readable event data is read. Repeated until all data is | ||
| 7 | * received. | ||
| 8 | * 3- test.iteration : A test for BEFORE_SLEEP and AFTER_SLEEP callbacks. | ||
| 9 | * Counters are incremented each time these events are | ||
| 10 | * fired. They should be equal and increment monotonically. | ||
| 11 | * 4- test.oneshot : Test for oneshot API | ||
| 12 | */ | ||
| 13 | |||
| 14 | #include "redismodule.h" | ||
| 15 | #include <stdlib.h> | ||
| 16 | #include <unistd.h> | ||
| 17 | #include <fcntl.h> | ||
| 18 | #include <memory.h> | ||
| 19 | #include <errno.h> | ||
| 20 | |||
| 21 | int fds[2]; | ||
| 22 | long long buf_size; | ||
| 23 | char *src; | ||
| 24 | long long src_offset; | ||
| 25 | char *dst; | ||
| 26 | long long dst_offset; | ||
| 27 | |||
| 28 | RedisModuleBlockedClient *bc; | ||
| 29 | RedisModuleCtx *reply_ctx; | ||
| 30 | |||
| 31 | void onReadable(int fd, void *user_data, int mask) { | ||
| 32 | REDISMODULE_NOT_USED(mask); | ||
| 33 | |||
| 34 | RedisModule_Assert(strcmp(user_data, "userdataread") == 0); | ||
| 35 | |||
| 36 | while (1) { | ||
| 37 | int rd = read(fd, dst + dst_offset, buf_size - dst_offset); | ||
| 38 | if (rd <= 0) | ||
| 39 | return; | ||
| 40 | dst_offset += rd; | ||
| 41 | |||
| 42 | /* Received all bytes */ | ||
| 43 | if (dst_offset == buf_size) { | ||
| 44 | if (memcmp(src, dst, buf_size) == 0) | ||
| 45 | RedisModule_ReplyWithSimpleString(reply_ctx, "OK"); | ||
| 46 | else | ||
| 47 | RedisModule_ReplyWithError(reply_ctx, "ERR bytes mismatch"); | ||
| 48 | |||
| 49 | RedisModule_EventLoopDel(fds[0], REDISMODULE_EVENTLOOP_READABLE); | ||
| 50 | RedisModule_EventLoopDel(fds[1], REDISMODULE_EVENTLOOP_WRITABLE); | ||
| 51 | RedisModule_Free(src); | ||
| 52 | RedisModule_Free(dst); | ||
| 53 | close(fds[0]); | ||
| 54 | close(fds[1]); | ||
| 55 | |||
| 56 | RedisModule_FreeThreadSafeContext(reply_ctx); | ||
| 57 | RedisModule_UnblockClient(bc, NULL); | ||
| 58 | return; | ||
| 59 | } | ||
| 60 | }; | ||
| 61 | } | ||
| 62 | |||
| 63 | void onWritable(int fd, void *user_data, int mask) { | ||
| 64 | REDISMODULE_NOT_USED(user_data); | ||
| 65 | REDISMODULE_NOT_USED(mask); | ||
| 66 | |||
| 67 | RedisModule_Assert(strcmp(user_data, "userdatawrite") == 0); | ||
| 68 | |||
| 69 | while (1) { | ||
| 70 | /* Check if we sent all data */ | ||
| 71 | if (src_offset >= buf_size) | ||
| 72 | return; | ||
| 73 | int written = write(fd, src + src_offset, buf_size - src_offset); | ||
| 74 | if (written <= 0) { | ||
| 75 | return; | ||
| 76 | } | ||
| 77 | |||
| 78 | src_offset += written; | ||
| 79 | }; | ||
| 80 | } | ||
| 81 | |||
| 82 | /* Create a pipe(), register pipe fds to the event loop and send/receive data | ||
| 83 | * using them. */ | ||
| 84 | int sendbytes(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 85 | if (argc != 2) { | ||
| 86 | RedisModule_WrongArity(ctx); | ||
| 87 | return REDISMODULE_OK; | ||
| 88 | } | ||
| 89 | |||
| 90 | if (RedisModule_StringToLongLong(argv[1], &buf_size) != REDISMODULE_OK || | ||
| 91 | buf_size == 0) { | ||
| 92 | RedisModule_ReplyWithError(ctx, "Invalid integer value"); | ||
| 93 | return REDISMODULE_OK; | ||
| 94 | } | ||
| 95 | |||
| 96 | bc = RedisModule_BlockClient(ctx, NULL, NULL, NULL, 0); | ||
| 97 | reply_ctx = RedisModule_GetThreadSafeContext(bc); | ||
| 98 | |||
| 99 | /* Allocate source buffer and write some random data */ | ||
| 100 | src = RedisModule_Calloc(1,buf_size); | ||
| 101 | src_offset = 0; | ||
| 102 | memset(src, rand() % 0xFF, buf_size); | ||
| 103 | memcpy(src, "randomtestdata", strlen("randomtestdata")); | ||
| 104 | |||
| 105 | dst = RedisModule_Calloc(1,buf_size); | ||
| 106 | dst_offset = 0; | ||
| 107 | |||
| 108 | /* Create a pipe and register it to the event loop. */ | ||
| 109 | if (pipe(fds) < 0) return REDISMODULE_ERR; | ||
| 110 | if (fcntl(fds[0], F_SETFL, O_NONBLOCK) < 0) return REDISMODULE_ERR; | ||
| 111 | if (fcntl(fds[1], F_SETFL, O_NONBLOCK) < 0) return REDISMODULE_ERR; | ||
| 112 | |||
| 113 | if (RedisModule_EventLoopAdd(fds[0], REDISMODULE_EVENTLOOP_READABLE, | ||
| 114 | onReadable, "userdataread") != REDISMODULE_OK) return REDISMODULE_ERR; | ||
| 115 | if (RedisModule_EventLoopAdd(fds[1], REDISMODULE_EVENTLOOP_WRITABLE, | ||
| 116 | onWritable, "userdatawrite") != REDISMODULE_OK) return REDISMODULE_ERR; | ||
| 117 | return REDISMODULE_OK; | ||
| 118 | } | ||
| 119 | |||
| 120 | int sanity(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 121 | REDISMODULE_NOT_USED(argv); | ||
| 122 | REDISMODULE_NOT_USED(argc); | ||
| 123 | |||
| 124 | if (pipe(fds) < 0) return REDISMODULE_ERR; | ||
| 125 | |||
| 126 | if (RedisModule_EventLoopAdd(fds[0], 9999999, onReadable, NULL) | ||
| 127 | == REDISMODULE_OK || errno != EINVAL) { | ||
| 128 | RedisModule_ReplyWithError(ctx, "ERR non-existing event type should fail"); | ||
| 129 | goto out; | ||
| 130 | } | ||
| 131 | if (RedisModule_EventLoopAdd(-1, REDISMODULE_EVENTLOOP_READABLE, onReadable, NULL) | ||
| 132 | == REDISMODULE_OK || errno != ERANGE) { | ||
| 133 | RedisModule_ReplyWithError(ctx, "ERR out of range fd should fail"); | ||
| 134 | goto out; | ||
| 135 | } | ||
| 136 | if (RedisModule_EventLoopAdd(99999999, REDISMODULE_EVENTLOOP_READABLE, onReadable, NULL) | ||
| 137 | == REDISMODULE_OK || errno != ERANGE) { | ||
| 138 | RedisModule_ReplyWithError(ctx, "ERR out of range fd should fail"); | ||
| 139 | goto out; | ||
| 140 | } | ||
| 141 | if (RedisModule_EventLoopAdd(fds[0], REDISMODULE_EVENTLOOP_READABLE, NULL, NULL) | ||
| 142 | == REDISMODULE_OK || errno != EINVAL) { | ||
| 143 | RedisModule_ReplyWithError(ctx, "ERR null callback should fail"); | ||
| 144 | goto out; | ||
| 145 | } | ||
| 146 | if (RedisModule_EventLoopAdd(fds[0], 9999999, onReadable, NULL) | ||
| 147 | == REDISMODULE_OK || errno != EINVAL) { | ||
| 148 | RedisModule_ReplyWithError(ctx, "ERR non-existing event type should fail"); | ||
| 149 | goto out; | ||
| 150 | } | ||
| 151 | if (RedisModule_EventLoopDel(fds[0], REDISMODULE_EVENTLOOP_READABLE) | ||
| 152 | != REDISMODULE_OK || errno != 0) { | ||
| 153 | RedisModule_ReplyWithError(ctx, "ERR del on non-registered fd should not fail"); | ||
| 154 | goto out; | ||
| 155 | } | ||
| 156 | if (RedisModule_EventLoopDel(fds[0], 9999999) == REDISMODULE_OK || | ||
| 157 | errno != EINVAL) { | ||
| 158 | RedisModule_ReplyWithError(ctx, "ERR non-existing event type should fail"); | ||
| 159 | goto out; | ||
| 160 | } | ||
| 161 | if (RedisModule_EventLoopDel(-1, REDISMODULE_EVENTLOOP_READABLE) | ||
| 162 | == REDISMODULE_OK || errno != ERANGE) { | ||
| 163 | RedisModule_ReplyWithError(ctx, "ERR out of range fd should fail"); | ||
| 164 | goto out; | ||
| 165 | } | ||
| 166 | if (RedisModule_EventLoopDel(99999999, REDISMODULE_EVENTLOOP_READABLE) | ||
| 167 | == REDISMODULE_OK || errno != ERANGE) { | ||
| 168 | RedisModule_ReplyWithError(ctx, "ERR out of range fd should fail"); | ||
| 169 | goto out; | ||
| 170 | } | ||
| 171 | if (RedisModule_EventLoopAdd(fds[0], REDISMODULE_EVENTLOOP_READABLE, onReadable, NULL) | ||
| 172 | != REDISMODULE_OK || errno != 0) { | ||
| 173 | RedisModule_ReplyWithError(ctx, "ERR Add failed"); | ||
| 174 | goto out; | ||
| 175 | } | ||
| 176 | if (RedisModule_EventLoopAdd(fds[0], REDISMODULE_EVENTLOOP_READABLE, onReadable, NULL) | ||
| 177 | != REDISMODULE_OK || errno != 0) { | ||
| 178 | RedisModule_ReplyWithError(ctx, "ERR Adding same fd twice failed"); | ||
| 179 | goto out; | ||
| 180 | } | ||
| 181 | if (RedisModule_EventLoopDel(fds[0], REDISMODULE_EVENTLOOP_READABLE) | ||
| 182 | != REDISMODULE_OK || errno != 0) { | ||
| 183 | RedisModule_ReplyWithError(ctx, "ERR Del failed"); | ||
| 184 | goto out; | ||
| 185 | } | ||
| 186 | if (RedisModule_EventLoopAddOneShot(NULL, NULL) == REDISMODULE_OK || errno != EINVAL) { | ||
| 187 | RedisModule_ReplyWithError(ctx, "ERR null callback should fail"); | ||
| 188 | goto out; | ||
| 189 | } | ||
| 190 | |||
| 191 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 192 | out: | ||
| 193 | close(fds[0]); | ||
| 194 | close(fds[1]); | ||
| 195 | return REDISMODULE_OK; | ||
| 196 | } | ||
| 197 | |||
| 198 | static long long beforeSleepCount; | ||
| 199 | static long long afterSleepCount; | ||
| 200 | |||
| 201 | int iteration(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 202 | REDISMODULE_NOT_USED(argv); | ||
| 203 | REDISMODULE_NOT_USED(argc); | ||
| 204 | /* On each event loop iteration, eventloopCallback() is called. We increment | ||
| 205 | * beforeSleepCount and afterSleepCount, so these two should be equal. | ||
| 206 | * We reply with iteration count, caller can test if iteration count | ||
| 207 | * increments monotonically */ | ||
| 208 | RedisModule_Assert(beforeSleepCount == afterSleepCount); | ||
| 209 | RedisModule_ReplyWithLongLong(ctx, beforeSleepCount); | ||
| 210 | return REDISMODULE_OK; | ||
| 211 | } | ||
| 212 | |||
| 213 | void oneshotCallback(void* arg) | ||
| 214 | { | ||
| 215 | RedisModule_Assert(strcmp(arg, "userdata") == 0); | ||
| 216 | RedisModule_ReplyWithSimpleString(reply_ctx, "OK"); | ||
| 217 | RedisModule_FreeThreadSafeContext(reply_ctx); | ||
| 218 | RedisModule_UnblockClient(bc, NULL); | ||
| 219 | } | ||
| 220 | |||
| 221 | int oneshot(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 222 | REDISMODULE_NOT_USED(argv); | ||
| 223 | REDISMODULE_NOT_USED(argc); | ||
| 224 | |||
| 225 | bc = RedisModule_BlockClient(ctx, NULL, NULL, NULL, 0); | ||
| 226 | reply_ctx = RedisModule_GetThreadSafeContext(bc); | ||
| 227 | |||
| 228 | if (RedisModule_EventLoopAddOneShot(oneshotCallback, "userdata") != REDISMODULE_OK) { | ||
| 229 | RedisModule_ReplyWithError(ctx, "ERR oneshot failed"); | ||
| 230 | RedisModule_FreeThreadSafeContext(reply_ctx); | ||
| 231 | RedisModule_UnblockClient(bc, NULL); | ||
| 232 | } | ||
| 233 | return REDISMODULE_OK; | ||
| 234 | } | ||
| 235 | |||
| 236 | void eventloopCallback(struct RedisModuleCtx *ctx, RedisModuleEvent eid, uint64_t subevent, void *data) { | ||
| 237 | REDISMODULE_NOT_USED(ctx); | ||
| 238 | REDISMODULE_NOT_USED(eid); | ||
| 239 | REDISMODULE_NOT_USED(subevent); | ||
| 240 | REDISMODULE_NOT_USED(data); | ||
| 241 | |||
| 242 | RedisModule_Assert(eid.id == REDISMODULE_EVENT_EVENTLOOP); | ||
| 243 | if (subevent == REDISMODULE_SUBEVENT_EVENTLOOP_BEFORE_SLEEP) | ||
| 244 | beforeSleepCount++; | ||
| 245 | else if (subevent == REDISMODULE_SUBEVENT_EVENTLOOP_AFTER_SLEEP) | ||
| 246 | afterSleepCount++; | ||
| 247 | } | ||
| 248 | |||
| 249 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 250 | REDISMODULE_NOT_USED(argv); | ||
| 251 | REDISMODULE_NOT_USED(argc); | ||
| 252 | |||
| 253 | if (RedisModule_Init(ctx,"eventloop",1,REDISMODULE_APIVER_1) | ||
| 254 | == REDISMODULE_ERR) return REDISMODULE_ERR; | ||
| 255 | |||
| 256 | /* Test basics. */ | ||
| 257 | if (RedisModule_CreateCommand(ctx, "test.sanity", sanity, "", 0, 0, 0) | ||
| 258 | == REDISMODULE_ERR) return REDISMODULE_ERR; | ||
| 259 | |||
| 260 | /* Register a command to create a pipe() and send data through it by using | ||
| 261 | * event loop API. */ | ||
| 262 | if (RedisModule_CreateCommand(ctx, "test.sendbytes", sendbytes, "", 0, 0, 0) | ||
| 263 | == REDISMODULE_ERR) return REDISMODULE_ERR; | ||
| 264 | |||
| 265 | /* Register a command to return event loop iteration count. */ | ||
| 266 | if (RedisModule_CreateCommand(ctx, "test.iteration", iteration, "", 0, 0, 0) | ||
| 267 | == REDISMODULE_ERR) return REDISMODULE_ERR; | ||
| 268 | |||
| 269 | if (RedisModule_CreateCommand(ctx, "test.oneshot", oneshot, "", 0, 0, 0) | ||
| 270 | == REDISMODULE_ERR) return REDISMODULE_ERR; | ||
| 271 | |||
| 272 | if (RedisModule_SubscribeToServerEvent(ctx, RedisModuleEvent_EventLoop, | ||
| 273 | eventloopCallback) != REDISMODULE_OK) return REDISMODULE_ERR; | ||
| 274 | |||
| 275 | return REDISMODULE_OK; | ||
| 276 | } | ||
diff --git a/examples/redis-unstable/tests/modules/fork.c b/examples/redis-unstable/tests/modules/fork.c new file mode 100644 index 0000000..d7a0d15 --- /dev/null +++ b/examples/redis-unstable/tests/modules/fork.c | |||
| @@ -0,0 +1,96 @@ | |||
| 1 | |||
| 2 | /* define macros for having usleep */ | ||
| 3 | #define _BSD_SOURCE | ||
| 4 | #define _DEFAULT_SOURCE | ||
| 5 | |||
| 6 | #include "redismodule.h" | ||
| 7 | #include <string.h> | ||
| 8 | #include <assert.h> | ||
| 9 | #include <unistd.h> | ||
| 10 | |||
| 11 | #define UNUSED(V) ((void) V) | ||
| 12 | |||
| 13 | int child_pid = -1; | ||
| 14 | int exitted_with_code = -1; | ||
| 15 | |||
| 16 | void done_handler(int exitcode, int bysignal, void *user_data) { | ||
| 17 | child_pid = -1; | ||
| 18 | exitted_with_code = exitcode; | ||
| 19 | assert(user_data==(void*)0xdeadbeef); | ||
| 20 | UNUSED(bysignal); | ||
| 21 | } | ||
| 22 | |||
| 23 | int fork_create(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 24 | { | ||
| 25 | long long code_to_exit_with; | ||
| 26 | long long usleep_us; | ||
| 27 | if (argc != 3) { | ||
| 28 | RedisModule_WrongArity(ctx); | ||
| 29 | return REDISMODULE_OK; | ||
| 30 | } | ||
| 31 | |||
| 32 | if(!RMAPI_FUNC_SUPPORTED(RedisModule_Fork)){ | ||
| 33 | RedisModule_ReplyWithError(ctx, "Fork api is not supported in the current redis version"); | ||
| 34 | return REDISMODULE_OK; | ||
| 35 | } | ||
| 36 | |||
| 37 | RedisModule_StringToLongLong(argv[1], &code_to_exit_with); | ||
| 38 | RedisModule_StringToLongLong(argv[2], &usleep_us); | ||
| 39 | exitted_with_code = -1; | ||
| 40 | int fork_child_pid = RedisModule_Fork(done_handler, (void*)0xdeadbeef); | ||
| 41 | if (fork_child_pid < 0) { | ||
| 42 | RedisModule_ReplyWithError(ctx, "Fork failed"); | ||
| 43 | return REDISMODULE_OK; | ||
| 44 | } else if (fork_child_pid > 0) { | ||
| 45 | /* parent */ | ||
| 46 | child_pid = fork_child_pid; | ||
| 47 | RedisModule_ReplyWithLongLong(ctx, child_pid); | ||
| 48 | return REDISMODULE_OK; | ||
| 49 | } | ||
| 50 | |||
| 51 | /* child */ | ||
| 52 | RedisModule_Log(ctx, "notice", "fork child started"); | ||
| 53 | usleep(usleep_us); | ||
| 54 | RedisModule_Log(ctx, "notice", "fork child exiting"); | ||
| 55 | RedisModule_ExitFromChild(code_to_exit_with); | ||
| 56 | /* unreachable */ | ||
| 57 | return 0; | ||
| 58 | } | ||
| 59 | |||
| 60 | int fork_exitcode(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 61 | { | ||
| 62 | UNUSED(argv); | ||
| 63 | UNUSED(argc); | ||
| 64 | RedisModule_ReplyWithLongLong(ctx, exitted_with_code); | ||
| 65 | return REDISMODULE_OK; | ||
| 66 | } | ||
| 67 | |||
| 68 | int fork_kill(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 69 | { | ||
| 70 | UNUSED(argv); | ||
| 71 | UNUSED(argc); | ||
| 72 | if (RedisModule_KillForkChild(child_pid) != REDISMODULE_OK) | ||
| 73 | RedisModule_ReplyWithError(ctx, "KillForkChild failed"); | ||
| 74 | else | ||
| 75 | RedisModule_ReplyWithLongLong(ctx, 1); | ||
| 76 | child_pid = -1; | ||
| 77 | return REDISMODULE_OK; | ||
| 78 | } | ||
| 79 | |||
| 80 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 81 | UNUSED(argv); | ||
| 82 | UNUSED(argc); | ||
| 83 | if (RedisModule_Init(ctx,"fork",1,REDISMODULE_APIVER_1)== REDISMODULE_ERR) | ||
| 84 | return REDISMODULE_ERR; | ||
| 85 | |||
| 86 | if (RedisModule_CreateCommand(ctx,"fork.create", fork_create,"",0,0,0) == REDISMODULE_ERR) | ||
| 87 | return REDISMODULE_ERR; | ||
| 88 | |||
| 89 | if (RedisModule_CreateCommand(ctx,"fork.exitcode", fork_exitcode,"",0,0,0) == REDISMODULE_ERR) | ||
| 90 | return REDISMODULE_ERR; | ||
| 91 | |||
| 92 | if (RedisModule_CreateCommand(ctx,"fork.kill", fork_kill,"",0,0,0) == REDISMODULE_ERR) | ||
| 93 | return REDISMODULE_ERR; | ||
| 94 | |||
| 95 | return REDISMODULE_OK; | ||
| 96 | } | ||
diff --git a/examples/redis-unstable/tests/modules/getchannels.c b/examples/redis-unstable/tests/modules/getchannels.c new file mode 100644 index 0000000..330531d --- /dev/null +++ b/examples/redis-unstable/tests/modules/getchannels.c | |||
| @@ -0,0 +1,69 @@ | |||
| 1 | #include "redismodule.h" | ||
| 2 | #include <strings.h> | ||
| 3 | #include <assert.h> | ||
| 4 | #include <unistd.h> | ||
| 5 | #include <errno.h> | ||
| 6 | |||
| 7 | /* A sample with declarable channels, that are used to validate against ACLs */ | ||
| 8 | int getChannels_subscribe(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 9 | if ((argc - 1) % 3 != 0) { | ||
| 10 | RedisModule_WrongArity(ctx); | ||
| 11 | return REDISMODULE_OK; | ||
| 12 | } | ||
| 13 | char *err = NULL; | ||
| 14 | |||
| 15 | /* getchannels.command [[subscribe|unsubscribe|publish] [pattern|literal] <channel> ...] | ||
| 16 | * This command marks the given channel is accessed based on the | ||
| 17 | * provided modifiers. */ | ||
| 18 | for (int i = 1; i < argc; i += 3) { | ||
| 19 | const char *operation = RedisModule_StringPtrLen(argv[i], NULL); | ||
| 20 | const char *type = RedisModule_StringPtrLen(argv[i+1], NULL); | ||
| 21 | int flags = 0; | ||
| 22 | |||
| 23 | if (!strcasecmp(operation, "subscribe")) { | ||
| 24 | flags |= REDISMODULE_CMD_CHANNEL_SUBSCRIBE; | ||
| 25 | } else if (!strcasecmp(operation, "unsubscribe")) { | ||
| 26 | flags |= REDISMODULE_CMD_CHANNEL_UNSUBSCRIBE; | ||
| 27 | } else if (!strcasecmp(operation, "publish")) { | ||
| 28 | flags |= REDISMODULE_CMD_CHANNEL_PUBLISH; | ||
| 29 | } else { | ||
| 30 | err = "Invalid channel operation"; | ||
| 31 | break; | ||
| 32 | } | ||
| 33 | |||
| 34 | if (!strcasecmp(type, "literal")) { | ||
| 35 | /* No op */ | ||
| 36 | } else if (!strcasecmp(type, "pattern")) { | ||
| 37 | flags |= REDISMODULE_CMD_CHANNEL_PATTERN; | ||
| 38 | } else { | ||
| 39 | err = "Invalid channel type"; | ||
| 40 | break; | ||
| 41 | } | ||
| 42 | if (RedisModule_IsChannelsPositionRequest(ctx)) { | ||
| 43 | RedisModule_ChannelAtPosWithFlags(ctx, i+2, flags); | ||
| 44 | } | ||
| 45 | } | ||
| 46 | |||
| 47 | if (!RedisModule_IsChannelsPositionRequest(ctx)) { | ||
| 48 | if (err) { | ||
| 49 | RedisModule_ReplyWithError(ctx, err); | ||
| 50 | } else { | ||
| 51 | /* Normal implementation would go here, but for tests just return okay */ | ||
| 52 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 53 | } | ||
| 54 | } | ||
| 55 | |||
| 56 | return REDISMODULE_OK; | ||
| 57 | } | ||
| 58 | |||
| 59 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 60 | REDISMODULE_NOT_USED(argv); | ||
| 61 | REDISMODULE_NOT_USED(argc); | ||
| 62 | if (RedisModule_Init(ctx, "getchannels", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) | ||
| 63 | return REDISMODULE_ERR; | ||
| 64 | |||
| 65 | if (RedisModule_CreateCommand(ctx, "getchannels.command", getChannels_subscribe, "getchannels-api", 0, 0, 0) == REDISMODULE_ERR) | ||
| 66 | return REDISMODULE_ERR; | ||
| 67 | |||
| 68 | return REDISMODULE_OK; | ||
| 69 | } | ||
diff --git a/examples/redis-unstable/tests/modules/getkeys.c b/examples/redis-unstable/tests/modules/getkeys.c new file mode 100644 index 0000000..cee3b3e --- /dev/null +++ b/examples/redis-unstable/tests/modules/getkeys.c | |||
| @@ -0,0 +1,178 @@ | |||
| 1 | |||
| 2 | #include "redismodule.h" | ||
| 3 | #include <strings.h> | ||
| 4 | #include <assert.h> | ||
| 5 | #include <unistd.h> | ||
| 6 | #include <errno.h> | ||
| 7 | |||
| 8 | #define UNUSED(V) ((void) V) | ||
| 9 | |||
| 10 | /* A sample movable keys command that returns a list of all | ||
| 11 | * arguments that follow a KEY argument, i.e. | ||
| 12 | */ | ||
| 13 | int getkeys_command(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 14 | { | ||
| 15 | int i; | ||
| 16 | int count = 0; | ||
| 17 | |||
| 18 | /* Handle getkeys-api introspection */ | ||
| 19 | if (RedisModule_IsKeysPositionRequest(ctx)) { | ||
| 20 | for (i = 0; i < argc; i++) { | ||
| 21 | size_t len; | ||
| 22 | const char *str = RedisModule_StringPtrLen(argv[i], &len); | ||
| 23 | |||
| 24 | if (len == 3 && !strncasecmp(str, "key", 3) && i + 1 < argc) | ||
| 25 | RedisModule_KeyAtPos(ctx, i + 1); | ||
| 26 | } | ||
| 27 | |||
| 28 | return REDISMODULE_OK; | ||
| 29 | } | ||
| 30 | |||
| 31 | /* Handle real command invocation */ | ||
| 32 | RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_LEN); | ||
| 33 | for (i = 0; i < argc; i++) { | ||
| 34 | size_t len; | ||
| 35 | const char *str = RedisModule_StringPtrLen(argv[i], &len); | ||
| 36 | |||
| 37 | if (len == 3 && !strncasecmp(str, "key", 3) && i + 1 < argc) { | ||
| 38 | RedisModule_ReplyWithString(ctx, argv[i+1]); | ||
| 39 | count++; | ||
| 40 | } | ||
| 41 | } | ||
| 42 | RedisModule_ReplySetArrayLength(ctx, count); | ||
| 43 | |||
| 44 | return REDISMODULE_OK; | ||
| 45 | } | ||
| 46 | |||
| 47 | int getkeys_command_with_flags(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 48 | { | ||
| 49 | int i; | ||
| 50 | int count = 0; | ||
| 51 | |||
| 52 | /* Handle getkeys-api introspection */ | ||
| 53 | if (RedisModule_IsKeysPositionRequest(ctx)) { | ||
| 54 | for (i = 0; i < argc; i++) { | ||
| 55 | size_t len; | ||
| 56 | const char *str = RedisModule_StringPtrLen(argv[i], &len); | ||
| 57 | |||
| 58 | if (len == 3 && !strncasecmp(str, "key", 3) && i + 1 < argc) | ||
| 59 | RedisModule_KeyAtPosWithFlags(ctx, i + 1, REDISMODULE_CMD_KEY_RO | REDISMODULE_CMD_KEY_ACCESS); | ||
| 60 | } | ||
| 61 | |||
| 62 | return REDISMODULE_OK; | ||
| 63 | } | ||
| 64 | |||
| 65 | /* Handle real command invocation */ | ||
| 66 | RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_LEN); | ||
| 67 | for (i = 0; i < argc; i++) { | ||
| 68 | size_t len; | ||
| 69 | const char *str = RedisModule_StringPtrLen(argv[i], &len); | ||
| 70 | |||
| 71 | if (len == 3 && !strncasecmp(str, "key", 3) && i + 1 < argc) { | ||
| 72 | RedisModule_ReplyWithString(ctx, argv[i+1]); | ||
| 73 | count++; | ||
| 74 | } | ||
| 75 | } | ||
| 76 | RedisModule_ReplySetArrayLength(ctx, count); | ||
| 77 | |||
| 78 | return REDISMODULE_OK; | ||
| 79 | } | ||
| 80 | |||
| 81 | int getkeys_fixed(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 82 | { | ||
| 83 | int i; | ||
| 84 | |||
| 85 | RedisModule_ReplyWithArray(ctx, argc - 1); | ||
| 86 | for (i = 1; i < argc; i++) { | ||
| 87 | RedisModule_ReplyWithString(ctx, argv[i]); | ||
| 88 | } | ||
| 89 | return REDISMODULE_OK; | ||
| 90 | } | ||
| 91 | |||
| 92 | /* Introspect a command using RM_GetCommandKeys() and returns the list | ||
| 93 | * of keys. Essentially this is COMMAND GETKEYS implemented in a module. | ||
| 94 | * INTROSPECT <with-flags> <cmd> <args> | ||
| 95 | */ | ||
| 96 | int getkeys_introspect(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 97 | { | ||
| 98 | long long with_flags = 0; | ||
| 99 | |||
| 100 | if (argc < 4) { | ||
| 101 | RedisModule_WrongArity(ctx); | ||
| 102 | return REDISMODULE_OK; | ||
| 103 | } | ||
| 104 | |||
| 105 | if (RedisModule_StringToLongLong(argv[1],&with_flags) != REDISMODULE_OK) | ||
| 106 | return RedisModule_ReplyWithError(ctx,"ERR invalid integer"); | ||
| 107 | |||
| 108 | int num_keys, *keyflags = NULL; | ||
| 109 | int *keyidx = RedisModule_GetCommandKeysWithFlags(ctx, &argv[2], argc - 2, &num_keys, with_flags ? &keyflags : NULL); | ||
| 110 | |||
| 111 | if (!keyidx) { | ||
| 112 | if (!errno) | ||
| 113 | RedisModule_ReplyWithEmptyArray(ctx); | ||
| 114 | else { | ||
| 115 | char err[100]; | ||
| 116 | switch (errno) { | ||
| 117 | case ENOENT: | ||
| 118 | RedisModule_ReplyWithError(ctx, "ERR ENOENT"); | ||
| 119 | break; | ||
| 120 | case EINVAL: | ||
| 121 | RedisModule_ReplyWithError(ctx, "ERR EINVAL"); | ||
| 122 | break; | ||
| 123 | default: | ||
| 124 | snprintf(err, sizeof(err) - 1, "ERR errno=%d", errno); | ||
| 125 | RedisModule_ReplyWithError(ctx, err); | ||
| 126 | break; | ||
| 127 | } | ||
| 128 | } | ||
| 129 | } else { | ||
| 130 | int i; | ||
| 131 | |||
| 132 | RedisModule_ReplyWithArray(ctx, num_keys); | ||
| 133 | for (i = 0; i < num_keys; i++) { | ||
| 134 | if (!with_flags) { | ||
| 135 | RedisModule_ReplyWithString(ctx, argv[2 + keyidx[i]]); | ||
| 136 | continue; | ||
| 137 | } | ||
| 138 | RedisModule_ReplyWithArray(ctx, 2); | ||
| 139 | RedisModule_ReplyWithString(ctx, argv[2 + keyidx[i]]); | ||
| 140 | char* sflags = ""; | ||
| 141 | if (keyflags[i] & REDISMODULE_CMD_KEY_RO) | ||
| 142 | sflags = "RO"; | ||
| 143 | else if (keyflags[i] & REDISMODULE_CMD_KEY_RW) | ||
| 144 | sflags = "RW"; | ||
| 145 | else if (keyflags[i] & REDISMODULE_CMD_KEY_OW) | ||
| 146 | sflags = "OW"; | ||
| 147 | else if (keyflags[i] & REDISMODULE_CMD_KEY_RM) | ||
| 148 | sflags = "RM"; | ||
| 149 | RedisModule_ReplyWithCString(ctx, sflags); | ||
| 150 | } | ||
| 151 | |||
| 152 | RedisModule_Free(keyidx); | ||
| 153 | RedisModule_Free(keyflags); | ||
| 154 | } | ||
| 155 | |||
| 156 | return REDISMODULE_OK; | ||
| 157 | } | ||
| 158 | |||
| 159 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 160 | UNUSED(argv); | ||
| 161 | UNUSED(argc); | ||
| 162 | if (RedisModule_Init(ctx,"getkeys",1,REDISMODULE_APIVER_1)== REDISMODULE_ERR) | ||
| 163 | return REDISMODULE_ERR; | ||
| 164 | |||
| 165 | if (RedisModule_CreateCommand(ctx,"getkeys.command", getkeys_command,"getkeys-api",0,0,0) == REDISMODULE_ERR) | ||
| 166 | return REDISMODULE_ERR; | ||
| 167 | |||
| 168 | if (RedisModule_CreateCommand(ctx,"getkeys.command_with_flags", getkeys_command_with_flags,"getkeys-api",0,0,0) == REDISMODULE_ERR) | ||
| 169 | return REDISMODULE_ERR; | ||
| 170 | |||
| 171 | if (RedisModule_CreateCommand(ctx,"getkeys.fixed", getkeys_fixed,"",2,4,1) == REDISMODULE_ERR) | ||
| 172 | return REDISMODULE_ERR; | ||
| 173 | |||
| 174 | if (RedisModule_CreateCommand(ctx,"getkeys.introspect", getkeys_introspect,"",0,0,0) == REDISMODULE_ERR) | ||
| 175 | return REDISMODULE_ERR; | ||
| 176 | |||
| 177 | return REDISMODULE_OK; | ||
| 178 | } | ||
diff --git a/examples/redis-unstable/tests/modules/hash.c b/examples/redis-unstable/tests/modules/hash.c new file mode 100644 index 0000000..462c21e --- /dev/null +++ b/examples/redis-unstable/tests/modules/hash.c | |||
| @@ -0,0 +1,244 @@ | |||
| 1 | #include "redismodule.h" | ||
| 2 | #include <strings.h> | ||
| 3 | #include <errno.h> | ||
| 4 | #include <stdlib.h> | ||
| 5 | |||
| 6 | #define UNUSED(x) (void)(x) | ||
| 7 | |||
| 8 | /* If a string is ":deleted:", the special value for deleted hash fields is | ||
| 9 | * returned; otherwise the input string is returned. */ | ||
| 10 | static RedisModuleString *value_or_delete(RedisModuleString *s) { | ||
| 11 | if (!strcasecmp(RedisModule_StringPtrLen(s, NULL), ":delete:")) | ||
| 12 | return REDISMODULE_HASH_DELETE; | ||
| 13 | else | ||
| 14 | return s; | ||
| 15 | } | ||
| 16 | |||
| 17 | /* HASH.SET key flags field1 value1 [field2 value2 ..] | ||
| 18 | * | ||
| 19 | * Sets 1-4 fields. Returns the same as RedisModule_HashSet(). | ||
| 20 | * Flags is a string of "nxa" where n = NX, x = XX, a = COUNT_ALL. | ||
| 21 | * To delete a field, use the value ":delete:". | ||
| 22 | */ | ||
| 23 | int hash_set(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 24 | if (argc < 5 || argc % 2 == 0 || argc > 11) | ||
| 25 | return RedisModule_WrongArity(ctx); | ||
| 26 | |||
| 27 | RedisModule_AutoMemory(ctx); | ||
| 28 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE); | ||
| 29 | |||
| 30 | size_t flags_len; | ||
| 31 | const char *flags_str = RedisModule_StringPtrLen(argv[2], &flags_len); | ||
| 32 | int flags = REDISMODULE_HASH_NONE; | ||
| 33 | for (size_t i = 0; i < flags_len; i++) { | ||
| 34 | switch (flags_str[i]) { | ||
| 35 | case 'n': flags |= REDISMODULE_HASH_NX; break; | ||
| 36 | case 'x': flags |= REDISMODULE_HASH_XX; break; | ||
| 37 | case 'a': flags |= REDISMODULE_HASH_COUNT_ALL; break; | ||
| 38 | } | ||
| 39 | } | ||
| 40 | |||
| 41 | /* Test some varargs. (In real-world, use a loop and set one at a time.) */ | ||
| 42 | int result; | ||
| 43 | errno = 0; | ||
| 44 | if (argc == 5) { | ||
| 45 | result = RedisModule_HashSet(key, flags, | ||
| 46 | argv[3], value_or_delete(argv[4]), | ||
| 47 | NULL); | ||
| 48 | } else if (argc == 7) { | ||
| 49 | result = RedisModule_HashSet(key, flags, | ||
| 50 | argv[3], value_or_delete(argv[4]), | ||
| 51 | argv[5], value_or_delete(argv[6]), | ||
| 52 | NULL); | ||
| 53 | } else if (argc == 9) { | ||
| 54 | result = RedisModule_HashSet(key, flags, | ||
| 55 | argv[3], value_or_delete(argv[4]), | ||
| 56 | argv[5], value_or_delete(argv[6]), | ||
| 57 | argv[7], value_or_delete(argv[8]), | ||
| 58 | NULL); | ||
| 59 | } else if (argc == 11) { | ||
| 60 | result = RedisModule_HashSet(key, flags, | ||
| 61 | argv[3], value_or_delete(argv[4]), | ||
| 62 | argv[5], value_or_delete(argv[6]), | ||
| 63 | argv[7], value_or_delete(argv[8]), | ||
| 64 | argv[9], value_or_delete(argv[10]), | ||
| 65 | NULL); | ||
| 66 | } else { | ||
| 67 | return RedisModule_ReplyWithError(ctx, "ERR too many fields"); | ||
| 68 | } | ||
| 69 | |||
| 70 | /* Check errno */ | ||
| 71 | if (result == 0) { | ||
| 72 | if (errno == ENOTSUP) | ||
| 73 | return RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE); | ||
| 74 | else | ||
| 75 | RedisModule_Assert(errno == ENOENT); | ||
| 76 | } | ||
| 77 | |||
| 78 | return RedisModule_ReplyWithLongLong(ctx, result); | ||
| 79 | } | ||
| 80 | |||
| 81 | RedisModuleKey* openKeyWithMode(RedisModuleCtx *ctx, RedisModuleString *keyName, int mode) { | ||
| 82 | int supportedMode = RedisModule_GetOpenKeyModesAll(); | ||
| 83 | if (!(supportedMode & REDISMODULE_READ) || ((supportedMode & mode)!=mode)) { | ||
| 84 | RedisModule_ReplyWithError(ctx, "OpenKey mode is not supported"); | ||
| 85 | return NULL; | ||
| 86 | } | ||
| 87 | |||
| 88 | RedisModuleKey *key = RedisModule_OpenKey(ctx, keyName, REDISMODULE_READ | mode); | ||
| 89 | if (!key) { | ||
| 90 | RedisModule_ReplyWithError(ctx, "key not found"); | ||
| 91 | return NULL; | ||
| 92 | } | ||
| 93 | |||
| 94 | return key; | ||
| 95 | } | ||
| 96 | |||
| 97 | int test_open_key_subexpired_hget(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 98 | if (argc<3) { | ||
| 99 | RedisModule_WrongArity(ctx); | ||
| 100 | return REDISMODULE_OK; | ||
| 101 | } | ||
| 102 | |||
| 103 | RedisModuleKey *key = openKeyWithMode(ctx, argv[1], REDISMODULE_OPEN_KEY_ACCESS_EXPIRED); | ||
| 104 | if (!key) return REDISMODULE_OK; | ||
| 105 | |||
| 106 | RedisModuleString *value; | ||
| 107 | RedisModule_HashGet(key,REDISMODULE_HASH_NONE,argv[2],&value,NULL); | ||
| 108 | |||
| 109 | /* return the value */ | ||
| 110 | if (value) { | ||
| 111 | RedisModule_ReplyWithString(ctx, value); | ||
| 112 | RedisModule_FreeString(ctx, value); | ||
| 113 | } else { | ||
| 114 | RedisModule_ReplyWithNull(ctx); | ||
| 115 | } | ||
| 116 | RedisModule_CloseKey(key); | ||
| 117 | return REDISMODULE_OK; | ||
| 118 | } | ||
| 119 | |||
| 120 | int test_open_key_hget_expire(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 121 | if (argc<3) { | ||
| 122 | RedisModule_WrongArity(ctx); | ||
| 123 | return REDISMODULE_OK; | ||
| 124 | } | ||
| 125 | |||
| 126 | RedisModuleKey *key = openKeyWithMode(ctx, argv[1], REDISMODULE_OPEN_KEY_ACCESS_EXPIRED); | ||
| 127 | if (!key) return REDISMODULE_OK; | ||
| 128 | |||
| 129 | mstime_t expireAt; | ||
| 130 | |||
| 131 | /* Let's test here that we get error if using invalid flags combination */ | ||
| 132 | RedisModule_Assert( | ||
| 133 | RedisModule_HashGet(key, | ||
| 134 | REDISMODULE_HASH_EXISTS | | ||
| 135 | REDISMODULE_HASH_EXPIRE_TIME, | ||
| 136 | argv[2], &expireAt, NULL) == REDISMODULE_ERR); | ||
| 137 | |||
| 138 | /* Now let's get the expire time */ | ||
| 139 | RedisModule_HashGet(key, REDISMODULE_HASH_EXPIRE_TIME,argv[2],&expireAt,NULL); | ||
| 140 | RedisModule_ReplyWithLongLong(ctx, expireAt); | ||
| 141 | RedisModule_CloseKey(key); | ||
| 142 | return REDISMODULE_OK; | ||
| 143 | } | ||
| 144 | |||
| 145 | /* Test variadic function to get two expiration times */ | ||
| 146 | int test_open_key_hget_two_expire(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 147 | if (argc<3) { | ||
| 148 | RedisModule_WrongArity(ctx); | ||
| 149 | return REDISMODULE_OK; | ||
| 150 | } | ||
| 151 | |||
| 152 | RedisModuleKey *key = openKeyWithMode(ctx, argv[1], REDISMODULE_OPEN_KEY_ACCESS_EXPIRED); | ||
| 153 | if (!key) return REDISMODULE_OK; | ||
| 154 | |||
| 155 | mstime_t expireAt1, expireAt2; | ||
| 156 | RedisModule_HashGet(key,REDISMODULE_HASH_EXPIRE_TIME,argv[2],&expireAt1,argv[3],&expireAt2,NULL); | ||
| 157 | |||
| 158 | /* return the two expire time */ | ||
| 159 | RedisModule_ReplyWithArray(ctx, 2); | ||
| 160 | RedisModule_ReplyWithLongLong(ctx, expireAt1); | ||
| 161 | RedisModule_ReplyWithLongLong(ctx, expireAt2); | ||
| 162 | RedisModule_CloseKey(key); | ||
| 163 | return REDISMODULE_OK; | ||
| 164 | } | ||
| 165 | |||
| 166 | int test_open_key_hget_min_expire(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 167 | if (argc!=2) { | ||
| 168 | RedisModule_WrongArity(ctx); | ||
| 169 | return REDISMODULE_OK; | ||
| 170 | } | ||
| 171 | |||
| 172 | RedisModuleKey *key = openKeyWithMode(ctx, argv[1], REDISMODULE_READ); | ||
| 173 | if (!key) return REDISMODULE_OK; | ||
| 174 | |||
| 175 | volatile mstime_t minExpire = RedisModule_HashFieldMinExpire(key); | ||
| 176 | RedisModule_ReplyWithLongLong(ctx, minExpire); | ||
| 177 | RedisModule_CloseKey(key); | ||
| 178 | return REDISMODULE_OK; | ||
| 179 | } | ||
| 180 | |||
| 181 | int numReplies; | ||
| 182 | void ScanCallback(RedisModuleKey *key, RedisModuleString *field, RedisModuleString *value, void *privdata) { | ||
| 183 | UNUSED(key); | ||
| 184 | RedisModuleCtx *ctx = (RedisModuleCtx *)privdata; | ||
| 185 | |||
| 186 | /* Reply with the field and value (or NULL for sets) */ | ||
| 187 | RedisModule_ReplyWithString(ctx, field); | ||
| 188 | if (value) { | ||
| 189 | RedisModule_ReplyWithString(ctx, value); | ||
| 190 | } else { | ||
| 191 | RedisModule_ReplyWithCString(ctx, "(null)"); | ||
| 192 | } | ||
| 193 | numReplies+=2; | ||
| 194 | } | ||
| 195 | |||
| 196 | int test_open_key_access_expired_hscan(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 197 | if (argc < 2) { | ||
| 198 | RedisModule_WrongArity(ctx); | ||
| 199 | return REDISMODULE_OK; | ||
| 200 | } | ||
| 201 | |||
| 202 | RedisModuleKey *key = openKeyWithMode(ctx, argv[1], REDISMODULE_OPEN_KEY_ACCESS_EXPIRED); | ||
| 203 | |||
| 204 | if (!key) | ||
| 205 | return RedisModule_ReplyWithError(ctx, "ERR key not exists"); | ||
| 206 | |||
| 207 | /* Verify it is a hash */ | ||
| 208 | if (RedisModule_KeyType(key) != REDISMODULE_KEYTYPE_HASH) { | ||
| 209 | RedisModule_CloseKey(key); | ||
| 210 | return RedisModule_ReplyWithError(ctx, "ERR key is not a hash"); | ||
| 211 | } | ||
| 212 | |||
| 213 | /* Scan the hash and reply pairs of key-value */ | ||
| 214 | RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_ARRAY_LEN); | ||
| 215 | numReplies = 0; | ||
| 216 | RedisModuleScanCursor *cursor = RedisModule_ScanCursorCreate(); | ||
| 217 | while (RedisModule_ScanKey(key, cursor, ScanCallback, ctx)); | ||
| 218 | RedisModule_ScanCursorDestroy(cursor); | ||
| 219 | RedisModule_CloseKey(key); | ||
| 220 | RedisModule_ReplySetArrayLength(ctx, numReplies); | ||
| 221 | return REDISMODULE_OK; | ||
| 222 | } | ||
| 223 | |||
| 224 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 225 | REDISMODULE_NOT_USED(argv); | ||
| 226 | REDISMODULE_NOT_USED(argc); | ||
| 227 | if (RedisModule_Init(ctx, "hash", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) | ||
| 228 | return REDISMODULE_ERR; | ||
| 229 | |||
| 230 | if (RedisModule_CreateCommand(ctx, "hash.set", hash_set, "write", 1, 1, 1) == REDISMODULE_ERR) | ||
| 231 | return REDISMODULE_ERR; | ||
| 232 | if (RedisModule_CreateCommand(ctx, "hash.hget_expired", test_open_key_subexpired_hget,"", 0, 0, 0) == REDISMODULE_ERR) | ||
| 233 | return REDISMODULE_ERR; | ||
| 234 | if (RedisModule_CreateCommand(ctx, "hash.hscan_expired", test_open_key_access_expired_hscan,"", 0, 0, 0) == REDISMODULE_ERR) | ||
| 235 | return REDISMODULE_ERR; | ||
| 236 | if (RedisModule_CreateCommand(ctx, "hash.hget_expire", test_open_key_hget_expire,"", 0, 0, 0) == REDISMODULE_ERR) | ||
| 237 | return REDISMODULE_ERR; | ||
| 238 | if (RedisModule_CreateCommand(ctx, "hash.hget_two_expire", test_open_key_hget_two_expire,"", 0, 0, 0) == REDISMODULE_ERR) | ||
| 239 | return REDISMODULE_ERR; | ||
| 240 | if (RedisModule_CreateCommand(ctx, "hash.hget_min_expire", test_open_key_hget_min_expire,"", 0, 0, 0) == REDISMODULE_ERR) | ||
| 241 | return REDISMODULE_ERR; | ||
| 242 | |||
| 243 | return REDISMODULE_OK; | ||
| 244 | } | ||
diff --git a/examples/redis-unstable/tests/modules/hooks.c b/examples/redis-unstable/tests/modules/hooks.c new file mode 100644 index 0000000..59c8492 --- /dev/null +++ b/examples/redis-unstable/tests/modules/hooks.c | |||
| @@ -0,0 +1,496 @@ | |||
| 1 | /* This module is used to test the server events hooks API. | ||
| 2 | * | ||
| 3 | * ----------------------------------------------------------------------------- | ||
| 4 | * | ||
| 5 | * Copyright (c) 2019-Present, Redis Ltd. | ||
| 6 | * All rights reserved. | ||
| 7 | * | ||
| 8 | * Licensed under your choice of (a) the Redis Source Available License 2.0 | ||
| 9 | * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the | ||
| 10 | * GNU Affero General Public License v3 (AGPLv3). | ||
| 11 | */ | ||
| 12 | |||
| 13 | #include "redismodule.h" | ||
| 14 | #include <stdio.h> | ||
| 15 | #include <string.h> | ||
| 16 | #include <strings.h> | ||
| 17 | #include <assert.h> | ||
| 18 | |||
| 19 | /* We need to store events to be able to test and see what we got, and we can't | ||
| 20 | * store them in the key-space since that would mess up rdb loading (duplicates) | ||
| 21 | * and be lost of flushdb. */ | ||
| 22 | RedisModuleDict *event_log = NULL; | ||
| 23 | /* stores all the keys on which we got 'removed' event */ | ||
| 24 | RedisModuleDict *removed_event_log = NULL; | ||
| 25 | /* stores all the subevent on which we got 'removed' event */ | ||
| 26 | RedisModuleDict *removed_subevent_type = NULL; | ||
| 27 | /* stores all the keys on which we got 'removed' event with expiry information */ | ||
| 28 | RedisModuleDict *removed_expiry_log = NULL; | ||
| 29 | |||
| 30 | typedef struct EventElement { | ||
| 31 | long count; | ||
| 32 | RedisModuleString *last_val_string; | ||
| 33 | long last_val_int; | ||
| 34 | } EventElement; | ||
| 35 | |||
| 36 | void LogStringEvent(RedisModuleCtx *ctx, const char* keyname, const char* data) { | ||
| 37 | EventElement *event = RedisModule_DictGetC(event_log, (void*)keyname, strlen(keyname), NULL); | ||
| 38 | if (!event) { | ||
| 39 | event = RedisModule_Alloc(sizeof(EventElement)); | ||
| 40 | memset(event, 0, sizeof(EventElement)); | ||
| 41 | RedisModule_DictSetC(event_log, (void*)keyname, strlen(keyname), event); | ||
| 42 | } | ||
| 43 | if (event->last_val_string) RedisModule_FreeString(ctx, event->last_val_string); | ||
| 44 | event->last_val_string = RedisModule_CreateString(ctx, data, strlen(data)); | ||
| 45 | event->count++; | ||
| 46 | } | ||
| 47 | |||
| 48 | void LogNumericEvent(RedisModuleCtx *ctx, const char* keyname, long data) { | ||
| 49 | REDISMODULE_NOT_USED(ctx); | ||
| 50 | EventElement *event = RedisModule_DictGetC(event_log, (void*)keyname, strlen(keyname), NULL); | ||
| 51 | if (!event) { | ||
| 52 | event = RedisModule_Alloc(sizeof(EventElement)); | ||
| 53 | memset(event, 0, sizeof(EventElement)); | ||
| 54 | RedisModule_DictSetC(event_log, (void*)keyname, strlen(keyname), event); | ||
| 55 | } | ||
| 56 | event->last_val_int = data; | ||
| 57 | event->count++; | ||
| 58 | } | ||
| 59 | |||
| 60 | void FreeEvent(RedisModuleCtx *ctx, EventElement *event) { | ||
| 61 | if (event->last_val_string) | ||
| 62 | RedisModule_FreeString(ctx, event->last_val_string); | ||
| 63 | RedisModule_Free(event); | ||
| 64 | } | ||
| 65 | |||
| 66 | int cmdEventCount(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 67 | { | ||
| 68 | if (argc != 2){ | ||
| 69 | RedisModule_WrongArity(ctx); | ||
| 70 | return REDISMODULE_OK; | ||
| 71 | } | ||
| 72 | |||
| 73 | EventElement *event = RedisModule_DictGet(event_log, argv[1], NULL); | ||
| 74 | RedisModule_ReplyWithLongLong(ctx, event? event->count: 0); | ||
| 75 | return REDISMODULE_OK; | ||
| 76 | } | ||
| 77 | |||
| 78 | int cmdEventLast(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 79 | { | ||
| 80 | if (argc != 2){ | ||
| 81 | RedisModule_WrongArity(ctx); | ||
| 82 | return REDISMODULE_OK; | ||
| 83 | } | ||
| 84 | |||
| 85 | EventElement *event = RedisModule_DictGet(event_log, argv[1], NULL); | ||
| 86 | if (event && event->last_val_string) | ||
| 87 | RedisModule_ReplyWithString(ctx, event->last_val_string); | ||
| 88 | else if (event) | ||
| 89 | RedisModule_ReplyWithLongLong(ctx, event->last_val_int); | ||
| 90 | else | ||
| 91 | RedisModule_ReplyWithNull(ctx); | ||
| 92 | return REDISMODULE_OK; | ||
| 93 | } | ||
| 94 | |||
| 95 | void clearEvents(RedisModuleCtx *ctx) | ||
| 96 | { | ||
| 97 | RedisModuleString *key; | ||
| 98 | EventElement *event; | ||
| 99 | RedisModuleDictIter *iter = RedisModule_DictIteratorStart(event_log, "^", NULL); | ||
| 100 | while((key = RedisModule_DictNext(ctx, iter, (void**)&event)) != NULL) { | ||
| 101 | event->count = 0; | ||
| 102 | event->last_val_int = 0; | ||
| 103 | if (event->last_val_string) RedisModule_FreeString(ctx, event->last_val_string); | ||
| 104 | event->last_val_string = NULL; | ||
| 105 | RedisModule_DictDel(event_log, key, NULL); | ||
| 106 | RedisModule_Free(event); | ||
| 107 | } | ||
| 108 | RedisModule_DictIteratorStop(iter); | ||
| 109 | } | ||
| 110 | |||
| 111 | int cmdEventsClear(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 112 | { | ||
| 113 | REDISMODULE_NOT_USED(argc); | ||
| 114 | REDISMODULE_NOT_USED(argv); | ||
| 115 | clearEvents(ctx); | ||
| 116 | return REDISMODULE_OK; | ||
| 117 | } | ||
| 118 | |||
| 119 | /* Client state change callback. */ | ||
| 120 | void clientChangeCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data) | ||
| 121 | { | ||
| 122 | REDISMODULE_NOT_USED(e); | ||
| 123 | |||
| 124 | RedisModuleClientInfo *ci = data; | ||
| 125 | char *keyname = (sub == REDISMODULE_SUBEVENT_CLIENT_CHANGE_CONNECTED) ? | ||
| 126 | "client-connected" : "client-disconnected"; | ||
| 127 | LogNumericEvent(ctx, keyname, ci->id); | ||
| 128 | } | ||
| 129 | |||
| 130 | void flushdbCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data) | ||
| 131 | { | ||
| 132 | REDISMODULE_NOT_USED(e); | ||
| 133 | |||
| 134 | RedisModuleFlushInfo *fi = data; | ||
| 135 | char *keyname = (sub == REDISMODULE_SUBEVENT_FLUSHDB_START) ? | ||
| 136 | "flush-start" : "flush-end"; | ||
| 137 | LogNumericEvent(ctx, keyname, fi->dbnum); | ||
| 138 | } | ||
| 139 | |||
| 140 | void roleChangeCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data) | ||
| 141 | { | ||
| 142 | REDISMODULE_NOT_USED(e); | ||
| 143 | REDISMODULE_NOT_USED(data); | ||
| 144 | |||
| 145 | RedisModuleReplicationInfo *ri = data; | ||
| 146 | char *keyname = (sub == REDISMODULE_EVENT_REPLROLECHANGED_NOW_MASTER) ? | ||
| 147 | "role-master" : "role-replica"; | ||
| 148 | LogStringEvent(ctx, keyname, ri->masterhost); | ||
| 149 | } | ||
| 150 | |||
| 151 | void replicationChangeCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data) | ||
| 152 | { | ||
| 153 | REDISMODULE_NOT_USED(e); | ||
| 154 | REDISMODULE_NOT_USED(data); | ||
| 155 | |||
| 156 | char *keyname = (sub == REDISMODULE_SUBEVENT_REPLICA_CHANGE_ONLINE) ? | ||
| 157 | "replica-online" : "replica-offline"; | ||
| 158 | LogNumericEvent(ctx, keyname, 0); | ||
| 159 | } | ||
| 160 | |||
| 161 | void rasterLinkChangeCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data) | ||
| 162 | { | ||
| 163 | REDISMODULE_NOT_USED(e); | ||
| 164 | REDISMODULE_NOT_USED(data); | ||
| 165 | |||
| 166 | char *keyname = (sub == REDISMODULE_SUBEVENT_MASTER_LINK_UP) ? | ||
| 167 | "masterlink-up" : "masterlink-down"; | ||
| 168 | LogNumericEvent(ctx, keyname, 0); | ||
| 169 | } | ||
| 170 | |||
| 171 | void persistenceCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data) | ||
| 172 | { | ||
| 173 | REDISMODULE_NOT_USED(e); | ||
| 174 | REDISMODULE_NOT_USED(data); | ||
| 175 | |||
| 176 | char *keyname = NULL; | ||
| 177 | switch (sub) { | ||
| 178 | case REDISMODULE_SUBEVENT_PERSISTENCE_RDB_START: keyname = "persistence-rdb-start"; break; | ||
| 179 | case REDISMODULE_SUBEVENT_PERSISTENCE_AOF_START: keyname = "persistence-aof-start"; break; | ||
| 180 | case REDISMODULE_SUBEVENT_PERSISTENCE_SYNC_AOF_START: keyname = "persistence-syncaof-start"; break; | ||
| 181 | case REDISMODULE_SUBEVENT_PERSISTENCE_SYNC_RDB_START: keyname = "persistence-syncrdb-start"; break; | ||
| 182 | case REDISMODULE_SUBEVENT_PERSISTENCE_ENDED: keyname = "persistence-end"; break; | ||
| 183 | case REDISMODULE_SUBEVENT_PERSISTENCE_FAILED: keyname = "persistence-failed"; break; | ||
| 184 | } | ||
| 185 | /* modifying the keyspace from the fork child is not an option, using log instead */ | ||
| 186 | RedisModule_Log(ctx, "warning", "module-event-%s", keyname); | ||
| 187 | if (sub == REDISMODULE_SUBEVENT_PERSISTENCE_SYNC_RDB_START || | ||
| 188 | sub == REDISMODULE_SUBEVENT_PERSISTENCE_SYNC_AOF_START) | ||
| 189 | { | ||
| 190 | LogNumericEvent(ctx, keyname, 0); | ||
| 191 | } | ||
| 192 | } | ||
| 193 | |||
| 194 | void loadingCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data) | ||
| 195 | { | ||
| 196 | REDISMODULE_NOT_USED(e); | ||
| 197 | REDISMODULE_NOT_USED(data); | ||
| 198 | |||
| 199 | char *keyname = NULL; | ||
| 200 | switch (sub) { | ||
| 201 | case REDISMODULE_SUBEVENT_LOADING_RDB_START: keyname = "loading-rdb-start"; break; | ||
| 202 | case REDISMODULE_SUBEVENT_LOADING_AOF_START: keyname = "loading-aof-start"; break; | ||
| 203 | case REDISMODULE_SUBEVENT_LOADING_REPL_START: keyname = "loading-repl-start"; break; | ||
| 204 | case REDISMODULE_SUBEVENT_LOADING_ENDED: keyname = "loading-end"; break; | ||
| 205 | case REDISMODULE_SUBEVENT_LOADING_FAILED: keyname = "loading-failed"; break; | ||
| 206 | } | ||
| 207 | LogNumericEvent(ctx, keyname, 0); | ||
| 208 | } | ||
| 209 | |||
| 210 | void loadingProgressCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data) | ||
| 211 | { | ||
| 212 | REDISMODULE_NOT_USED(e); | ||
| 213 | |||
| 214 | RedisModuleLoadingProgress *ei = data; | ||
| 215 | char *keyname = (sub == REDISMODULE_SUBEVENT_LOADING_PROGRESS_RDB) ? | ||
| 216 | "loading-progress-rdb" : "loading-progress-aof"; | ||
| 217 | LogNumericEvent(ctx, keyname, ei->progress); | ||
| 218 | } | ||
| 219 | |||
| 220 | void shutdownCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data) | ||
| 221 | { | ||
| 222 | REDISMODULE_NOT_USED(e); | ||
| 223 | REDISMODULE_NOT_USED(data); | ||
| 224 | REDISMODULE_NOT_USED(sub); | ||
| 225 | |||
| 226 | RedisModule_Log(ctx, "warning", "module-event-%s", "shutdown"); | ||
| 227 | } | ||
| 228 | |||
| 229 | void cronLoopCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data) | ||
| 230 | { | ||
| 231 | REDISMODULE_NOT_USED(e); | ||
| 232 | REDISMODULE_NOT_USED(sub); | ||
| 233 | |||
| 234 | RedisModuleCronLoop *ei = data; | ||
| 235 | LogNumericEvent(ctx, "cron-loop", ei->hz); | ||
| 236 | } | ||
| 237 | |||
| 238 | void moduleChangeCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data) | ||
| 239 | { | ||
| 240 | REDISMODULE_NOT_USED(e); | ||
| 241 | |||
| 242 | RedisModuleModuleChange *ei = data; | ||
| 243 | char *keyname = (sub == REDISMODULE_SUBEVENT_MODULE_LOADED) ? | ||
| 244 | "module-loaded" : "module-unloaded"; | ||
| 245 | LogStringEvent(ctx, keyname, ei->module_name); | ||
| 246 | } | ||
| 247 | |||
| 248 | void swapDbCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data) | ||
| 249 | { | ||
| 250 | REDISMODULE_NOT_USED(e); | ||
| 251 | REDISMODULE_NOT_USED(sub); | ||
| 252 | |||
| 253 | RedisModuleSwapDbInfo *ei = data; | ||
| 254 | LogNumericEvent(ctx, "swapdb-first", ei->dbnum_first); | ||
| 255 | LogNumericEvent(ctx, "swapdb-second", ei->dbnum_second); | ||
| 256 | } | ||
| 257 | |||
| 258 | void configChangeCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data) | ||
| 259 | { | ||
| 260 | REDISMODULE_NOT_USED(e); | ||
| 261 | if (sub != REDISMODULE_SUBEVENT_CONFIG_CHANGE) { | ||
| 262 | return; | ||
| 263 | } | ||
| 264 | |||
| 265 | RedisModuleConfigChangeV1 *ei = data; | ||
| 266 | LogNumericEvent(ctx, "config-change-count", ei->num_changes); | ||
| 267 | LogStringEvent(ctx, "config-change-first", ei->config_names[0]); | ||
| 268 | } | ||
| 269 | |||
| 270 | void keyInfoCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data) | ||
| 271 | { | ||
| 272 | REDISMODULE_NOT_USED(e); | ||
| 273 | |||
| 274 | RedisModuleKeyInfoV1 *ei = data; | ||
| 275 | RedisModuleKey *kp = ei->key; | ||
| 276 | RedisModuleString *key = (RedisModuleString *) RedisModule_GetKeyNameFromModuleKey(kp); | ||
| 277 | const char *keyname = RedisModule_StringPtrLen(key, NULL); | ||
| 278 | RedisModuleString *event_keyname = RedisModule_CreateStringPrintf(ctx, "key-info-%s", keyname); | ||
| 279 | LogStringEvent(ctx, RedisModule_StringPtrLen(event_keyname, NULL), keyname); | ||
| 280 | RedisModule_FreeString(ctx, event_keyname); | ||
| 281 | |||
| 282 | /* Despite getting a key object from the callback, we also try to re-open it | ||
| 283 | * to make sure the callback is called before it is actually removed from the keyspace. */ | ||
| 284 | RedisModuleKey *kp_open = RedisModule_OpenKey(ctx, key, REDISMODULE_READ); | ||
| 285 | assert(RedisModule_ValueLength(kp) == RedisModule_ValueLength(kp_open)); | ||
| 286 | RedisModule_CloseKey(kp_open); | ||
| 287 | |||
| 288 | /* We also try to RM_Call a command that accesses that key, also to make sure it's still in the keyspace. */ | ||
| 289 | char *size_command = NULL; | ||
| 290 | int key_type = RedisModule_KeyType(kp); | ||
| 291 | if (key_type == REDISMODULE_KEYTYPE_STRING) { | ||
| 292 | size_command = "STRLEN"; | ||
| 293 | } else if (key_type == REDISMODULE_KEYTYPE_LIST) { | ||
| 294 | size_command = "LLEN"; | ||
| 295 | } else if (key_type == REDISMODULE_KEYTYPE_HASH) { | ||
| 296 | size_command = "HLEN"; | ||
| 297 | } else if (key_type == REDISMODULE_KEYTYPE_SET) { | ||
| 298 | size_command = "SCARD"; | ||
| 299 | } else if (key_type == REDISMODULE_KEYTYPE_ZSET) { | ||
| 300 | size_command = "ZCARD"; | ||
| 301 | } else if (key_type == REDISMODULE_KEYTYPE_STREAM) { | ||
| 302 | size_command = "XLEN"; | ||
| 303 | } | ||
| 304 | if (size_command != NULL) { | ||
| 305 | RedisModuleCallReply *reply = RedisModule_Call(ctx, size_command, "s", key); | ||
| 306 | assert(reply != NULL); | ||
| 307 | assert(RedisModule_ValueLength(kp) == (size_t) RedisModule_CallReplyInteger(reply)); | ||
| 308 | RedisModule_FreeCallReply(reply); | ||
| 309 | } | ||
| 310 | |||
| 311 | /* Now use the key object we got from the callback for various validations. */ | ||
| 312 | RedisModuleString *prev = RedisModule_DictGetC(removed_event_log, (void*)keyname, strlen(keyname), NULL); | ||
| 313 | /* We keep object length */ | ||
| 314 | RedisModuleString *v = RedisModule_CreateStringPrintf(ctx, "%zd", RedisModule_ValueLength(kp)); | ||
| 315 | /* For string type, we keep value instead of length */ | ||
| 316 | if (RedisModule_KeyType(kp) == REDISMODULE_KEYTYPE_STRING) { | ||
| 317 | RedisModule_FreeString(ctx, v); | ||
| 318 | size_t len; | ||
| 319 | /* We need to access the string value with RedisModule_StringDMA. | ||
| 320 | * RedisModule_StringDMA may call dbUnshareStringValue to free the origin object, | ||
| 321 | * so we also can test it. */ | ||
| 322 | char *s = RedisModule_StringDMA(kp, &len, REDISMODULE_READ); | ||
| 323 | v = RedisModule_CreateString(ctx, s, len); | ||
| 324 | } | ||
| 325 | RedisModule_DictReplaceC(removed_event_log, (void*)keyname, strlen(keyname), v); | ||
| 326 | if (prev != NULL) { | ||
| 327 | RedisModule_FreeString(ctx, prev); | ||
| 328 | } | ||
| 329 | |||
| 330 | const char *subevent = "deleted"; | ||
| 331 | if (sub == REDISMODULE_SUBEVENT_KEY_EXPIRED) { | ||
| 332 | subevent = "expired"; | ||
| 333 | } else if (sub == REDISMODULE_SUBEVENT_KEY_EVICTED) { | ||
| 334 | subevent = "evicted"; | ||
| 335 | } else if (sub == REDISMODULE_SUBEVENT_KEY_OVERWRITTEN) { | ||
| 336 | subevent = "overwritten"; | ||
| 337 | } | ||
| 338 | RedisModule_DictReplaceC(removed_subevent_type, (void*)keyname, strlen(keyname), (void *)subevent); | ||
| 339 | |||
| 340 | RedisModuleString *prevexpire = RedisModule_DictGetC(removed_expiry_log, (void*)keyname, strlen(keyname), NULL); | ||
| 341 | RedisModuleString *expire = RedisModule_CreateStringPrintf(ctx, "%lld", RedisModule_GetAbsExpire(kp)); | ||
| 342 | RedisModule_DictReplaceC(removed_expiry_log, (void*)keyname, strlen(keyname), (void *)expire); | ||
| 343 | if (prevexpire != NULL) { | ||
| 344 | RedisModule_FreeString(ctx, prevexpire); | ||
| 345 | } | ||
| 346 | } | ||
| 347 | |||
| 348 | static int cmdIsKeyRemoved(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){ | ||
| 349 | if(argc != 2){ | ||
| 350 | return RedisModule_WrongArity(ctx); | ||
| 351 | } | ||
| 352 | |||
| 353 | const char *key = RedisModule_StringPtrLen(argv[1], NULL); | ||
| 354 | |||
| 355 | RedisModuleString *value = RedisModule_DictGetC(removed_event_log, (void*)key, strlen(key), NULL); | ||
| 356 | |||
| 357 | if (value == NULL) { | ||
| 358 | return RedisModule_ReplyWithError(ctx, "ERR Key was not removed"); | ||
| 359 | } | ||
| 360 | |||
| 361 | const char *subevent = RedisModule_DictGetC(removed_subevent_type, (void*)key, strlen(key), NULL); | ||
| 362 | RedisModule_ReplyWithArray(ctx, 2); | ||
| 363 | RedisModule_ReplyWithString(ctx, value); | ||
| 364 | RedisModule_ReplyWithSimpleString(ctx, subevent); | ||
| 365 | |||
| 366 | return REDISMODULE_OK; | ||
| 367 | } | ||
| 368 | |||
| 369 | static int cmdKeyExpiry(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){ | ||
| 370 | if(argc != 2){ | ||
| 371 | return RedisModule_WrongArity(ctx); | ||
| 372 | } | ||
| 373 | |||
| 374 | const char* key = RedisModule_StringPtrLen(argv[1], NULL); | ||
| 375 | RedisModuleString *expire = RedisModule_DictGetC(removed_expiry_log, (void*)key, strlen(key), NULL); | ||
| 376 | if (expire == NULL) { | ||
| 377 | return RedisModule_ReplyWithError(ctx, "ERR Key was not removed"); | ||
| 378 | } | ||
| 379 | RedisModule_ReplyWithString(ctx, expire); | ||
| 380 | return REDISMODULE_OK; | ||
| 381 | } | ||
| 382 | |||
| 383 | /* This function must be present on each Redis module. It is used in order to | ||
| 384 | * register the commands into the Redis server. */ | ||
| 385 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 386 | #define VerifySubEventSupported(e, s) \ | ||
| 387 | if (!RedisModule_IsSubEventSupported(e, s)) { \ | ||
| 388 | return REDISMODULE_ERR; \ | ||
| 389 | } | ||
| 390 | |||
| 391 | if (RedisModule_Init(ctx,"testhook",1,REDISMODULE_APIVER_1) | ||
| 392 | == REDISMODULE_ERR) return REDISMODULE_ERR; | ||
| 393 | |||
| 394 | /* Example on how to check if a server sub event is supported */ | ||
| 395 | if (!RedisModule_IsSubEventSupported(RedisModuleEvent_ReplicationRoleChanged, REDISMODULE_EVENT_REPLROLECHANGED_NOW_MASTER)) { | ||
| 396 | return REDISMODULE_ERR; | ||
| 397 | } | ||
| 398 | |||
| 399 | /* replication related hooks */ | ||
| 400 | RedisModule_SubscribeToServerEvent(ctx, | ||
| 401 | RedisModuleEvent_ReplicationRoleChanged, roleChangeCallback); | ||
| 402 | RedisModule_SubscribeToServerEvent(ctx, | ||
| 403 | RedisModuleEvent_ReplicaChange, replicationChangeCallback); | ||
| 404 | RedisModule_SubscribeToServerEvent(ctx, | ||
| 405 | RedisModuleEvent_MasterLinkChange, rasterLinkChangeCallback); | ||
| 406 | |||
| 407 | /* persistence related hooks */ | ||
| 408 | RedisModule_SubscribeToServerEvent(ctx, | ||
| 409 | RedisModuleEvent_Persistence, persistenceCallback); | ||
| 410 | RedisModule_SubscribeToServerEvent(ctx, | ||
| 411 | RedisModuleEvent_Loading, loadingCallback); | ||
| 412 | RedisModule_SubscribeToServerEvent(ctx, | ||
| 413 | RedisModuleEvent_LoadingProgress, loadingProgressCallback); | ||
| 414 | |||
| 415 | /* other hooks */ | ||
| 416 | RedisModule_SubscribeToServerEvent(ctx, | ||
| 417 | RedisModuleEvent_ClientChange, clientChangeCallback); | ||
| 418 | RedisModule_SubscribeToServerEvent(ctx, | ||
| 419 | RedisModuleEvent_FlushDB, flushdbCallback); | ||
| 420 | RedisModule_SubscribeToServerEvent(ctx, | ||
| 421 | RedisModuleEvent_Shutdown, shutdownCallback); | ||
| 422 | RedisModule_SubscribeToServerEvent(ctx, | ||
| 423 | RedisModuleEvent_CronLoop, cronLoopCallback); | ||
| 424 | |||
| 425 | RedisModule_SubscribeToServerEvent(ctx, | ||
| 426 | RedisModuleEvent_ModuleChange, moduleChangeCallback); | ||
| 427 | RedisModule_SubscribeToServerEvent(ctx, | ||
| 428 | RedisModuleEvent_SwapDB, swapDbCallback); | ||
| 429 | |||
| 430 | RedisModule_SubscribeToServerEvent(ctx, | ||
| 431 | RedisModuleEvent_Config, configChangeCallback); | ||
| 432 | |||
| 433 | RedisModule_SubscribeToServerEvent(ctx, | ||
| 434 | RedisModuleEvent_Key, keyInfoCallback); | ||
| 435 | |||
| 436 | event_log = RedisModule_CreateDict(ctx); | ||
| 437 | removed_event_log = RedisModule_CreateDict(ctx); | ||
| 438 | removed_subevent_type = RedisModule_CreateDict(ctx); | ||
| 439 | removed_expiry_log = RedisModule_CreateDict(ctx); | ||
| 440 | |||
| 441 | if (RedisModule_CreateCommand(ctx,"hooks.event_count", cmdEventCount,"",0,0,0) == REDISMODULE_ERR) | ||
| 442 | return REDISMODULE_ERR; | ||
| 443 | if (RedisModule_CreateCommand(ctx,"hooks.event_last", cmdEventLast,"",0,0,0) == REDISMODULE_ERR) | ||
| 444 | return REDISMODULE_ERR; | ||
| 445 | if (RedisModule_CreateCommand(ctx,"hooks.clear", cmdEventsClear,"",0,0,0) == REDISMODULE_ERR) | ||
| 446 | return REDISMODULE_ERR; | ||
| 447 | if (RedisModule_CreateCommand(ctx,"hooks.is_key_removed", cmdIsKeyRemoved,"",0,0,0) == REDISMODULE_ERR) | ||
| 448 | return REDISMODULE_ERR; | ||
| 449 | if (RedisModule_CreateCommand(ctx,"hooks.pexpireat", cmdKeyExpiry,"",0,0,0) == REDISMODULE_ERR) | ||
| 450 | return REDISMODULE_ERR; | ||
| 451 | |||
| 452 | if (argc == 1) { | ||
| 453 | const char *ptr = RedisModule_StringPtrLen(argv[0], NULL); | ||
| 454 | if (!strcasecmp(ptr, "noload")) { | ||
| 455 | /* This is a hint that we return ERR at the last moment of OnLoad. */ | ||
| 456 | RedisModule_FreeDict(ctx, event_log); | ||
| 457 | RedisModule_FreeDict(ctx, removed_event_log); | ||
| 458 | RedisModule_FreeDict(ctx, removed_subevent_type); | ||
| 459 | RedisModule_FreeDict(ctx, removed_expiry_log); | ||
| 460 | return REDISMODULE_ERR; | ||
| 461 | } | ||
| 462 | } | ||
| 463 | |||
| 464 | return REDISMODULE_OK; | ||
| 465 | } | ||
| 466 | |||
| 467 | int RedisModule_OnUnload(RedisModuleCtx *ctx) { | ||
| 468 | clearEvents(ctx); | ||
| 469 | RedisModule_FreeDict(ctx, event_log); | ||
| 470 | event_log = NULL; | ||
| 471 | |||
| 472 | RedisModuleDictIter *iter = RedisModule_DictIteratorStartC(removed_event_log, "^", NULL, 0); | ||
| 473 | char* key; | ||
| 474 | size_t keyLen; | ||
| 475 | RedisModuleString* val; | ||
| 476 | while((key = RedisModule_DictNextC(iter, &keyLen, (void**)&val))){ | ||
| 477 | RedisModule_FreeString(ctx, val); | ||
| 478 | } | ||
| 479 | RedisModule_FreeDict(ctx, removed_event_log); | ||
| 480 | RedisModule_DictIteratorStop(iter); | ||
| 481 | removed_event_log = NULL; | ||
| 482 | |||
| 483 | RedisModule_FreeDict(ctx, removed_subevent_type); | ||
| 484 | removed_subevent_type = NULL; | ||
| 485 | |||
| 486 | iter = RedisModule_DictIteratorStartC(removed_expiry_log, "^", NULL, 0); | ||
| 487 | while((key = RedisModule_DictNextC(iter, &keyLen, (void**)&val))){ | ||
| 488 | RedisModule_FreeString(ctx, val); | ||
| 489 | } | ||
| 490 | RedisModule_FreeDict(ctx, removed_expiry_log); | ||
| 491 | RedisModule_DictIteratorStop(iter); | ||
| 492 | removed_expiry_log = NULL; | ||
| 493 | |||
| 494 | return REDISMODULE_OK; | ||
| 495 | } | ||
| 496 | |||
diff --git a/examples/redis-unstable/tests/modules/infotest.c b/examples/redis-unstable/tests/modules/infotest.c new file mode 100644 index 0000000..b93a0c4 --- /dev/null +++ b/examples/redis-unstable/tests/modules/infotest.c | |||
| @@ -0,0 +1,119 @@ | |||
| 1 | #include "redismodule.h" | ||
| 2 | |||
| 3 | #include <string.h> | ||
| 4 | |||
| 5 | void InfoFunc(RedisModuleInfoCtx *ctx, int for_crash_report) { | ||
| 6 | RedisModule_InfoAddSection(ctx, ""); | ||
| 7 | RedisModule_InfoAddFieldLongLong(ctx, "global", -2); | ||
| 8 | RedisModule_InfoAddFieldULongLong(ctx, "uglobal", (unsigned long long)-2); | ||
| 9 | |||
| 10 | RedisModule_InfoAddSection(ctx, "Spanish"); | ||
| 11 | RedisModule_InfoAddFieldCString(ctx, "uno", "one"); | ||
| 12 | RedisModule_InfoAddFieldLongLong(ctx, "dos", 2); | ||
| 13 | |||
| 14 | RedisModule_InfoAddSection(ctx, "Italian"); | ||
| 15 | RedisModule_InfoAddFieldLongLong(ctx, "due", 2); | ||
| 16 | RedisModule_InfoAddFieldDouble(ctx, "tre", 3.3); | ||
| 17 | |||
| 18 | RedisModule_InfoAddSection(ctx, "keyspace"); | ||
| 19 | RedisModule_InfoBeginDictField(ctx, "db0"); | ||
| 20 | RedisModule_InfoAddFieldLongLong(ctx, "keys", 3); | ||
| 21 | RedisModule_InfoAddFieldLongLong(ctx, "expires", 1); | ||
| 22 | RedisModule_InfoEndDictField(ctx); | ||
| 23 | |||
| 24 | RedisModule_InfoAddSection(ctx, "unsafe"); | ||
| 25 | RedisModule_InfoBeginDictField(ctx, "unsafe:field"); | ||
| 26 | RedisModule_InfoAddFieldLongLong(ctx, "value", 1); | ||
| 27 | RedisModule_InfoEndDictField(ctx); | ||
| 28 | |||
| 29 | if (for_crash_report) { | ||
| 30 | RedisModule_InfoAddSection(ctx, "Klingon"); | ||
| 31 | RedisModule_InfoAddFieldCString(ctx, "one", "wa'"); | ||
| 32 | RedisModule_InfoAddFieldCString(ctx, "two", "cha'"); | ||
| 33 | RedisModule_InfoAddFieldCString(ctx, "three", "wej"); | ||
| 34 | } | ||
| 35 | |||
| 36 | } | ||
| 37 | |||
| 38 | int info_get(RedisModuleCtx *ctx, RedisModuleString **argv, int argc, char field_type) | ||
| 39 | { | ||
| 40 | if (argc != 3 && argc != 4) { | ||
| 41 | RedisModule_WrongArity(ctx); | ||
| 42 | return REDISMODULE_OK; | ||
| 43 | } | ||
| 44 | int err = REDISMODULE_OK; | ||
| 45 | const char *section, *field; | ||
| 46 | section = RedisModule_StringPtrLen(argv[1], NULL); | ||
| 47 | field = RedisModule_StringPtrLen(argv[2], NULL); | ||
| 48 | RedisModuleServerInfoData *info = RedisModule_GetServerInfo(ctx, section); | ||
| 49 | if (field_type=='i') { | ||
| 50 | long long ll = RedisModule_ServerInfoGetFieldSigned(info, field, &err); | ||
| 51 | if (err==REDISMODULE_OK) | ||
| 52 | RedisModule_ReplyWithLongLong(ctx, ll); | ||
| 53 | } else if (field_type=='u') { | ||
| 54 | unsigned long long ll = (unsigned long long)RedisModule_ServerInfoGetFieldUnsigned(info, field, &err); | ||
| 55 | if (err==REDISMODULE_OK) | ||
| 56 | RedisModule_ReplyWithLongLong(ctx, ll); | ||
| 57 | } else if (field_type=='d') { | ||
| 58 | double d = RedisModule_ServerInfoGetFieldDouble(info, field, &err); | ||
| 59 | if (err==REDISMODULE_OK) | ||
| 60 | RedisModule_ReplyWithDouble(ctx, d); | ||
| 61 | } else if (field_type=='c') { | ||
| 62 | const char *str = RedisModule_ServerInfoGetFieldC(info, field); | ||
| 63 | if (str) | ||
| 64 | RedisModule_ReplyWithCString(ctx, str); | ||
| 65 | } else { | ||
| 66 | RedisModuleString *str = RedisModule_ServerInfoGetField(ctx, info, field); | ||
| 67 | if (str) { | ||
| 68 | RedisModule_ReplyWithString(ctx, str); | ||
| 69 | RedisModule_FreeString(ctx, str); | ||
| 70 | } else | ||
| 71 | err=REDISMODULE_ERR; | ||
| 72 | } | ||
| 73 | if (err!=REDISMODULE_OK) | ||
| 74 | RedisModule_ReplyWithError(ctx, "not found"); | ||
| 75 | RedisModule_FreeServerInfo(ctx, info); | ||
| 76 | return REDISMODULE_OK; | ||
| 77 | } | ||
| 78 | |||
| 79 | int info_gets(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 80 | return info_get(ctx, argv, argc, 's'); | ||
| 81 | } | ||
| 82 | |||
| 83 | int info_getc(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 84 | return info_get(ctx, argv, argc, 'c'); | ||
| 85 | } | ||
| 86 | |||
| 87 | int info_geti(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 88 | return info_get(ctx, argv, argc, 'i'); | ||
| 89 | } | ||
| 90 | |||
| 91 | int info_getu(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 92 | return info_get(ctx, argv, argc, 'u'); | ||
| 93 | } | ||
| 94 | |||
| 95 | int info_getd(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 96 | return info_get(ctx, argv, argc, 'd'); | ||
| 97 | } | ||
| 98 | |||
| 99 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 100 | REDISMODULE_NOT_USED(argv); | ||
| 101 | REDISMODULE_NOT_USED(argc); | ||
| 102 | if (RedisModule_Init(ctx,"infotest",1,REDISMODULE_APIVER_1) | ||
| 103 | == REDISMODULE_ERR) return REDISMODULE_ERR; | ||
| 104 | |||
| 105 | if (RedisModule_RegisterInfoFunc(ctx, InfoFunc) == REDISMODULE_ERR) return REDISMODULE_ERR; | ||
| 106 | |||
| 107 | if (RedisModule_CreateCommand(ctx,"info.gets", info_gets,"",0,0,0) == REDISMODULE_ERR) | ||
| 108 | return REDISMODULE_ERR; | ||
| 109 | if (RedisModule_CreateCommand(ctx,"info.getc", info_getc,"",0,0,0) == REDISMODULE_ERR) | ||
| 110 | return REDISMODULE_ERR; | ||
| 111 | if (RedisModule_CreateCommand(ctx,"info.geti", info_geti,"",0,0,0) == REDISMODULE_ERR) | ||
| 112 | return REDISMODULE_ERR; | ||
| 113 | if (RedisModule_CreateCommand(ctx,"info.getu", info_getu,"",0,0,0) == REDISMODULE_ERR) | ||
| 114 | return REDISMODULE_ERR; | ||
| 115 | if (RedisModule_CreateCommand(ctx,"info.getd", info_getd,"",0,0,0) == REDISMODULE_ERR) | ||
| 116 | return REDISMODULE_ERR; | ||
| 117 | |||
| 118 | return REDISMODULE_OK; | ||
| 119 | } | ||
diff --git a/examples/redis-unstable/tests/modules/internalsecret.c b/examples/redis-unstable/tests/modules/internalsecret.c new file mode 100644 index 0000000..043089c --- /dev/null +++ b/examples/redis-unstable/tests/modules/internalsecret.c | |||
| @@ -0,0 +1,154 @@ | |||
| 1 | #include "redismodule.h" | ||
| 2 | #include <errno.h> | ||
| 3 | |||
| 4 | int InternalAuth_GetInternalSecret(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 5 | REDISMODULE_NOT_USED(argv); | ||
| 6 | REDISMODULE_NOT_USED(argc); | ||
| 7 | |||
| 8 | /* NOTE: The internal secret SHOULD NOT be exposed by any module. This is | ||
| 9 | done for testing purposes only. */ | ||
| 10 | size_t len; | ||
| 11 | const char *secret = RedisModule_GetInternalSecret(ctx, &len); | ||
| 12 | if(secret) { | ||
| 13 | RedisModule_ReplyWithStringBuffer(ctx, secret, len); | ||
| 14 | } else { | ||
| 15 | RedisModule_ReplyWithError(ctx, "ERR no internal secret available"); | ||
| 16 | } | ||
| 17 | return REDISMODULE_OK; | ||
| 18 | } | ||
| 19 | |||
| 20 | int InternalAuth_InternalCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 21 | REDISMODULE_NOT_USED(argv); | ||
| 22 | REDISMODULE_NOT_USED(argc); | ||
| 23 | |||
| 24 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 25 | return REDISMODULE_OK; | ||
| 26 | } | ||
| 27 | |||
| 28 | typedef enum { | ||
| 29 | RM_CALL_REGULAR = 0, | ||
| 30 | RM_CALL_WITHUSER = 1, | ||
| 31 | RM_CALL_WITHDETACHEDCLIENT = 2, | ||
| 32 | RM_CALL_REPLICATED = 3 | ||
| 33 | } RMCallMode; | ||
| 34 | |||
| 35 | int call_rm_call(RedisModuleCtx *ctx, RedisModuleString **argv, int argc, RMCallMode mode) { | ||
| 36 | if(argc < 2){ | ||
| 37 | return RedisModule_WrongArity(ctx); | ||
| 38 | } | ||
| 39 | RedisModuleCallReply *rep = NULL; | ||
| 40 | RedisModuleCtx *detached_ctx = NULL; | ||
| 41 | const char* cmd = RedisModule_StringPtrLen(argv[1], NULL); | ||
| 42 | |||
| 43 | switch (mode) { | ||
| 44 | case RM_CALL_REGULAR: | ||
| 45 | // Regular call, with the unrestricted user. | ||
| 46 | rep = RedisModule_Call(ctx, cmd, "vE", argv + 2, (size_t)argc - 2); | ||
| 47 | break; | ||
| 48 | case RM_CALL_WITHUSER: | ||
| 49 | // Simply call the command with the current client. | ||
| 50 | rep = RedisModule_Call(ctx, cmd, "vCE", argv + 2, (size_t)argc - 2); | ||
| 51 | break; | ||
| 52 | case RM_CALL_WITHDETACHEDCLIENT: | ||
| 53 | // Use a context created with the thread-safe-context API | ||
| 54 | detached_ctx = RedisModule_GetThreadSafeContext(NULL); | ||
| 55 | if(!detached_ctx){ | ||
| 56 | RedisModule_ReplyWithError(ctx, "ERR failed to create detached context"); | ||
| 57 | return REDISMODULE_ERR; | ||
| 58 | } | ||
| 59 | // Dispatch the command with the detached context | ||
| 60 | rep = RedisModule_Call(detached_ctx, cmd, "vCE", argv + 2, (size_t)argc - 2); | ||
| 61 | break; | ||
| 62 | case RM_CALL_REPLICATED: | ||
| 63 | rep = RedisModule_Call(ctx, cmd, "vE", argv + 2, (size_t)argc - 2); | ||
| 64 | } | ||
| 65 | |||
| 66 | if(!rep) { | ||
| 67 | char err[100]; | ||
| 68 | switch (errno) { | ||
| 69 | case EACCES: | ||
| 70 | RedisModule_ReplyWithError(ctx, "ERR NOPERM"); | ||
| 71 | break; | ||
| 72 | case ENOENT: | ||
| 73 | RedisModule_ReplyWithError(ctx, "ERR unknown command"); | ||
| 74 | break; | ||
| 75 | default: | ||
| 76 | snprintf(err, sizeof(err) - 1, "ERR errno=%d", errno); | ||
| 77 | RedisModule_ReplyWithError(ctx, err); | ||
| 78 | break; | ||
| 79 | } | ||
| 80 | } else { | ||
| 81 | RedisModule_ReplyWithCallReply(ctx, rep); | ||
| 82 | RedisModule_FreeCallReply(rep); | ||
| 83 | if (mode == RM_CALL_REPLICATED) | ||
| 84 | RedisModule_ReplicateVerbatim(ctx); | ||
| 85 | } | ||
| 86 | |||
| 87 | if (mode == RM_CALL_WITHDETACHEDCLIENT) { | ||
| 88 | RedisModule_FreeThreadSafeContext(detached_ctx); | ||
| 89 | } | ||
| 90 | |||
| 91 | return REDISMODULE_OK; | ||
| 92 | } | ||
| 93 | |||
| 94 | int internal_rmcall(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 95 | return call_rm_call(ctx, argv, argc, RM_CALL_REGULAR); | ||
| 96 | } | ||
| 97 | |||
| 98 | int noninternal_rmcall(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 99 | return call_rm_call(ctx, argv, argc, RM_CALL_REGULAR); | ||
| 100 | } | ||
| 101 | |||
| 102 | int noninternal_rmcall_withuser(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 103 | return call_rm_call(ctx, argv, argc, RM_CALL_WITHUSER); | ||
| 104 | } | ||
| 105 | |||
| 106 | int noninternal_rmcall_detachedcontext_withuser(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 107 | return call_rm_call(ctx, argv, argc, RM_CALL_WITHDETACHEDCLIENT); | ||
| 108 | } | ||
| 109 | |||
| 110 | int internal_rmcall_replicated(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 111 | return call_rm_call(ctx, argv, argc, RM_CALL_REPLICATED); | ||
| 112 | } | ||
| 113 | |||
| 114 | /* This function must be present on each Redis module. It is used in order to | ||
| 115 | * register the commands into the Redis server. */ | ||
| 116 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 117 | REDISMODULE_NOT_USED(argv); | ||
| 118 | REDISMODULE_NOT_USED(argc); | ||
| 119 | |||
| 120 | if (RedisModule_Init(ctx,"testinternalsecret",1,REDISMODULE_APIVER_1) | ||
| 121 | == REDISMODULE_ERR) return REDISMODULE_ERR; | ||
| 122 | |||
| 123 | /* WARNING: A module should NEVER expose the internal secret - this is for | ||
| 124 | * testing purposes only. */ | ||
| 125 | if (RedisModule_CreateCommand(ctx,"internalauth.getinternalsecret", | ||
| 126 | InternalAuth_GetInternalSecret,"",0,0,0) == REDISMODULE_ERR) | ||
| 127 | return REDISMODULE_ERR; | ||
| 128 | |||
| 129 | if (RedisModule_CreateCommand(ctx,"internalauth.internalcommand", | ||
| 130 | InternalAuth_InternalCommand,"internal",0,0,0) == REDISMODULE_ERR) | ||
| 131 | return REDISMODULE_ERR; | ||
| 132 | |||
| 133 | if (RedisModule_CreateCommand(ctx,"internalauth.internal_rmcall", | ||
| 134 | internal_rmcall,"write internal",0,0,0) == REDISMODULE_ERR) | ||
| 135 | return REDISMODULE_ERR; | ||
| 136 | |||
| 137 | if (RedisModule_CreateCommand(ctx,"internalauth.noninternal_rmcall", | ||
| 138 | noninternal_rmcall,"write",0,0,0) == REDISMODULE_ERR) | ||
| 139 | return REDISMODULE_ERR; | ||
| 140 | |||
| 141 | if (RedisModule_CreateCommand(ctx,"internalauth.noninternal_rmcall_withuser", | ||
| 142 | noninternal_rmcall_withuser,"write",0,0,0) == REDISMODULE_ERR) | ||
| 143 | return REDISMODULE_ERR; | ||
| 144 | |||
| 145 | if (RedisModule_CreateCommand(ctx,"internalauth.noninternal_rmcall_detachedcontext_withuser", | ||
| 146 | noninternal_rmcall_detachedcontext_withuser,"write",0,0,0) == REDISMODULE_ERR) | ||
| 147 | return REDISMODULE_ERR; | ||
| 148 | |||
| 149 | if (RedisModule_CreateCommand(ctx,"internalauth.internal_rmcall_replicated", | ||
| 150 | internal_rmcall_replicated,"write internal",0,0,0) == REDISMODULE_ERR) | ||
| 151 | return REDISMODULE_ERR; | ||
| 152 | |||
| 153 | return REDISMODULE_OK; | ||
| 154 | } | ||
diff --git a/examples/redis-unstable/tests/modules/keyspace_events.c b/examples/redis-unstable/tests/modules/keyspace_events.c new file mode 100644 index 0000000..146261f --- /dev/null +++ b/examples/redis-unstable/tests/modules/keyspace_events.c | |||
| @@ -0,0 +1,479 @@ | |||
| 1 | /* This module is used to test the server keyspace events API. | ||
| 2 | * | ||
| 3 | * ----------------------------------------------------------------------------- | ||
| 4 | * | ||
| 5 | * Copyright (c) 2020-Present, Redis Ltd. | ||
| 6 | * All rights reserved. | ||
| 7 | * | ||
| 8 | * Licensed under your choice of (a) the Redis Source Available License 2.0 | ||
| 9 | * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the | ||
| 10 | * GNU Affero General Public License v3 (AGPLv3). | ||
| 11 | */ | ||
| 12 | |||
| 13 | #define _BSD_SOURCE | ||
| 14 | #define _DEFAULT_SOURCE /* For usleep */ | ||
| 15 | |||
| 16 | #include "redismodule.h" | ||
| 17 | #include <stdio.h> | ||
| 18 | #include <string.h> | ||
| 19 | #include <strings.h> | ||
| 20 | #include <unistd.h> | ||
| 21 | |||
| 22 | ustime_t cached_time = 0; | ||
| 23 | |||
| 24 | /** stores all the keys on which we got 'loaded' keyspace notification **/ | ||
| 25 | RedisModuleDict *loaded_event_log = NULL; | ||
| 26 | /** stores all the keys on which we got 'module' keyspace notification **/ | ||
| 27 | RedisModuleDict *module_event_log = NULL; | ||
| 28 | |||
| 29 | /** Counts how many deleted KSN we got on keys with a prefix of "count_dels_" **/ | ||
| 30 | static size_t dels = 0; | ||
| 31 | |||
| 32 | static int KeySpace_NotificationLoaded(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key){ | ||
| 33 | REDISMODULE_NOT_USED(ctx); | ||
| 34 | REDISMODULE_NOT_USED(type); | ||
| 35 | |||
| 36 | if(strcmp(event, "loaded") == 0){ | ||
| 37 | const char* keyName = RedisModule_StringPtrLen(key, NULL); | ||
| 38 | int nokey; | ||
| 39 | RedisModule_DictGetC(loaded_event_log, (void*)keyName, strlen(keyName), &nokey); | ||
| 40 | if(nokey){ | ||
| 41 | RedisModule_DictSetC(loaded_event_log, (void*)keyName, strlen(keyName), RedisModule_HoldString(ctx, key)); | ||
| 42 | } | ||
| 43 | } | ||
| 44 | |||
| 45 | return REDISMODULE_OK; | ||
| 46 | } | ||
| 47 | |||
| 48 | static long long callback_call_count = 0; | ||
| 49 | static int KeySpace_NotificationGeneric(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key) { | ||
| 50 | REDISMODULE_NOT_USED(type); | ||
| 51 | callback_call_count++; | ||
| 52 | const char *key_str = RedisModule_StringPtrLen(key, NULL); | ||
| 53 | if (strncmp(key_str, "count_dels_", 11) == 0 && strcmp(event, "del") == 0) { | ||
| 54 | if (RedisModule_GetContextFlags(ctx) & REDISMODULE_CTX_FLAGS_MASTER) { | ||
| 55 | dels++; | ||
| 56 | RedisModule_Replicate(ctx, "keyspace.incr_dels", ""); | ||
| 57 | } | ||
| 58 | return REDISMODULE_OK; | ||
| 59 | } | ||
| 60 | if (cached_time) { | ||
| 61 | RedisModule_Assert(cached_time == RedisModule_CachedMicroseconds()); | ||
| 62 | usleep(1); | ||
| 63 | RedisModule_Assert(cached_time != RedisModule_Microseconds()); | ||
| 64 | } | ||
| 65 | |||
| 66 | if (strcmp(event, "del") == 0) { | ||
| 67 | RedisModuleString *copykey = RedisModule_CreateStringPrintf(ctx, "%s_copy", RedisModule_StringPtrLen(key, NULL)); | ||
| 68 | RedisModuleCallReply* rep = RedisModule_Call(ctx, "DEL", "s!", copykey); | ||
| 69 | RedisModule_FreeString(ctx, copykey); | ||
| 70 | RedisModule_FreeCallReply(rep); | ||
| 71 | |||
| 72 | int ctx_flags = RedisModule_GetContextFlags(ctx); | ||
| 73 | if (ctx_flags & REDISMODULE_CTX_FLAGS_LUA) { | ||
| 74 | RedisModuleCallReply* rep = RedisModule_Call(ctx, "INCR", "c", "lua"); | ||
| 75 | RedisModule_FreeCallReply(rep); | ||
| 76 | } | ||
| 77 | if (ctx_flags & REDISMODULE_CTX_FLAGS_MULTI) { | ||
| 78 | RedisModuleCallReply* rep = RedisModule_Call(ctx, "INCR", "c", "multi"); | ||
| 79 | RedisModule_FreeCallReply(rep); | ||
| 80 | } | ||
| 81 | } | ||
| 82 | |||
| 83 | return REDISMODULE_OK; | ||
| 84 | } | ||
| 85 | |||
| 86 | static int KeySpace_NotificationExpired(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key) { | ||
| 87 | REDISMODULE_NOT_USED(type); | ||
| 88 | REDISMODULE_NOT_USED(event); | ||
| 89 | REDISMODULE_NOT_USED(key); | ||
| 90 | |||
| 91 | RedisModuleCallReply* rep = RedisModule_Call(ctx, "INCR", "c!", "testkeyspace:expired"); | ||
| 92 | RedisModule_FreeCallReply(rep); | ||
| 93 | |||
| 94 | return REDISMODULE_OK; | ||
| 95 | } | ||
| 96 | |||
| 97 | /* This key miss notification handler is performing a write command inside the notification callback. | ||
| 98 | * Notice, it is discourage and currently wrong to perform a write command inside key miss event. | ||
| 99 | * It can cause read commands to be replicated to the replica/aof. This test is here temporary (for coverage and | ||
| 100 | * verification that it's not crashing). */ | ||
| 101 | static int KeySpace_NotificationModuleKeyMiss(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key) { | ||
| 102 | REDISMODULE_NOT_USED(type); | ||
| 103 | REDISMODULE_NOT_USED(event); | ||
| 104 | REDISMODULE_NOT_USED(key); | ||
| 105 | |||
| 106 | int flags = RedisModule_GetContextFlags(ctx); | ||
| 107 | if (!(flags & REDISMODULE_CTX_FLAGS_MASTER)) { | ||
| 108 | return REDISMODULE_OK; // ignore the event on replica | ||
| 109 | } | ||
| 110 | |||
| 111 | RedisModuleCallReply* rep = RedisModule_Call(ctx, "incr", "!c", "missed"); | ||
| 112 | RedisModule_FreeCallReply(rep); | ||
| 113 | |||
| 114 | return REDISMODULE_OK; | ||
| 115 | } | ||
| 116 | |||
| 117 | static int KeySpace_NotificationModuleString(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key) { | ||
| 118 | REDISMODULE_NOT_USED(type); | ||
| 119 | REDISMODULE_NOT_USED(event); | ||
| 120 | RedisModuleKey *redis_key = RedisModule_OpenKey(ctx, key, REDISMODULE_READ); | ||
| 121 | |||
| 122 | size_t len = 0; | ||
| 123 | /* RedisModule_StringDMA could change the data format and cause the old robj to be freed. | ||
| 124 | * This code verifies that such format change will not cause any crashes.*/ | ||
| 125 | char *data = RedisModule_StringDMA(redis_key, &len, REDISMODULE_READ); | ||
| 126 | int res = strncmp(data, "dummy", 5); | ||
| 127 | REDISMODULE_NOT_USED(res); | ||
| 128 | |||
| 129 | RedisModule_CloseKey(redis_key); | ||
| 130 | |||
| 131 | return REDISMODULE_OK; | ||
| 132 | } | ||
| 133 | |||
| 134 | static void KeySpace_PostNotificationStringFreePD(void *pd) { | ||
| 135 | RedisModule_FreeString(NULL, pd); | ||
| 136 | } | ||
| 137 | |||
| 138 | static void KeySpace_PostNotificationString(RedisModuleCtx *ctx, void *pd) { | ||
| 139 | REDISMODULE_NOT_USED(ctx); | ||
| 140 | RedisModuleCallReply* rep = RedisModule_Call(ctx, "incr", "!s", pd); | ||
| 141 | RedisModule_FreeCallReply(rep); | ||
| 142 | } | ||
| 143 | |||
| 144 | static int KeySpace_NotificationModuleStringPostNotificationJob(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key) { | ||
| 145 | REDISMODULE_NOT_USED(ctx); | ||
| 146 | REDISMODULE_NOT_USED(type); | ||
| 147 | REDISMODULE_NOT_USED(event); | ||
| 148 | |||
| 149 | const char *key_str = RedisModule_StringPtrLen(key, NULL); | ||
| 150 | |||
| 151 | if (strncmp(key_str, "string1_", 8) != 0) { | ||
| 152 | return REDISMODULE_OK; | ||
| 153 | } | ||
| 154 | |||
| 155 | RedisModuleString *new_key = RedisModule_CreateStringPrintf(NULL, "string_changed{%s}", key_str); | ||
| 156 | RedisModule_AddPostNotificationJob(ctx, KeySpace_PostNotificationString, new_key, KeySpace_PostNotificationStringFreePD); | ||
| 157 | return REDISMODULE_OK; | ||
| 158 | } | ||
| 159 | |||
| 160 | static int KeySpace_NotificationModule(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key) { | ||
| 161 | REDISMODULE_NOT_USED(ctx); | ||
| 162 | REDISMODULE_NOT_USED(type); | ||
| 163 | REDISMODULE_NOT_USED(event); | ||
| 164 | |||
| 165 | const char* keyName = RedisModule_StringPtrLen(key, NULL); | ||
| 166 | int nokey; | ||
| 167 | RedisModule_DictGetC(module_event_log, (void*)keyName, strlen(keyName), &nokey); | ||
| 168 | if(nokey){ | ||
| 169 | RedisModule_DictSetC(module_event_log, (void*)keyName, strlen(keyName), RedisModule_HoldString(ctx, key)); | ||
| 170 | } | ||
| 171 | return REDISMODULE_OK; | ||
| 172 | } | ||
| 173 | |||
| 174 | static int cmdNotify(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){ | ||
| 175 | if(argc != 2){ | ||
| 176 | return RedisModule_WrongArity(ctx); | ||
| 177 | } | ||
| 178 | |||
| 179 | RedisModule_NotifyKeyspaceEvent(ctx, REDISMODULE_NOTIFY_MODULE, "notify", argv[1]); | ||
| 180 | RedisModule_ReplyWithNull(ctx); | ||
| 181 | return REDISMODULE_OK; | ||
| 182 | } | ||
| 183 | |||
| 184 | static int cmdIsModuleKeyNotified(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){ | ||
| 185 | if(argc != 2){ | ||
| 186 | return RedisModule_WrongArity(ctx); | ||
| 187 | } | ||
| 188 | |||
| 189 | const char* key = RedisModule_StringPtrLen(argv[1], NULL); | ||
| 190 | |||
| 191 | int nokey; | ||
| 192 | RedisModuleString* keyStr = RedisModule_DictGetC(module_event_log, (void*)key, strlen(key), &nokey); | ||
| 193 | |||
| 194 | RedisModule_ReplyWithArray(ctx, 2); | ||
| 195 | RedisModule_ReplyWithLongLong(ctx, !nokey); | ||
| 196 | if(nokey){ | ||
| 197 | RedisModule_ReplyWithNull(ctx); | ||
| 198 | }else{ | ||
| 199 | RedisModule_ReplyWithString(ctx, keyStr); | ||
| 200 | } | ||
| 201 | return REDISMODULE_OK; | ||
| 202 | } | ||
| 203 | |||
| 204 | static int cmdIsKeyLoaded(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){ | ||
| 205 | if(argc != 2){ | ||
| 206 | return RedisModule_WrongArity(ctx); | ||
| 207 | } | ||
| 208 | |||
| 209 | const char* key = RedisModule_StringPtrLen(argv[1], NULL); | ||
| 210 | |||
| 211 | int nokey; | ||
| 212 | RedisModuleString* keyStr = RedisModule_DictGetC(loaded_event_log, (void*)key, strlen(key), &nokey); | ||
| 213 | |||
| 214 | RedisModule_ReplyWithArray(ctx, 2); | ||
| 215 | RedisModule_ReplyWithLongLong(ctx, !nokey); | ||
| 216 | if(nokey){ | ||
| 217 | RedisModule_ReplyWithNull(ctx); | ||
| 218 | }else{ | ||
| 219 | RedisModule_ReplyWithString(ctx, keyStr); | ||
| 220 | } | ||
| 221 | return REDISMODULE_OK; | ||
| 222 | } | ||
| 223 | |||
| 224 | static int cmdDelKeyCopy(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 225 | if (argc != 2) | ||
| 226 | return RedisModule_WrongArity(ctx); | ||
| 227 | |||
| 228 | cached_time = RedisModule_CachedMicroseconds(); | ||
| 229 | |||
| 230 | RedisModuleCallReply* rep = RedisModule_Call(ctx, "DEL", "s!", argv[1]); | ||
| 231 | if (!rep) { | ||
| 232 | RedisModule_ReplyWithError(ctx, "NULL reply returned"); | ||
| 233 | } else { | ||
| 234 | RedisModule_ReplyWithCallReply(ctx, rep); | ||
| 235 | RedisModule_FreeCallReply(rep); | ||
| 236 | } | ||
| 237 | cached_time = 0; | ||
| 238 | return REDISMODULE_OK; | ||
| 239 | } | ||
| 240 | |||
| 241 | /* Call INCR and propagate using RM_Call with `!`. */ | ||
| 242 | static int cmdIncrCase1(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 243 | if (argc != 2) | ||
| 244 | return RedisModule_WrongArity(ctx); | ||
| 245 | |||
| 246 | RedisModuleCallReply* rep = RedisModule_Call(ctx, "INCR", "s!", argv[1]); | ||
| 247 | if (!rep) { | ||
| 248 | RedisModule_ReplyWithError(ctx, "NULL reply returned"); | ||
| 249 | } else { | ||
| 250 | RedisModule_ReplyWithCallReply(ctx, rep); | ||
| 251 | RedisModule_FreeCallReply(rep); | ||
| 252 | } | ||
| 253 | return REDISMODULE_OK; | ||
| 254 | } | ||
| 255 | |||
| 256 | /* Call INCR and propagate using RM_Replicate. */ | ||
| 257 | static int cmdIncrCase2(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 258 | if (argc != 2) | ||
| 259 | return RedisModule_WrongArity(ctx); | ||
| 260 | |||
| 261 | RedisModuleCallReply* rep = RedisModule_Call(ctx, "INCR", "s", argv[1]); | ||
| 262 | if (!rep) { | ||
| 263 | RedisModule_ReplyWithError(ctx, "NULL reply returned"); | ||
| 264 | } else { | ||
| 265 | RedisModule_ReplyWithCallReply(ctx, rep); | ||
| 266 | RedisModule_FreeCallReply(rep); | ||
| 267 | } | ||
| 268 | RedisModule_Replicate(ctx, "INCR", "s", argv[1]); | ||
| 269 | return REDISMODULE_OK; | ||
| 270 | } | ||
| 271 | |||
| 272 | /* Call INCR and propagate using RM_ReplicateVerbatim. */ | ||
| 273 | static int cmdIncrCase3(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 274 | if (argc != 2) | ||
| 275 | return RedisModule_WrongArity(ctx); | ||
| 276 | |||
| 277 | RedisModuleCallReply* rep = RedisModule_Call(ctx, "INCR", "s", argv[1]); | ||
| 278 | if (!rep) { | ||
| 279 | RedisModule_ReplyWithError(ctx, "NULL reply returned"); | ||
| 280 | } else { | ||
| 281 | RedisModule_ReplyWithCallReply(ctx, rep); | ||
| 282 | RedisModule_FreeCallReply(rep); | ||
| 283 | } | ||
| 284 | RedisModule_ReplicateVerbatim(ctx); | ||
| 285 | return REDISMODULE_OK; | ||
| 286 | } | ||
| 287 | |||
| 288 | static int cmdIncrDels(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 289 | REDISMODULE_NOT_USED(argv); | ||
| 290 | REDISMODULE_NOT_USED(argc); | ||
| 291 | dels++; | ||
| 292 | return RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 293 | } | ||
| 294 | |||
| 295 | static int cmdGetDels(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 296 | REDISMODULE_NOT_USED(argv); | ||
| 297 | REDISMODULE_NOT_USED(argc); | ||
| 298 | return RedisModule_ReplyWithLongLong(ctx, dels); | ||
| 299 | } | ||
| 300 | |||
| 301 | static RedisModuleNotificationFunc get_callback_for_event(int event_mask) { | ||
| 302 | switch(event_mask) { | ||
| 303 | case REDISMODULE_NOTIFY_LOADED: | ||
| 304 | return KeySpace_NotificationLoaded; | ||
| 305 | case REDISMODULE_NOTIFY_GENERIC: | ||
| 306 | return KeySpace_NotificationGeneric; | ||
| 307 | case REDISMODULE_NOTIFY_EXPIRED: | ||
| 308 | return KeySpace_NotificationExpired; | ||
| 309 | case REDISMODULE_NOTIFY_MODULE: | ||
| 310 | return KeySpace_NotificationModule; | ||
| 311 | case REDISMODULE_NOTIFY_KEY_MISS: | ||
| 312 | return KeySpace_NotificationModuleKeyMiss; | ||
| 313 | case REDISMODULE_NOTIFY_STRING: | ||
| 314 | // We have two callbacks for STRING events in your OnLoad, | ||
| 315 | // For simplicity, pick the first: | ||
| 316 | return KeySpace_NotificationModuleString; | ||
| 317 | default: | ||
| 318 | return NULL; | ||
| 319 | } | ||
| 320 | } | ||
| 321 | |||
| 322 | int GetCallbackCountCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 323 | REDISMODULE_NOT_USED(argv); | ||
| 324 | REDISMODULE_NOT_USED(argc); | ||
| 325 | RedisModule_ReplyWithLongLong(ctx, callback_call_count); | ||
| 326 | return REDISMODULE_OK; | ||
| 327 | } | ||
| 328 | |||
| 329 | static int CmdUnsub(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 330 | if (argc != 2) { | ||
| 331 | return RedisModule_WrongArity(ctx); | ||
| 332 | } | ||
| 333 | |||
| 334 | long long event_mask; | ||
| 335 | if (RedisModule_StringToLongLong(argv[1], &event_mask) != REDISMODULE_OK) { | ||
| 336 | return RedisModule_ReplyWithError(ctx, "ERR invalid event mask"); | ||
| 337 | } | ||
| 338 | |||
| 339 | RedisModuleNotificationFunc cb = get_callback_for_event((int)event_mask); | ||
| 340 | if (cb == NULL) { | ||
| 341 | return RedisModule_ReplyWithError(ctx, "ERR unknown event mask"); | ||
| 342 | } | ||
| 343 | |||
| 344 | if (RedisModule_UnsubscribeFromKeyspaceEvents(ctx, (int)event_mask, cb) != REDISMODULE_OK) { | ||
| 345 | return RedisModule_ReplyWithError(ctx, "ERR unsubscribe failed"); | ||
| 346 | } | ||
| 347 | |||
| 348 | return RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 349 | } | ||
| 350 | /* This function must be present on each Redis module. It is used in order to | ||
| 351 | * register the commands into the Redis server. */ | ||
| 352 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 353 | if (RedisModule_Init(ctx,"testkeyspace",1,REDISMODULE_APIVER_1) == REDISMODULE_ERR){ | ||
| 354 | return REDISMODULE_ERR; | ||
| 355 | } | ||
| 356 | |||
| 357 | loaded_event_log = RedisModule_CreateDict(ctx); | ||
| 358 | module_event_log = RedisModule_CreateDict(ctx); | ||
| 359 | |||
| 360 | int keySpaceAll = RedisModule_GetKeyspaceNotificationFlagsAll(); | ||
| 361 | |||
| 362 | if (!(keySpaceAll & REDISMODULE_NOTIFY_LOADED)) { | ||
| 363 | // REDISMODULE_NOTIFY_LOADED event are not supported we can not start | ||
| 364 | return REDISMODULE_ERR; | ||
| 365 | } | ||
| 366 | |||
| 367 | if(RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_LOADED, KeySpace_NotificationLoaded) != REDISMODULE_OK){ | ||
| 368 | return REDISMODULE_ERR; | ||
| 369 | } | ||
| 370 | |||
| 371 | if(RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_GENERIC, KeySpace_NotificationGeneric) != REDISMODULE_OK){ | ||
| 372 | return REDISMODULE_ERR; | ||
| 373 | } | ||
| 374 | |||
| 375 | if(RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_EXPIRED, KeySpace_NotificationExpired) != REDISMODULE_OK){ | ||
| 376 | return REDISMODULE_ERR; | ||
| 377 | } | ||
| 378 | |||
| 379 | if(RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_MODULE, KeySpace_NotificationModule) != REDISMODULE_OK){ | ||
| 380 | return REDISMODULE_ERR; | ||
| 381 | } | ||
| 382 | |||
| 383 | if(RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_KEY_MISS, KeySpace_NotificationModuleKeyMiss) != REDISMODULE_OK){ | ||
| 384 | return REDISMODULE_ERR; | ||
| 385 | } | ||
| 386 | |||
| 387 | if(RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_STRING, KeySpace_NotificationModuleString) != REDISMODULE_OK){ | ||
| 388 | return REDISMODULE_ERR; | ||
| 389 | } | ||
| 390 | |||
| 391 | if(RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_STRING, KeySpace_NotificationModuleStringPostNotificationJob) != REDISMODULE_OK){ | ||
| 392 | return REDISMODULE_ERR; | ||
| 393 | } | ||
| 394 | |||
| 395 | if (RedisModule_CreateCommand(ctx,"keyspace.notify", cmdNotify,"",0,0,0) == REDISMODULE_ERR){ | ||
| 396 | return REDISMODULE_ERR; | ||
| 397 | } | ||
| 398 | |||
| 399 | if (RedisModule_CreateCommand(ctx,"keyspace.is_module_key_notified", cmdIsModuleKeyNotified,"",0,0,0) == REDISMODULE_ERR){ | ||
| 400 | return REDISMODULE_ERR; | ||
| 401 | } | ||
| 402 | |||
| 403 | if (RedisModule_CreateCommand(ctx,"keyspace.is_key_loaded", cmdIsKeyLoaded,"",0,0,0) == REDISMODULE_ERR){ | ||
| 404 | return REDISMODULE_ERR; | ||
| 405 | } | ||
| 406 | |||
| 407 | if (RedisModule_CreateCommand(ctx, "keyspace.del_key_copy", cmdDelKeyCopy, | ||
| 408 | "write", 0, 0, 0) == REDISMODULE_ERR){ | ||
| 409 | return REDISMODULE_ERR; | ||
| 410 | } | ||
| 411 | |||
| 412 | if (RedisModule_CreateCommand(ctx, "keyspace.incr_case1", cmdIncrCase1, | ||
| 413 | "write", 0, 0, 0) == REDISMODULE_ERR){ | ||
| 414 | return REDISMODULE_ERR; | ||
| 415 | } | ||
| 416 | |||
| 417 | if (RedisModule_CreateCommand(ctx, "keyspace.incr_case2", cmdIncrCase2, | ||
| 418 | "write", 0, 0, 0) == REDISMODULE_ERR){ | ||
| 419 | return REDISMODULE_ERR; | ||
| 420 | } | ||
| 421 | |||
| 422 | if (RedisModule_CreateCommand(ctx, "keyspace.incr_case3", cmdIncrCase3, | ||
| 423 | "write", 0, 0, 0) == REDISMODULE_ERR){ | ||
| 424 | return REDISMODULE_ERR; | ||
| 425 | } | ||
| 426 | |||
| 427 | if (RedisModule_CreateCommand(ctx, "keyspace.incr_dels", cmdIncrDels, | ||
| 428 | "write", 0, 0, 0) == REDISMODULE_ERR){ | ||
| 429 | return REDISMODULE_ERR; | ||
| 430 | } | ||
| 431 | |||
| 432 | if (RedisModule_CreateCommand(ctx, "keyspace.get_dels", cmdGetDels, | ||
| 433 | "readonly", 0, 0, 0) == REDISMODULE_ERR){ | ||
| 434 | return REDISMODULE_ERR; | ||
| 435 | } | ||
| 436 | |||
| 437 | if (RedisModule_CreateCommand(ctx, "keyspace.unsubscribe", CmdUnsub, "write", 0, 0, 0) == REDISMODULE_ERR){ | ||
| 438 | return REDISMODULE_ERR; | ||
| 439 | } | ||
| 440 | |||
| 441 | if (RedisModule_CreateCommand(ctx, "keyspace.callback_count", GetCallbackCountCommand, "", 0, 0, 0)== REDISMODULE_ERR){ | ||
| 442 | return REDISMODULE_ERR; | ||
| 443 | } | ||
| 444 | |||
| 445 | if (argc == 1) { | ||
| 446 | const char *ptr = RedisModule_StringPtrLen(argv[0], NULL); | ||
| 447 | if (!strcasecmp(ptr, "noload")) { | ||
| 448 | /* This is a hint that we return ERR at the last moment of OnLoad. */ | ||
| 449 | RedisModule_FreeDict(ctx, loaded_event_log); | ||
| 450 | RedisModule_FreeDict(ctx, module_event_log); | ||
| 451 | return REDISMODULE_ERR; | ||
| 452 | } | ||
| 453 | } | ||
| 454 | |||
| 455 | return REDISMODULE_OK; | ||
| 456 | } | ||
| 457 | |||
| 458 | int RedisModule_OnUnload(RedisModuleCtx *ctx) { | ||
| 459 | RedisModuleDictIter *iter = RedisModule_DictIteratorStartC(loaded_event_log, "^", NULL, 0); | ||
| 460 | char* key; | ||
| 461 | size_t keyLen; | ||
| 462 | RedisModuleString* val; | ||
| 463 | while((key = RedisModule_DictNextC(iter, &keyLen, (void**)&val))){ | ||
| 464 | RedisModule_FreeString(ctx, val); | ||
| 465 | } | ||
| 466 | RedisModule_FreeDict(ctx, loaded_event_log); | ||
| 467 | RedisModule_DictIteratorStop(iter); | ||
| 468 | loaded_event_log = NULL; | ||
| 469 | |||
| 470 | iter = RedisModule_DictIteratorStartC(module_event_log, "^", NULL, 0); | ||
| 471 | while((key = RedisModule_DictNextC(iter, &keyLen, (void**)&val))){ | ||
| 472 | RedisModule_FreeString(ctx, val); | ||
| 473 | } | ||
| 474 | RedisModule_FreeDict(ctx, module_event_log); | ||
| 475 | RedisModule_DictIteratorStop(iter); | ||
| 476 | module_event_log = NULL; | ||
| 477 | |||
| 478 | return REDISMODULE_OK; | ||
| 479 | } | ||
diff --git a/examples/redis-unstable/tests/modules/keyspecs.c b/examples/redis-unstable/tests/modules/keyspecs.c new file mode 100644 index 0000000..0a70de8 --- /dev/null +++ b/examples/redis-unstable/tests/modules/keyspecs.c | |||
| @@ -0,0 +1,236 @@ | |||
| 1 | #include "redismodule.h" | ||
| 2 | |||
| 3 | #define UNUSED(V) ((void) V) | ||
| 4 | |||
| 5 | /* This function implements all commands in this module. All we care about is | ||
| 6 | * the COMMAND metadata anyway. */ | ||
| 7 | int kspec_impl(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 8 | UNUSED(argv); | ||
| 9 | UNUSED(argc); | ||
| 10 | |||
| 11 | /* Handle getkeys-api introspection (for "kspec.nonewithgetkeys") */ | ||
| 12 | if (RedisModule_IsKeysPositionRequest(ctx)) { | ||
| 13 | for (int i = 1; i < argc; i += 2) | ||
| 14 | RedisModule_KeyAtPosWithFlags(ctx, i, REDISMODULE_CMD_KEY_RO | REDISMODULE_CMD_KEY_ACCESS); | ||
| 15 | |||
| 16 | return REDISMODULE_OK; | ||
| 17 | } | ||
| 18 | |||
| 19 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 20 | return REDISMODULE_OK; | ||
| 21 | } | ||
| 22 | |||
| 23 | int createKspecNone(RedisModuleCtx *ctx) { | ||
| 24 | /* A command without keyspecs; only the legacy (first,last,step) triple (MSET like spec). */ | ||
| 25 | if (RedisModule_CreateCommand(ctx,"kspec.none",kspec_impl,"",1,-1,2) == REDISMODULE_ERR) | ||
| 26 | return REDISMODULE_ERR; | ||
| 27 | return REDISMODULE_OK; | ||
| 28 | } | ||
| 29 | |||
| 30 | int createKspecNoneWithGetkeys(RedisModuleCtx *ctx) { | ||
| 31 | /* A command without keyspecs; only the legacy (first,last,step) triple (MSET like spec), but also has a getkeys callback */ | ||
| 32 | if (RedisModule_CreateCommand(ctx,"kspec.nonewithgetkeys",kspec_impl,"getkeys-api",1,-1,2) == REDISMODULE_ERR) | ||
| 33 | return REDISMODULE_ERR; | ||
| 34 | return REDISMODULE_OK; | ||
| 35 | } | ||
| 36 | |||
| 37 | int createKspecTwoRanges(RedisModuleCtx *ctx) { | ||
| 38 | /* Test that two position/range-based key specs are combined to produce the | ||
| 39 | * legacy (first,last,step) values representing both keys. */ | ||
| 40 | if (RedisModule_CreateCommand(ctx,"kspec.tworanges",kspec_impl,"",0,0,0) == REDISMODULE_ERR) | ||
| 41 | return REDISMODULE_ERR; | ||
| 42 | |||
| 43 | RedisModuleCommand *command = RedisModule_GetCommand(ctx,"kspec.tworanges"); | ||
| 44 | RedisModuleCommandInfo info = { | ||
| 45 | .version = REDISMODULE_COMMAND_INFO_VERSION, | ||
| 46 | .arity = -2, | ||
| 47 | .key_specs = (RedisModuleCommandKeySpec[]){ | ||
| 48 | { | ||
| 49 | .flags = REDISMODULE_CMD_KEY_RO | REDISMODULE_CMD_KEY_ACCESS, | ||
| 50 | .begin_search_type = REDISMODULE_KSPEC_BS_INDEX, | ||
| 51 | .bs.index.pos = 1, | ||
| 52 | .find_keys_type = REDISMODULE_KSPEC_FK_RANGE, | ||
| 53 | .fk.range = {0,1,0} | ||
| 54 | }, | ||
| 55 | { | ||
| 56 | .flags = REDISMODULE_CMD_KEY_RW | REDISMODULE_CMD_KEY_UPDATE, | ||
| 57 | .begin_search_type = REDISMODULE_KSPEC_BS_INDEX, | ||
| 58 | .bs.index.pos = 2, | ||
| 59 | /* Omitted find_keys_type is shorthand for RANGE {0,1,0} */ | ||
| 60 | }, | ||
| 61 | {0} | ||
| 62 | } | ||
| 63 | }; | ||
| 64 | if (RedisModule_SetCommandInfo(command, &info) == REDISMODULE_ERR) | ||
| 65 | return REDISMODULE_ERR; | ||
| 66 | |||
| 67 | return REDISMODULE_OK; | ||
| 68 | } | ||
| 69 | |||
| 70 | int createKspecTwoRangesWithGap(RedisModuleCtx *ctx) { | ||
| 71 | /* Test that two position/range-based key specs are combined to produce the | ||
| 72 | * legacy (first,last,step) values representing just one key. */ | ||
| 73 | if (RedisModule_CreateCommand(ctx,"kspec.tworangeswithgap",kspec_impl,"",0,0,0) == REDISMODULE_ERR) | ||
| 74 | return REDISMODULE_ERR; | ||
| 75 | |||
| 76 | RedisModuleCommand *command = RedisModule_GetCommand(ctx,"kspec.tworangeswithgap"); | ||
| 77 | RedisModuleCommandInfo info = { | ||
| 78 | .version = REDISMODULE_COMMAND_INFO_VERSION, | ||
| 79 | .arity = -2, | ||
| 80 | .key_specs = (RedisModuleCommandKeySpec[]){ | ||
| 81 | { | ||
| 82 | .flags = REDISMODULE_CMD_KEY_RO | REDISMODULE_CMD_KEY_ACCESS, | ||
| 83 | .begin_search_type = REDISMODULE_KSPEC_BS_INDEX, | ||
| 84 | .bs.index.pos = 1, | ||
| 85 | .find_keys_type = REDISMODULE_KSPEC_FK_RANGE, | ||
| 86 | .fk.range = {0,1,0} | ||
| 87 | }, | ||
| 88 | { | ||
| 89 | .flags = REDISMODULE_CMD_KEY_RW | REDISMODULE_CMD_KEY_UPDATE, | ||
| 90 | .begin_search_type = REDISMODULE_KSPEC_BS_INDEX, | ||
| 91 | .bs.index.pos = 3, | ||
| 92 | /* Omitted find_keys_type is shorthand for RANGE {0,1,0} */ | ||
| 93 | }, | ||
| 94 | {0} | ||
| 95 | } | ||
| 96 | }; | ||
| 97 | if (RedisModule_SetCommandInfo(command, &info) == REDISMODULE_ERR) | ||
| 98 | return REDISMODULE_ERR; | ||
| 99 | |||
| 100 | return REDISMODULE_OK; | ||
| 101 | } | ||
| 102 | |||
| 103 | int createKspecKeyword(RedisModuleCtx *ctx) { | ||
| 104 | /* Only keyword-based specs. The legacy triple is wiped and set to (0,0,0). */ | ||
| 105 | if (RedisModule_CreateCommand(ctx,"kspec.keyword",kspec_impl,"",3,-1,1) == REDISMODULE_ERR) | ||
| 106 | return REDISMODULE_ERR; | ||
| 107 | |||
| 108 | RedisModuleCommand *command = RedisModule_GetCommand(ctx,"kspec.keyword"); | ||
| 109 | RedisModuleCommandInfo info = { | ||
| 110 | .version = REDISMODULE_COMMAND_INFO_VERSION, | ||
| 111 | .key_specs = (RedisModuleCommandKeySpec[]){ | ||
| 112 | { | ||
| 113 | .flags = REDISMODULE_CMD_KEY_RO | REDISMODULE_CMD_KEY_ACCESS, | ||
| 114 | .begin_search_type = REDISMODULE_KSPEC_BS_KEYWORD, | ||
| 115 | .bs.keyword.keyword = "KEYS", | ||
| 116 | .bs.keyword.startfrom = 1, | ||
| 117 | .find_keys_type = REDISMODULE_KSPEC_FK_RANGE, | ||
| 118 | .fk.range = {-1,1,0} | ||
| 119 | }, | ||
| 120 | {0} | ||
| 121 | } | ||
| 122 | }; | ||
| 123 | if (RedisModule_SetCommandInfo(command, &info) == REDISMODULE_ERR) | ||
| 124 | return REDISMODULE_ERR; | ||
| 125 | |||
| 126 | return REDISMODULE_OK; | ||
| 127 | } | ||
| 128 | |||
| 129 | int createKspecComplex1(RedisModuleCtx *ctx) { | ||
| 130 | /* First is a range a single key. The rest are keyword-based specs. */ | ||
| 131 | if (RedisModule_CreateCommand(ctx,"kspec.complex1",kspec_impl,"",1,1,1) == REDISMODULE_ERR) | ||
| 132 | return REDISMODULE_ERR; | ||
| 133 | |||
| 134 | RedisModuleCommand *command = RedisModule_GetCommand(ctx,"kspec.complex1"); | ||
| 135 | RedisModuleCommandInfo info = { | ||
| 136 | .version = REDISMODULE_COMMAND_INFO_VERSION, | ||
| 137 | .key_specs = (RedisModuleCommandKeySpec[]){ | ||
| 138 | { | ||
| 139 | .flags = REDISMODULE_CMD_KEY_RO, | ||
| 140 | .begin_search_type = REDISMODULE_KSPEC_BS_INDEX, | ||
| 141 | .bs.index.pos = 1, | ||
| 142 | }, | ||
| 143 | { | ||
| 144 | .flags = REDISMODULE_CMD_KEY_RW | REDISMODULE_CMD_KEY_UPDATE, | ||
| 145 | .begin_search_type = REDISMODULE_KSPEC_BS_KEYWORD, | ||
| 146 | .bs.keyword.keyword = "STORE", | ||
| 147 | .bs.keyword.startfrom = 2, | ||
| 148 | }, | ||
| 149 | { | ||
| 150 | .flags = REDISMODULE_CMD_KEY_RO | REDISMODULE_CMD_KEY_ACCESS, | ||
| 151 | .begin_search_type = REDISMODULE_KSPEC_BS_KEYWORD, | ||
| 152 | .bs.keyword.keyword = "KEYS", | ||
| 153 | .bs.keyword.startfrom = 2, | ||
| 154 | .find_keys_type = REDISMODULE_KSPEC_FK_KEYNUM, | ||
| 155 | .fk.keynum = {0,1,1} | ||
| 156 | }, | ||
| 157 | {0} | ||
| 158 | } | ||
| 159 | }; | ||
| 160 | if (RedisModule_SetCommandInfo(command, &info) == REDISMODULE_ERR) | ||
| 161 | return REDISMODULE_ERR; | ||
| 162 | |||
| 163 | return REDISMODULE_OK; | ||
| 164 | } | ||
| 165 | |||
| 166 | int createKspecComplex2(RedisModuleCtx *ctx) { | ||
| 167 | /* First is not legacy, more than STATIC_KEYS_SPECS_NUM specs */ | ||
| 168 | if (RedisModule_CreateCommand(ctx,"kspec.complex2",kspec_impl,"",0,0,0) == REDISMODULE_ERR) | ||
| 169 | return REDISMODULE_ERR; | ||
| 170 | |||
| 171 | RedisModuleCommand *command = RedisModule_GetCommand(ctx,"kspec.complex2"); | ||
| 172 | RedisModuleCommandInfo info = { | ||
| 173 | .version = REDISMODULE_COMMAND_INFO_VERSION, | ||
| 174 | .key_specs = (RedisModuleCommandKeySpec[]){ | ||
| 175 | { | ||
| 176 | .flags = REDISMODULE_CMD_KEY_RW | REDISMODULE_CMD_KEY_UPDATE, | ||
| 177 | .begin_search_type = REDISMODULE_KSPEC_BS_KEYWORD, | ||
| 178 | .bs.keyword.keyword = "STORE", | ||
| 179 | .bs.keyword.startfrom = 5, | ||
| 180 | .find_keys_type = REDISMODULE_KSPEC_FK_RANGE, | ||
| 181 | .fk.range = {0,1,0} | ||
| 182 | }, | ||
| 183 | { | ||
| 184 | .flags = REDISMODULE_CMD_KEY_RO | REDISMODULE_CMD_KEY_ACCESS, | ||
| 185 | .begin_search_type = REDISMODULE_KSPEC_BS_INDEX, | ||
| 186 | .bs.index.pos = 1, | ||
| 187 | .find_keys_type = REDISMODULE_KSPEC_FK_RANGE, | ||
| 188 | .fk.range = {0,1,0} | ||
| 189 | }, | ||
| 190 | { | ||
| 191 | .flags = REDISMODULE_CMD_KEY_RO | REDISMODULE_CMD_KEY_ACCESS, | ||
| 192 | .begin_search_type = REDISMODULE_KSPEC_BS_INDEX, | ||
| 193 | .bs.index.pos = 2, | ||
| 194 | .find_keys_type = REDISMODULE_KSPEC_FK_RANGE, | ||
| 195 | .fk.range = {0,1,0} | ||
| 196 | }, | ||
| 197 | { | ||
| 198 | .flags = REDISMODULE_CMD_KEY_RW | REDISMODULE_CMD_KEY_UPDATE, | ||
| 199 | .begin_search_type = REDISMODULE_KSPEC_BS_INDEX, | ||
| 200 | .bs.index.pos = 3, | ||
| 201 | .find_keys_type = REDISMODULE_KSPEC_FK_KEYNUM, | ||
| 202 | .fk.keynum = {0,1,1} | ||
| 203 | }, | ||
| 204 | { | ||
| 205 | .flags = REDISMODULE_CMD_KEY_RW | REDISMODULE_CMD_KEY_UPDATE, | ||
| 206 | .begin_search_type = REDISMODULE_KSPEC_BS_KEYWORD, | ||
| 207 | .bs.keyword.keyword = "MOREKEYS", | ||
| 208 | .bs.keyword.startfrom = 5, | ||
| 209 | .find_keys_type = REDISMODULE_KSPEC_FK_RANGE, | ||
| 210 | .fk.range = {-1,1,0} | ||
| 211 | }, | ||
| 212 | {0} | ||
| 213 | } | ||
| 214 | }; | ||
| 215 | if (RedisModule_SetCommandInfo(command, &info) == REDISMODULE_ERR) | ||
| 216 | return REDISMODULE_ERR; | ||
| 217 | |||
| 218 | return REDISMODULE_OK; | ||
| 219 | } | ||
| 220 | |||
| 221 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 222 | REDISMODULE_NOT_USED(argv); | ||
| 223 | REDISMODULE_NOT_USED(argc); | ||
| 224 | |||
| 225 | if (RedisModule_Init(ctx, "keyspecs", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) | ||
| 226 | return REDISMODULE_ERR; | ||
| 227 | |||
| 228 | if (createKspecNone(ctx) == REDISMODULE_ERR) return REDISMODULE_ERR; | ||
| 229 | if (createKspecNoneWithGetkeys(ctx) == REDISMODULE_ERR) return REDISMODULE_ERR; | ||
| 230 | if (createKspecTwoRanges(ctx) == REDISMODULE_ERR) return REDISMODULE_ERR; | ||
| 231 | if (createKspecTwoRangesWithGap(ctx) == REDISMODULE_ERR) return REDISMODULE_ERR; | ||
| 232 | if (createKspecKeyword(ctx) == REDISMODULE_ERR) return REDISMODULE_ERR; | ||
| 233 | if (createKspecComplex1(ctx) == REDISMODULE_ERR) return REDISMODULE_ERR; | ||
| 234 | if (createKspecComplex2(ctx) == REDISMODULE_ERR) return REDISMODULE_ERR; | ||
| 235 | return REDISMODULE_OK; | ||
| 236 | } | ||
diff --git a/examples/redis-unstable/tests/modules/list.c b/examples/redis-unstable/tests/modules/list.c new file mode 100644 index 0000000..401b2d8 --- /dev/null +++ b/examples/redis-unstable/tests/modules/list.c | |||
| @@ -0,0 +1,252 @@ | |||
| 1 | #include "redismodule.h" | ||
| 2 | #include <assert.h> | ||
| 3 | #include <errno.h> | ||
| 4 | #include <strings.h> | ||
| 5 | |||
| 6 | /* LIST.GETALL key [REVERSE] */ | ||
| 7 | int list_getall(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 8 | if (argc < 2 || argc > 3) return RedisModule_WrongArity(ctx); | ||
| 9 | int reverse = (argc == 3 && | ||
| 10 | !strcasecmp(RedisModule_StringPtrLen(argv[2], NULL), | ||
| 11 | "REVERSE")); | ||
| 12 | RedisModule_AutoMemory(ctx); | ||
| 13 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ); | ||
| 14 | if (RedisModule_KeyType(key) != REDISMODULE_KEYTYPE_LIST) { | ||
| 15 | return RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE); | ||
| 16 | } | ||
| 17 | long n = RedisModule_ValueLength(key); | ||
| 18 | RedisModule_ReplyWithArray(ctx, n); | ||
| 19 | if (!reverse) { | ||
| 20 | for (long i = 0; i < n; i++) { | ||
| 21 | RedisModuleString *elem = RedisModule_ListGet(key, i); | ||
| 22 | RedisModule_ReplyWithString(ctx, elem); | ||
| 23 | RedisModule_FreeString(ctx, elem); | ||
| 24 | } | ||
| 25 | } else { | ||
| 26 | for (long i = -1; i >= -n; i--) { | ||
| 27 | RedisModuleString *elem = RedisModule_ListGet(key, i); | ||
| 28 | RedisModule_ReplyWithString(ctx, elem); | ||
| 29 | RedisModule_FreeString(ctx, elem); | ||
| 30 | } | ||
| 31 | } | ||
| 32 | |||
| 33 | /* Test error condition: index out of bounds */ | ||
| 34 | assert(RedisModule_ListGet(key, n) == NULL); | ||
| 35 | assert(errno == EDOM); /* no more elements in list */ | ||
| 36 | |||
| 37 | /* RedisModule_CloseKey(key); //implicit, done by auto memory */ | ||
| 38 | return REDISMODULE_OK; | ||
| 39 | } | ||
| 40 | |||
| 41 | /* LIST.EDIT key [REVERSE] cmdstr [value ..] | ||
| 42 | * | ||
| 43 | * cmdstr is a string of the following characters: | ||
| 44 | * | ||
| 45 | * k -- keep | ||
| 46 | * d -- delete | ||
| 47 | * i -- insert value from args | ||
| 48 | * r -- replace with value from args | ||
| 49 | * | ||
| 50 | * The number of occurrences of "i" and "r" in cmdstr) should correspond to the | ||
| 51 | * number of args after cmdstr. | ||
| 52 | * | ||
| 53 | * Reply with a RESP3 Map, containing the number of edits (inserts, replaces, deletes) | ||
| 54 | * performed, as well as the last index and the entry it points to. | ||
| 55 | */ | ||
| 56 | int list_edit(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 57 | if (argc < 3) return RedisModule_WrongArity(ctx); | ||
| 58 | RedisModule_AutoMemory(ctx); | ||
| 59 | int argpos = 1; /* the next arg */ | ||
| 60 | |||
| 61 | /* key */ | ||
| 62 | int keymode = REDISMODULE_READ | REDISMODULE_WRITE; | ||
| 63 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[argpos++], keymode); | ||
| 64 | if (RedisModule_KeyType(key) != REDISMODULE_KEYTYPE_LIST) { | ||
| 65 | return RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE); | ||
| 66 | } | ||
| 67 | |||
| 68 | /* REVERSE */ | ||
| 69 | int reverse = 0; | ||
| 70 | if (argc >= 4 && | ||
| 71 | !strcasecmp(RedisModule_StringPtrLen(argv[argpos], NULL), "REVERSE")) { | ||
| 72 | reverse = 1; | ||
| 73 | argpos++; | ||
| 74 | } | ||
| 75 | |||
| 76 | /* cmdstr */ | ||
| 77 | size_t cmdstr_len; | ||
| 78 | const char *cmdstr = RedisModule_StringPtrLen(argv[argpos++], &cmdstr_len); | ||
| 79 | |||
| 80 | /* validate cmdstr vs. argc */ | ||
| 81 | long num_req_args = 0; | ||
| 82 | long min_list_length = 0; | ||
| 83 | for (size_t cmdpos = 0; cmdpos < cmdstr_len; cmdpos++) { | ||
| 84 | char c = cmdstr[cmdpos]; | ||
| 85 | if (c == 'i' || c == 'r') num_req_args++; | ||
| 86 | if (c == 'd' || c == 'r' || c == 'k') min_list_length++; | ||
| 87 | } | ||
| 88 | if (argc < argpos + num_req_args) { | ||
| 89 | return RedisModule_ReplyWithError(ctx, "ERR too few args"); | ||
| 90 | } | ||
| 91 | if ((long)RedisModule_ValueLength(key) < min_list_length) { | ||
| 92 | return RedisModule_ReplyWithError(ctx, "ERR list too short"); | ||
| 93 | } | ||
| 94 | |||
| 95 | /* Iterate over the chars in cmdstr (edit instructions) */ | ||
| 96 | long long num_inserts = 0, num_deletes = 0, num_replaces = 0; | ||
| 97 | long index = reverse ? -1 : 0; | ||
| 98 | RedisModuleString *value; | ||
| 99 | |||
| 100 | for (size_t cmdpos = 0; cmdpos < cmdstr_len; cmdpos++) { | ||
| 101 | switch (cmdstr[cmdpos]) { | ||
| 102 | case 'i': /* insert */ | ||
| 103 | value = argv[argpos++]; | ||
| 104 | assert(RedisModule_ListInsert(key, index, value) == REDISMODULE_OK); | ||
| 105 | index += reverse ? -1 : 1; | ||
| 106 | num_inserts++; | ||
| 107 | break; | ||
| 108 | case 'd': /* delete */ | ||
| 109 | assert(RedisModule_ListDelete(key, index) == REDISMODULE_OK); | ||
| 110 | num_deletes++; | ||
| 111 | break; | ||
| 112 | case 'r': /* replace */ | ||
| 113 | value = argv[argpos++]; | ||
| 114 | assert(RedisModule_ListSet(key, index, value) == REDISMODULE_OK); | ||
| 115 | index += reverse ? -1 : 1; | ||
| 116 | num_replaces++; | ||
| 117 | break; | ||
| 118 | case 'k': /* keep */ | ||
| 119 | index += reverse ? -1 : 1; | ||
| 120 | break; | ||
| 121 | } | ||
| 122 | } | ||
| 123 | |||
| 124 | RedisModuleString *v = RedisModule_ListGet(key, index); | ||
| 125 | RedisModule_ReplyWithMap(ctx, v ? 5 : 4); | ||
| 126 | RedisModule_ReplyWithCString(ctx, "i"); | ||
| 127 | RedisModule_ReplyWithLongLong(ctx, num_inserts); | ||
| 128 | RedisModule_ReplyWithCString(ctx, "d"); | ||
| 129 | RedisModule_ReplyWithLongLong(ctx, num_deletes); | ||
| 130 | RedisModule_ReplyWithCString(ctx, "r"); | ||
| 131 | RedisModule_ReplyWithLongLong(ctx, num_replaces); | ||
| 132 | RedisModule_ReplyWithCString(ctx, "index"); | ||
| 133 | RedisModule_ReplyWithLongLong(ctx, index); | ||
| 134 | if (v) { | ||
| 135 | RedisModule_ReplyWithCString(ctx, "entry"); | ||
| 136 | RedisModule_ReplyWithString(ctx, v); | ||
| 137 | RedisModule_FreeString(ctx, v); | ||
| 138 | } | ||
| 139 | |||
| 140 | RedisModule_CloseKey(key); | ||
| 141 | return REDISMODULE_OK; | ||
| 142 | } | ||
| 143 | |||
| 144 | /* Reply based on errno as set by the List API functions. */ | ||
| 145 | static int replyByErrno(RedisModuleCtx *ctx) { | ||
| 146 | switch (errno) { | ||
| 147 | case EDOM: | ||
| 148 | return RedisModule_ReplyWithError(ctx, "ERR index out of bounds"); | ||
| 149 | case ENOTSUP: | ||
| 150 | return RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE); | ||
| 151 | default: assert(0); /* Can't happen */ | ||
| 152 | } | ||
| 153 | } | ||
| 154 | |||
| 155 | /* LIST.GET key index */ | ||
| 156 | int list_get(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 157 | if (argc != 3) return RedisModule_WrongArity(ctx); | ||
| 158 | long long index; | ||
| 159 | if (RedisModule_StringToLongLong(argv[2], &index) != REDISMODULE_OK) { | ||
| 160 | return RedisModule_ReplyWithError(ctx, "ERR index must be a number"); | ||
| 161 | } | ||
| 162 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ); | ||
| 163 | RedisModuleString *value = RedisModule_ListGet(key, index); | ||
| 164 | if (value) { | ||
| 165 | RedisModule_ReplyWithString(ctx, value); | ||
| 166 | RedisModule_FreeString(ctx, value); | ||
| 167 | } else { | ||
| 168 | replyByErrno(ctx); | ||
| 169 | } | ||
| 170 | RedisModule_CloseKey(key); | ||
| 171 | return REDISMODULE_OK; | ||
| 172 | } | ||
| 173 | |||
| 174 | /* LIST.SET key index value */ | ||
| 175 | int list_set(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 176 | if (argc != 4) return RedisModule_WrongArity(ctx); | ||
| 177 | long long index; | ||
| 178 | if (RedisModule_StringToLongLong(argv[2], &index) != REDISMODULE_OK) { | ||
| 179 | RedisModule_ReplyWithError(ctx, "ERR index must be a number"); | ||
| 180 | return REDISMODULE_OK; | ||
| 181 | } | ||
| 182 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE); | ||
| 183 | if (RedisModule_ListSet(key, index, argv[3]) == REDISMODULE_OK) { | ||
| 184 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 185 | } else { | ||
| 186 | replyByErrno(ctx); | ||
| 187 | } | ||
| 188 | RedisModule_CloseKey(key); | ||
| 189 | return REDISMODULE_OK; | ||
| 190 | } | ||
| 191 | |||
| 192 | /* LIST.INSERT key index value | ||
| 193 | * | ||
| 194 | * If index is negative, value is inserted after, otherwise before the element | ||
| 195 | * at index. | ||
| 196 | */ | ||
| 197 | int list_insert(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 198 | if (argc != 4) return RedisModule_WrongArity(ctx); | ||
| 199 | long long index; | ||
| 200 | if (RedisModule_StringToLongLong(argv[2], &index) != REDISMODULE_OK) { | ||
| 201 | RedisModule_ReplyWithError(ctx, "ERR index must be a number"); | ||
| 202 | return REDISMODULE_OK; | ||
| 203 | } | ||
| 204 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE); | ||
| 205 | if (RedisModule_ListInsert(key, index, argv[3]) == REDISMODULE_OK) { | ||
| 206 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 207 | } else { | ||
| 208 | replyByErrno(ctx); | ||
| 209 | } | ||
| 210 | RedisModule_CloseKey(key); | ||
| 211 | return REDISMODULE_OK; | ||
| 212 | } | ||
| 213 | |||
| 214 | /* LIST.DELETE key index */ | ||
| 215 | int list_delete(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 216 | if (argc != 3) return RedisModule_WrongArity(ctx); | ||
| 217 | long long index; | ||
| 218 | if (RedisModule_StringToLongLong(argv[2], &index) != REDISMODULE_OK) { | ||
| 219 | RedisModule_ReplyWithError(ctx, "ERR index must be a number"); | ||
| 220 | return REDISMODULE_OK; | ||
| 221 | } | ||
| 222 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE); | ||
| 223 | if (RedisModule_ListDelete(key, index) == REDISMODULE_OK) { | ||
| 224 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 225 | } else { | ||
| 226 | replyByErrno(ctx); | ||
| 227 | } | ||
| 228 | RedisModule_CloseKey(key); | ||
| 229 | return REDISMODULE_OK; | ||
| 230 | } | ||
| 231 | |||
| 232 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 233 | REDISMODULE_NOT_USED(argv); | ||
| 234 | REDISMODULE_NOT_USED(argc); | ||
| 235 | if (RedisModule_Init(ctx, "list", 1, REDISMODULE_APIVER_1) == REDISMODULE_OK && | ||
| 236 | RedisModule_CreateCommand(ctx, "list.getall", list_getall, "", | ||
| 237 | 1, 1, 1) == REDISMODULE_OK && | ||
| 238 | RedisModule_CreateCommand(ctx, "list.edit", list_edit, "write", | ||
| 239 | 1, 1, 1) == REDISMODULE_OK && | ||
| 240 | RedisModule_CreateCommand(ctx, "list.get", list_get, "write", | ||
| 241 | 1, 1, 1) == REDISMODULE_OK && | ||
| 242 | RedisModule_CreateCommand(ctx, "list.set", list_set, "write", | ||
| 243 | 1, 1, 1) == REDISMODULE_OK && | ||
| 244 | RedisModule_CreateCommand(ctx, "list.insert", list_insert, "write", | ||
| 245 | 1, 1, 1) == REDISMODULE_OK && | ||
| 246 | RedisModule_CreateCommand(ctx, "list.delete", list_delete, "write", | ||
| 247 | 1, 1, 1) == REDISMODULE_OK) { | ||
| 248 | return REDISMODULE_OK; | ||
| 249 | } else { | ||
| 250 | return REDISMODULE_ERR; | ||
| 251 | } | ||
| 252 | } | ||
diff --git a/examples/redis-unstable/tests/modules/mallocsize.c b/examples/redis-unstable/tests/modules/mallocsize.c new file mode 100644 index 0000000..a1d31c1 --- /dev/null +++ b/examples/redis-unstable/tests/modules/mallocsize.c | |||
| @@ -0,0 +1,237 @@ | |||
| 1 | #include "redismodule.h" | ||
| 2 | #include <string.h> | ||
| 3 | #include <assert.h> | ||
| 4 | #include <unistd.h> | ||
| 5 | |||
| 6 | #define UNUSED(V) ((void) V) | ||
| 7 | |||
| 8 | /* Registered type */ | ||
| 9 | RedisModuleType *mallocsize_type = NULL; | ||
| 10 | |||
| 11 | typedef enum { | ||
| 12 | UDT_RAW, | ||
| 13 | UDT_STRING, | ||
| 14 | UDT_DICT | ||
| 15 | } udt_type_t; | ||
| 16 | |||
| 17 | typedef struct { | ||
| 18 | void *ptr; | ||
| 19 | size_t len; | ||
| 20 | } raw_t; | ||
| 21 | |||
| 22 | typedef struct { | ||
| 23 | udt_type_t type; | ||
| 24 | union { | ||
| 25 | raw_t raw; | ||
| 26 | RedisModuleString *str; | ||
| 27 | RedisModuleDict *dict; | ||
| 28 | } data; | ||
| 29 | } udt_t; | ||
| 30 | |||
| 31 | void udt_free(void *value) { | ||
| 32 | udt_t *udt = value; | ||
| 33 | switch (udt->type) { | ||
| 34 | case (UDT_RAW): { | ||
| 35 | RedisModule_Free(udt->data.raw.ptr); | ||
| 36 | break; | ||
| 37 | } | ||
| 38 | case (UDT_STRING): { | ||
| 39 | RedisModule_FreeString(NULL, udt->data.str); | ||
| 40 | break; | ||
| 41 | } | ||
| 42 | case (UDT_DICT): { | ||
| 43 | RedisModuleString *dk, *dv; | ||
| 44 | RedisModuleDictIter *iter = RedisModule_DictIteratorStartC(udt->data.dict, "^", NULL, 0); | ||
| 45 | while((dk = RedisModule_DictNext(NULL, iter, (void **)&dv)) != NULL) { | ||
| 46 | RedisModule_FreeString(NULL, dk); | ||
| 47 | RedisModule_FreeString(NULL, dv); | ||
| 48 | } | ||
| 49 | RedisModule_DictIteratorStop(iter); | ||
| 50 | RedisModule_FreeDict(NULL, udt->data.dict); | ||
| 51 | break; | ||
| 52 | } | ||
| 53 | } | ||
| 54 | RedisModule_Free(udt); | ||
| 55 | } | ||
| 56 | |||
| 57 | void udt_rdb_save(RedisModuleIO *rdb, void *value) { | ||
| 58 | udt_t *udt = value; | ||
| 59 | RedisModule_SaveUnsigned(rdb, udt->type); | ||
| 60 | switch (udt->type) { | ||
| 61 | case (UDT_RAW): { | ||
| 62 | RedisModule_SaveStringBuffer(rdb, udt->data.raw.ptr, udt->data.raw.len); | ||
| 63 | break; | ||
| 64 | } | ||
| 65 | case (UDT_STRING): { | ||
| 66 | RedisModule_SaveString(rdb, udt->data.str); | ||
| 67 | break; | ||
| 68 | } | ||
| 69 | case (UDT_DICT): { | ||
| 70 | RedisModule_SaveUnsigned(rdb, RedisModule_DictSize(udt->data.dict)); | ||
| 71 | RedisModuleString *dk, *dv; | ||
| 72 | RedisModuleDictIter *iter = RedisModule_DictIteratorStartC(udt->data.dict, "^", NULL, 0); | ||
| 73 | while((dk = RedisModule_DictNext(NULL, iter, (void **)&dv)) != NULL) { | ||
| 74 | RedisModule_SaveString(rdb, dk); | ||
| 75 | RedisModule_SaveString(rdb, dv); | ||
| 76 | RedisModule_FreeString(NULL, dk); /* Allocated by RedisModule_DictNext */ | ||
| 77 | } | ||
| 78 | RedisModule_DictIteratorStop(iter); | ||
| 79 | break; | ||
| 80 | } | ||
| 81 | } | ||
| 82 | } | ||
| 83 | |||
| 84 | void *udt_rdb_load(RedisModuleIO *rdb, int encver) { | ||
| 85 | if (encver != 0) | ||
| 86 | return NULL; | ||
| 87 | udt_t *udt = RedisModule_Alloc(sizeof(*udt)); | ||
| 88 | udt->type = RedisModule_LoadUnsigned(rdb); | ||
| 89 | switch (udt->type) { | ||
| 90 | case (UDT_RAW): { | ||
| 91 | udt->data.raw.ptr = RedisModule_LoadStringBuffer(rdb, &udt->data.raw.len); | ||
| 92 | break; | ||
| 93 | } | ||
| 94 | case (UDT_STRING): { | ||
| 95 | udt->data.str = RedisModule_LoadString(rdb); | ||
| 96 | break; | ||
| 97 | } | ||
| 98 | case (UDT_DICT): { | ||
| 99 | long long dict_len = RedisModule_LoadUnsigned(rdb); | ||
| 100 | udt->data.dict = RedisModule_CreateDict(NULL); | ||
| 101 | for (int i = 0; i < dict_len; i += 2) { | ||
| 102 | RedisModuleString *key = RedisModule_LoadString(rdb); | ||
| 103 | RedisModuleString *val = RedisModule_LoadString(rdb); | ||
| 104 | RedisModule_DictSet(udt->data.dict, key, val); | ||
| 105 | } | ||
| 106 | break; | ||
| 107 | } | ||
| 108 | } | ||
| 109 | |||
| 110 | return udt; | ||
| 111 | } | ||
| 112 | |||
| 113 | size_t udt_mem_usage(RedisModuleKeyOptCtx *ctx, const void *value, size_t sample_size) { | ||
| 114 | UNUSED(ctx); | ||
| 115 | UNUSED(sample_size); | ||
| 116 | |||
| 117 | const udt_t *udt = value; | ||
| 118 | size_t size = sizeof(*udt); | ||
| 119 | |||
| 120 | switch (udt->type) { | ||
| 121 | case (UDT_RAW): { | ||
| 122 | size += RedisModule_MallocSize(udt->data.raw.ptr); | ||
| 123 | break; | ||
| 124 | } | ||
| 125 | case (UDT_STRING): { | ||
| 126 | size += RedisModule_MallocSizeString(udt->data.str); | ||
| 127 | break; | ||
| 128 | } | ||
| 129 | case (UDT_DICT): { | ||
| 130 | void *dk; | ||
| 131 | size_t keylen; | ||
| 132 | RedisModuleString *dv; | ||
| 133 | RedisModuleDictIter *iter = RedisModule_DictIteratorStartC(udt->data.dict, "^", NULL, 0); | ||
| 134 | while((dk = RedisModule_DictNextC(iter, &keylen, (void **)&dv)) != NULL) { | ||
| 135 | size += keylen; | ||
| 136 | size += RedisModule_MallocSizeString(dv); | ||
| 137 | } | ||
| 138 | RedisModule_DictIteratorStop(iter); | ||
| 139 | break; | ||
| 140 | } | ||
| 141 | } | ||
| 142 | |||
| 143 | return size; | ||
| 144 | } | ||
| 145 | |||
| 146 | /* MALLOCSIZE.SETRAW key len */ | ||
| 147 | int cmd_setraw(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 148 | if (argc != 3) | ||
| 149 | return RedisModule_WrongArity(ctx); | ||
| 150 | |||
| 151 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE); | ||
| 152 | |||
| 153 | udt_t *udt = RedisModule_Alloc(sizeof(*udt)); | ||
| 154 | udt->type = UDT_RAW; | ||
| 155 | |||
| 156 | long long raw_len; | ||
| 157 | RedisModule_StringToLongLong(argv[2], &raw_len); | ||
| 158 | udt->data.raw.ptr = RedisModule_Alloc(raw_len); | ||
| 159 | udt->data.raw.len = raw_len; | ||
| 160 | |||
| 161 | RedisModule_ModuleTypeSetValue(key, mallocsize_type, udt); | ||
| 162 | RedisModule_CloseKey(key); | ||
| 163 | |||
| 164 | return RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 165 | } | ||
| 166 | |||
| 167 | /* MALLOCSIZE.SETSTR key string */ | ||
| 168 | int cmd_setstr(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 169 | if (argc != 3) | ||
| 170 | return RedisModule_WrongArity(ctx); | ||
| 171 | |||
| 172 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE); | ||
| 173 | |||
| 174 | udt_t *udt = RedisModule_Alloc(sizeof(*udt)); | ||
| 175 | udt->type = UDT_STRING; | ||
| 176 | |||
| 177 | udt->data.str = argv[2]; | ||
| 178 | RedisModule_RetainString(ctx, argv[2]); | ||
| 179 | |||
| 180 | RedisModule_ModuleTypeSetValue(key, mallocsize_type, udt); | ||
| 181 | RedisModule_CloseKey(key); | ||
| 182 | |||
| 183 | return RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 184 | } | ||
| 185 | |||
| 186 | /* MALLOCSIZE.SETDICT key field value [field value ...] */ | ||
| 187 | int cmd_setdict(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 188 | if (argc < 4 || argc % 2) | ||
| 189 | return RedisModule_WrongArity(ctx); | ||
| 190 | |||
| 191 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE); | ||
| 192 | |||
| 193 | udt_t *udt = RedisModule_Alloc(sizeof(*udt)); | ||
| 194 | udt->type = UDT_DICT; | ||
| 195 | |||
| 196 | udt->data.dict = RedisModule_CreateDict(ctx); | ||
| 197 | for (int i = 2; i < argc; i += 2) { | ||
| 198 | RedisModule_DictSet(udt->data.dict, argv[i], argv[i+1]); | ||
| 199 | /* No need to retain argv[i], it is copied as the rax key */ | ||
| 200 | RedisModule_RetainString(ctx, argv[i+1]); | ||
| 201 | } | ||
| 202 | |||
| 203 | RedisModule_ModuleTypeSetValue(key, mallocsize_type, udt); | ||
| 204 | RedisModule_CloseKey(key); | ||
| 205 | |||
| 206 | return RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 207 | } | ||
| 208 | |||
| 209 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 210 | UNUSED(argv); | ||
| 211 | UNUSED(argc); | ||
| 212 | if (RedisModule_Init(ctx,"mallocsize",1,REDISMODULE_APIVER_1)== REDISMODULE_ERR) | ||
| 213 | return REDISMODULE_ERR; | ||
| 214 | |||
| 215 | RedisModuleTypeMethods tm = { | ||
| 216 | .version = REDISMODULE_TYPE_METHOD_VERSION, | ||
| 217 | .rdb_load = udt_rdb_load, | ||
| 218 | .rdb_save = udt_rdb_save, | ||
| 219 | .free = udt_free, | ||
| 220 | .mem_usage2 = udt_mem_usage, | ||
| 221 | }; | ||
| 222 | |||
| 223 | mallocsize_type = RedisModule_CreateDataType(ctx, "allocsize", 0, &tm); | ||
| 224 | if (mallocsize_type == NULL) | ||
| 225 | return REDISMODULE_ERR; | ||
| 226 | |||
| 227 | if (RedisModule_CreateCommand(ctx, "mallocsize.setraw", cmd_setraw, "", 1, 1, 1) == REDISMODULE_ERR) | ||
| 228 | return REDISMODULE_ERR; | ||
| 229 | |||
| 230 | if (RedisModule_CreateCommand(ctx, "mallocsize.setstr", cmd_setstr, "", 1, 1, 1) == REDISMODULE_ERR) | ||
| 231 | return REDISMODULE_ERR; | ||
| 232 | |||
| 233 | if (RedisModule_CreateCommand(ctx, "mallocsize.setdict", cmd_setdict, "", 1, 1, 1) == REDISMODULE_ERR) | ||
| 234 | return REDISMODULE_ERR; | ||
| 235 | |||
| 236 | return REDISMODULE_OK; | ||
| 237 | } | ||
diff --git a/examples/redis-unstable/tests/modules/misc.c b/examples/redis-unstable/tests/modules/misc.c new file mode 100644 index 0000000..dbf0fec --- /dev/null +++ b/examples/redis-unstable/tests/modules/misc.c | |||
| @@ -0,0 +1,642 @@ | |||
| 1 | #include "redismodule.h" | ||
| 2 | |||
| 3 | #include <string.h> | ||
| 4 | #include <assert.h> | ||
| 5 | #include <unistd.h> | ||
| 6 | #include <errno.h> | ||
| 7 | #include <limits.h> | ||
| 8 | |||
| 9 | #define UNUSED(x) (void)(x) | ||
| 10 | |||
| 11 | static int n_events = 0; | ||
| 12 | |||
| 13 | static int KeySpace_NotificationModuleKeyMissExpired(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key) { | ||
| 14 | UNUSED(ctx); | ||
| 15 | UNUSED(type); | ||
| 16 | UNUSED(event); | ||
| 17 | UNUSED(key); | ||
| 18 | n_events++; | ||
| 19 | return REDISMODULE_OK; | ||
| 20 | } | ||
| 21 | |||
| 22 | int test_clear_n_events(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 23 | UNUSED(argv); | ||
| 24 | UNUSED(argc); | ||
| 25 | n_events = 0; | ||
| 26 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 27 | return REDISMODULE_OK; | ||
| 28 | } | ||
| 29 | |||
| 30 | int test_get_n_events(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 31 | UNUSED(argv); | ||
| 32 | UNUSED(argc); | ||
| 33 | RedisModule_ReplyWithLongLong(ctx, n_events); | ||
| 34 | return REDISMODULE_OK; | ||
| 35 | } | ||
| 36 | |||
| 37 | int test_open_key_no_effects(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 38 | if (argc<2) { | ||
| 39 | RedisModule_WrongArity(ctx); | ||
| 40 | return REDISMODULE_OK; | ||
| 41 | } | ||
| 42 | |||
| 43 | int supportedMode = RedisModule_GetOpenKeyModesAll(); | ||
| 44 | if (!(supportedMode & REDISMODULE_READ) || !(supportedMode & REDISMODULE_OPEN_KEY_NOEFFECTS)) { | ||
| 45 | RedisModule_ReplyWithError(ctx, "OpenKey modes are not supported"); | ||
| 46 | return REDISMODULE_OK; | ||
| 47 | } | ||
| 48 | |||
| 49 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ | REDISMODULE_OPEN_KEY_NOEFFECTS); | ||
| 50 | if (!key) { | ||
| 51 | RedisModule_ReplyWithError(ctx, "key not found"); | ||
| 52 | return REDISMODULE_OK; | ||
| 53 | } | ||
| 54 | |||
| 55 | RedisModule_CloseKey(key); | ||
| 56 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 57 | return REDISMODULE_OK; | ||
| 58 | } | ||
| 59 | |||
| 60 | int test_call_generic(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 61 | { | ||
| 62 | if (argc<2) { | ||
| 63 | RedisModule_WrongArity(ctx); | ||
| 64 | return REDISMODULE_OK; | ||
| 65 | } | ||
| 66 | |||
| 67 | const char* cmdname = RedisModule_StringPtrLen(argv[1], NULL); | ||
| 68 | RedisModuleCallReply *reply = RedisModule_Call(ctx, cmdname, "v", argv+2, (size_t)argc-2); | ||
| 69 | if (reply) { | ||
| 70 | RedisModule_ReplyWithCallReply(ctx, reply); | ||
| 71 | RedisModule_FreeCallReply(reply); | ||
| 72 | } else { | ||
| 73 | RedisModule_ReplyWithError(ctx, strerror(errno)); | ||
| 74 | } | ||
| 75 | return REDISMODULE_OK; | ||
| 76 | } | ||
| 77 | |||
| 78 | int test_call_info(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 79 | { | ||
| 80 | RedisModuleCallReply *reply; | ||
| 81 | if (argc>1) | ||
| 82 | reply = RedisModule_Call(ctx, "info", "s", argv[1]); | ||
| 83 | else | ||
| 84 | reply = RedisModule_Call(ctx, "info", ""); | ||
| 85 | if (reply) { | ||
| 86 | RedisModule_ReplyWithCallReply(ctx, reply); | ||
| 87 | RedisModule_FreeCallReply(reply); | ||
| 88 | } else { | ||
| 89 | RedisModule_ReplyWithError(ctx, strerror(errno)); | ||
| 90 | } | ||
| 91 | return REDISMODULE_OK; | ||
| 92 | } | ||
| 93 | |||
| 94 | int test_ld_conv(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 95 | UNUSED(argv); | ||
| 96 | UNUSED(argc); | ||
| 97 | long double ld = 0.00000000000000001L; | ||
| 98 | const char *ldstr = "0.00000000000000001"; | ||
| 99 | RedisModuleString *s1 = RedisModule_CreateStringFromLongDouble(ctx, ld, 1); | ||
| 100 | RedisModuleString *s2 = | ||
| 101 | RedisModule_CreateString(ctx, ldstr, strlen(ldstr)); | ||
| 102 | if (RedisModule_StringCompare(s1, s2) != 0) { | ||
| 103 | char err[4096]; | ||
| 104 | snprintf(err, 4096, | ||
| 105 | "Failed to convert long double to string ('%s' != '%s')", | ||
| 106 | RedisModule_StringPtrLen(s1, NULL), | ||
| 107 | RedisModule_StringPtrLen(s2, NULL)); | ||
| 108 | RedisModule_ReplyWithError(ctx, err); | ||
| 109 | goto final; | ||
| 110 | } | ||
| 111 | long double ld2 = 0; | ||
| 112 | if (RedisModule_StringToLongDouble(s2, &ld2) == REDISMODULE_ERR) { | ||
| 113 | RedisModule_ReplyWithError(ctx, | ||
| 114 | "Failed to convert string to long double"); | ||
| 115 | goto final; | ||
| 116 | } | ||
| 117 | if (ld2 != ld) { | ||
| 118 | char err[4096]; | ||
| 119 | snprintf(err, 4096, | ||
| 120 | "Failed to convert string to long double (%.40Lf != %.40Lf)", | ||
| 121 | ld2, | ||
| 122 | ld); | ||
| 123 | RedisModule_ReplyWithError(ctx, err); | ||
| 124 | goto final; | ||
| 125 | } | ||
| 126 | |||
| 127 | /* Make sure we can't convert a string that has \0 in it */ | ||
| 128 | char buf[4] = "123"; | ||
| 129 | buf[1] = '\0'; | ||
| 130 | RedisModuleString *s3 = RedisModule_CreateString(ctx, buf, 3); | ||
| 131 | long double ld3; | ||
| 132 | if (RedisModule_StringToLongDouble(s3, &ld3) == REDISMODULE_OK) { | ||
| 133 | RedisModule_ReplyWithError(ctx, "Invalid string successfully converted to long double"); | ||
| 134 | RedisModule_FreeString(ctx, s3); | ||
| 135 | goto final; | ||
| 136 | } | ||
| 137 | RedisModule_FreeString(ctx, s3); | ||
| 138 | |||
| 139 | RedisModule_ReplyWithLongDouble(ctx, ld2); | ||
| 140 | final: | ||
| 141 | RedisModule_FreeString(ctx, s1); | ||
| 142 | RedisModule_FreeString(ctx, s2); | ||
| 143 | return REDISMODULE_OK; | ||
| 144 | } | ||
| 145 | |||
| 146 | int test_flushall(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 147 | { | ||
| 148 | REDISMODULE_NOT_USED(argv); | ||
| 149 | REDISMODULE_NOT_USED(argc); | ||
| 150 | RedisModule_ResetDataset(1, 0); | ||
| 151 | RedisModule_ReplyWithCString(ctx, "Ok"); | ||
| 152 | return REDISMODULE_OK; | ||
| 153 | } | ||
| 154 | |||
| 155 | int test_dbsize(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 156 | { | ||
| 157 | REDISMODULE_NOT_USED(argv); | ||
| 158 | REDISMODULE_NOT_USED(argc); | ||
| 159 | long long ll = RedisModule_DbSize(ctx); | ||
| 160 | RedisModule_ReplyWithLongLong(ctx, ll); | ||
| 161 | return REDISMODULE_OK; | ||
| 162 | } | ||
| 163 | |||
| 164 | int test_randomkey(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 165 | { | ||
| 166 | REDISMODULE_NOT_USED(argv); | ||
| 167 | REDISMODULE_NOT_USED(argc); | ||
| 168 | RedisModuleString *str = RedisModule_RandomKey(ctx); | ||
| 169 | RedisModule_ReplyWithString(ctx, str); | ||
| 170 | RedisModule_FreeString(ctx, str); | ||
| 171 | return REDISMODULE_OK; | ||
| 172 | } | ||
| 173 | |||
| 174 | int test_keyexists(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 175 | if (argc < 2) return RedisModule_WrongArity(ctx); | ||
| 176 | RedisModuleString *key = argv[1]; | ||
| 177 | int exists = RedisModule_KeyExists(ctx, key); | ||
| 178 | return RedisModule_ReplyWithBool(ctx, exists); | ||
| 179 | } | ||
| 180 | |||
| 181 | RedisModuleKey *open_key_or_reply(RedisModuleCtx *ctx, RedisModuleString *keyname, int mode) { | ||
| 182 | RedisModuleKey *key = RedisModule_OpenKey(ctx, keyname, mode); | ||
| 183 | if (!key) { | ||
| 184 | RedisModule_ReplyWithError(ctx, "key not found"); | ||
| 185 | return NULL; | ||
| 186 | } | ||
| 187 | return key; | ||
| 188 | } | ||
| 189 | |||
| 190 | int test_getlru(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 191 | { | ||
| 192 | if (argc<2) { | ||
| 193 | RedisModule_WrongArity(ctx); | ||
| 194 | return REDISMODULE_OK; | ||
| 195 | } | ||
| 196 | RedisModuleKey *key = open_key_or_reply(ctx, argv[1], REDISMODULE_READ|REDISMODULE_OPEN_KEY_NOTOUCH); | ||
| 197 | mstime_t lru; | ||
| 198 | RedisModule_GetLRU(key, &lru); | ||
| 199 | RedisModule_ReplyWithLongLong(ctx, lru); | ||
| 200 | RedisModule_CloseKey(key); | ||
| 201 | return REDISMODULE_OK; | ||
| 202 | } | ||
| 203 | |||
| 204 | int test_setlru(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 205 | { | ||
| 206 | if (argc<3) { | ||
| 207 | RedisModule_WrongArity(ctx); | ||
| 208 | return REDISMODULE_OK; | ||
| 209 | } | ||
| 210 | RedisModuleKey *key = open_key_or_reply(ctx, argv[1], REDISMODULE_READ|REDISMODULE_OPEN_KEY_NOTOUCH); | ||
| 211 | mstime_t lru; | ||
| 212 | if (RedisModule_StringToLongLong(argv[2], &lru) != REDISMODULE_OK) { | ||
| 213 | RedisModule_ReplyWithError(ctx, "invalid idle time"); | ||
| 214 | return REDISMODULE_OK; | ||
| 215 | } | ||
| 216 | int was_set = RedisModule_SetLRU(key, lru)==REDISMODULE_OK; | ||
| 217 | RedisModule_ReplyWithLongLong(ctx, was_set); | ||
| 218 | RedisModule_CloseKey(key); | ||
| 219 | return REDISMODULE_OK; | ||
| 220 | } | ||
| 221 | |||
| 222 | int test_getlfu(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 223 | { | ||
| 224 | if (argc<2) { | ||
| 225 | RedisModule_WrongArity(ctx); | ||
| 226 | return REDISMODULE_OK; | ||
| 227 | } | ||
| 228 | RedisModuleKey *key = open_key_or_reply(ctx, argv[1], REDISMODULE_READ|REDISMODULE_OPEN_KEY_NOTOUCH); | ||
| 229 | mstime_t lfu; | ||
| 230 | RedisModule_GetLFU(key, &lfu); | ||
| 231 | RedisModule_ReplyWithLongLong(ctx, lfu); | ||
| 232 | RedisModule_CloseKey(key); | ||
| 233 | return REDISMODULE_OK; | ||
| 234 | } | ||
| 235 | |||
| 236 | int test_setlfu(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 237 | { | ||
| 238 | if (argc<3) { | ||
| 239 | RedisModule_WrongArity(ctx); | ||
| 240 | return REDISMODULE_OK; | ||
| 241 | } | ||
| 242 | RedisModuleKey *key = open_key_or_reply(ctx, argv[1], REDISMODULE_READ|REDISMODULE_OPEN_KEY_NOTOUCH); | ||
| 243 | mstime_t lfu; | ||
| 244 | if (RedisModule_StringToLongLong(argv[2], &lfu) != REDISMODULE_OK) { | ||
| 245 | RedisModule_ReplyWithError(ctx, "invalid freq"); | ||
| 246 | return REDISMODULE_OK; | ||
| 247 | } | ||
| 248 | int was_set = RedisModule_SetLFU(key, lfu)==REDISMODULE_OK; | ||
| 249 | RedisModule_ReplyWithLongLong(ctx, was_set); | ||
| 250 | RedisModule_CloseKey(key); | ||
| 251 | return REDISMODULE_OK; | ||
| 252 | } | ||
| 253 | |||
| 254 | int test_redisversion(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){ | ||
| 255 | (void) argv; | ||
| 256 | (void) argc; | ||
| 257 | |||
| 258 | int version = RedisModule_GetServerVersion(); | ||
| 259 | int patch = version & 0x000000ff; | ||
| 260 | int minor = (version & 0x0000ff00) >> 8; | ||
| 261 | int major = (version & 0x00ff0000) >> 16; | ||
| 262 | |||
| 263 | RedisModuleString* vStr = RedisModule_CreateStringPrintf(ctx, "%d.%d.%d", major, minor, patch); | ||
| 264 | RedisModule_ReplyWithString(ctx, vStr); | ||
| 265 | RedisModule_FreeString(ctx, vStr); | ||
| 266 | |||
| 267 | return REDISMODULE_OK; | ||
| 268 | } | ||
| 269 | |||
| 270 | int test_getclientcert(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 271 | { | ||
| 272 | (void) argv; | ||
| 273 | (void) argc; | ||
| 274 | |||
| 275 | RedisModuleString *cert = RedisModule_GetClientCertificate(ctx, | ||
| 276 | RedisModule_GetClientId(ctx)); | ||
| 277 | if (!cert) { | ||
| 278 | RedisModule_ReplyWithNull(ctx); | ||
| 279 | } else { | ||
| 280 | RedisModule_ReplyWithString(ctx, cert); | ||
| 281 | RedisModule_FreeString(ctx, cert); | ||
| 282 | } | ||
| 283 | |||
| 284 | return REDISMODULE_OK; | ||
| 285 | } | ||
| 286 | |||
| 287 | int test_clientinfo(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 288 | { | ||
| 289 | (void) argv; | ||
| 290 | (void) argc; | ||
| 291 | |||
| 292 | RedisModuleClientInfoV1 ci = REDISMODULE_CLIENTINFO_INITIALIZER_V1; | ||
| 293 | uint64_t client_id = RedisModule_GetClientId(ctx); | ||
| 294 | |||
| 295 | /* Check expected result from the V1 initializer. */ | ||
| 296 | assert(ci.version == 1); | ||
| 297 | /* Trying to populate a future version of the struct should fail. */ | ||
| 298 | ci.version = REDISMODULE_CLIENTINFO_VERSION + 1; | ||
| 299 | assert(RedisModule_GetClientInfoById(&ci, client_id) == REDISMODULE_ERR); | ||
| 300 | |||
| 301 | ci.version = 1; | ||
| 302 | if (RedisModule_GetClientInfoById(&ci, client_id) == REDISMODULE_ERR) { | ||
| 303 | RedisModule_ReplyWithError(ctx, "failed to get client info"); | ||
| 304 | return REDISMODULE_OK; | ||
| 305 | } | ||
| 306 | |||
| 307 | RedisModule_ReplyWithArray(ctx, 10); | ||
| 308 | char flags[512]; | ||
| 309 | snprintf(flags, sizeof(flags) - 1, "%s:%s:%s:%s:%s:%s", | ||
| 310 | ci.flags & REDISMODULE_CLIENTINFO_FLAG_SSL ? "ssl" : "", | ||
| 311 | ci.flags & REDISMODULE_CLIENTINFO_FLAG_PUBSUB ? "pubsub" : "", | ||
| 312 | ci.flags & REDISMODULE_CLIENTINFO_FLAG_BLOCKED ? "blocked" : "", | ||
| 313 | ci.flags & REDISMODULE_CLIENTINFO_FLAG_TRACKING ? "tracking" : "", | ||
| 314 | ci.flags & REDISMODULE_CLIENTINFO_FLAG_UNIXSOCKET ? "unixsocket" : "", | ||
| 315 | ci.flags & REDISMODULE_CLIENTINFO_FLAG_MULTI ? "multi" : ""); | ||
| 316 | |||
| 317 | RedisModule_ReplyWithCString(ctx, "flags"); | ||
| 318 | RedisModule_ReplyWithCString(ctx, flags); | ||
| 319 | RedisModule_ReplyWithCString(ctx, "id"); | ||
| 320 | RedisModule_ReplyWithLongLong(ctx, ci.id); | ||
| 321 | RedisModule_ReplyWithCString(ctx, "addr"); | ||
| 322 | RedisModule_ReplyWithCString(ctx, ci.addr); | ||
| 323 | RedisModule_ReplyWithCString(ctx, "port"); | ||
| 324 | RedisModule_ReplyWithLongLong(ctx, ci.port); | ||
| 325 | RedisModule_ReplyWithCString(ctx, "db"); | ||
| 326 | RedisModule_ReplyWithLongLong(ctx, ci.db); | ||
| 327 | |||
| 328 | return REDISMODULE_OK; | ||
| 329 | } | ||
| 330 | |||
| 331 | int test_getname(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 332 | (void)argv; | ||
| 333 | if (argc != 1) return RedisModule_WrongArity(ctx); | ||
| 334 | unsigned long long id = RedisModule_GetClientId(ctx); | ||
| 335 | RedisModuleString *name = RedisModule_GetClientNameById(ctx, id); | ||
| 336 | if (name == NULL) | ||
| 337 | return RedisModule_ReplyWithError(ctx, "-ERR No name"); | ||
| 338 | RedisModule_ReplyWithString(ctx, name); | ||
| 339 | RedisModule_FreeString(ctx, name); | ||
| 340 | return REDISMODULE_OK; | ||
| 341 | } | ||
| 342 | |||
| 343 | int test_setname(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 344 | if (argc != 2) return RedisModule_WrongArity(ctx); | ||
| 345 | unsigned long long id = RedisModule_GetClientId(ctx); | ||
| 346 | if (RedisModule_SetClientNameById(id, argv[1]) == REDISMODULE_OK) | ||
| 347 | return RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 348 | else | ||
| 349 | return RedisModule_ReplyWithError(ctx, strerror(errno)); | ||
| 350 | } | ||
| 351 | |||
| 352 | int test_log_tsctx(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 353 | { | ||
| 354 | RedisModuleCtx *tsctx = RedisModule_GetDetachedThreadSafeContext(ctx); | ||
| 355 | |||
| 356 | if (argc != 3) { | ||
| 357 | RedisModule_WrongArity(ctx); | ||
| 358 | return REDISMODULE_OK; | ||
| 359 | } | ||
| 360 | |||
| 361 | char level[50]; | ||
| 362 | size_t level_len; | ||
| 363 | const char *level_str = RedisModule_StringPtrLen(argv[1], &level_len); | ||
| 364 | snprintf(level, sizeof(level) - 1, "%.*s", (int) level_len, level_str); | ||
| 365 | |||
| 366 | size_t msg_len; | ||
| 367 | const char *msg_str = RedisModule_StringPtrLen(argv[2], &msg_len); | ||
| 368 | |||
| 369 | RedisModule_Log(tsctx, level, "%.*s", (int) msg_len, msg_str); | ||
| 370 | RedisModule_FreeThreadSafeContext(tsctx); | ||
| 371 | |||
| 372 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 373 | return REDISMODULE_OK; | ||
| 374 | } | ||
| 375 | |||
| 376 | int test_weird_cmd(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 377 | REDISMODULE_NOT_USED(argv); | ||
| 378 | REDISMODULE_NOT_USED(argc); | ||
| 379 | |||
| 380 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 381 | return REDISMODULE_OK; | ||
| 382 | } | ||
| 383 | |||
| 384 | int test_monotonic_time(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 385 | REDISMODULE_NOT_USED(argv); | ||
| 386 | REDISMODULE_NOT_USED(argc); | ||
| 387 | |||
| 388 | RedisModule_ReplyWithLongLong(ctx, RedisModule_MonotonicMicroseconds()); | ||
| 389 | return REDISMODULE_OK; | ||
| 390 | } | ||
| 391 | |||
| 392 | /* wrapper for RM_Call */ | ||
| 393 | int test_rm_call(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){ | ||
| 394 | if(argc < 2){ | ||
| 395 | return RedisModule_WrongArity(ctx); | ||
| 396 | } | ||
| 397 | |||
| 398 | const char* cmd = RedisModule_StringPtrLen(argv[1], NULL); | ||
| 399 | |||
| 400 | RedisModuleCallReply* rep = RedisModule_Call(ctx, cmd, "Ev", argv + 2, (size_t)argc - 2); | ||
| 401 | if(!rep){ | ||
| 402 | RedisModule_ReplyWithError(ctx, "NULL reply returned"); | ||
| 403 | }else{ | ||
| 404 | RedisModule_ReplyWithCallReply(ctx, rep); | ||
| 405 | RedisModule_FreeCallReply(rep); | ||
| 406 | } | ||
| 407 | |||
| 408 | return REDISMODULE_OK; | ||
| 409 | } | ||
| 410 | |||
| 411 | /* wrapper for RM_Call which also replicates the module command */ | ||
| 412 | int test_rm_call_replicate(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){ | ||
| 413 | test_rm_call(ctx, argv, argc); | ||
| 414 | RedisModule_ReplicateVerbatim(ctx); | ||
| 415 | |||
| 416 | return REDISMODULE_OK; | ||
| 417 | } | ||
| 418 | |||
| 419 | /* wrapper for RM_Call with flags */ | ||
| 420 | int test_rm_call_flags(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){ | ||
| 421 | if(argc < 3){ | ||
| 422 | return RedisModule_WrongArity(ctx); | ||
| 423 | } | ||
| 424 | |||
| 425 | /* Append Ev to the provided flags. */ | ||
| 426 | RedisModuleString *flags = RedisModule_CreateStringFromString(ctx, argv[1]); | ||
| 427 | RedisModule_StringAppendBuffer(ctx, flags, "Ev", 2); | ||
| 428 | |||
| 429 | const char* flg = RedisModule_StringPtrLen(flags, NULL); | ||
| 430 | const char* cmd = RedisModule_StringPtrLen(argv[2], NULL); | ||
| 431 | |||
| 432 | RedisModuleCallReply* rep = RedisModule_Call(ctx, cmd, flg, argv + 3, (size_t)argc - 3); | ||
| 433 | if(!rep){ | ||
| 434 | RedisModule_ReplyWithError(ctx, "NULL reply returned"); | ||
| 435 | }else{ | ||
| 436 | RedisModule_ReplyWithCallReply(ctx, rep); | ||
| 437 | RedisModule_FreeCallReply(rep); | ||
| 438 | } | ||
| 439 | RedisModule_FreeString(ctx, flags); | ||
| 440 | |||
| 441 | return REDISMODULE_OK; | ||
| 442 | } | ||
| 443 | |||
| 444 | int test_ull_conv(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 445 | UNUSED(argv); | ||
| 446 | UNUSED(argc); | ||
| 447 | unsigned long long ull = 18446744073709551615ULL; | ||
| 448 | const char *ullstr = "18446744073709551615"; | ||
| 449 | |||
| 450 | RedisModuleString *s1 = RedisModule_CreateStringFromULongLong(ctx, ull); | ||
| 451 | RedisModuleString *s2 = | ||
| 452 | RedisModule_CreateString(ctx, ullstr, strlen(ullstr)); | ||
| 453 | if (RedisModule_StringCompare(s1, s2) != 0) { | ||
| 454 | char err[4096]; | ||
| 455 | snprintf(err, 4096, | ||
| 456 | "Failed to convert unsigned long long to string ('%s' != '%s')", | ||
| 457 | RedisModule_StringPtrLen(s1, NULL), | ||
| 458 | RedisModule_StringPtrLen(s2, NULL)); | ||
| 459 | RedisModule_ReplyWithError(ctx, err); | ||
| 460 | goto final; | ||
| 461 | } | ||
| 462 | unsigned long long ull2 = 0; | ||
| 463 | if (RedisModule_StringToULongLong(s2, &ull2) == REDISMODULE_ERR) { | ||
| 464 | RedisModule_ReplyWithError(ctx, | ||
| 465 | "Failed to convert string to unsigned long long"); | ||
| 466 | goto final; | ||
| 467 | } | ||
| 468 | if (ull2 != ull) { | ||
| 469 | char err[4096]; | ||
| 470 | snprintf(err, 4096, | ||
| 471 | "Failed to convert string to unsigned long long (%llu != %llu)", | ||
| 472 | ull2, | ||
| 473 | ull); | ||
| 474 | RedisModule_ReplyWithError(ctx, err); | ||
| 475 | goto final; | ||
| 476 | } | ||
| 477 | |||
| 478 | /* Make sure we can't convert a string more than ULLONG_MAX or less than 0 */ | ||
| 479 | ullstr = "18446744073709551616"; | ||
| 480 | RedisModuleString *s3 = RedisModule_CreateString(ctx, ullstr, strlen(ullstr)); | ||
| 481 | unsigned long long ull3; | ||
| 482 | if (RedisModule_StringToULongLong(s3, &ull3) == REDISMODULE_OK) { | ||
| 483 | RedisModule_ReplyWithError(ctx, "Invalid string successfully converted to unsigned long long"); | ||
| 484 | RedisModule_FreeString(ctx, s3); | ||
| 485 | goto final; | ||
| 486 | } | ||
| 487 | RedisModule_FreeString(ctx, s3); | ||
| 488 | ullstr = "-1"; | ||
| 489 | RedisModuleString *s4 = RedisModule_CreateString(ctx, ullstr, strlen(ullstr)); | ||
| 490 | unsigned long long ull4; | ||
| 491 | if (RedisModule_StringToULongLong(s4, &ull4) == REDISMODULE_OK) { | ||
| 492 | RedisModule_ReplyWithError(ctx, "Invalid string successfully converted to unsigned long long"); | ||
| 493 | RedisModule_FreeString(ctx, s4); | ||
| 494 | goto final; | ||
| 495 | } | ||
| 496 | RedisModule_FreeString(ctx, s4); | ||
| 497 | |||
| 498 | RedisModule_ReplyWithSimpleString(ctx, "ok"); | ||
| 499 | |||
| 500 | final: | ||
| 501 | RedisModule_FreeString(ctx, s1); | ||
| 502 | RedisModule_FreeString(ctx, s2); | ||
| 503 | return REDISMODULE_OK; | ||
| 504 | } | ||
| 505 | |||
| 506 | int test_malloc_api(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 507 | UNUSED(argv); | ||
| 508 | UNUSED(argc); | ||
| 509 | |||
| 510 | void *p; | ||
| 511 | |||
| 512 | p = RedisModule_TryAlloc(1024); | ||
| 513 | memset(p, 0, 1024); | ||
| 514 | RedisModule_Free(p); | ||
| 515 | |||
| 516 | p = RedisModule_TryCalloc(1, 1024); | ||
| 517 | memset(p, 1, 1024); | ||
| 518 | |||
| 519 | p = RedisModule_TryRealloc(p, 5 * 1024); | ||
| 520 | memset(p, 1, 5 * 1024); | ||
| 521 | RedisModule_Free(p); | ||
| 522 | |||
| 523 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 524 | return REDISMODULE_OK; | ||
| 525 | } | ||
| 526 | |||
| 527 | int test_keyslot(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 528 | /* Static check of the ClusterKeySlot + ClusterKeySlotC + ClusterCanonicalKeyNameInSlot | ||
| 529 | * round-trip for all slots. */ | ||
| 530 | for (unsigned int slot = 0; slot < 16384; slot++) { | ||
| 531 | const char *tag = RedisModule_ClusterCanonicalKeyNameInSlot(slot); | ||
| 532 | RedisModuleString *key = RedisModule_CreateStringPrintf(ctx, "x{%s}y", tag); | ||
| 533 | assert(slot == RedisModule_ClusterKeySlot(key)); | ||
| 534 | size_t len; | ||
| 535 | const char *key_c = RedisModule_StringPtrLen(key, &len); | ||
| 536 | assert(slot == RedisModule_ClusterKeySlotC(key_c, len)); | ||
| 537 | RedisModule_FreeString(ctx, key); | ||
| 538 | } | ||
| 539 | if (argc != 2){ | ||
| 540 | return RedisModule_WrongArity(ctx); | ||
| 541 | } | ||
| 542 | unsigned int slot = RedisModule_ClusterKeySlot(argv[1]); | ||
| 543 | return RedisModule_ReplyWithLongLong(ctx, slot); | ||
| 544 | } | ||
| 545 | |||
| 546 | int only_reply_ok(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 547 | REDISMODULE_NOT_USED(argv); | ||
| 548 | REDISMODULE_NOT_USED(argc); | ||
| 549 | |||
| 550 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 551 | return REDISMODULE_OK; | ||
| 552 | } | ||
| 553 | |||
| 554 | /* Test command for RM_SignalModifiedKey. */ | ||
| 555 | int test_signalmodifiedkey(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 556 | if (argc != 2) return RedisModule_WrongArity(ctx); | ||
| 557 | |||
| 558 | /* Manually signal that the key was modified */ | ||
| 559 | RedisModule_SignalModifiedKey(ctx, argv[1]); | ||
| 560 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 561 | return REDISMODULE_OK; | ||
| 562 | } | ||
| 563 | |||
| 564 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 565 | REDISMODULE_NOT_USED(argv); | ||
| 566 | REDISMODULE_NOT_USED(argc); | ||
| 567 | if (RedisModule_Init(ctx,"misc",1,REDISMODULE_APIVER_1)== REDISMODULE_ERR) | ||
| 568 | return REDISMODULE_ERR; | ||
| 569 | |||
| 570 | if(RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_KEY_MISS | REDISMODULE_NOTIFY_EXPIRED, KeySpace_NotificationModuleKeyMissExpired) != REDISMODULE_OK){ | ||
| 571 | return REDISMODULE_ERR; | ||
| 572 | } | ||
| 573 | |||
| 574 | if (RedisModule_CreateCommand(ctx,"test.call_generic", test_call_generic,"",0,0,0) == REDISMODULE_ERR) | ||
| 575 | return REDISMODULE_ERR; | ||
| 576 | if (RedisModule_CreateCommand(ctx,"test.call_info", test_call_info,"",0,0,0) == REDISMODULE_ERR) | ||
| 577 | return REDISMODULE_ERR; | ||
| 578 | if (RedisModule_CreateCommand(ctx,"test.ld_conversion", test_ld_conv, "",0,0,0) == REDISMODULE_ERR) | ||
| 579 | return REDISMODULE_ERR; | ||
| 580 | if (RedisModule_CreateCommand(ctx,"test.ull_conversion", test_ull_conv, "",0,0,0) == REDISMODULE_ERR) | ||
| 581 | return REDISMODULE_ERR; | ||
| 582 | if (RedisModule_CreateCommand(ctx,"test.flushall", test_flushall,"",0,0,0) == REDISMODULE_ERR) | ||
| 583 | return REDISMODULE_ERR; | ||
| 584 | if (RedisModule_CreateCommand(ctx,"test.dbsize", test_dbsize,"",0,0,0) == REDISMODULE_ERR) | ||
| 585 | return REDISMODULE_ERR; | ||
| 586 | if (RedisModule_CreateCommand(ctx,"test.randomkey", test_randomkey,"",0,0,0) == REDISMODULE_ERR) | ||
| 587 | return REDISMODULE_ERR; | ||
| 588 | if (RedisModule_CreateCommand(ctx,"test.keyexists", test_keyexists,"",1,1,1) == REDISMODULE_ERR) | ||
| 589 | return REDISMODULE_ERR; | ||
| 590 | if (RedisModule_CreateCommand(ctx,"test.setlru", test_setlru,"",0,0,0) == REDISMODULE_ERR) | ||
| 591 | return REDISMODULE_ERR; | ||
| 592 | if (RedisModule_CreateCommand(ctx,"test.getlru", test_getlru,"",0,0,0) == REDISMODULE_ERR) | ||
| 593 | return REDISMODULE_ERR; | ||
| 594 | if (RedisModule_CreateCommand(ctx,"test.setlfu", test_setlfu,"",0,0,0) == REDISMODULE_ERR) | ||
| 595 | return REDISMODULE_ERR; | ||
| 596 | if (RedisModule_CreateCommand(ctx,"test.getlfu", test_getlfu,"",0,0,0) == REDISMODULE_ERR) | ||
| 597 | return REDISMODULE_ERR; | ||
| 598 | if (RedisModule_CreateCommand(ctx,"test.clientinfo", test_clientinfo,"",0,0,0) == REDISMODULE_ERR) | ||
| 599 | return REDISMODULE_ERR; | ||
| 600 | if (RedisModule_CreateCommand(ctx,"test.getname", test_getname,"",0,0,0) == REDISMODULE_ERR) | ||
| 601 | return REDISMODULE_ERR; | ||
| 602 | if (RedisModule_CreateCommand(ctx,"test.setname", test_setname,"",0,0,0) == REDISMODULE_ERR) | ||
| 603 | return REDISMODULE_ERR; | ||
| 604 | if (RedisModule_CreateCommand(ctx,"test.redisversion", test_redisversion,"",0,0,0) == REDISMODULE_ERR) | ||
| 605 | return REDISMODULE_ERR; | ||
| 606 | if (RedisModule_CreateCommand(ctx,"test.getclientcert", test_getclientcert,"",0,0,0) == REDISMODULE_ERR) | ||
| 607 | return REDISMODULE_ERR; | ||
| 608 | if (RedisModule_CreateCommand(ctx,"test.log_tsctx", test_log_tsctx,"",0,0,0) == REDISMODULE_ERR) | ||
| 609 | return REDISMODULE_ERR; | ||
| 610 | /* Add a command with ':' in it's name, so that we can check commandstats sanitization. */ | ||
| 611 | if (RedisModule_CreateCommand(ctx,"test.weird:cmd", test_weird_cmd,"readonly",0,0,0) == REDISMODULE_ERR) | ||
| 612 | return REDISMODULE_ERR; | ||
| 613 | if (RedisModule_CreateCommand(ctx,"test.monotonic_time", test_monotonic_time,"",0,0,0) == REDISMODULE_ERR) | ||
| 614 | return REDISMODULE_ERR; | ||
| 615 | if (RedisModule_CreateCommand(ctx, "test.rm_call", test_rm_call,"allow-stale", 0, 0, 0) == REDISMODULE_ERR) | ||
| 616 | return REDISMODULE_ERR; | ||
| 617 | if (RedisModule_CreateCommand(ctx, "test.rm_call_flags", test_rm_call_flags,"allow-stale", 0, 0, 0) == REDISMODULE_ERR) | ||
| 618 | return REDISMODULE_ERR; | ||
| 619 | if (RedisModule_CreateCommand(ctx, "test.rm_call_replicate", test_rm_call_replicate,"allow-stale", 0, 0, 0) == REDISMODULE_ERR) | ||
| 620 | return REDISMODULE_ERR; | ||
| 621 | if (RedisModule_CreateCommand(ctx, "test.silent_open_key", test_open_key_no_effects,"", 0, 0, 0) == REDISMODULE_ERR) | ||
| 622 | return REDISMODULE_ERR; | ||
| 623 | if (RedisModule_CreateCommand(ctx, "test.get_n_events", test_get_n_events,"", 0, 0, 0) == REDISMODULE_ERR) | ||
| 624 | return REDISMODULE_ERR; | ||
| 625 | if (RedisModule_CreateCommand(ctx, "test.clear_n_events", test_clear_n_events,"", 0, 0, 0) == REDISMODULE_ERR) | ||
| 626 | return REDISMODULE_ERR; | ||
| 627 | if (RedisModule_CreateCommand(ctx, "test.malloc_api", test_malloc_api,"", 0, 0, 0) == REDISMODULE_ERR) | ||
| 628 | return REDISMODULE_ERR; | ||
| 629 | if (RedisModule_CreateCommand(ctx, "test.keyslot", test_keyslot, "", 0, 0, 0) == REDISMODULE_ERR) | ||
| 630 | return REDISMODULE_ERR; | ||
| 631 | if (RedisModule_CreateCommand(ctx, "test.incompatible_cluster_cmd", only_reply_ok, "", 1, -1, 2) == REDISMODULE_ERR) | ||
| 632 | return REDISMODULE_ERR; | ||
| 633 | if (RedisModule_CreateCommand(ctx, "test.no_cluster_cmd", NULL, "no-cluster", 0, 0, 0) == REDISMODULE_ERR) | ||
| 634 | return REDISMODULE_ERR; | ||
| 635 | RedisModuleCommand *parent = RedisModule_GetCommand(ctx, "test.no_cluster_cmd"); | ||
| 636 | if (RedisModule_CreateSubcommand(parent, "set", only_reply_ok, "no-cluster", 0, 0, 0) == REDISMODULE_ERR) | ||
| 637 | return REDISMODULE_ERR; | ||
| 638 | if (RedisModule_CreateCommand(ctx, "test.signalmodifiedkey", test_signalmodifiedkey, "write", 1, 1, 1) == REDISMODULE_ERR) | ||
| 639 | return REDISMODULE_ERR; | ||
| 640 | |||
| 641 | return REDISMODULE_OK; | ||
| 642 | } | ||
diff --git a/examples/redis-unstable/tests/modules/moduleauthtwo.c b/examples/redis-unstable/tests/modules/moduleauthtwo.c new file mode 100644 index 0000000..0a4f56b --- /dev/null +++ b/examples/redis-unstable/tests/modules/moduleauthtwo.c | |||
| @@ -0,0 +1,43 @@ | |||
| 1 | #include "redismodule.h" | ||
| 2 | |||
| 3 | #include <string.h> | ||
| 4 | |||
| 5 | /* This is a second sample module to validate that module authentication callbacks can be registered | ||
| 6 | * from multiple modules. */ | ||
| 7 | |||
| 8 | /* Non Blocking Module Auth callback / implementation. */ | ||
| 9 | int auth_cb(RedisModuleCtx *ctx, RedisModuleString *username, RedisModuleString *password, RedisModuleString **err) { | ||
| 10 | const char *user = RedisModule_StringPtrLen(username, NULL); | ||
| 11 | const char *pwd = RedisModule_StringPtrLen(password, NULL); | ||
| 12 | if (!strcmp(user,"foo") && !strcmp(pwd,"allow_two")) { | ||
| 13 | RedisModule_AuthenticateClientWithACLUser(ctx, "foo", 3, NULL, NULL, NULL); | ||
| 14 | return REDISMODULE_AUTH_HANDLED; | ||
| 15 | } | ||
| 16 | else if (!strcmp(user,"foo") && !strcmp(pwd,"deny_two")) { | ||
| 17 | RedisModuleString *log = RedisModule_CreateString(ctx, "Module Auth", 11); | ||
| 18 | RedisModule_ACLAddLogEntryByUserName(ctx, username, log, REDISMODULE_ACL_LOG_AUTH); | ||
| 19 | RedisModule_FreeString(ctx, log); | ||
| 20 | const char *err_msg = "Auth denied by Misc Module."; | ||
| 21 | *err = RedisModule_CreateString(ctx, err_msg, strlen(err_msg)); | ||
| 22 | return REDISMODULE_AUTH_HANDLED; | ||
| 23 | } | ||
| 24 | return REDISMODULE_AUTH_NOT_HANDLED; | ||
| 25 | } | ||
| 26 | |||
| 27 | int test_rm_register_auth_cb(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 28 | REDISMODULE_NOT_USED(argv); | ||
| 29 | REDISMODULE_NOT_USED(argc); | ||
| 30 | RedisModule_RegisterAuthCallback(ctx, auth_cb); | ||
| 31 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 32 | return REDISMODULE_OK; | ||
| 33 | } | ||
| 34 | |||
| 35 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 36 | REDISMODULE_NOT_USED(argv); | ||
| 37 | REDISMODULE_NOT_USED(argc); | ||
| 38 | if (RedisModule_Init(ctx,"moduleauthtwo",1,REDISMODULE_APIVER_1)== REDISMODULE_ERR) | ||
| 39 | return REDISMODULE_ERR; | ||
| 40 | if (RedisModule_CreateCommand(ctx,"testmoduletwo.rm_register_auth_cb", test_rm_register_auth_cb,"",0,0,0) == REDISMODULE_ERR) | ||
| 41 | return REDISMODULE_ERR; | ||
| 42 | return REDISMODULE_OK; | ||
| 43 | } \ No newline at end of file | ||
diff --git a/examples/redis-unstable/tests/modules/moduleconfigs.c b/examples/redis-unstable/tests/modules/moduleconfigs.c new file mode 100644 index 0000000..04974f7 --- /dev/null +++ b/examples/redis-unstable/tests/modules/moduleconfigs.c | |||
| @@ -0,0 +1,287 @@ | |||
| 1 | #include "redismodule.h" | ||
| 2 | #include <strings.h> | ||
| 3 | int mutable_bool_val, no_prefix_bool, no_prefix_bool2; | ||
| 4 | int immutable_bool_val; | ||
| 5 | long long longval, no_prefix_longval; | ||
| 6 | long long memval, no_prefix_memval; | ||
| 7 | RedisModuleString *strval = NULL; | ||
| 8 | RedisModuleString *strval2 = NULL; | ||
| 9 | int enumval, no_prefix_enumval; | ||
| 10 | int flagsval; | ||
| 11 | |||
| 12 | /* Series of get and set callbacks for each type of config, these rely on the privdata ptr | ||
| 13 | * to point to the config, and they register the configs as such. Note that one could also just | ||
| 14 | * use names if they wanted, and store anything in privdata. */ | ||
| 15 | int getBoolConfigCommand(const char *name, void *privdata) { | ||
| 16 | REDISMODULE_NOT_USED(name); | ||
| 17 | return (*(int *)privdata); | ||
| 18 | } | ||
| 19 | |||
| 20 | int setBoolConfigCommand(const char *name, int new, void *privdata, RedisModuleString **err) { | ||
| 21 | REDISMODULE_NOT_USED(name); | ||
| 22 | REDISMODULE_NOT_USED(err); | ||
| 23 | *(int *)privdata = new; | ||
| 24 | return REDISMODULE_OK; | ||
| 25 | } | ||
| 26 | |||
| 27 | long long getNumericConfigCommand(const char *name, void *privdata) { | ||
| 28 | REDISMODULE_NOT_USED(name); | ||
| 29 | return (*(long long *) privdata); | ||
| 30 | } | ||
| 31 | |||
| 32 | int setNumericConfigCommand(const char *name, long long new, void *privdata, RedisModuleString **err) { | ||
| 33 | REDISMODULE_NOT_USED(name); | ||
| 34 | REDISMODULE_NOT_USED(err); | ||
| 35 | *(long long *)privdata = new; | ||
| 36 | return REDISMODULE_OK; | ||
| 37 | } | ||
| 38 | |||
| 39 | RedisModuleString *getStringConfigCommand(const char *name, void *privdata) { | ||
| 40 | REDISMODULE_NOT_USED(name); | ||
| 41 | REDISMODULE_NOT_USED(privdata); | ||
| 42 | return strval; | ||
| 43 | } | ||
| 44 | int setStringConfigCommand(const char *name, RedisModuleString *new, void *privdata, RedisModuleString **err) { | ||
| 45 | REDISMODULE_NOT_USED(name); | ||
| 46 | REDISMODULE_NOT_USED(err); | ||
| 47 | REDISMODULE_NOT_USED(privdata); | ||
| 48 | size_t len; | ||
| 49 | if (!strcasecmp(RedisModule_StringPtrLen(new, &len), "rejectisfreed")) { | ||
| 50 | *err = RedisModule_CreateString(NULL, "Cannot set string to 'rejectisfreed'", 36); | ||
| 51 | return REDISMODULE_ERR; | ||
| 52 | } | ||
| 53 | if (strval) RedisModule_FreeString(NULL, strval); | ||
| 54 | RedisModule_RetainString(NULL, new); | ||
| 55 | strval = new; | ||
| 56 | return REDISMODULE_OK; | ||
| 57 | } | ||
| 58 | |||
| 59 | int getEnumConfigCommand(const char *name, void *privdata) { | ||
| 60 | REDISMODULE_NOT_USED(name); | ||
| 61 | REDISMODULE_NOT_USED(privdata); | ||
| 62 | return enumval; | ||
| 63 | } | ||
| 64 | |||
| 65 | int setEnumConfigCommand(const char *name, int val, void *privdata, RedisModuleString **err) { | ||
| 66 | REDISMODULE_NOT_USED(name); | ||
| 67 | REDISMODULE_NOT_USED(err); | ||
| 68 | REDISMODULE_NOT_USED(privdata); | ||
| 69 | enumval = val; | ||
| 70 | return REDISMODULE_OK; | ||
| 71 | } | ||
| 72 | |||
| 73 | int getFlagsConfigCommand(const char *name, void *privdata) { | ||
| 74 | REDISMODULE_NOT_USED(name); | ||
| 75 | REDISMODULE_NOT_USED(privdata); | ||
| 76 | return flagsval; | ||
| 77 | } | ||
| 78 | |||
| 79 | int setFlagsConfigCommand(const char *name, int val, void *privdata, RedisModuleString **err) { | ||
| 80 | REDISMODULE_NOT_USED(name); | ||
| 81 | REDISMODULE_NOT_USED(err); | ||
| 82 | REDISMODULE_NOT_USED(privdata); | ||
| 83 | flagsval = val; | ||
| 84 | return REDISMODULE_OK; | ||
| 85 | } | ||
| 86 | |||
| 87 | int boolApplyFunc(RedisModuleCtx *ctx, void *privdata, RedisModuleString **err) { | ||
| 88 | REDISMODULE_NOT_USED(ctx); | ||
| 89 | REDISMODULE_NOT_USED(privdata); | ||
| 90 | if (mutable_bool_val && immutable_bool_val) { | ||
| 91 | *err = RedisModule_CreateString(NULL, "Bool configs cannot both be yes.", 32); | ||
| 92 | return REDISMODULE_ERR; | ||
| 93 | } | ||
| 94 | return REDISMODULE_OK; | ||
| 95 | } | ||
| 96 | |||
| 97 | int longlongApplyFunc(RedisModuleCtx *ctx, void *privdata, RedisModuleString **err) { | ||
| 98 | REDISMODULE_NOT_USED(ctx); | ||
| 99 | REDISMODULE_NOT_USED(privdata); | ||
| 100 | if (longval == memval) { | ||
| 101 | *err = RedisModule_CreateString(NULL, "These configs cannot equal each other.", 38); | ||
| 102 | return REDISMODULE_ERR; | ||
| 103 | } | ||
| 104 | return REDISMODULE_OK; | ||
| 105 | } | ||
| 106 | |||
| 107 | RedisModuleString *getStringConfigUnprefix(const char *name, void *privdata) { | ||
| 108 | REDISMODULE_NOT_USED(name); | ||
| 109 | REDISMODULE_NOT_USED(privdata); | ||
| 110 | return strval2; | ||
| 111 | } | ||
| 112 | |||
| 113 | int setStringConfigUnprefix(const char *name, RedisModuleString *new, void *privdata, RedisModuleString **err) { | ||
| 114 | REDISMODULE_NOT_USED(name); | ||
| 115 | REDISMODULE_NOT_USED(err); | ||
| 116 | REDISMODULE_NOT_USED(privdata); | ||
| 117 | if (strval2) RedisModule_FreeString(NULL, strval2); | ||
| 118 | RedisModule_RetainString(NULL, new); | ||
| 119 | strval2 = new; | ||
| 120 | return REDISMODULE_OK; | ||
| 121 | } | ||
| 122 | |||
| 123 | int getEnumConfigUnprefix(const char *name, void *privdata) { | ||
| 124 | REDISMODULE_NOT_USED(name); | ||
| 125 | REDISMODULE_NOT_USED(privdata); | ||
| 126 | return no_prefix_enumval; | ||
| 127 | } | ||
| 128 | |||
| 129 | int setEnumConfigUnprefix(const char *name, int val, void *privdata, RedisModuleString **err) { | ||
| 130 | REDISMODULE_NOT_USED(name); | ||
| 131 | REDISMODULE_NOT_USED(err); | ||
| 132 | REDISMODULE_NOT_USED(privdata); | ||
| 133 | no_prefix_enumval = val; | ||
| 134 | return REDISMODULE_OK; | ||
| 135 | } | ||
| 136 | |||
| 137 | int registerBlockCheck(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 138 | REDISMODULE_NOT_USED(argv); | ||
| 139 | REDISMODULE_NOT_USED(argc); | ||
| 140 | int response_ok = 0; | ||
| 141 | int result = RedisModule_RegisterBoolConfig(ctx, "mutable_bool", 1, REDISMODULE_CONFIG_DEFAULT, getBoolConfigCommand, setBoolConfigCommand, boolApplyFunc, &mutable_bool_val); | ||
| 142 | response_ok |= (result == REDISMODULE_OK); | ||
| 143 | |||
| 144 | result = RedisModule_RegisterStringConfig(ctx, "string", "secret password", REDISMODULE_CONFIG_DEFAULT, getStringConfigCommand, setStringConfigCommand, NULL, NULL); | ||
| 145 | response_ok |= (result == REDISMODULE_OK); | ||
| 146 | |||
| 147 | const char *enum_vals[] = {"none", "five", "one", "two", "four"}; | ||
| 148 | const int int_vals[] = {0, 5, 1, 2, 4}; | ||
| 149 | result = RedisModule_RegisterEnumConfig(ctx, "enum", 1, REDISMODULE_CONFIG_DEFAULT, enum_vals, int_vals, 5, getEnumConfigCommand, setEnumConfigCommand, NULL, NULL); | ||
| 150 | response_ok |= (result == REDISMODULE_OK); | ||
| 151 | |||
| 152 | result = RedisModule_RegisterNumericConfig(ctx, "numeric", -1, REDISMODULE_CONFIG_DEFAULT, -5, 2000, getNumericConfigCommand, setNumericConfigCommand, longlongApplyFunc, &longval); | ||
| 153 | response_ok |= (result == REDISMODULE_OK); | ||
| 154 | |||
| 155 | result = RedisModule_LoadConfigs(ctx); | ||
| 156 | response_ok |= (result == REDISMODULE_OK); | ||
| 157 | |||
| 158 | /* This validates that it's not possible to register/load configs outside OnLoad, | ||
| 159 | * thus returns an error if they succeed. */ | ||
| 160 | if (response_ok) { | ||
| 161 | RedisModule_ReplyWithError(ctx, "UNEXPECTEDOK"); | ||
| 162 | } else { | ||
| 163 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 164 | } | ||
| 165 | return REDISMODULE_OK; | ||
| 166 | } | ||
| 167 | |||
| 168 | void cleanup(RedisModuleCtx *ctx) { | ||
| 169 | if (strval) { | ||
| 170 | RedisModule_FreeString(ctx, strval); | ||
| 171 | strval = NULL; | ||
| 172 | } | ||
| 173 | if (strval2) { | ||
| 174 | RedisModule_FreeString(ctx, strval2); | ||
| 175 | strval2 = NULL; | ||
| 176 | } | ||
| 177 | } | ||
| 178 | |||
| 179 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 180 | REDISMODULE_NOT_USED(argv); | ||
| 181 | REDISMODULE_NOT_USED(argc); | ||
| 182 | |||
| 183 | if (RedisModule_Init(ctx, "moduleconfigs", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) { | ||
| 184 | RedisModule_Log(ctx, "warning", "Failed to init module"); | ||
| 185 | return REDISMODULE_ERR; | ||
| 186 | } | ||
| 187 | |||
| 188 | if (RedisModule_RegisterBoolConfig(ctx, "mutable_bool", 1, REDISMODULE_CONFIG_DEFAULT, getBoolConfigCommand, setBoolConfigCommand, boolApplyFunc, &mutable_bool_val) == REDISMODULE_ERR) { | ||
| 189 | RedisModule_Log(ctx, "warning", "Failed to register mutable_bool"); | ||
| 190 | return REDISMODULE_ERR; | ||
| 191 | } | ||
| 192 | /* Immutable config here. */ | ||
| 193 | if (RedisModule_RegisterBoolConfig(ctx, "immutable_bool", 0, REDISMODULE_CONFIG_IMMUTABLE, getBoolConfigCommand, setBoolConfigCommand, boolApplyFunc, &immutable_bool_val) == REDISMODULE_ERR) { | ||
| 194 | RedisModule_Log(ctx, "warning", "Failed to register immutable_bool"); | ||
| 195 | return REDISMODULE_ERR; | ||
| 196 | } | ||
| 197 | if (RedisModule_RegisterStringConfig(ctx, "string", "secret password", REDISMODULE_CONFIG_DEFAULT, getStringConfigCommand, setStringConfigCommand, NULL, NULL) == REDISMODULE_ERR) { | ||
| 198 | RedisModule_Log(ctx, "warning", "Failed to register string"); | ||
| 199 | return REDISMODULE_ERR; | ||
| 200 | } | ||
| 201 | |||
| 202 | /* On the stack to make sure we're copying them. */ | ||
| 203 | const char *enum_vals[] = {"none", "five", "one", "two", "four"}; | ||
| 204 | const int int_vals[] = {0, 5, 1, 2, 4}; | ||
| 205 | |||
| 206 | if (RedisModule_RegisterEnumConfig(ctx, "enum", 1, REDISMODULE_CONFIG_DEFAULT, enum_vals, int_vals, 5, getEnumConfigCommand, setEnumConfigCommand, NULL, NULL) == REDISMODULE_ERR) { | ||
| 207 | RedisModule_Log(ctx, "warning", "Failed to register enum"); | ||
| 208 | return REDISMODULE_ERR; | ||
| 209 | } | ||
| 210 | if (RedisModule_RegisterEnumConfig(ctx, "flags", 3, REDISMODULE_CONFIG_DEFAULT | REDISMODULE_CONFIG_BITFLAGS, enum_vals, int_vals, 5, getFlagsConfigCommand, setFlagsConfigCommand, NULL, NULL) == REDISMODULE_ERR) { | ||
| 211 | RedisModule_Log(ctx, "warning", "Failed to register flags"); | ||
| 212 | return REDISMODULE_ERR; | ||
| 213 | } | ||
| 214 | /* Memory config here. */ | ||
| 215 | if (RedisModule_RegisterNumericConfig(ctx, "memory_numeric", 1024, REDISMODULE_CONFIG_DEFAULT | REDISMODULE_CONFIG_MEMORY, 0, 3000000, getNumericConfigCommand, setNumericConfigCommand, longlongApplyFunc, &memval) == REDISMODULE_ERR) { | ||
| 216 | RedisModule_Log(ctx, "warning", "Failed to register memory_numeric"); | ||
| 217 | return REDISMODULE_ERR; | ||
| 218 | } | ||
| 219 | if (RedisModule_RegisterNumericConfig(ctx, "numeric", -1, REDISMODULE_CONFIG_DEFAULT, -5, 2000, getNumericConfigCommand, setNumericConfigCommand, longlongApplyFunc, &longval) == REDISMODULE_ERR) { | ||
| 220 | RedisModule_Log(ctx, "warning", "Failed to register numeric"); | ||
| 221 | return REDISMODULE_ERR; | ||
| 222 | } | ||
| 223 | |||
| 224 | /*** unprefixed and aliased configuration ***/ | ||
| 225 | if (RedisModule_RegisterBoolConfig(ctx, "unprefix-bool|unprefix-bool-alias", 1, REDISMODULE_CONFIG_DEFAULT|REDISMODULE_CONFIG_UNPREFIXED, | ||
| 226 | getBoolConfigCommand, setBoolConfigCommand, NULL, &no_prefix_bool) == REDISMODULE_ERR) { | ||
| 227 | RedisModule_Log(ctx, "warning", "Failed to register unprefix-bool"); | ||
| 228 | return REDISMODULE_ERR; | ||
| 229 | } | ||
| 230 | if (RedisModule_RegisterBoolConfig(ctx, "unprefix-noalias-bool", 1, REDISMODULE_CONFIG_DEFAULT|REDISMODULE_CONFIG_UNPREFIXED, | ||
| 231 | getBoolConfigCommand, setBoolConfigCommand, NULL, &no_prefix_bool2) == REDISMODULE_ERR) { | ||
| 232 | RedisModule_Log(ctx, "warning", "Failed to register unprefix-noalias-bool"); | ||
| 233 | return REDISMODULE_ERR; | ||
| 234 | } | ||
| 235 | if (RedisModule_RegisterNumericConfig(ctx, "unprefix.numeric|unprefix.numeric-alias", -1, REDISMODULE_CONFIG_DEFAULT|REDISMODULE_CONFIG_UNPREFIXED, | ||
| 236 | -5, 2000, getNumericConfigCommand, setNumericConfigCommand, NULL, &no_prefix_longval) == REDISMODULE_ERR) { | ||
| 237 | RedisModule_Log(ctx, "warning", "Failed to register unprefix.numeric"); | ||
| 238 | return REDISMODULE_ERR; | ||
| 239 | } | ||
| 240 | if (RedisModule_RegisterStringConfig(ctx, "unprefix-string|unprefix.string-alias", "secret unprefix", REDISMODULE_CONFIG_DEFAULT|REDISMODULE_CONFIG_UNPREFIXED, | ||
| 241 | getStringConfigUnprefix, setStringConfigUnprefix, NULL, NULL) == REDISMODULE_ERR) { | ||
| 242 | RedisModule_Log(ctx, "warning", "Failed to register unprefix-string"); | ||
| 243 | return REDISMODULE_ERR; | ||
| 244 | } | ||
| 245 | if (RedisModule_RegisterEnumConfig(ctx, "unprefix-enum|unprefix-enum-alias", 1, REDISMODULE_CONFIG_DEFAULT|REDISMODULE_CONFIG_UNPREFIXED, | ||
| 246 | enum_vals, int_vals, 5, getEnumConfigUnprefix, setEnumConfigUnprefix, NULL, NULL) == REDISMODULE_ERR) { | ||
| 247 | RedisModule_Log(ctx, "warning", "Failed to register unprefix-enum"); | ||
| 248 | return REDISMODULE_ERR; | ||
| 249 | } | ||
| 250 | |||
| 251 | RedisModule_Log(ctx, "debug", "Registered configuration"); | ||
| 252 | size_t len; | ||
| 253 | if (argc && !strcasecmp(RedisModule_StringPtrLen(argv[0], &len), "noload")) { | ||
| 254 | return REDISMODULE_OK; | ||
| 255 | } else if (argc && !strcasecmp(RedisModule_StringPtrLen(argv[0], &len), "override-default")) { | ||
| 256 | if (RedisModule_LoadDefaultConfigs(ctx) == REDISMODULE_ERR) { | ||
| 257 | RedisModule_Log(ctx, "warning", "Failed to load default configuration"); | ||
| 258 | goto err; | ||
| 259 | } | ||
| 260 | // simulate configuration values being overwritten by the command line | ||
| 261 | RedisModule_Log(ctx, "debug", "Overriding configuration values"); | ||
| 262 | if (strval) RedisModule_FreeString(ctx, strval); | ||
| 263 | strval = RedisModule_CreateString(ctx, "foo", 3); | ||
| 264 | longval = memval = 123; | ||
| 265 | } | ||
| 266 | RedisModule_Log(ctx, "debug", "Loading configuration"); | ||
| 267 | if (RedisModule_LoadConfigs(ctx) == REDISMODULE_ERR) { | ||
| 268 | RedisModule_Log(ctx, "warning", "Failed to load configuration"); | ||
| 269 | goto err; | ||
| 270 | } | ||
| 271 | /* Creates a command which registers configs outside OnLoad() function. */ | ||
| 272 | if (RedisModule_CreateCommand(ctx,"block.register.configs.outside.onload", registerBlockCheck, "write", 0, 0, 0) == REDISMODULE_ERR) { | ||
| 273 | RedisModule_Log(ctx, "warning", "Failed to register command"); | ||
| 274 | goto err; | ||
| 275 | } | ||
| 276 | |||
| 277 | return REDISMODULE_OK; | ||
| 278 | err: | ||
| 279 | cleanup(ctx); | ||
| 280 | return REDISMODULE_ERR; | ||
| 281 | } | ||
| 282 | |||
| 283 | int RedisModule_OnUnload(RedisModuleCtx *ctx) { | ||
| 284 | REDISMODULE_NOT_USED(ctx); | ||
| 285 | cleanup(ctx); | ||
| 286 | return REDISMODULE_OK; | ||
| 287 | } | ||
diff --git a/examples/redis-unstable/tests/modules/moduleconfigstwo.c b/examples/redis-unstable/tests/modules/moduleconfigstwo.c new file mode 100644 index 0000000..c0e8f91 --- /dev/null +++ b/examples/redis-unstable/tests/modules/moduleconfigstwo.c | |||
| @@ -0,0 +1,39 @@ | |||
| 1 | #include "redismodule.h" | ||
| 2 | #include <strings.h> | ||
| 3 | |||
| 4 | /* Second module configs module, for testing. | ||
| 5 | * Need to make sure that multiple modules with configs don't interfere with each other */ | ||
| 6 | int bool_config; | ||
| 7 | |||
| 8 | int getBoolConfigCommand(const char *name, void *privdata) { | ||
| 9 | REDISMODULE_NOT_USED(privdata); | ||
| 10 | if (!strcasecmp(name, "test")) { | ||
| 11 | return bool_config; | ||
| 12 | } | ||
| 13 | return 0; | ||
| 14 | } | ||
| 15 | |||
| 16 | int setBoolConfigCommand(const char *name, int new, void *privdata, RedisModuleString **err) { | ||
| 17 | REDISMODULE_NOT_USED(privdata); | ||
| 18 | REDISMODULE_NOT_USED(err); | ||
| 19 | if (!strcasecmp(name, "test")) { | ||
| 20 | bool_config = new; | ||
| 21 | return REDISMODULE_OK; | ||
| 22 | } | ||
| 23 | return REDISMODULE_ERR; | ||
| 24 | } | ||
| 25 | |||
| 26 | /* No arguments are expected */ | ||
| 27 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 28 | REDISMODULE_NOT_USED(argv); | ||
| 29 | REDISMODULE_NOT_USED(argc); | ||
| 30 | if (RedisModule_Init(ctx, "configs", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) return REDISMODULE_ERR; | ||
| 31 | |||
| 32 | if (RedisModule_RegisterBoolConfig(ctx, "test", 1, REDISMODULE_CONFIG_DEFAULT, getBoolConfigCommand, setBoolConfigCommand, NULL, &argc) == REDISMODULE_ERR) { | ||
| 33 | return REDISMODULE_ERR; | ||
| 34 | } | ||
| 35 | if (RedisModule_LoadConfigs(ctx) == REDISMODULE_ERR) { | ||
| 36 | return REDISMODULE_ERR; | ||
| 37 | } | ||
| 38 | return REDISMODULE_OK; | ||
| 39 | } \ No newline at end of file | ||
diff --git a/examples/redis-unstable/tests/modules/postnotifications.c b/examples/redis-unstable/tests/modules/postnotifications.c new file mode 100644 index 0000000..96fb859 --- /dev/null +++ b/examples/redis-unstable/tests/modules/postnotifications.c | |||
| @@ -0,0 +1,289 @@ | |||
| 1 | /* This module is used to test the server post keyspace jobs API. | ||
| 2 | * | ||
| 3 | * ----------------------------------------------------------------------------- | ||
| 4 | * | ||
| 5 | * Copyright (c) 2020-Present, Redis Ltd. | ||
| 6 | * All rights reserved. | ||
| 7 | * | ||
| 8 | * Licensed under your choice of (a) the Redis Source Available License 2.0 | ||
| 9 | * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the | ||
| 10 | * GNU Affero General Public License v3 (AGPLv3). | ||
| 11 | */ | ||
| 12 | |||
| 13 | /* This module allow to verify 'RedisModule_AddPostNotificationJob' by registering to 3 | ||
| 14 | * key space event: | ||
| 15 | * * STRINGS - the module register to all strings notifications and set post notification job | ||
| 16 | * that increase a counter indicating how many times the string key was changed. | ||
| 17 | * In addition, it increase another counter that counts the total changes that | ||
| 18 | * was made on all strings keys. | ||
| 19 | * * EXPIRED - the module register to expired event and set post notification job that that | ||
| 20 | * counts the total number of expired events. | ||
| 21 | * * EVICTED - the module register to evicted event and set post notification job that that | ||
| 22 | * counts the total number of evicted events. | ||
| 23 | * | ||
| 24 | * In addition, the module register a new command, 'postnotification.async_set', that performs a set | ||
| 25 | * command from a background thread. This allows to check the 'RedisModule_AddPostNotificationJob' on | ||
| 26 | * notifications that was triggered on a background thread. */ | ||
| 27 | |||
| 28 | #define _BSD_SOURCE | ||
| 29 | #define _DEFAULT_SOURCE /* For usleep */ | ||
| 30 | |||
| 31 | #include "redismodule.h" | ||
| 32 | #include <stdio.h> | ||
| 33 | #include <string.h> | ||
| 34 | #include <unistd.h> | ||
| 35 | #include <pthread.h> | ||
| 36 | |||
| 37 | static void KeySpace_PostNotificationStringFreePD(void *pd) { | ||
| 38 | RedisModule_FreeString(NULL, pd); | ||
| 39 | } | ||
| 40 | |||
| 41 | static void KeySpace_PostNotificationReadKey(RedisModuleCtx *ctx, void *pd) { | ||
| 42 | RedisModuleCallReply* rep = RedisModule_Call(ctx, "get", "!s", pd); | ||
| 43 | RedisModule_FreeCallReply(rep); | ||
| 44 | } | ||
| 45 | |||
| 46 | static void KeySpace_PostNotificationString(RedisModuleCtx *ctx, void *pd) { | ||
| 47 | REDISMODULE_NOT_USED(ctx); | ||
| 48 | RedisModuleCallReply* rep = RedisModule_Call(ctx, "incr", "!s", pd); | ||
| 49 | RedisModule_FreeCallReply(rep); | ||
| 50 | } | ||
| 51 | |||
| 52 | static int KeySpace_NotificationExpired(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key){ | ||
| 53 | REDISMODULE_NOT_USED(type); | ||
| 54 | REDISMODULE_NOT_USED(event); | ||
| 55 | REDISMODULE_NOT_USED(key); | ||
| 56 | |||
| 57 | RedisModuleString *new_key = RedisModule_CreateString(NULL, "expired", 7); | ||
| 58 | int res = RedisModule_AddPostNotificationJob(ctx, KeySpace_PostNotificationString, new_key, KeySpace_PostNotificationStringFreePD); | ||
| 59 | if (res == REDISMODULE_ERR) KeySpace_PostNotificationStringFreePD(new_key); | ||
| 60 | return REDISMODULE_OK; | ||
| 61 | } | ||
| 62 | |||
| 63 | static int KeySpace_NotificationEvicted(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key){ | ||
| 64 | REDISMODULE_NOT_USED(type); | ||
| 65 | REDISMODULE_NOT_USED(event); | ||
| 66 | REDISMODULE_NOT_USED(key); | ||
| 67 | |||
| 68 | const char *key_str = RedisModule_StringPtrLen(key, NULL); | ||
| 69 | |||
| 70 | if (strncmp(key_str, "evicted", 7) == 0) { | ||
| 71 | return REDISMODULE_OK; /* do not count the evicted key */ | ||
| 72 | } | ||
| 73 | |||
| 74 | if (strncmp(key_str, "before_evicted", 14) == 0) { | ||
| 75 | return REDISMODULE_OK; /* do not count the before_evicted key */ | ||
| 76 | } | ||
| 77 | |||
| 78 | RedisModuleString *new_key = RedisModule_CreateString(NULL, "evicted", 7); | ||
| 79 | int res = RedisModule_AddPostNotificationJob(ctx, KeySpace_PostNotificationString, new_key, KeySpace_PostNotificationStringFreePD); | ||
| 80 | if (res == REDISMODULE_ERR) KeySpace_PostNotificationStringFreePD(new_key); | ||
| 81 | return REDISMODULE_OK; | ||
| 82 | } | ||
| 83 | |||
| 84 | static int KeySpace_NotificationString(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key){ | ||
| 85 | REDISMODULE_NOT_USED(ctx); | ||
| 86 | REDISMODULE_NOT_USED(type); | ||
| 87 | REDISMODULE_NOT_USED(event); | ||
| 88 | |||
| 89 | const char *key_str = RedisModule_StringPtrLen(key, NULL); | ||
| 90 | |||
| 91 | if (strncmp(key_str, "string_", 7) != 0) { | ||
| 92 | return REDISMODULE_OK; | ||
| 93 | } | ||
| 94 | |||
| 95 | if (strcmp(key_str, "string_total") == 0) { | ||
| 96 | return REDISMODULE_OK; | ||
| 97 | } | ||
| 98 | |||
| 99 | RedisModuleString *new_key; | ||
| 100 | if (strncmp(key_str, "string_changed{", 15) == 0) { | ||
| 101 | new_key = RedisModule_CreateString(NULL, "string_total", 12); | ||
| 102 | } else { | ||
| 103 | new_key = RedisModule_CreateStringPrintf(NULL, "string_changed{%s}", key_str); | ||
| 104 | } | ||
| 105 | |||
| 106 | int res = RedisModule_AddPostNotificationJob(ctx, KeySpace_PostNotificationString, new_key, KeySpace_PostNotificationStringFreePD); | ||
| 107 | if (res == REDISMODULE_ERR) KeySpace_PostNotificationStringFreePD(new_key); | ||
| 108 | return REDISMODULE_OK; | ||
| 109 | } | ||
| 110 | |||
| 111 | static int KeySpace_LazyExpireInsidePostNotificationJob(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key){ | ||
| 112 | REDISMODULE_NOT_USED(ctx); | ||
| 113 | REDISMODULE_NOT_USED(type); | ||
| 114 | REDISMODULE_NOT_USED(event); | ||
| 115 | |||
| 116 | const char *key_str = RedisModule_StringPtrLen(key, NULL); | ||
| 117 | |||
| 118 | if (strncmp(key_str, "read_", 5) != 0) { | ||
| 119 | return REDISMODULE_OK; | ||
| 120 | } | ||
| 121 | |||
| 122 | RedisModuleString *new_key = RedisModule_CreateString(NULL, key_str + 5, strlen(key_str) - 5);; | ||
| 123 | int res = RedisModule_AddPostNotificationJob(ctx, KeySpace_PostNotificationReadKey, new_key, KeySpace_PostNotificationStringFreePD); | ||
| 124 | if (res == REDISMODULE_ERR) KeySpace_PostNotificationStringFreePD(new_key); | ||
| 125 | return REDISMODULE_OK; | ||
| 126 | } | ||
| 127 | |||
| 128 | static int KeySpace_NestedNotification(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key){ | ||
| 129 | REDISMODULE_NOT_USED(ctx); | ||
| 130 | REDISMODULE_NOT_USED(type); | ||
| 131 | REDISMODULE_NOT_USED(event); | ||
| 132 | |||
| 133 | const char *key_str = RedisModule_StringPtrLen(key, NULL); | ||
| 134 | |||
| 135 | if (strncmp(key_str, "write_sync_", 11) != 0) { | ||
| 136 | return REDISMODULE_OK; | ||
| 137 | } | ||
| 138 | |||
| 139 | /* This test was only meant to check REDISMODULE_OPTIONS_ALLOW_NESTED_KEYSPACE_NOTIFICATIONS. | ||
| 140 | * In general it is wrong and discourage to perform any writes inside a notification callback. */ | ||
| 141 | RedisModuleString *new_key = RedisModule_CreateString(NULL, key_str + 11, strlen(key_str) - 11);; | ||
| 142 | RedisModuleCallReply* rep = RedisModule_Call(ctx, "set", "!sc", new_key, "1"); | ||
| 143 | RedisModule_FreeCallReply(rep); | ||
| 144 | RedisModule_FreeString(NULL, new_key); | ||
| 145 | return REDISMODULE_OK; | ||
| 146 | } | ||
| 147 | |||
| 148 | static void *KeySpace_PostNotificationsAsyncSetInner(void *arg) { | ||
| 149 | RedisModuleBlockedClient *bc = arg; | ||
| 150 | RedisModuleCtx *ctx = RedisModule_GetThreadSafeContext(bc); | ||
| 151 | RedisModule_ThreadSafeContextLock(ctx); | ||
| 152 | RedisModuleCallReply* rep = RedisModule_Call(ctx, "set", "!cc", "string_x", "1"); | ||
| 153 | RedisModule_ThreadSafeContextUnlock(ctx); | ||
| 154 | RedisModule_ReplyWithCallReply(ctx, rep); | ||
| 155 | RedisModule_FreeCallReply(rep); | ||
| 156 | |||
| 157 | RedisModule_UnblockClient(bc, NULL); | ||
| 158 | RedisModule_FreeThreadSafeContext(ctx); | ||
| 159 | return NULL; | ||
| 160 | } | ||
| 161 | |||
| 162 | static int KeySpace_PostNotificationsAsyncSet(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 163 | REDISMODULE_NOT_USED(argv); | ||
| 164 | if (argc != 1) | ||
| 165 | return RedisModule_WrongArity(ctx); | ||
| 166 | |||
| 167 | pthread_t tid; | ||
| 168 | RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx,NULL,NULL,NULL,0); | ||
| 169 | |||
| 170 | if (pthread_create(&tid,NULL,KeySpace_PostNotificationsAsyncSetInner,bc) != 0) { | ||
| 171 | RedisModule_AbortBlock(bc); | ||
| 172 | return RedisModule_ReplyWithError(ctx,"-ERR Can't start thread"); | ||
| 173 | } | ||
| 174 | pthread_detach(tid); | ||
| 175 | return REDISMODULE_OK; | ||
| 176 | } | ||
| 177 | |||
| 178 | typedef struct KeySpace_EventPostNotificationCtx { | ||
| 179 | RedisModuleString *triggered_on; | ||
| 180 | RedisModuleString *new_key; | ||
| 181 | } KeySpace_EventPostNotificationCtx; | ||
| 182 | |||
| 183 | static void KeySpace_ServerEventPostNotificationFree(void *pd) { | ||
| 184 | KeySpace_EventPostNotificationCtx *pn_ctx = pd; | ||
| 185 | RedisModule_FreeString(NULL, pn_ctx->new_key); | ||
| 186 | RedisModule_FreeString(NULL, pn_ctx->triggered_on); | ||
| 187 | RedisModule_Free(pn_ctx); | ||
| 188 | } | ||
| 189 | |||
| 190 | static void KeySpace_ServerEventPostNotification(RedisModuleCtx *ctx, void *pd) { | ||
| 191 | REDISMODULE_NOT_USED(ctx); | ||
| 192 | KeySpace_EventPostNotificationCtx *pn_ctx = pd; | ||
| 193 | RedisModuleCallReply* rep = RedisModule_Call(ctx, "lpush", "!ss", pn_ctx->new_key, pn_ctx->triggered_on); | ||
| 194 | RedisModule_FreeCallReply(rep); | ||
| 195 | } | ||
| 196 | |||
| 197 | static void KeySpace_ServerEventCallback(RedisModuleCtx *ctx, RedisModuleEvent eid, uint64_t subevent, void *data) { | ||
| 198 | REDISMODULE_NOT_USED(eid); | ||
| 199 | REDISMODULE_NOT_USED(data); | ||
| 200 | if (subevent > 3) { | ||
| 201 | RedisModule_Log(ctx, "warning", "Got an unexpected subevent '%llu'", (unsigned long long)subevent); | ||
| 202 | return; | ||
| 203 | } | ||
| 204 | static const char* events[] = { | ||
| 205 | "before_deleted", | ||
| 206 | "before_expired", | ||
| 207 | "before_evicted", | ||
| 208 | "before_overwritten", | ||
| 209 | }; | ||
| 210 | |||
| 211 | const RedisModuleString *key_name = RedisModule_GetKeyNameFromModuleKey(((RedisModuleKeyInfo*)data)->key); | ||
| 212 | const char *key_str = RedisModule_StringPtrLen(key_name, NULL); | ||
| 213 | |||
| 214 | for (int i = 0 ; i < 4 ; ++i) { | ||
| 215 | const char *event = events[i]; | ||
| 216 | if (strncmp(key_str, event , strlen(event)) == 0) { | ||
| 217 | return; /* don't log any event on our tracking keys */ | ||
| 218 | } | ||
| 219 | } | ||
| 220 | |||
| 221 | KeySpace_EventPostNotificationCtx *pn_ctx = RedisModule_Alloc(sizeof(*pn_ctx)); | ||
| 222 | pn_ctx->triggered_on = RedisModule_HoldString(NULL, (RedisModuleString*)key_name); | ||
| 223 | pn_ctx->new_key = RedisModule_CreateString(NULL, events[subevent], strlen(events[subevent])); | ||
| 224 | int res = RedisModule_AddPostNotificationJob(ctx, KeySpace_ServerEventPostNotification, pn_ctx, KeySpace_ServerEventPostNotificationFree); | ||
| 225 | if (res == REDISMODULE_ERR) KeySpace_ServerEventPostNotificationFree(pn_ctx); | ||
| 226 | } | ||
| 227 | |||
| 228 | /* This function must be present on each Redis module. It is used in order to | ||
| 229 | * register the commands into the Redis server. */ | ||
| 230 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 231 | REDISMODULE_NOT_USED(argv); | ||
| 232 | REDISMODULE_NOT_USED(argc); | ||
| 233 | |||
| 234 | if (RedisModule_Init(ctx,"postnotifications",1,REDISMODULE_APIVER_1) == REDISMODULE_ERR){ | ||
| 235 | return REDISMODULE_ERR; | ||
| 236 | } | ||
| 237 | |||
| 238 | if (!(RedisModule_GetModuleOptionsAll() & REDISMODULE_OPTIONS_ALLOW_NESTED_KEYSPACE_NOTIFICATIONS)) { | ||
| 239 | return REDISMODULE_ERR; | ||
| 240 | } | ||
| 241 | |||
| 242 | int with_key_events = 0; | ||
| 243 | if (argc >= 1) { | ||
| 244 | const char *arg = RedisModule_StringPtrLen(argv[0], 0); | ||
| 245 | if (strcmp(arg, "with_key_events") == 0) { | ||
| 246 | with_key_events = 1; | ||
| 247 | } | ||
| 248 | } | ||
| 249 | |||
| 250 | RedisModule_SetModuleOptions(ctx, REDISMODULE_OPTIONS_ALLOW_NESTED_KEYSPACE_NOTIFICATIONS); | ||
| 251 | |||
| 252 | if(RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_STRING, KeySpace_NotificationString) != REDISMODULE_OK){ | ||
| 253 | return REDISMODULE_ERR; | ||
| 254 | } | ||
| 255 | |||
| 256 | if(RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_STRING, KeySpace_LazyExpireInsidePostNotificationJob) != REDISMODULE_OK){ | ||
| 257 | return REDISMODULE_ERR; | ||
| 258 | } | ||
| 259 | |||
| 260 | if(RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_STRING, KeySpace_NestedNotification) != REDISMODULE_OK){ | ||
| 261 | return REDISMODULE_ERR; | ||
| 262 | } | ||
| 263 | |||
| 264 | if(RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_EXPIRED, KeySpace_NotificationExpired) != REDISMODULE_OK){ | ||
| 265 | return REDISMODULE_ERR; | ||
| 266 | } | ||
| 267 | |||
| 268 | if(RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_EVICTED, KeySpace_NotificationEvicted) != REDISMODULE_OK){ | ||
| 269 | return REDISMODULE_ERR; | ||
| 270 | } | ||
| 271 | |||
| 272 | if (with_key_events) { | ||
| 273 | if(RedisModule_SubscribeToServerEvent(ctx, RedisModuleEvent_Key, KeySpace_ServerEventCallback) != REDISMODULE_OK){ | ||
| 274 | return REDISMODULE_ERR; | ||
| 275 | } | ||
| 276 | } | ||
| 277 | |||
| 278 | if (RedisModule_CreateCommand(ctx, "postnotification.async_set", KeySpace_PostNotificationsAsyncSet, | ||
| 279 | "write", 0, 0, 0) == REDISMODULE_ERR){ | ||
| 280 | return REDISMODULE_ERR; | ||
| 281 | } | ||
| 282 | |||
| 283 | return REDISMODULE_OK; | ||
| 284 | } | ||
| 285 | |||
| 286 | int RedisModule_OnUnload(RedisModuleCtx *ctx) { | ||
| 287 | REDISMODULE_NOT_USED(ctx); | ||
| 288 | return REDISMODULE_OK; | ||
| 289 | } | ||
diff --git a/examples/redis-unstable/tests/modules/propagate.c b/examples/redis-unstable/tests/modules/propagate.c new file mode 100644 index 0000000..689d688 --- /dev/null +++ b/examples/redis-unstable/tests/modules/propagate.c | |||
| @@ -0,0 +1,403 @@ | |||
| 1 | /* This module is used to test the propagation (replication + AOF) of | ||
| 2 | * commands, via the RedisModule_Replicate() interface, in asynchronous | ||
| 3 | * contexts, such as callbacks not implementing commands, and thread safe | ||
| 4 | * contexts. | ||
| 5 | * | ||
| 6 | * We create a timer callback and a threads using a thread safe context. | ||
| 7 | * Using both we try to propagate counters increments, and later we check | ||
| 8 | * if the replica contains the changes as expected. | ||
| 9 | * | ||
| 10 | * ----------------------------------------------------------------------------- | ||
| 11 | * | ||
| 12 | * Copyright (c) 2019-Present, Redis Ltd. | ||
| 13 | * All rights reserved. | ||
| 14 | * | ||
| 15 | * Licensed under your choice of (a) the Redis Source Available License 2.0 | ||
| 16 | * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the | ||
| 17 | * GNU Affero General Public License v3 (AGPLv3). | ||
| 18 | */ | ||
| 19 | |||
| 20 | #include "redismodule.h" | ||
| 21 | #include <pthread.h> | ||
| 22 | #include <errno.h> | ||
| 23 | |||
| 24 | #define UNUSED(V) ((void) V) | ||
| 25 | |||
| 26 | RedisModuleCtx *detached_ctx = NULL; | ||
| 27 | |||
| 28 | static int KeySpace_NotificationGeneric(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key) { | ||
| 29 | REDISMODULE_NOT_USED(type); | ||
| 30 | REDISMODULE_NOT_USED(event); | ||
| 31 | REDISMODULE_NOT_USED(key); | ||
| 32 | |||
| 33 | RedisModuleCallReply* rep = RedisModule_Call(ctx, "INCR", "c!", "notifications"); | ||
| 34 | RedisModule_FreeCallReply(rep); | ||
| 35 | |||
| 36 | return REDISMODULE_OK; | ||
| 37 | } | ||
| 38 | |||
| 39 | /* Timer callback. */ | ||
| 40 | void timerHandler(RedisModuleCtx *ctx, void *data) { | ||
| 41 | REDISMODULE_NOT_USED(ctx); | ||
| 42 | REDISMODULE_NOT_USED(data); | ||
| 43 | |||
| 44 | static int times = 0; | ||
| 45 | |||
| 46 | RedisModule_Replicate(ctx,"INCR","c","timer"); | ||
| 47 | times++; | ||
| 48 | |||
| 49 | if (times < 3) | ||
| 50 | RedisModule_CreateTimer(ctx,100,timerHandler,NULL); | ||
| 51 | else | ||
| 52 | times = 0; | ||
| 53 | } | ||
| 54 | |||
| 55 | int propagateTestTimerCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 56 | { | ||
| 57 | REDISMODULE_NOT_USED(argv); | ||
| 58 | REDISMODULE_NOT_USED(argc); | ||
| 59 | |||
| 60 | RedisModuleTimerID timer_id = | ||
| 61 | RedisModule_CreateTimer(ctx,100,timerHandler,NULL); | ||
| 62 | REDISMODULE_NOT_USED(timer_id); | ||
| 63 | |||
| 64 | RedisModule_ReplyWithSimpleString(ctx,"OK"); | ||
| 65 | return REDISMODULE_OK; | ||
| 66 | } | ||
| 67 | |||
| 68 | /* Timer callback. */ | ||
| 69 | void timerNestedHandler(RedisModuleCtx *ctx, void *data) { | ||
| 70 | int repl = (long long)data; | ||
| 71 | |||
| 72 | /* The goal is the trigger a module command that calls RM_Replicate | ||
| 73 | * in order to test MULTI/EXEC structure */ | ||
| 74 | RedisModule_Replicate(ctx,"INCRBY","cc","timer-nested-start","1"); | ||
| 75 | RedisModuleCallReply *reply = RedisModule_Call(ctx,"propagate-test.nested", repl? "!" : ""); | ||
| 76 | RedisModule_FreeCallReply(reply); | ||
| 77 | reply = RedisModule_Call(ctx, "INCR", repl? "c!" : "c", "timer-nested-middle"); | ||
| 78 | RedisModule_FreeCallReply(reply); | ||
| 79 | RedisModule_Replicate(ctx,"INCRBY","cc","timer-nested-end","1"); | ||
| 80 | } | ||
| 81 | |||
| 82 | int propagateTestTimerNestedCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 83 | { | ||
| 84 | REDISMODULE_NOT_USED(argv); | ||
| 85 | REDISMODULE_NOT_USED(argc); | ||
| 86 | |||
| 87 | RedisModuleTimerID timer_id = | ||
| 88 | RedisModule_CreateTimer(ctx,100,timerNestedHandler,(void*)0); | ||
| 89 | REDISMODULE_NOT_USED(timer_id); | ||
| 90 | |||
| 91 | RedisModule_ReplyWithSimpleString(ctx,"OK"); | ||
| 92 | return REDISMODULE_OK; | ||
| 93 | } | ||
| 94 | |||
| 95 | int propagateTestTimerNestedReplCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 96 | { | ||
| 97 | REDISMODULE_NOT_USED(argv); | ||
| 98 | REDISMODULE_NOT_USED(argc); | ||
| 99 | |||
| 100 | RedisModuleTimerID timer_id = | ||
| 101 | RedisModule_CreateTimer(ctx,100,timerNestedHandler,(void*)1); | ||
| 102 | REDISMODULE_NOT_USED(timer_id); | ||
| 103 | |||
| 104 | RedisModule_ReplyWithSimpleString(ctx,"OK"); | ||
| 105 | return REDISMODULE_OK; | ||
| 106 | } | ||
| 107 | |||
| 108 | void timerHandlerMaxmemory(RedisModuleCtx *ctx, void *data) { | ||
| 109 | REDISMODULE_NOT_USED(ctx); | ||
| 110 | REDISMODULE_NOT_USED(data); | ||
| 111 | |||
| 112 | RedisModuleCallReply *reply = RedisModule_Call(ctx,"SETEX","ccc!","timer-maxmemory-volatile-start","100","1"); | ||
| 113 | RedisModule_FreeCallReply(reply); | ||
| 114 | reply = RedisModule_Call(ctx, "CONFIG", "ccc!", "SET", "maxmemory", "1"); | ||
| 115 | RedisModule_FreeCallReply(reply); | ||
| 116 | |||
| 117 | RedisModule_Replicate(ctx, "INCR", "c", "timer-maxmemory-middle"); | ||
| 118 | |||
| 119 | reply = RedisModule_Call(ctx,"SETEX","ccc!","timer-maxmemory-volatile-end","100","1"); | ||
| 120 | RedisModule_FreeCallReply(reply); | ||
| 121 | } | ||
| 122 | |||
| 123 | int propagateTestTimerMaxmemoryCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 124 | { | ||
| 125 | REDISMODULE_NOT_USED(argv); | ||
| 126 | REDISMODULE_NOT_USED(argc); | ||
| 127 | |||
| 128 | RedisModuleTimerID timer_id = | ||
| 129 | RedisModule_CreateTimer(ctx,100,timerHandlerMaxmemory,(void*)1); | ||
| 130 | REDISMODULE_NOT_USED(timer_id); | ||
| 131 | |||
| 132 | RedisModule_ReplyWithSimpleString(ctx,"OK"); | ||
| 133 | return REDISMODULE_OK; | ||
| 134 | } | ||
| 135 | |||
| 136 | void timerHandlerEval(RedisModuleCtx *ctx, void *data) { | ||
| 137 | REDISMODULE_NOT_USED(ctx); | ||
| 138 | REDISMODULE_NOT_USED(data); | ||
| 139 | |||
| 140 | RedisModuleCallReply *reply = RedisModule_Call(ctx,"INCRBY","cc!","timer-eval-start","1"); | ||
| 141 | RedisModule_FreeCallReply(reply); | ||
| 142 | reply = RedisModule_Call(ctx, "EVAL", "cccc!", "redis.call('set',KEYS[1],ARGV[1])", "1", "foo", "bar"); | ||
| 143 | RedisModule_FreeCallReply(reply); | ||
| 144 | |||
| 145 | RedisModule_Replicate(ctx, "INCR", "c", "timer-eval-middle"); | ||
| 146 | |||
| 147 | reply = RedisModule_Call(ctx,"INCRBY","cc!","timer-eval-end","1"); | ||
| 148 | RedisModule_FreeCallReply(reply); | ||
| 149 | } | ||
| 150 | |||
| 151 | int propagateTestTimerEvalCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 152 | { | ||
| 153 | REDISMODULE_NOT_USED(argv); | ||
| 154 | REDISMODULE_NOT_USED(argc); | ||
| 155 | |||
| 156 | RedisModuleTimerID timer_id = | ||
| 157 | RedisModule_CreateTimer(ctx,100,timerHandlerEval,(void*)1); | ||
| 158 | REDISMODULE_NOT_USED(timer_id); | ||
| 159 | |||
| 160 | RedisModule_ReplyWithSimpleString(ctx,"OK"); | ||
| 161 | return REDISMODULE_OK; | ||
| 162 | } | ||
| 163 | |||
| 164 | /* The thread entry point. */ | ||
| 165 | void *threadMain(void *arg) { | ||
| 166 | REDISMODULE_NOT_USED(arg); | ||
| 167 | RedisModuleCtx *ctx = RedisModule_GetThreadSafeContext(NULL); | ||
| 168 | RedisModule_SelectDb(ctx,9); /* Tests ran in database number 9. */ | ||
| 169 | for (int i = 0; i < 3; i++) { | ||
| 170 | RedisModule_ThreadSafeContextLock(ctx); | ||
| 171 | RedisModule_Replicate(ctx,"INCR","c","a-from-thread"); | ||
| 172 | RedisModuleCallReply *reply = RedisModule_Call(ctx,"INCR","c!","thread-call"); | ||
| 173 | RedisModule_FreeCallReply(reply); | ||
| 174 | RedisModule_Replicate(ctx,"INCR","c","b-from-thread"); | ||
| 175 | RedisModule_ThreadSafeContextUnlock(ctx); | ||
| 176 | } | ||
| 177 | RedisModule_FreeThreadSafeContext(ctx); | ||
| 178 | return NULL; | ||
| 179 | } | ||
| 180 | |||
| 181 | int propagateTestThreadCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 182 | { | ||
| 183 | REDISMODULE_NOT_USED(argv); | ||
| 184 | REDISMODULE_NOT_USED(argc); | ||
| 185 | |||
| 186 | pthread_t tid; | ||
| 187 | if (pthread_create(&tid,NULL,threadMain,NULL) != 0) | ||
| 188 | return RedisModule_ReplyWithError(ctx,"-ERR Can't start thread"); | ||
| 189 | pthread_detach(tid); | ||
| 190 | |||
| 191 | RedisModule_ReplyWithSimpleString(ctx,"OK"); | ||
| 192 | return REDISMODULE_OK; | ||
| 193 | } | ||
| 194 | |||
| 195 | /* The thread entry point. */ | ||
| 196 | void *threadDetachedMain(void *arg) { | ||
| 197 | REDISMODULE_NOT_USED(arg); | ||
| 198 | RedisModule_SelectDb(detached_ctx,9); /* Tests ran in database number 9. */ | ||
| 199 | |||
| 200 | RedisModule_ThreadSafeContextLock(detached_ctx); | ||
| 201 | RedisModule_Replicate(detached_ctx,"INCR","c","thread-detached-before"); | ||
| 202 | RedisModuleCallReply *reply = RedisModule_Call(detached_ctx,"INCR","c!","thread-detached-1"); | ||
| 203 | RedisModule_FreeCallReply(reply); | ||
| 204 | reply = RedisModule_Call(detached_ctx,"INCR","c!","thread-detached-2"); | ||
| 205 | RedisModule_FreeCallReply(reply); | ||
| 206 | RedisModule_Replicate(detached_ctx,"INCR","c","thread-detached-after"); | ||
| 207 | RedisModule_ThreadSafeContextUnlock(detached_ctx); | ||
| 208 | |||
| 209 | return NULL; | ||
| 210 | } | ||
| 211 | |||
| 212 | int propagateTestDetachedThreadCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 213 | { | ||
| 214 | REDISMODULE_NOT_USED(argv); | ||
| 215 | REDISMODULE_NOT_USED(argc); | ||
| 216 | |||
| 217 | pthread_t tid; | ||
| 218 | if (pthread_create(&tid,NULL,threadDetachedMain,NULL) != 0) | ||
| 219 | return RedisModule_ReplyWithError(ctx,"-ERR Can't start thread"); | ||
| 220 | pthread_detach(tid); | ||
| 221 | |||
| 222 | RedisModule_ReplyWithSimpleString(ctx,"OK"); | ||
| 223 | return REDISMODULE_OK; | ||
| 224 | } | ||
| 225 | |||
| 226 | int propagateTestSimpleCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 227 | { | ||
| 228 | REDISMODULE_NOT_USED(argv); | ||
| 229 | REDISMODULE_NOT_USED(argc); | ||
| 230 | |||
| 231 | /* Replicate two commands to test MULTI/EXEC wrapping. */ | ||
| 232 | RedisModule_Replicate(ctx,"INCR","c","counter-1"); | ||
| 233 | RedisModule_Replicate(ctx,"INCR","c","counter-2"); | ||
| 234 | RedisModule_ReplyWithSimpleString(ctx,"OK"); | ||
| 235 | return REDISMODULE_OK; | ||
| 236 | } | ||
| 237 | |||
| 238 | int propagateTestMixedCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 239 | { | ||
| 240 | REDISMODULE_NOT_USED(argv); | ||
| 241 | REDISMODULE_NOT_USED(argc); | ||
| 242 | RedisModuleCallReply *reply; | ||
| 243 | |||
| 244 | /* This test mixes multiple propagation systems. */ | ||
| 245 | reply = RedisModule_Call(ctx, "INCR", "c!", "using-call"); | ||
| 246 | RedisModule_FreeCallReply(reply); | ||
| 247 | |||
| 248 | RedisModule_Replicate(ctx,"INCR","c","counter-1"); | ||
| 249 | RedisModule_Replicate(ctx,"INCR","c","counter-2"); | ||
| 250 | |||
| 251 | reply = RedisModule_Call(ctx, "INCR", "c!", "after-call"); | ||
| 252 | RedisModule_FreeCallReply(reply); | ||
| 253 | |||
| 254 | RedisModule_ReplyWithSimpleString(ctx,"OK"); | ||
| 255 | return REDISMODULE_OK; | ||
| 256 | } | ||
| 257 | |||
| 258 | int propagateTestNestedCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 259 | { | ||
| 260 | REDISMODULE_NOT_USED(argv); | ||
| 261 | REDISMODULE_NOT_USED(argc); | ||
| 262 | RedisModuleCallReply *reply; | ||
| 263 | |||
| 264 | /* This test mixes multiple propagation systems. */ | ||
| 265 | reply = RedisModule_Call(ctx, "INCR", "c!", "using-call"); | ||
| 266 | RedisModule_FreeCallReply(reply); | ||
| 267 | |||
| 268 | reply = RedisModule_Call(ctx,"propagate-test.simple", "!"); | ||
| 269 | RedisModule_FreeCallReply(reply); | ||
| 270 | |||
| 271 | RedisModule_Replicate(ctx,"INCR","c","counter-3"); | ||
| 272 | RedisModule_Replicate(ctx,"INCR","c","counter-4"); | ||
| 273 | |||
| 274 | reply = RedisModule_Call(ctx, "INCR", "c!", "after-call"); | ||
| 275 | RedisModule_FreeCallReply(reply); | ||
| 276 | |||
| 277 | reply = RedisModule_Call(ctx, "INCR", "c!", "before-call-2"); | ||
| 278 | RedisModule_FreeCallReply(reply); | ||
| 279 | |||
| 280 | reply = RedisModule_Call(ctx, "keyspace.incr_case1", "c!", "asdf"); /* Propagates INCR */ | ||
| 281 | RedisModule_FreeCallReply(reply); | ||
| 282 | |||
| 283 | reply = RedisModule_Call(ctx, "keyspace.del_key_copy", "c!", "asdf"); /* Propagates DEL */ | ||
| 284 | RedisModule_FreeCallReply(reply); | ||
| 285 | |||
| 286 | reply = RedisModule_Call(ctx, "INCR", "c!", "after-call-2"); | ||
| 287 | RedisModule_FreeCallReply(reply); | ||
| 288 | |||
| 289 | RedisModule_ReplyWithSimpleString(ctx,"OK"); | ||
| 290 | return REDISMODULE_OK; | ||
| 291 | } | ||
| 292 | |||
| 293 | int propagateTestIncr(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 294 | { | ||
| 295 | REDISMODULE_NOT_USED(argc); | ||
| 296 | RedisModuleCallReply *reply; | ||
| 297 | |||
| 298 | /* This test propagates the module command, not the INCR it executes. */ | ||
| 299 | reply = RedisModule_Call(ctx, "INCR", "s", argv[1]); | ||
| 300 | RedisModule_ReplyWithCallReply(ctx,reply); | ||
| 301 | RedisModule_FreeCallReply(reply); | ||
| 302 | RedisModule_ReplicateVerbatim(ctx); | ||
| 303 | return REDISMODULE_OK; | ||
| 304 | } | ||
| 305 | |||
| 306 | int propagateTestVerbatim(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 307 | if (argc < 2){ | ||
| 308 | RedisModule_WrongArity(ctx); | ||
| 309 | return REDISMODULE_OK; | ||
| 310 | } | ||
| 311 | |||
| 312 | long long replicate_num; | ||
| 313 | RedisModule_StringToLongLong(argv[1], &replicate_num); | ||
| 314 | /* Replicate the command verbatim for the specified number of times. */ | ||
| 315 | for (long long i = 0; i < replicate_num; i++) | ||
| 316 | RedisModule_ReplicateVerbatim(ctx); | ||
| 317 | RedisModule_ReplyWithSimpleString(ctx,"OK"); | ||
| 318 | return REDISMODULE_OK; | ||
| 319 | } | ||
| 320 | |||
| 321 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 322 | REDISMODULE_NOT_USED(argv); | ||
| 323 | REDISMODULE_NOT_USED(argc); | ||
| 324 | |||
| 325 | if (RedisModule_Init(ctx,"propagate-test",1,REDISMODULE_APIVER_1) | ||
| 326 | == REDISMODULE_ERR) return REDISMODULE_ERR; | ||
| 327 | |||
| 328 | detached_ctx = RedisModule_GetDetachedThreadSafeContext(ctx); | ||
| 329 | |||
| 330 | if (RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_ALL, KeySpace_NotificationGeneric) == REDISMODULE_ERR) | ||
| 331 | return REDISMODULE_ERR; | ||
| 332 | |||
| 333 | if (RedisModule_CreateCommand(ctx,"propagate-test.timer", | ||
| 334 | propagateTestTimerCommand, | ||
| 335 | "",1,1,1) == REDISMODULE_ERR) | ||
| 336 | return REDISMODULE_ERR; | ||
| 337 | |||
| 338 | if (RedisModule_CreateCommand(ctx,"propagate-test.timer-nested", | ||
| 339 | propagateTestTimerNestedCommand, | ||
| 340 | "",1,1,1) == REDISMODULE_ERR) | ||
| 341 | return REDISMODULE_ERR; | ||
| 342 | |||
| 343 | if (RedisModule_CreateCommand(ctx,"propagate-test.timer-nested-repl", | ||
| 344 | propagateTestTimerNestedReplCommand, | ||
| 345 | "",1,1,1) == REDISMODULE_ERR) | ||
| 346 | return REDISMODULE_ERR; | ||
| 347 | |||
| 348 | if (RedisModule_CreateCommand(ctx,"propagate-test.timer-maxmemory", | ||
| 349 | propagateTestTimerMaxmemoryCommand, | ||
| 350 | "",1,1,1) == REDISMODULE_ERR) | ||
| 351 | return REDISMODULE_ERR; | ||
| 352 | |||
| 353 | if (RedisModule_CreateCommand(ctx,"propagate-test.timer-eval", | ||
| 354 | propagateTestTimerEvalCommand, | ||
| 355 | "",1,1,1) == REDISMODULE_ERR) | ||
| 356 | return REDISMODULE_ERR; | ||
| 357 | |||
| 358 | if (RedisModule_CreateCommand(ctx,"propagate-test.thread", | ||
| 359 | propagateTestThreadCommand, | ||
| 360 | "",1,1,1) == REDISMODULE_ERR) | ||
| 361 | return REDISMODULE_ERR; | ||
| 362 | |||
| 363 | if (RedisModule_CreateCommand(ctx,"propagate-test.detached-thread", | ||
| 364 | propagateTestDetachedThreadCommand, | ||
| 365 | "",1,1,1) == REDISMODULE_ERR) | ||
| 366 | return REDISMODULE_ERR; | ||
| 367 | |||
| 368 | if (RedisModule_CreateCommand(ctx,"propagate-test.simple", | ||
| 369 | propagateTestSimpleCommand, | ||
| 370 | "",1,1,1) == REDISMODULE_ERR) | ||
| 371 | return REDISMODULE_ERR; | ||
| 372 | |||
| 373 | if (RedisModule_CreateCommand(ctx,"propagate-test.mixed", | ||
| 374 | propagateTestMixedCommand, | ||
| 375 | "write",1,1,1) == REDISMODULE_ERR) | ||
| 376 | return REDISMODULE_ERR; | ||
| 377 | |||
| 378 | if (RedisModule_CreateCommand(ctx,"propagate-test.nested", | ||
| 379 | propagateTestNestedCommand, | ||
| 380 | "write",1,1,1) == REDISMODULE_ERR) | ||
| 381 | return REDISMODULE_ERR; | ||
| 382 | |||
| 383 | if (RedisModule_CreateCommand(ctx,"propagate-test.incr", | ||
| 384 | propagateTestIncr, | ||
| 385 | "write",1,1,1) == REDISMODULE_ERR) | ||
| 386 | return REDISMODULE_ERR; | ||
| 387 | |||
| 388 | if (RedisModule_CreateCommand(ctx,"propagate-test.verbatim", | ||
| 389 | propagateTestVerbatim, | ||
| 390 | "write",1,1,1) == REDISMODULE_ERR) | ||
| 391 | return REDISMODULE_ERR; | ||
| 392 | |||
| 393 | return REDISMODULE_OK; | ||
| 394 | } | ||
| 395 | |||
| 396 | int RedisModule_OnUnload(RedisModuleCtx *ctx) { | ||
| 397 | UNUSED(ctx); | ||
| 398 | |||
| 399 | if (detached_ctx) | ||
| 400 | RedisModule_FreeThreadSafeContext(detached_ctx); | ||
| 401 | |||
| 402 | return REDISMODULE_OK; | ||
| 403 | } | ||
diff --git a/examples/redis-unstable/tests/modules/publish.c b/examples/redis-unstable/tests/modules/publish.c new file mode 100644 index 0000000..ff276d8 --- /dev/null +++ b/examples/redis-unstable/tests/modules/publish.c | |||
| @@ -0,0 +1,57 @@ | |||
| 1 | #include "redismodule.h" | ||
| 2 | #include <string.h> | ||
| 3 | #include <assert.h> | ||
| 4 | #include <unistd.h> | ||
| 5 | |||
| 6 | #define UNUSED(V) ((void) V) | ||
| 7 | |||
| 8 | int cmd_publish_classic_multi(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 9 | { | ||
| 10 | if (argc < 3) | ||
| 11 | return RedisModule_WrongArity(ctx); | ||
| 12 | RedisModule_ReplyWithArray(ctx, argc-2); | ||
| 13 | for (int i = 2; i < argc; i++) { | ||
| 14 | int receivers = RedisModule_PublishMessage(ctx, argv[1], argv[i]); | ||
| 15 | RedisModule_ReplyWithLongLong(ctx, receivers); | ||
| 16 | } | ||
| 17 | return REDISMODULE_OK; | ||
| 18 | } | ||
| 19 | |||
| 20 | int cmd_publish_classic(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 21 | { | ||
| 22 | if (argc != 3) | ||
| 23 | return RedisModule_WrongArity(ctx); | ||
| 24 | |||
| 25 | int receivers = RedisModule_PublishMessage(ctx, argv[1], argv[2]); | ||
| 26 | RedisModule_ReplyWithLongLong(ctx, receivers); | ||
| 27 | return REDISMODULE_OK; | ||
| 28 | } | ||
| 29 | |||
| 30 | int cmd_publish_shard(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 31 | { | ||
| 32 | if (argc != 3) | ||
| 33 | return RedisModule_WrongArity(ctx); | ||
| 34 | |||
| 35 | int receivers = RedisModule_PublishMessageShard(ctx, argv[1], argv[2]); | ||
| 36 | RedisModule_ReplyWithLongLong(ctx, receivers); | ||
| 37 | return REDISMODULE_OK; | ||
| 38 | } | ||
| 39 | |||
| 40 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 41 | UNUSED(argv); | ||
| 42 | UNUSED(argc); | ||
| 43 | |||
| 44 | if (RedisModule_Init(ctx,"publish",1,REDISMODULE_APIVER_1)== REDISMODULE_ERR) | ||
| 45 | return REDISMODULE_ERR; | ||
| 46 | |||
| 47 | if (RedisModule_CreateCommand(ctx,"publish.classic",cmd_publish_classic,"",0,0,0) == REDISMODULE_ERR) | ||
| 48 | return REDISMODULE_ERR; | ||
| 49 | |||
| 50 | if (RedisModule_CreateCommand(ctx,"publish.classic_multi",cmd_publish_classic_multi,"",0,0,0) == REDISMODULE_ERR) | ||
| 51 | return REDISMODULE_ERR; | ||
| 52 | |||
| 53 | if (RedisModule_CreateCommand(ctx,"publish.shard",cmd_publish_shard,"",0,0,0) == REDISMODULE_ERR) | ||
| 54 | return REDISMODULE_ERR; | ||
| 55 | |||
| 56 | return REDISMODULE_OK; | ||
| 57 | } | ||
diff --git a/examples/redis-unstable/tests/modules/rdbloadsave.c b/examples/redis-unstable/tests/modules/rdbloadsave.c new file mode 100644 index 0000000..687269a --- /dev/null +++ b/examples/redis-unstable/tests/modules/rdbloadsave.c | |||
| @@ -0,0 +1,162 @@ | |||
| 1 | #include "redismodule.h" | ||
| 2 | |||
| 3 | #include <stdlib.h> | ||
| 4 | #include <unistd.h> | ||
| 5 | #include <fcntl.h> | ||
| 6 | #include <memory.h> | ||
| 7 | #include <errno.h> | ||
| 8 | |||
| 9 | /* Sanity tests to verify inputs and return values. */ | ||
| 10 | int sanity(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 11 | REDISMODULE_NOT_USED(argv); | ||
| 12 | REDISMODULE_NOT_USED(argc); | ||
| 13 | |||
| 14 | RedisModuleRdbStream *s = RedisModule_RdbStreamCreateFromFile("dbnew.rdb"); | ||
| 15 | |||
| 16 | /* NULL stream should fail. */ | ||
| 17 | if (RedisModule_RdbLoad(ctx, NULL, 0) == REDISMODULE_OK || errno != EINVAL) { | ||
| 18 | RedisModule_ReplyWithError(ctx, strerror(errno)); | ||
| 19 | goto out; | ||
| 20 | } | ||
| 21 | |||
| 22 | /* Invalid flags should fail. */ | ||
| 23 | if (RedisModule_RdbLoad(ctx, s, 188) == REDISMODULE_OK || errno != EINVAL) { | ||
| 24 | RedisModule_ReplyWithError(ctx, strerror(errno)); | ||
| 25 | goto out; | ||
| 26 | } | ||
| 27 | |||
| 28 | /* Missing file should fail. */ | ||
| 29 | if (RedisModule_RdbLoad(ctx, s, 0) == REDISMODULE_OK || errno != ENOENT) { | ||
| 30 | RedisModule_ReplyWithError(ctx, strerror(errno)); | ||
| 31 | goto out; | ||
| 32 | } | ||
| 33 | |||
| 34 | /* Save RDB file. */ | ||
| 35 | if (RedisModule_RdbSave(ctx, s, 0) != REDISMODULE_OK || errno != 0) { | ||
| 36 | RedisModule_ReplyWithError(ctx, strerror(errno)); | ||
| 37 | goto out; | ||
| 38 | } | ||
| 39 | |||
| 40 | /* Load the saved RDB file. */ | ||
| 41 | if (RedisModule_RdbLoad(ctx, s, 0) != REDISMODULE_OK || errno != 0) { | ||
| 42 | RedisModule_ReplyWithError(ctx, strerror(errno)); | ||
| 43 | goto out; | ||
| 44 | } | ||
| 45 | |||
| 46 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 47 | |||
| 48 | out: | ||
| 49 | RedisModule_RdbStreamFree(s); | ||
| 50 | return REDISMODULE_OK; | ||
| 51 | } | ||
| 52 | |||
| 53 | int cmd_rdbsave(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 54 | if (argc != 2) { | ||
| 55 | RedisModule_WrongArity(ctx); | ||
| 56 | return REDISMODULE_OK; | ||
| 57 | } | ||
| 58 | |||
| 59 | size_t len; | ||
| 60 | const char *filename = RedisModule_StringPtrLen(argv[1], &len); | ||
| 61 | |||
| 62 | char tmp[len + 1]; | ||
| 63 | memcpy(tmp, filename, len); | ||
| 64 | tmp[len] = '\0'; | ||
| 65 | |||
| 66 | RedisModuleRdbStream *stream = RedisModule_RdbStreamCreateFromFile(tmp); | ||
| 67 | |||
| 68 | if (RedisModule_RdbSave(ctx, stream, 0) != REDISMODULE_OK || errno != 0) { | ||
| 69 | RedisModule_ReplyWithError(ctx, strerror(errno)); | ||
| 70 | goto out; | ||
| 71 | } | ||
| 72 | |||
| 73 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 74 | |||
| 75 | out: | ||
| 76 | RedisModule_RdbStreamFree(stream); | ||
| 77 | return REDISMODULE_OK; | ||
| 78 | } | ||
| 79 | |||
| 80 | /* Fork before calling RM_RdbSave(). */ | ||
| 81 | int cmd_rdbsave_fork(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 82 | if (argc != 2) { | ||
| 83 | RedisModule_WrongArity(ctx); | ||
| 84 | return REDISMODULE_OK; | ||
| 85 | } | ||
| 86 | |||
| 87 | size_t len; | ||
| 88 | const char *filename = RedisModule_StringPtrLen(argv[1], &len); | ||
| 89 | |||
| 90 | char tmp[len + 1]; | ||
| 91 | memcpy(tmp, filename, len); | ||
| 92 | tmp[len] = '\0'; | ||
| 93 | |||
| 94 | int fork_child_pid = RedisModule_Fork(NULL, NULL); | ||
| 95 | if (fork_child_pid < 0) { | ||
| 96 | RedisModule_ReplyWithError(ctx, strerror(errno)); | ||
| 97 | return REDISMODULE_OK; | ||
| 98 | } else if (fork_child_pid > 0) { | ||
| 99 | /* parent */ | ||
| 100 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 101 | return REDISMODULE_OK; | ||
| 102 | } | ||
| 103 | |||
| 104 | RedisModuleRdbStream *stream = RedisModule_RdbStreamCreateFromFile(tmp); | ||
| 105 | |||
| 106 | int ret = 0; | ||
| 107 | if (RedisModule_RdbSave(ctx, stream, 0) != REDISMODULE_OK) { | ||
| 108 | ret = errno; | ||
| 109 | } | ||
| 110 | RedisModule_RdbStreamFree(stream); | ||
| 111 | |||
| 112 | RedisModule_ExitFromChild(ret); | ||
| 113 | return REDISMODULE_OK; | ||
| 114 | } | ||
| 115 | |||
| 116 | int cmd_rdbload(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 117 | if (argc != 2) { | ||
| 118 | RedisModule_WrongArity(ctx); | ||
| 119 | return REDISMODULE_OK; | ||
| 120 | } | ||
| 121 | |||
| 122 | size_t len; | ||
| 123 | const char *filename = RedisModule_StringPtrLen(argv[1], &len); | ||
| 124 | |||
| 125 | char tmp[len + 1]; | ||
| 126 | memcpy(tmp, filename, len); | ||
| 127 | tmp[len] = '\0'; | ||
| 128 | |||
| 129 | RedisModuleRdbStream *stream = RedisModule_RdbStreamCreateFromFile(tmp); | ||
| 130 | |||
| 131 | if (RedisModule_RdbLoad(ctx, stream, 0) != REDISMODULE_OK || errno != 0) { | ||
| 132 | RedisModule_RdbStreamFree(stream); | ||
| 133 | RedisModule_ReplyWithError(ctx, strerror(errno)); | ||
| 134 | return REDISMODULE_OK; | ||
| 135 | } | ||
| 136 | |||
| 137 | RedisModule_RdbStreamFree(stream); | ||
| 138 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 139 | return REDISMODULE_OK; | ||
| 140 | } | ||
| 141 | |||
| 142 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 143 | REDISMODULE_NOT_USED(argv); | ||
| 144 | REDISMODULE_NOT_USED(argc); | ||
| 145 | |||
| 146 | if (RedisModule_Init(ctx, "rdbloadsave", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) | ||
| 147 | return REDISMODULE_ERR; | ||
| 148 | |||
| 149 | if (RedisModule_CreateCommand(ctx, "test.sanity", sanity, "", 0, 0, 0) == REDISMODULE_ERR) | ||
| 150 | return REDISMODULE_ERR; | ||
| 151 | |||
| 152 | if (RedisModule_CreateCommand(ctx, "test.rdbsave", cmd_rdbsave, "", 0, 0, 0) == REDISMODULE_ERR) | ||
| 153 | return REDISMODULE_ERR; | ||
| 154 | |||
| 155 | if (RedisModule_CreateCommand(ctx, "test.rdbsave_fork", cmd_rdbsave_fork, "", 0, 0, 0) == REDISMODULE_ERR) | ||
| 156 | return REDISMODULE_ERR; | ||
| 157 | |||
| 158 | if (RedisModule_CreateCommand(ctx, "test.rdbload", cmd_rdbload, "", 0, 0, 0) == REDISMODULE_ERR) | ||
| 159 | return REDISMODULE_ERR; | ||
| 160 | |||
| 161 | return REDISMODULE_OK; | ||
| 162 | } | ||
diff --git a/examples/redis-unstable/tests/modules/reply.c b/examples/redis-unstable/tests/modules/reply.c new file mode 100644 index 0000000..c5baa66 --- /dev/null +++ b/examples/redis-unstable/tests/modules/reply.c | |||
| @@ -0,0 +1,214 @@ | |||
| 1 | /* | ||
| 2 | * A module the tests RM_ReplyWith family of commands | ||
| 3 | */ | ||
| 4 | |||
| 5 | #include "redismodule.h" | ||
| 6 | #include <math.h> | ||
| 7 | |||
| 8 | int rw_string(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 9 | if (argc != 2) return RedisModule_WrongArity(ctx); | ||
| 10 | |||
| 11 | return RedisModule_ReplyWithString(ctx, argv[1]); | ||
| 12 | } | ||
| 13 | |||
| 14 | int rw_cstring(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 15 | REDISMODULE_NOT_USED(argv); | ||
| 16 | if (argc != 1) return RedisModule_WrongArity(ctx); | ||
| 17 | |||
| 18 | return RedisModule_ReplyWithSimpleString(ctx, "A simple string"); | ||
| 19 | } | ||
| 20 | |||
| 21 | int rw_int(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 22 | if (argc != 2) return RedisModule_WrongArity(ctx); | ||
| 23 | |||
| 24 | long long integer; | ||
| 25 | if (RedisModule_StringToLongLong(argv[1], &integer) != REDISMODULE_OK) | ||
| 26 | return RedisModule_ReplyWithError(ctx, "Arg cannot be parsed as an integer"); | ||
| 27 | |||
| 28 | return RedisModule_ReplyWithLongLong(ctx, integer); | ||
| 29 | } | ||
| 30 | |||
| 31 | /* When one argument is given, it is returned as a double, | ||
| 32 | * when two arguments are given, it returns a/b. */ | ||
| 33 | int rw_double(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 34 | if (argc==1) | ||
| 35 | return RedisModule_ReplyWithDouble(ctx, NAN); | ||
| 36 | |||
| 37 | if (argc != 2 && argc != 3) return RedisModule_WrongArity(ctx); | ||
| 38 | |||
| 39 | double dbl, dbl2; | ||
| 40 | if (RedisModule_StringToDouble(argv[1], &dbl) != REDISMODULE_OK) | ||
| 41 | return RedisModule_ReplyWithError(ctx, "Arg cannot be parsed as a double"); | ||
| 42 | if (argc == 3) { | ||
| 43 | if (RedisModule_StringToDouble(argv[2], &dbl2) != REDISMODULE_OK) | ||
| 44 | return RedisModule_ReplyWithError(ctx, "Arg cannot be parsed as a double"); | ||
| 45 | dbl /= dbl2; | ||
| 46 | } | ||
| 47 | |||
| 48 | return RedisModule_ReplyWithDouble(ctx, dbl); | ||
| 49 | } | ||
| 50 | |||
| 51 | int rw_longdouble(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 52 | if (argc != 2) return RedisModule_WrongArity(ctx); | ||
| 53 | |||
| 54 | long double longdbl; | ||
| 55 | if (RedisModule_StringToLongDouble(argv[1], &longdbl) != REDISMODULE_OK) | ||
| 56 | return RedisModule_ReplyWithError(ctx, "Arg cannot be parsed as a double"); | ||
| 57 | |||
| 58 | return RedisModule_ReplyWithLongDouble(ctx, longdbl); | ||
| 59 | } | ||
| 60 | |||
| 61 | int rw_bignumber(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 62 | if (argc != 2) return RedisModule_WrongArity(ctx); | ||
| 63 | |||
| 64 | size_t bignum_len; | ||
| 65 | const char *bignum_str = RedisModule_StringPtrLen(argv[1], &bignum_len); | ||
| 66 | |||
| 67 | return RedisModule_ReplyWithBigNumber(ctx, bignum_str, bignum_len); | ||
| 68 | } | ||
| 69 | |||
| 70 | int rw_array(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 71 | if (argc != 2) return RedisModule_WrongArity(ctx); | ||
| 72 | |||
| 73 | long long integer; | ||
| 74 | if (RedisModule_StringToLongLong(argv[1], &integer) != REDISMODULE_OK) | ||
| 75 | return RedisModule_ReplyWithError(ctx, "Arg cannot be parsed as a integer"); | ||
| 76 | |||
| 77 | RedisModule_ReplyWithArray(ctx, integer); | ||
| 78 | for (int i = 0; i < integer; ++i) { | ||
| 79 | RedisModule_ReplyWithLongLong(ctx, i); | ||
| 80 | } | ||
| 81 | |||
| 82 | return REDISMODULE_OK; | ||
| 83 | } | ||
| 84 | |||
| 85 | int rw_map(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 86 | if (argc != 2) return RedisModule_WrongArity(ctx); | ||
| 87 | |||
| 88 | long long integer; | ||
| 89 | if (RedisModule_StringToLongLong(argv[1], &integer) != REDISMODULE_OK) | ||
| 90 | return RedisModule_ReplyWithError(ctx, "Arg cannot be parsed as a integer"); | ||
| 91 | |||
| 92 | RedisModule_ReplyWithMap(ctx, integer); | ||
| 93 | for (int i = 0; i < integer; ++i) { | ||
| 94 | RedisModule_ReplyWithLongLong(ctx, i); | ||
| 95 | RedisModule_ReplyWithDouble(ctx, i * 1.5); | ||
| 96 | } | ||
| 97 | |||
| 98 | return REDISMODULE_OK; | ||
| 99 | } | ||
| 100 | |||
| 101 | int rw_set(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 102 | if (argc != 2) return RedisModule_WrongArity(ctx); | ||
| 103 | |||
| 104 | long long integer; | ||
| 105 | if (RedisModule_StringToLongLong(argv[1], &integer) != REDISMODULE_OK) | ||
| 106 | return RedisModule_ReplyWithError(ctx, "Arg cannot be parsed as a integer"); | ||
| 107 | |||
| 108 | RedisModule_ReplyWithSet(ctx, integer); | ||
| 109 | for (int i = 0; i < integer; ++i) { | ||
| 110 | RedisModule_ReplyWithLongLong(ctx, i); | ||
| 111 | } | ||
| 112 | |||
| 113 | return REDISMODULE_OK; | ||
| 114 | } | ||
| 115 | |||
| 116 | int rw_attribute(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 117 | if (argc != 2) return RedisModule_WrongArity(ctx); | ||
| 118 | |||
| 119 | long long integer; | ||
| 120 | if (RedisModule_StringToLongLong(argv[1], &integer) != REDISMODULE_OK) | ||
| 121 | return RedisModule_ReplyWithError(ctx, "Arg cannot be parsed as a integer"); | ||
| 122 | |||
| 123 | if (RedisModule_ReplyWithAttribute(ctx, integer) != REDISMODULE_OK) { | ||
| 124 | return RedisModule_ReplyWithError(ctx, "Attributes aren't supported by RESP 2"); | ||
| 125 | } | ||
| 126 | |||
| 127 | for (int i = 0; i < integer; ++i) { | ||
| 128 | RedisModule_ReplyWithLongLong(ctx, i); | ||
| 129 | RedisModule_ReplyWithDouble(ctx, i * 1.5); | ||
| 130 | } | ||
| 131 | |||
| 132 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 133 | return REDISMODULE_OK; | ||
| 134 | } | ||
| 135 | |||
| 136 | int rw_bool(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 137 | REDISMODULE_NOT_USED(argv); | ||
| 138 | if (argc != 1) return RedisModule_WrongArity(ctx); | ||
| 139 | |||
| 140 | RedisModule_ReplyWithArray(ctx, 2); | ||
| 141 | RedisModule_ReplyWithBool(ctx, 0); | ||
| 142 | return RedisModule_ReplyWithBool(ctx, 1); | ||
| 143 | } | ||
| 144 | |||
| 145 | int rw_null(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 146 | REDISMODULE_NOT_USED(argv); | ||
| 147 | if (argc != 1) return RedisModule_WrongArity(ctx); | ||
| 148 | |||
| 149 | return RedisModule_ReplyWithNull(ctx); | ||
| 150 | } | ||
| 151 | |||
| 152 | int rw_error(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 153 | REDISMODULE_NOT_USED(argv); | ||
| 154 | if (argc != 1) return RedisModule_WrongArity(ctx); | ||
| 155 | |||
| 156 | return RedisModule_ReplyWithError(ctx, "An error"); | ||
| 157 | } | ||
| 158 | |||
| 159 | int rw_error_format(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 160 | if (argc != 3) return RedisModule_WrongArity(ctx); | ||
| 161 | |||
| 162 | return RedisModule_ReplyWithErrorFormat(ctx, | ||
| 163 | RedisModule_StringPtrLen(argv[1], NULL), | ||
| 164 | RedisModule_StringPtrLen(argv[2], NULL)); | ||
| 165 | } | ||
| 166 | |||
| 167 | int rw_verbatim(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 168 | if (argc != 2) return RedisModule_WrongArity(ctx); | ||
| 169 | |||
| 170 | size_t verbatim_len; | ||
| 171 | const char *verbatim_str = RedisModule_StringPtrLen(argv[1], &verbatim_len); | ||
| 172 | |||
| 173 | return RedisModule_ReplyWithVerbatimString(ctx, verbatim_str, verbatim_len); | ||
| 174 | } | ||
| 175 | |||
| 176 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 177 | REDISMODULE_NOT_USED(argv); | ||
| 178 | REDISMODULE_NOT_USED(argc); | ||
| 179 | if (RedisModule_Init(ctx, "replywith", 1, REDISMODULE_APIVER_1) != REDISMODULE_OK) | ||
| 180 | return REDISMODULE_ERR; | ||
| 181 | |||
| 182 | if (RedisModule_CreateCommand(ctx,"rw.string",rw_string,"",0,0,0) != REDISMODULE_OK) | ||
| 183 | return REDISMODULE_ERR; | ||
| 184 | if (RedisModule_CreateCommand(ctx,"rw.cstring",rw_cstring,"",0,0,0) != REDISMODULE_OK) | ||
| 185 | return REDISMODULE_ERR; | ||
| 186 | if (RedisModule_CreateCommand(ctx,"rw.bignumber",rw_bignumber,"",0,0,0) != REDISMODULE_OK) | ||
| 187 | return REDISMODULE_ERR; | ||
| 188 | if (RedisModule_CreateCommand(ctx,"rw.int",rw_int,"",0,0,0) != REDISMODULE_OK) | ||
| 189 | return REDISMODULE_ERR; | ||
| 190 | if (RedisModule_CreateCommand(ctx,"rw.double",rw_double,"",0,0,0) != REDISMODULE_OK) | ||
| 191 | return REDISMODULE_ERR; | ||
| 192 | if (RedisModule_CreateCommand(ctx,"rw.longdouble",rw_longdouble,"",0,0,0) != REDISMODULE_OK) | ||
| 193 | return REDISMODULE_ERR; | ||
| 194 | if (RedisModule_CreateCommand(ctx,"rw.array",rw_array,"",0,0,0) != REDISMODULE_OK) | ||
| 195 | return REDISMODULE_ERR; | ||
| 196 | if (RedisModule_CreateCommand(ctx,"rw.map",rw_map,"",0,0,0) != REDISMODULE_OK) | ||
| 197 | return REDISMODULE_ERR; | ||
| 198 | if (RedisModule_CreateCommand(ctx,"rw.attribute",rw_attribute,"",0,0,0) != REDISMODULE_OK) | ||
| 199 | return REDISMODULE_ERR; | ||
| 200 | if (RedisModule_CreateCommand(ctx,"rw.set",rw_set,"",0,0,0) != REDISMODULE_OK) | ||
| 201 | return REDISMODULE_ERR; | ||
| 202 | if (RedisModule_CreateCommand(ctx,"rw.bool",rw_bool,"",0,0,0) != REDISMODULE_OK) | ||
| 203 | return REDISMODULE_ERR; | ||
| 204 | if (RedisModule_CreateCommand(ctx,"rw.null",rw_null,"",0,0,0) != REDISMODULE_OK) | ||
| 205 | return REDISMODULE_ERR; | ||
| 206 | if (RedisModule_CreateCommand(ctx,"rw.error",rw_error,"",0,0,0) != REDISMODULE_OK) | ||
| 207 | return REDISMODULE_ERR; | ||
| 208 | if (RedisModule_CreateCommand(ctx,"rw.error_format",rw_error_format,"",0,0,0) != REDISMODULE_OK) | ||
| 209 | return REDISMODULE_ERR; | ||
| 210 | if (RedisModule_CreateCommand(ctx,"rw.verbatim",rw_verbatim,"",0,0,0) != REDISMODULE_OK) | ||
| 211 | return REDISMODULE_ERR; | ||
| 212 | |||
| 213 | return REDISMODULE_OK; | ||
| 214 | } | ||
diff --git a/examples/redis-unstable/tests/modules/scan.c b/examples/redis-unstable/tests/modules/scan.c new file mode 100644 index 0000000..1723d30 --- /dev/null +++ b/examples/redis-unstable/tests/modules/scan.c | |||
| @@ -0,0 +1,121 @@ | |||
| 1 | #include "redismodule.h" | ||
| 2 | |||
| 3 | #include <string.h> | ||
| 4 | #include <assert.h> | ||
| 5 | #include <unistd.h> | ||
| 6 | |||
| 7 | typedef struct { | ||
| 8 | size_t nkeys; | ||
| 9 | } scan_strings_pd; | ||
| 10 | |||
| 11 | void scan_strings_callback(RedisModuleCtx *ctx, RedisModuleString* keyname, RedisModuleKey* key, void *privdata) { | ||
| 12 | scan_strings_pd* pd = privdata; | ||
| 13 | int was_opened = 0; | ||
| 14 | if (!key) { | ||
| 15 | key = RedisModule_OpenKey(ctx, keyname, REDISMODULE_READ); | ||
| 16 | was_opened = 1; | ||
| 17 | } | ||
| 18 | |||
| 19 | if (RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_STRING) { | ||
| 20 | size_t len; | ||
| 21 | char * data = RedisModule_StringDMA(key, &len, REDISMODULE_READ); | ||
| 22 | RedisModule_ReplyWithArray(ctx, 2); | ||
| 23 | RedisModule_ReplyWithString(ctx, keyname); | ||
| 24 | RedisModule_ReplyWithStringBuffer(ctx, data, len); | ||
| 25 | pd->nkeys++; | ||
| 26 | } | ||
| 27 | if (was_opened) | ||
| 28 | RedisModule_CloseKey(key); | ||
| 29 | } | ||
| 30 | |||
| 31 | int scan_strings(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 32 | { | ||
| 33 | REDISMODULE_NOT_USED(argv); | ||
| 34 | REDISMODULE_NOT_USED(argc); | ||
| 35 | scan_strings_pd pd = { | ||
| 36 | .nkeys = 0, | ||
| 37 | }; | ||
| 38 | |||
| 39 | RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_LEN); | ||
| 40 | |||
| 41 | RedisModuleScanCursor* cursor = RedisModule_ScanCursorCreate(); | ||
| 42 | while(RedisModule_Scan(ctx, cursor, scan_strings_callback, &pd)); | ||
| 43 | RedisModule_ScanCursorDestroy(cursor); | ||
| 44 | |||
| 45 | RedisModule_ReplySetArrayLength(ctx, pd.nkeys); | ||
| 46 | return REDISMODULE_OK; | ||
| 47 | } | ||
| 48 | |||
| 49 | typedef struct { | ||
| 50 | RedisModuleCtx *ctx; | ||
| 51 | size_t nreplies; | ||
| 52 | } scan_key_pd; | ||
| 53 | |||
| 54 | void scan_key_callback(RedisModuleKey *key, RedisModuleString* field, RedisModuleString* value, void *privdata) { | ||
| 55 | REDISMODULE_NOT_USED(key); | ||
| 56 | scan_key_pd* pd = privdata; | ||
| 57 | RedisModule_ReplyWithArray(pd->ctx, 2); | ||
| 58 | size_t fieldCStrLen; | ||
| 59 | |||
| 60 | // The implementation of RedisModuleString is robj with lots of encodings. | ||
| 61 | // We want to make sure the robj that passes to this callback in | ||
| 62 | // String encoded, this is why we use RedisModule_StringPtrLen and | ||
| 63 | // RedisModule_ReplyWithStringBuffer instead of directly use | ||
| 64 | // RedisModule_ReplyWithString. | ||
| 65 | const char* fieldCStr = RedisModule_StringPtrLen(field, &fieldCStrLen); | ||
| 66 | RedisModule_ReplyWithStringBuffer(pd->ctx, fieldCStr, fieldCStrLen); | ||
| 67 | if(value){ | ||
| 68 | size_t valueCStrLen; | ||
| 69 | const char* valueCStr = RedisModule_StringPtrLen(value, &valueCStrLen); | ||
| 70 | RedisModule_ReplyWithStringBuffer(pd->ctx, valueCStr, valueCStrLen); | ||
| 71 | } else { | ||
| 72 | RedisModule_ReplyWithNull(pd->ctx); | ||
| 73 | } | ||
| 74 | |||
| 75 | pd->nreplies++; | ||
| 76 | } | ||
| 77 | |||
| 78 | int scan_key(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 79 | { | ||
| 80 | if (argc != 2) { | ||
| 81 | RedisModule_WrongArity(ctx); | ||
| 82 | return REDISMODULE_OK; | ||
| 83 | } | ||
| 84 | scan_key_pd pd = { | ||
| 85 | .ctx = ctx, | ||
| 86 | .nreplies = 0, | ||
| 87 | }; | ||
| 88 | |||
| 89 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ); | ||
| 90 | if (!key) { | ||
| 91 | RedisModule_ReplyWithError(ctx, "not found"); | ||
| 92 | return REDISMODULE_OK; | ||
| 93 | } | ||
| 94 | |||
| 95 | RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_ARRAY_LEN); | ||
| 96 | |||
| 97 | RedisModuleScanCursor* cursor = RedisModule_ScanCursorCreate(); | ||
| 98 | while(RedisModule_ScanKey(key, cursor, scan_key_callback, &pd)); | ||
| 99 | RedisModule_ScanCursorDestroy(cursor); | ||
| 100 | |||
| 101 | RedisModule_ReplySetArrayLength(ctx, pd.nreplies); | ||
| 102 | RedisModule_CloseKey(key); | ||
| 103 | return REDISMODULE_OK; | ||
| 104 | } | ||
| 105 | |||
| 106 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 107 | REDISMODULE_NOT_USED(argv); | ||
| 108 | REDISMODULE_NOT_USED(argc); | ||
| 109 | if (RedisModule_Init(ctx, "scan", 1, REDISMODULE_APIVER_1)== REDISMODULE_ERR) | ||
| 110 | return REDISMODULE_ERR; | ||
| 111 | |||
| 112 | if (RedisModule_CreateCommand(ctx, "scan.scan_strings", scan_strings, "", 0, 0, 0) == REDISMODULE_ERR) | ||
| 113 | return REDISMODULE_ERR; | ||
| 114 | |||
| 115 | if (RedisModule_CreateCommand(ctx, "scan.scan_key", scan_key, "", 0, 0, 0) == REDISMODULE_ERR) | ||
| 116 | return REDISMODULE_ERR; | ||
| 117 | |||
| 118 | return REDISMODULE_OK; | ||
| 119 | } | ||
| 120 | |||
| 121 | |||
diff --git a/examples/redis-unstable/tests/modules/stream.c b/examples/redis-unstable/tests/modules/stream.c new file mode 100644 index 0000000..65762a3 --- /dev/null +++ b/examples/redis-unstable/tests/modules/stream.c | |||
| @@ -0,0 +1,258 @@ | |||
| 1 | #include "redismodule.h" | ||
| 2 | |||
| 3 | #include <string.h> | ||
| 4 | #include <strings.h> | ||
| 5 | #include <assert.h> | ||
| 6 | #include <unistd.h> | ||
| 7 | #include <errno.h> | ||
| 8 | |||
| 9 | /* Command which adds a stream entry with automatic ID, like XADD *. | ||
| 10 | * | ||
| 11 | * Syntax: STREAM.ADD key field1 value1 [ field2 value2 ... ] | ||
| 12 | * | ||
| 13 | * The response is the ID of the added stream entry or an error message. | ||
| 14 | */ | ||
| 15 | int stream_add(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 16 | if (argc < 2 || argc % 2 != 0) { | ||
| 17 | RedisModule_WrongArity(ctx); | ||
| 18 | return REDISMODULE_OK; | ||
| 19 | } | ||
| 20 | |||
| 21 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE); | ||
| 22 | RedisModuleStreamID id; | ||
| 23 | if (RedisModule_StreamAdd(key, REDISMODULE_STREAM_ADD_AUTOID, &id, | ||
| 24 | &argv[2], (argc-2)/2) == REDISMODULE_OK) { | ||
| 25 | RedisModuleString *id_str = RedisModule_CreateStringFromStreamID(ctx, &id); | ||
| 26 | RedisModule_ReplyWithString(ctx, id_str); | ||
| 27 | RedisModule_FreeString(ctx, id_str); | ||
| 28 | } else { | ||
| 29 | RedisModule_ReplyWithError(ctx, "ERR StreamAdd failed"); | ||
| 30 | } | ||
| 31 | RedisModule_CloseKey(key); | ||
| 32 | return REDISMODULE_OK; | ||
| 33 | } | ||
| 34 | |||
| 35 | /* Command which adds a stream entry N times. | ||
| 36 | * | ||
| 37 | * Syntax: STREAM.ADD key N field1 value1 [ field2 value2 ... ] | ||
| 38 | * | ||
| 39 | * Returns the number of successfully added entries. | ||
| 40 | */ | ||
| 41 | int stream_addn(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 42 | if (argc < 3 || argc % 2 == 0) { | ||
| 43 | RedisModule_WrongArity(ctx); | ||
| 44 | return REDISMODULE_OK; | ||
| 45 | } | ||
| 46 | |||
| 47 | long long n, i; | ||
| 48 | if (RedisModule_StringToLongLong(argv[2], &n) == REDISMODULE_ERR) { | ||
| 49 | RedisModule_ReplyWithError(ctx, "N must be a number"); | ||
| 50 | return REDISMODULE_OK; | ||
| 51 | } | ||
| 52 | |||
| 53 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE); | ||
| 54 | for (i = 0; i < n; i++) { | ||
| 55 | if (RedisModule_StreamAdd(key, REDISMODULE_STREAM_ADD_AUTOID, NULL, | ||
| 56 | &argv[3], (argc-3)/2) == REDISMODULE_ERR) | ||
| 57 | break; | ||
| 58 | } | ||
| 59 | RedisModule_ReplyWithLongLong(ctx, i); | ||
| 60 | RedisModule_CloseKey(key); | ||
| 61 | return REDISMODULE_OK; | ||
| 62 | } | ||
| 63 | |||
| 64 | /* STREAM.DELETE key stream-id */ | ||
| 65 | int stream_delete(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 66 | if (argc != 3) return RedisModule_WrongArity(ctx); | ||
| 67 | RedisModuleStreamID id; | ||
| 68 | if (RedisModule_StringToStreamID(argv[2], &id) != REDISMODULE_OK) { | ||
| 69 | return RedisModule_ReplyWithError(ctx, "Invalid stream ID"); | ||
| 70 | } | ||
| 71 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE); | ||
| 72 | if (RedisModule_StreamDelete(key, &id) == REDISMODULE_OK) { | ||
| 73 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 74 | } else { | ||
| 75 | RedisModule_ReplyWithError(ctx, "ERR StreamDelete failed"); | ||
| 76 | } | ||
| 77 | RedisModule_CloseKey(key); | ||
| 78 | return REDISMODULE_OK; | ||
| 79 | } | ||
| 80 | |||
| 81 | /* STREAM.RANGE key start-id end-id | ||
| 82 | * | ||
| 83 | * Returns an array of stream items. Each item is an array on the form | ||
| 84 | * [stream-id, [field1, value1, field2, value2, ...]]. | ||
| 85 | * | ||
| 86 | * A funny side-effect used for testing RM_StreamIteratorDelete() is that if any | ||
| 87 | * entry has a field named "selfdestruct", the stream entry is deleted. It is | ||
| 88 | * however included in the results of this command. | ||
| 89 | */ | ||
| 90 | int stream_range(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 91 | if (argc != 4) { | ||
| 92 | RedisModule_WrongArity(ctx); | ||
| 93 | return REDISMODULE_OK; | ||
| 94 | } | ||
| 95 | |||
| 96 | RedisModuleStreamID startid, endid; | ||
| 97 | if (RedisModule_StringToStreamID(argv[2], &startid) != REDISMODULE_OK || | ||
| 98 | RedisModule_StringToStreamID(argv[3], &endid) != REDISMODULE_OK) { | ||
| 99 | RedisModule_ReplyWithError(ctx, "Invalid stream ID"); | ||
| 100 | return REDISMODULE_OK; | ||
| 101 | } | ||
| 102 | |||
| 103 | /* If startid > endid, we swap and set the reverse flag. */ | ||
| 104 | int flags = 0; | ||
| 105 | if (startid.ms > endid.ms || | ||
| 106 | (startid.ms == endid.ms && startid.seq > endid.seq)) { | ||
| 107 | RedisModuleStreamID tmp = startid; | ||
| 108 | startid = endid; | ||
| 109 | endid = tmp; | ||
| 110 | flags |= REDISMODULE_STREAM_ITERATOR_REVERSE; | ||
| 111 | } | ||
| 112 | |||
| 113 | /* Open key and start iterator. */ | ||
| 114 | int openflags = REDISMODULE_READ | REDISMODULE_WRITE; | ||
| 115 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], openflags); | ||
| 116 | if (RedisModule_StreamIteratorStart(key, flags, | ||
| 117 | &startid, &endid) != REDISMODULE_OK) { | ||
| 118 | /* Key is not a stream, etc. */ | ||
| 119 | RedisModule_ReplyWithError(ctx, "ERR StreamIteratorStart failed"); | ||
| 120 | RedisModule_CloseKey(key); | ||
| 121 | return REDISMODULE_OK; | ||
| 122 | } | ||
| 123 | |||
| 124 | /* Check error handling: Delete current entry when no current entry. */ | ||
| 125 | assert(RedisModule_StreamIteratorDelete(key) == | ||
| 126 | REDISMODULE_ERR); | ||
| 127 | assert(errno == ENOENT); | ||
| 128 | |||
| 129 | /* Check error handling: Fetch fields when no current entry. */ | ||
| 130 | assert(RedisModule_StreamIteratorNextField(key, NULL, NULL) == | ||
| 131 | REDISMODULE_ERR); | ||
| 132 | assert(errno == ENOENT); | ||
| 133 | |||
| 134 | /* Return array. */ | ||
| 135 | RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_LEN); | ||
| 136 | RedisModule_AutoMemory(ctx); | ||
| 137 | RedisModuleStreamID id; | ||
| 138 | long numfields; | ||
| 139 | long len = 0; | ||
| 140 | while (RedisModule_StreamIteratorNextID(key, &id, | ||
| 141 | &numfields) == REDISMODULE_OK) { | ||
| 142 | RedisModule_ReplyWithArray(ctx, 2); | ||
| 143 | RedisModuleString *id_str = RedisModule_CreateStringFromStreamID(ctx, &id); | ||
| 144 | RedisModule_ReplyWithString(ctx, id_str); | ||
| 145 | RedisModule_ReplyWithArray(ctx, numfields * 2); | ||
| 146 | int delete = 0; | ||
| 147 | RedisModuleString *field, *value; | ||
| 148 | for (long i = 0; i < numfields; i++) { | ||
| 149 | assert(RedisModule_StreamIteratorNextField(key, &field, &value) == | ||
| 150 | REDISMODULE_OK); | ||
| 151 | RedisModule_ReplyWithString(ctx, field); | ||
| 152 | RedisModule_ReplyWithString(ctx, value); | ||
| 153 | /* check if this is a "selfdestruct" field */ | ||
| 154 | size_t field_len; | ||
| 155 | const char *field_str = RedisModule_StringPtrLen(field, &field_len); | ||
| 156 | if (!strncmp(field_str, "selfdestruct", field_len)) delete = 1; | ||
| 157 | } | ||
| 158 | if (delete) { | ||
| 159 | assert(RedisModule_StreamIteratorDelete(key) == REDISMODULE_OK); | ||
| 160 | } | ||
| 161 | /* check error handling: no more fields to fetch */ | ||
| 162 | assert(RedisModule_StreamIteratorNextField(key, &field, &value) == | ||
| 163 | REDISMODULE_ERR); | ||
| 164 | assert(errno == ENOENT); | ||
| 165 | len++; | ||
| 166 | } | ||
| 167 | RedisModule_ReplySetArrayLength(ctx, len); | ||
| 168 | RedisModule_StreamIteratorStop(key); | ||
| 169 | RedisModule_CloseKey(key); | ||
| 170 | return REDISMODULE_OK; | ||
| 171 | } | ||
| 172 | |||
| 173 | /* | ||
| 174 | * STREAM.TRIM key (MAXLEN (=|~) length | MINID (=|~) id) | ||
| 175 | */ | ||
| 176 | int stream_trim(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 177 | if (argc != 5) { | ||
| 178 | RedisModule_WrongArity(ctx); | ||
| 179 | return REDISMODULE_OK; | ||
| 180 | } | ||
| 181 | |||
| 182 | /* Parse args */ | ||
| 183 | int trim_by_id = 0; /* 0 = maxlen, 1 = minid */ | ||
| 184 | long long maxlen; | ||
| 185 | RedisModuleStreamID minid; | ||
| 186 | size_t arg_len; | ||
| 187 | const char *arg = RedisModule_StringPtrLen(argv[2], &arg_len); | ||
| 188 | if (!strcasecmp(arg, "minid")) { | ||
| 189 | trim_by_id = 1; | ||
| 190 | if (RedisModule_StringToStreamID(argv[4], &minid) != REDISMODULE_OK) { | ||
| 191 | RedisModule_ReplyWithError(ctx, "ERR Invalid stream ID"); | ||
| 192 | return REDISMODULE_OK; | ||
| 193 | } | ||
| 194 | } else if (!strcasecmp(arg, "maxlen")) { | ||
| 195 | if (RedisModule_StringToLongLong(argv[4], &maxlen) == REDISMODULE_ERR) { | ||
| 196 | RedisModule_ReplyWithError(ctx, "ERR Maxlen must be a number"); | ||
| 197 | return REDISMODULE_OK; | ||
| 198 | } | ||
| 199 | } else { | ||
| 200 | RedisModule_ReplyWithError(ctx, "ERR Invalid arguments"); | ||
| 201 | return REDISMODULE_OK; | ||
| 202 | } | ||
| 203 | |||
| 204 | /* Approx or exact */ | ||
| 205 | int flags; | ||
| 206 | arg = RedisModule_StringPtrLen(argv[3], &arg_len); | ||
| 207 | if (arg_len == 1 && arg[0] == '~') { | ||
| 208 | flags = REDISMODULE_STREAM_TRIM_APPROX; | ||
| 209 | } else if (arg_len == 1 && arg[0] == '=') { | ||
| 210 | flags = 0; | ||
| 211 | } else { | ||
| 212 | RedisModule_ReplyWithError(ctx, "ERR Invalid approx-or-exact mark"); | ||
| 213 | return REDISMODULE_OK; | ||
| 214 | } | ||
| 215 | |||
| 216 | /* Trim */ | ||
| 217 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE); | ||
| 218 | long long trimmed; | ||
| 219 | if (trim_by_id) { | ||
| 220 | trimmed = RedisModule_StreamTrimByID(key, flags, &minid); | ||
| 221 | } else { | ||
| 222 | trimmed = RedisModule_StreamTrimByLength(key, flags, maxlen); | ||
| 223 | } | ||
| 224 | |||
| 225 | /* Return result */ | ||
| 226 | if (trimmed < 0) { | ||
| 227 | RedisModule_ReplyWithError(ctx, "ERR Trimming failed"); | ||
| 228 | } else { | ||
| 229 | RedisModule_ReplyWithLongLong(ctx, trimmed); | ||
| 230 | } | ||
| 231 | RedisModule_CloseKey(key); | ||
| 232 | return REDISMODULE_OK; | ||
| 233 | } | ||
| 234 | |||
| 235 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 236 | REDISMODULE_NOT_USED(argv); | ||
| 237 | REDISMODULE_NOT_USED(argc); | ||
| 238 | if (RedisModule_Init(ctx, "stream", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) | ||
| 239 | return REDISMODULE_ERR; | ||
| 240 | |||
| 241 | if (RedisModule_CreateCommand(ctx, "stream.add", stream_add, "write", | ||
| 242 | 1, 1, 1) == REDISMODULE_ERR) | ||
| 243 | return REDISMODULE_ERR; | ||
| 244 | if (RedisModule_CreateCommand(ctx, "stream.addn", stream_addn, "write", | ||
| 245 | 1, 1, 1) == REDISMODULE_ERR) | ||
| 246 | return REDISMODULE_ERR; | ||
| 247 | if (RedisModule_CreateCommand(ctx, "stream.delete", stream_delete, "write", | ||
| 248 | 1, 1, 1) == REDISMODULE_ERR) | ||
| 249 | return REDISMODULE_ERR; | ||
| 250 | if (RedisModule_CreateCommand(ctx, "stream.range", stream_range, "write", | ||
| 251 | 1, 1, 1) == REDISMODULE_ERR) | ||
| 252 | return REDISMODULE_ERR; | ||
| 253 | if (RedisModule_CreateCommand(ctx, "stream.trim", stream_trim, "write", | ||
| 254 | 1, 1, 1) == REDISMODULE_ERR) | ||
| 255 | return REDISMODULE_ERR; | ||
| 256 | |||
| 257 | return REDISMODULE_OK; | ||
| 258 | } | ||
diff --git a/examples/redis-unstable/tests/modules/subcommands.c b/examples/redis-unstable/tests/modules/subcommands.c new file mode 100644 index 0000000..0961032 --- /dev/null +++ b/examples/redis-unstable/tests/modules/subcommands.c | |||
| @@ -0,0 +1,119 @@ | |||
| 1 | #include "redismodule.h" | ||
| 2 | |||
| 3 | #define UNUSED(V) ((void) V) | ||
| 4 | |||
| 5 | int cmd_set(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 6 | UNUSED(argv); | ||
| 7 | UNUSED(argc); | ||
| 8 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 9 | return REDISMODULE_OK; | ||
| 10 | } | ||
| 11 | |||
| 12 | int cmd_get(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 13 | UNUSED(argv); | ||
| 14 | |||
| 15 | if (argc > 4) /* For testing */ | ||
| 16 | return RedisModule_WrongArity(ctx); | ||
| 17 | |||
| 18 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 19 | return REDISMODULE_OK; | ||
| 20 | } | ||
| 21 | |||
| 22 | int cmd_get_fullname(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 23 | UNUSED(argv); | ||
| 24 | UNUSED(argc); | ||
| 25 | |||
| 26 | const char *command_name = RedisModule_GetCurrentCommandName(ctx); | ||
| 27 | RedisModule_ReplyWithSimpleString(ctx, command_name); | ||
| 28 | return REDISMODULE_OK; | ||
| 29 | } | ||
| 30 | |||
| 31 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 32 | REDISMODULE_NOT_USED(argv); | ||
| 33 | REDISMODULE_NOT_USED(argc); | ||
| 34 | |||
| 35 | if (RedisModule_Init(ctx, "subcommands", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) | ||
| 36 | return REDISMODULE_ERR; | ||
| 37 | |||
| 38 | /* Module command names cannot contain special characters. */ | ||
| 39 | RedisModule_Assert(RedisModule_CreateCommand(ctx,"subcommands.char\r",NULL,"",0,0,0) == REDISMODULE_ERR); | ||
| 40 | RedisModule_Assert(RedisModule_CreateCommand(ctx,"subcommands.char\n",NULL,"",0,0,0) == REDISMODULE_ERR); | ||
| 41 | RedisModule_Assert(RedisModule_CreateCommand(ctx,"subcommands.char ",NULL,"",0,0,0) == REDISMODULE_ERR); | ||
| 42 | |||
| 43 | if (RedisModule_CreateCommand(ctx,"subcommands.bitarray",NULL,"",0,0,0) == REDISMODULE_ERR) | ||
| 44 | return REDISMODULE_ERR; | ||
| 45 | RedisModuleCommand *parent = RedisModule_GetCommand(ctx,"subcommands.bitarray"); | ||
| 46 | |||
| 47 | if (RedisModule_CreateSubcommand(parent,"set",cmd_set,"",0,0,0) == REDISMODULE_ERR) | ||
| 48 | return REDISMODULE_ERR; | ||
| 49 | |||
| 50 | /* Module subcommand names cannot contain special characters. */ | ||
| 51 | RedisModule_Assert(RedisModule_CreateSubcommand(parent,"char|",cmd_set,"",0,0,0) == REDISMODULE_ERR); | ||
| 52 | RedisModule_Assert(RedisModule_CreateSubcommand(parent,"char@",cmd_set,"",0,0,0) == REDISMODULE_ERR); | ||
| 53 | RedisModule_Assert(RedisModule_CreateSubcommand(parent,"char=",cmd_set,"",0,0,0) == REDISMODULE_ERR); | ||
| 54 | |||
| 55 | RedisModuleCommand *subcmd = RedisModule_GetCommand(ctx,"subcommands.bitarray|set"); | ||
| 56 | RedisModuleCommandInfo cmd_set_info = { | ||
| 57 | .version = REDISMODULE_COMMAND_INFO_VERSION, | ||
| 58 | .key_specs = (RedisModuleCommandKeySpec[]){ | ||
| 59 | { | ||
| 60 | .flags = REDISMODULE_CMD_KEY_RW | REDISMODULE_CMD_KEY_UPDATE, | ||
| 61 | .begin_search_type = REDISMODULE_KSPEC_BS_INDEX, | ||
| 62 | .bs.index.pos = 1, | ||
| 63 | .find_keys_type = REDISMODULE_KSPEC_FK_RANGE, | ||
| 64 | .fk.range = {0,1,0} | ||
| 65 | }, | ||
| 66 | {0} | ||
| 67 | } | ||
| 68 | }; | ||
| 69 | if (RedisModule_SetCommandInfo(subcmd, &cmd_set_info) == REDISMODULE_ERR) | ||
| 70 | return REDISMODULE_ERR; | ||
| 71 | |||
| 72 | if (RedisModule_CreateSubcommand(parent,"get",cmd_get,"",0,0,0) == REDISMODULE_ERR) | ||
| 73 | return REDISMODULE_ERR; | ||
| 74 | subcmd = RedisModule_GetCommand(ctx,"subcommands.bitarray|get"); | ||
| 75 | RedisModuleCommandInfo cmd_get_info = { | ||
| 76 | .version = REDISMODULE_COMMAND_INFO_VERSION, | ||
| 77 | .key_specs = (RedisModuleCommandKeySpec[]){ | ||
| 78 | { | ||
| 79 | .flags = REDISMODULE_CMD_KEY_RO | REDISMODULE_CMD_KEY_ACCESS, | ||
| 80 | .begin_search_type = REDISMODULE_KSPEC_BS_INDEX, | ||
| 81 | .bs.index.pos = 1, | ||
| 82 | .find_keys_type = REDISMODULE_KSPEC_FK_RANGE, | ||
| 83 | .fk.range = {0,1,0} | ||
| 84 | }, | ||
| 85 | {0} | ||
| 86 | } | ||
| 87 | }; | ||
| 88 | if (RedisModule_SetCommandInfo(subcmd, &cmd_get_info) == REDISMODULE_ERR) | ||
| 89 | return REDISMODULE_ERR; | ||
| 90 | |||
| 91 | /* Get the name of the command currently running. */ | ||
| 92 | if (RedisModule_CreateCommand(ctx,"subcommands.parent_get_fullname",cmd_get_fullname,"",0,0,0) == REDISMODULE_ERR) | ||
| 93 | return REDISMODULE_ERR; | ||
| 94 | |||
| 95 | /* Get the name of the subcommand currently running. */ | ||
| 96 | if (RedisModule_CreateCommand(ctx,"subcommands.sub",NULL,"",0,0,0) == REDISMODULE_ERR) | ||
| 97 | return REDISMODULE_ERR; | ||
| 98 | |||
| 99 | RedisModuleCommand *fullname_parent = RedisModule_GetCommand(ctx,"subcommands.sub"); | ||
| 100 | if (RedisModule_CreateSubcommand(fullname_parent,"get_fullname",cmd_get_fullname,"",0,0,0) == REDISMODULE_ERR) | ||
| 101 | return REDISMODULE_ERR; | ||
| 102 | |||
| 103 | /* Sanity */ | ||
| 104 | |||
| 105 | /* Trying to create the same subcommand fails */ | ||
| 106 | RedisModule_Assert(RedisModule_CreateSubcommand(parent,"get",NULL,"",0,0,0) == REDISMODULE_ERR); | ||
| 107 | |||
| 108 | /* Trying to create a sub-subcommand fails */ | ||
| 109 | RedisModule_Assert(RedisModule_CreateSubcommand(subcmd,"get",NULL,"",0,0,0) == REDISMODULE_ERR); | ||
| 110 | |||
| 111 | /* Internal container command for testing */ | ||
| 112 | if (RedisModule_CreateCommand(ctx,"subcommands.internal_container",NULL,"internal",0,0,0) == REDISMODULE_ERR) | ||
| 113 | return REDISMODULE_ERR; | ||
| 114 | RedisModuleCommand *internal_parent = RedisModule_GetCommand(ctx,"subcommands.internal_container"); | ||
| 115 | if (RedisModule_CreateSubcommand(internal_parent,"test",cmd_set,"internal",0,0,0) == REDISMODULE_ERR) | ||
| 116 | return REDISMODULE_ERR; | ||
| 117 | |||
| 118 | return REDISMODULE_OK; | ||
| 119 | } | ||
diff --git a/examples/redis-unstable/tests/modules/test_keymeta.c b/examples/redis-unstable/tests/modules/test_keymeta.c new file mode 100644 index 0000000..ca5d622 --- /dev/null +++ b/examples/redis-unstable/tests/modules/test_keymeta.c | |||
| @@ -0,0 +1,585 @@ | |||
| 1 | /* An example module for attaching metadata to keys. | ||
| 2 | * | ||
| 3 | * This example lets tests create metadata-key classes and then SET and GET metadata | ||
| 4 | * to keys. The 8-byte slot stores a handle to a module-managed allocation; here | ||
| 5 | * we use to attach a string per-key. | ||
| 6 | * | ||
| 7 | * The module pre-registers several metadata classes during initialization and exposes | ||
| 8 | * the following commands (via RedisModule_CreateCommand): | ||
| 9 | * | ||
| 10 | * 1) KEYMETA.REGISTER <4-char-id> <version> [FLAGS] | ||
| 11 | * Register a new metadata-key class during module load. | ||
| 12 | * Returns the <keymeta-class-id> index (Returned from RedisModule_CreateKeyMetaClass) | ||
| 13 | * On failure, returns nil | ||
| 14 | * In a real module it should be registered "automatically" via OnLoad. | ||
| 15 | * | ||
| 16 | * FLAGS (colon-separated): | ||
| 17 | * KEEPONCOPY - Keep metadata on COPY operation | ||
| 18 | * KEEPONRENAME - Keep metadata on RENAME operation | ||
| 19 | * KEEPONMOVE - Keep metadata on MOVE operation | ||
| 20 | * UNLINKFREE - Use unlink callback for async free | ||
| 21 | * RDBLOAD - Enable rdb_load callback (metadata can be loaded from RDB) | ||
| 22 | * RDBSAVE - Enable rdb_save callback (metadata can be saved to RDB) | ||
| 23 | * ALLOWIGNORE - Enable ALLOW_IGNORE flag (graceful discard on load if | ||
| 24 | * class not registered or no rdb_load callback) | ||
| 25 | * | ||
| 26 | * Example: > keymeta.register KMT1 1 KEEPONCOPY:KEEPONRENAME:ALLOWIGNORE:RDBLOAD:RDBSAVE | ||
| 27 | * Example: > keymeta.register KMT2 1 ALLOWIGNORE | ||
| 28 | * | ||
| 29 | * 2) KEYMETA.SET <4-char-id> <key> <string-value> | ||
| 30 | * Set the string value as metadata to given key. | ||
| 31 | * Note: | ||
| 32 | * - If already set earlier, then it is expected that it will released before setting a | ||
| 33 | * new string. That is why this command should start with trying to get first | ||
| 34 | * metadata for given key. | ||
| 35 | * | ||
| 36 | * 3) KEYMETA.GET <4-char-id> <key> | ||
| 37 | * Get the metadata attached to the key for the given class. | ||
| 38 | * Returns a string attached to the given key. Or nil if nothing is attached. | ||
| 39 | * | ||
| 40 | * 4) KEYMETA.UNREGISTER <4-char-id> | ||
| 41 | * This will mark the key metadata class as released. It can later be reused again | ||
| 42 | * by the same class (consider comment above). | ||
| 43 | * Return REDISMODULE_OK/REDISMODULE_ERR. | ||
| 44 | * | ||
| 45 | * 5) KEYMETA.ACTIVE | ||
| 46 | * Return total number of active metadata at the moment. | ||
| 47 | */ | ||
| 48 | |||
| 49 | #include "redismodule.h" | ||
| 50 | #include <string.h> | ||
| 51 | #include <stdlib.h> | ||
| 52 | #include <assert.h> | ||
| 53 | |||
| 54 | /* Virtualize class IDs for testing. Values: 0 unused, 1..7 used, -1 released */ | ||
| 55 | RedisModuleKeyMetaClassId class_ids[8] = { 0 }; | ||
| 56 | |||
| 57 | /* Mapping from 4-char-id to class-id */ | ||
| 58 | typedef struct { | ||
| 59 | char name[5]; /* 4 chars + null terminator */ | ||
| 60 | RedisModuleKeyMetaClassId class_id; | ||
| 61 | } ClassMapping; | ||
| 62 | |||
| 63 | #define MAX_CLASS_MAPPINGS 8 | ||
| 64 | static ClassMapping class_mappings[MAX_CLASS_MAPPINGS]; | ||
| 65 | static int num_class_mappings = 0; | ||
| 66 | |||
| 67 | /* Reverse lookup: given a class_id, find the 4-char-id name */ | ||
| 68 | static const char* lookupClassName(RedisModuleKeyMetaClassId class_id) { | ||
| 69 | for (int i = 0; i < num_class_mappings; i++) { | ||
| 70 | if (class_mappings[i].class_id == class_id) { | ||
| 71 | return class_mappings[i].name; | ||
| 72 | } | ||
| 73 | } | ||
| 74 | return NULL; | ||
| 75 | } | ||
| 76 | |||
| 77 | /* Track active metadata instances (not yet freed) */ | ||
| 78 | static long long active_metadata_count = 0; | ||
| 79 | |||
| 80 | /* Helper functions for class mapping */ | ||
| 81 | |||
| 82 | /* Add a mapping from 4-char-id to class-id */ | ||
| 83 | static int addClassMapping(const char *name, RedisModuleKeyMetaClassId class_id) { | ||
| 84 | if (num_class_mappings >= MAX_CLASS_MAPPINGS) { | ||
| 85 | return 0; /* No space */ | ||
| 86 | } | ||
| 87 | strncpy(class_mappings[num_class_mappings].name, name, 4); | ||
| 88 | class_mappings[num_class_mappings].name[4] = '\0'; | ||
| 89 | class_mappings[num_class_mappings].class_id = class_id; | ||
| 90 | num_class_mappings++; | ||
| 91 | return 1; | ||
| 92 | } | ||
| 93 | |||
| 94 | /* Lookup class-id by 4-char-id. Returns -1 if not found. */ | ||
| 95 | static RedisModuleKeyMetaClassId lookupClassId(const char *name) { | ||
| 96 | for (int i = 0; i < num_class_mappings; i++) { | ||
| 97 | if (strncmp(class_mappings[i].name, name, 4) == 0) { | ||
| 98 | return class_mappings[i].class_id; | ||
| 99 | } | ||
| 100 | } | ||
| 101 | return -1; | ||
| 102 | } | ||
| 103 | |||
| 104 | /* Remove a mapping by 4-char-id */ | ||
| 105 | static int removeClassMapping(const char *name) { | ||
| 106 | for (int i = 0; i < num_class_mappings; i++) { | ||
| 107 | if (strncmp(class_mappings[i].name, name, 4) == 0) { | ||
| 108 | /* Shift remaining entries down */ | ||
| 109 | for (int j = i; j < num_class_mappings - 1; j++) { | ||
| 110 | class_mappings[j] = class_mappings[j + 1]; | ||
| 111 | } | ||
| 112 | num_class_mappings--; | ||
| 113 | return 1; | ||
| 114 | } | ||
| 115 | } | ||
| 116 | return 0; | ||
| 117 | } | ||
| 118 | |||
| 119 | /* Callback functions for metadata lifecycle */ | ||
| 120 | |||
| 121 | /* Copy callback - called when a key is copied */ | ||
| 122 | static int KeyMetaCopyCallback(RedisModuleKeyOptCtx *ctx, uint64_t *meta) { | ||
| 123 | REDISMODULE_NOT_USED(ctx); | ||
| 124 | char *str = (char *)*meta; | ||
| 125 | /* Note, condition is redundant since cb only invoked when meta != reset_value */ | ||
| 126 | if (str) { | ||
| 127 | char *new_str = strdup(str); | ||
| 128 | *meta = (uint64_t)new_str; | ||
| 129 | active_metadata_count++; /* New metadata instance created */ | ||
| 130 | } | ||
| 131 | return 1; /* Keep metadata */ | ||
| 132 | } | ||
| 133 | |||
| 134 | /* Rename callback - called when a key is renamed. */ | ||
| 135 | static int KeyMetaRenameDiscardCallback(RedisModuleKeyOptCtx *ctx, uint64_t *meta) { | ||
| 136 | REDISMODULE_NOT_USED(ctx); | ||
| 137 | REDISMODULE_NOT_USED(meta); | ||
| 138 | return 0; | ||
| 139 | } | ||
| 140 | |||
| 141 | /* Unlink callback - called when a key is unlinked */ | ||
| 142 | static void KeyMetaUnlinkCallback(RedisModuleKeyOptCtx *ctx, uint64_t *meta) { | ||
| 143 | /* Let's challenge and free early on before free callback */ | ||
| 144 | /* Note, condition is redundant since cb only invoked when meta != reset_value */ | ||
| 145 | if (*meta != 0) { | ||
| 146 | char *str = (char *)*meta; | ||
| 147 | free(str); | ||
| 148 | *meta = 0; /* Set to reset_value !!! */ | ||
| 149 | active_metadata_count--; /* Metadata instance freed */ | ||
| 150 | } | ||
| 151 | REDISMODULE_NOT_USED(ctx); | ||
| 152 | } | ||
| 153 | |||
| 154 | /* Free callback - called when metadata needs to be freed */ | ||
| 155 | static void KeyMetaFreeCallback(const char *keyname, uint64_t meta) { | ||
| 156 | REDISMODULE_NOT_USED(keyname); | ||
| 157 | /* Note, condition is redundant since cb only invoked when meta != reset_value */ | ||
| 158 | if (meta != 0) { | ||
| 159 | char *str = (char *)meta; | ||
| 160 | free(str); | ||
| 161 | active_metadata_count--; /* Metadata instance freed */ | ||
| 162 | } | ||
| 163 | } | ||
| 164 | |||
| 165 | static int KeyMetaMoveDiscardCallback(RedisModuleKeyOptCtx *ctx, uint64_t *meta) { | ||
| 166 | REDISMODULE_NOT_USED(ctx); | ||
| 167 | REDISMODULE_NOT_USED(meta); | ||
| 168 | return 0; /* discard metadata */ | ||
| 169 | } | ||
| 170 | |||
| 171 | /* RDB Save Callback - Serialize metadata to RDB | ||
| 172 | * This callback is invoked during RDB save to write the metadata value. | ||
| 173 | * | ||
| 174 | * Parameters: | ||
| 175 | * - rdb: RedisModuleIO context for writing to RDB | ||
| 176 | * - value: The kvobj (key-value object) - not used in this implementation | ||
| 177 | * - meta: Pointer to the 8-byte metadata value (pointer to our string) | ||
| 178 | */ | ||
| 179 | static void KeyMetaRDBSaveCallback(RedisModuleIO *rdb, void *value, uint64_t *meta) { | ||
| 180 | REDISMODULE_NOT_USED(value); | ||
| 181 | |||
| 182 | /* If metadata is NULL (reset_value), don't save anything */ | ||
| 183 | if (*meta == 0) return; | ||
| 184 | |||
| 185 | /* Extract the string from the metadata pointer */ | ||
| 186 | char *metadata_string = (char *)*meta; | ||
| 187 | |||
| 188 | /* Save the string to RDB using SaveStringBuffer */ | ||
| 189 | RedisModule_SaveStringBuffer(rdb, metadata_string, strlen(metadata_string)); | ||
| 190 | /* Save more silly data */ | ||
| 191 | RedisModule_SaveSigned(rdb, 1); | ||
| 192 | RedisModule_SaveFloat(rdb, 1.5); | ||
| 193 | RedisModule_SaveLongDouble(rdb, 0.333333333333333333L); | ||
| 194 | } | ||
| 195 | |||
| 196 | /* RDB Load Callback - Deserialize metadata from RDB | ||
| 197 | * This callback is invoked during RDB load to read the metadata value. | ||
| 198 | * | ||
| 199 | * Parameters: | ||
| 200 | * - rdb: RedisModuleIO context for reading from RDB | ||
| 201 | * - meta: Pointer to store the loaded 8-byte metadata value | ||
| 202 | * - encver: Encoding version (class version from RDB) | ||
| 203 | * | ||
| 204 | * Returns: | ||
| 205 | * - 1: Attach metadata to key (success) | ||
| 206 | * - 0: Ignore/skip metadata (not an error) | ||
| 207 | * - -1: Error - abort RDB load | ||
| 208 | */ | ||
| 209 | static int KeyMetaRDBLoadCallback(RedisModuleIO *rdb, uint64_t *meta, int encver) { | ||
| 210 | REDISMODULE_NOT_USED(encver); | ||
| 211 | |||
| 212 | /* Load the string from RDB using LoadStringBuffer */ | ||
| 213 | size_t len; | ||
| 214 | char *loaded_string = RedisModule_LoadStringBuffer(rdb, &len); | ||
| 215 | |||
| 216 | if (loaded_string == NULL) { | ||
| 217 | /* Error loading string */ | ||
| 218 | return -1; | ||
| 219 | } | ||
| 220 | |||
| 221 | /* Allocate and copy the string (LoadStringBuffer returns a buffer that must be freed) */ | ||
| 222 | char *metadata_string = malloc(len + 1); | ||
| 223 | if (metadata_string == NULL) { | ||
| 224 | RedisModule_Free(loaded_string); | ||
| 225 | return -1; | ||
| 226 | } | ||
| 227 | |||
| 228 | memcpy(metadata_string, loaded_string, len); | ||
| 229 | metadata_string[len] = '\0'; | ||
| 230 | RedisModule_Free(loaded_string); | ||
| 231 | |||
| 232 | /* Load the additional data that was saved (must match rdb_save) */ | ||
| 233 | int64_t signed_val = RedisModule_LoadSigned(rdb); | ||
| 234 | float float_val = RedisModule_LoadFloat(rdb); | ||
| 235 | long double ldouble_val = RedisModule_LoadLongDouble(rdb); | ||
| 236 | /* We don't use these values, just need to consume them from the stream */ | ||
| 237 | (void)signed_val; | ||
| 238 | (void)float_val; | ||
| 239 | (void)ldouble_val; | ||
| 240 | |||
| 241 | /* Store the pointer in metadata */ | ||
| 242 | *meta = (uint64_t)metadata_string; | ||
| 243 | active_metadata_count++; /* New metadata instance created */ | ||
| 244 | |||
| 245 | /* Return 1 to attach metadata to the key */ | ||
| 246 | return 1; | ||
| 247 | } | ||
| 248 | |||
| 249 | /* AOF Rewrite Callback - Common implementation for all classes | ||
| 250 | * This callback is invoked during AOF rewrite to emit commands that will | ||
| 251 | * recreate the metadata when the AOF is loaded. | ||
| 252 | * | ||
| 253 | * Parameters: | ||
| 254 | * - aof: RedisModuleIO context for writing to AOF | ||
| 255 | * - value: The kvobj (key-value object) - not used in this implementation | ||
| 256 | * - meta: The 8-byte metadata value (pointer to our string) | ||
| 257 | * - class_id: The class ID for this metadata | ||
| 258 | */ | ||
| 259 | static void KeyMetaAOFRewriteCallback_Class(RedisModuleIO *aof, void *value, uint64_t meta, RedisModuleKeyMetaClassId class_id) { | ||
| 260 | REDISMODULE_NOT_USED(value); | ||
| 261 | |||
| 262 | /* If metadata is NULL (reset_value), don't emit anything */ | ||
| 263 | if (meta == 0) return; | ||
| 264 | |||
| 265 | /* Extract the string from the metadata pointer */ | ||
| 266 | char *metadata_string = (char *)meta; | ||
| 267 | |||
| 268 | /* Lookup the 9-byte-id name for this class */ | ||
| 269 | const char *class_name = lookupClassName(class_id); | ||
| 270 | if (!class_name) { | ||
| 271 | /* This shouldn't happen, but handle gracefully */ | ||
| 272 | return; | ||
| 273 | } | ||
| 274 | |||
| 275 | /* Get the key name from the AOF IO context */ | ||
| 276 | const RedisModuleString *key = RedisModule_GetKeyNameFromIO(aof); | ||
| 277 | if (!key) { | ||
| 278 | /* Key name not available - shouldn't happen during AOF rewrite */ | ||
| 279 | return; | ||
| 280 | } | ||
| 281 | |||
| 282 | /* Emit the KEYMETA.SET command to recreate this metadata | ||
| 283 | * Format: KEYMETA.SET <9-byte-id> <key> <string-value> */ | ||
| 284 | RedisModule_EmitAOF(aof, "KEYMETA.SET", "csc", | ||
| 285 | class_name, /* c: 9-byte-id (C string) */ | ||
| 286 | key, /* s: key name (RedisModuleString) */ | ||
| 287 | metadata_string); /* c: metadata value (C string) */ | ||
| 288 | } | ||
| 289 | |||
| 290 | /* Individual AOF rewrite callbacks for each class (1-7) | ||
| 291 | * Each callback wraps the common implementation with its specific class ID */ | ||
| 292 | static void KeyMetaAOFRewriteCb1(RedisModuleIO *aof, void *value, uint64_t meta) { | ||
| 293 | KeyMetaAOFRewriteCallback_Class(aof, value, meta, 1); | ||
| 294 | } | ||
| 295 | |||
| 296 | static void KeyMetaAOFRewriteCb2(RedisModuleIO *aof, void *value, uint64_t meta) { | ||
| 297 | KeyMetaAOFRewriteCallback_Class(aof, value, meta, 2); | ||
| 298 | } | ||
| 299 | |||
| 300 | static void KeyMetaAOFRewriteCb3(RedisModuleIO *aof, void *value, uint64_t meta) { | ||
| 301 | KeyMetaAOFRewriteCallback_Class(aof, value, meta, 3); | ||
| 302 | } | ||
| 303 | |||
| 304 | static void KeyMetaAOFRewriteCb4(RedisModuleIO *aof, void *value, uint64_t meta) { | ||
| 305 | KeyMetaAOFRewriteCallback_Class(aof, value, meta, 4); | ||
| 306 | } | ||
| 307 | |||
| 308 | static void KeyMetaAOFRewriteCb5(RedisModuleIO *aof, void *value, uint64_t meta) { | ||
| 309 | KeyMetaAOFRewriteCallback_Class(aof, value, meta, 5); | ||
| 310 | } | ||
| 311 | |||
| 312 | static void KeyMetaAOFRewriteCb6(RedisModuleIO *aof, void *value, uint64_t meta) { | ||
| 313 | KeyMetaAOFRewriteCallback_Class(aof, value, meta, 6); | ||
| 314 | } | ||
| 315 | |||
| 316 | static void KeyMetaAOFRewriteCb7(RedisModuleIO *aof, void *value, uint64_t meta) { | ||
| 317 | KeyMetaAOFRewriteCallback_Class(aof, value, meta, 7); | ||
| 318 | } | ||
| 319 | |||
| 320 | /* KEYMETA.REGISTER <4-char-id> <version> [KEEPONCOPY:KEEPONRENAME:UNLINKFREE:ALLOWIGNORE:NORDBLOAD:NORDBSAVE] */ | ||
| 321 | static int KeyMetaRegister_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 322 | if (argc < 3 || argc > 4) { | ||
| 323 | return RedisModule_WrongArity(ctx); | ||
| 324 | } | ||
| 325 | |||
| 326 | /* argv[1]: key metadata class name */ | ||
| 327 | size_t namelen; | ||
| 328 | const char *metaname = RedisModule_StringPtrLen(argv[1], &namelen); | ||
| 329 | |||
| 330 | /* argv[2]: key metadata class version */ | ||
| 331 | long long metaver; | ||
| 332 | if (RedisModule_StringToLongLong(argv[2], &metaver) != REDISMODULE_OK) { | ||
| 333 | RedisModule_ReplyWithError(ctx, "ERR invalid version number"); | ||
| 334 | return REDISMODULE_OK; | ||
| 335 | } | ||
| 336 | |||
| 337 | /* Parse optional callback flags */ | ||
| 338 | int keep_on_copy = 0, keep_on_rename = 0, unlink_free = 0, keep_on_move = 0; | ||
| 339 | int allow_ignore = 0; /* Default: ALLOW_IGNORE disabled */ | ||
| 340 | int rdb_load = 0; /* Default: rdb_load disabled */ | ||
| 341 | int rdb_save = 0; /* Default: rdb_save disabled */ | ||
| 342 | |||
| 343 | if (argc == 4) { | ||
| 344 | const char *flags = RedisModule_StringPtrLen(argv[3], NULL); | ||
| 345 | if (strstr(flags, "KEEPONCOPY")) keep_on_copy = 1; | ||
| 346 | if (strstr(flags, "KEEPONRENAME")) keep_on_rename = 1; | ||
| 347 | if (strstr(flags, "UNLINKFREE")) unlink_free = 1; | ||
| 348 | if (strstr(flags, "KEEPONMOVE")) keep_on_move = 1; | ||
| 349 | if (strstr(flags, "ALLOWIGNORE")) allow_ignore = 1; /* Enable ALLOW_IGNORE */ | ||
| 350 | if (strstr(flags, "RDBLOAD")) rdb_load = 1; /* Enable rdb_load */ | ||
| 351 | if (strstr(flags, "RDBSAVE")) rdb_save = 1; /* Enable rdb_save */ | ||
| 352 | } | ||
| 353 | |||
| 354 | /* Setup configuration */ | ||
| 355 | RedisModuleKeyMetaClassConfig config = {0}; | ||
| 356 | config.version = REDISMODULE_KEY_META_VERSION; | ||
| 357 | config.flags = allow_ignore ? (1 << REDISMODULE_META_ALLOW_IGNORE) : 0; | ||
| 358 | config.reset_value = (uint64_t)NULL; /* NULL pointer means no resource to free */ | ||
| 359 | config.rdb_load = rdb_load ? KeyMetaRDBLoadCallback : NULL; | ||
| 360 | config.rdb_save = rdb_save ? KeyMetaRDBSaveCallback : NULL; | ||
| 361 | switch (num_class_mappings + 1) { /* distinct cb per class */ | ||
| 362 | case 1: config.aof_rewrite = KeyMetaAOFRewriteCb1; break; | ||
| 363 | case 2: config.aof_rewrite = KeyMetaAOFRewriteCb2; break; | ||
| 364 | case 3: config.aof_rewrite = KeyMetaAOFRewriteCb3; break; | ||
| 365 | case 4: config.aof_rewrite = KeyMetaAOFRewriteCb4; break; | ||
| 366 | case 5: config.aof_rewrite = KeyMetaAOFRewriteCb5; break; | ||
| 367 | case 6: config.aof_rewrite = KeyMetaAOFRewriteCb6; break; | ||
| 368 | case 7: config.aof_rewrite = KeyMetaAOFRewriteCb7; break; | ||
| 369 | default: config.aof_rewrite = NULL; break; | ||
| 370 | } | ||
| 371 | config.free = KeyMetaFreeCallback; | ||
| 372 | config.copy = keep_on_copy ? KeyMetaCopyCallback : NULL; | ||
| 373 | config.rename = keep_on_rename ? NULL : KeyMetaRenameDiscardCallback; | ||
| 374 | config.move = keep_on_move ? NULL : KeyMetaMoveDiscardCallback; | ||
| 375 | config.defrag = NULL; | ||
| 376 | config.unlink = unlink_free ? KeyMetaUnlinkCallback : NULL; | ||
| 377 | config.mem_usage = NULL; | ||
| 378 | config.free_effort = NULL; | ||
| 379 | |||
| 380 | /* Create the metadata class */ | ||
| 381 | RedisModuleKeyMetaClassId class_id = RedisModule_CreateKeyMetaClass(ctx, metaname, (int)metaver, &config); | ||
| 382 | |||
| 383 | if (class_id < 0) { | ||
| 384 | RedisModule_ReplyWithError(ctx, "ERR failed to create metadata class"); | ||
| 385 | return REDISMODULE_OK; | ||
| 386 | } else { | ||
| 387 | /* Store the mapping from 9-byte-id to class-id */ | ||
| 388 | if (!addClassMapping(metaname, class_id)) { | ||
| 389 | RedisModule_ReplyWithError(ctx, "ERR failed to store class mapping"); | ||
| 390 | return REDISMODULE_OK; | ||
| 391 | } | ||
| 392 | RedisModule_ReplyWithLongLong(ctx, class_id); | ||
| 393 | } | ||
| 394 | |||
| 395 | return REDISMODULE_OK; | ||
| 396 | } | ||
| 397 | |||
| 398 | /* KEYMETA.SET <9-byte-id> <key> <string-value> */ | ||
| 399 | static int KeyMetaSet_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 400 | if (argc != 4) { | ||
| 401 | return RedisModule_WrongArity(ctx); | ||
| 402 | } | ||
| 403 | |||
| 404 | /* Parse arguments */ | ||
| 405 | const char *metaname = RedisModule_StringPtrLen(argv[1], NULL); | ||
| 406 | RedisModuleString *keyname = argv[2]; | ||
| 407 | const char *value = RedisModule_StringPtrLen(argv[3], NULL); | ||
| 408 | |||
| 409 | /* Lookup the metadata class by name */ | ||
| 410 | RedisModuleKeyMetaClassId class_id = lookupClassId(metaname); | ||
| 411 | if (class_id < 0) { | ||
| 412 | RedisModule_ReplyWithError(ctx, "ERR metadata class not found"); | ||
| 413 | return REDISMODULE_OK; | ||
| 414 | } | ||
| 415 | |||
| 416 | /* Open the key for writing */ | ||
| 417 | RedisModuleKey *key = RedisModule_OpenKey(ctx, keyname, REDISMODULE_READ | REDISMODULE_WRITE); | ||
| 418 | |||
| 419 | if (RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_EMPTY) { | ||
| 420 | RedisModule_ReplyWithNull(ctx); | ||
| 421 | RedisModule_CloseKey(key); | ||
| 422 | return REDISMODULE_OK; | ||
| 423 | } | ||
| 424 | |||
| 425 | /* Check if metadata already exists and free it first. | ||
| 426 | * | ||
| 427 | * Note: The caller is responsible for retrieving and freeing any existing | ||
| 428 | * pointer-based metadata before RM_SetKeyMeta() to a new value | ||
| 429 | */ | ||
| 430 | uint64_t meta = 0; | ||
| 431 | if (RedisModule_GetKeyMeta(class_id, key, &meta) == REDISMODULE_OK) { | ||
| 432 | if (meta != 0) { | ||
| 433 | free((char *)meta); | ||
| 434 | active_metadata_count--; /* Old metadata freed */ | ||
| 435 | } | ||
| 436 | } | ||
| 437 | |||
| 438 | char *new_str = strdup(value); | ||
| 439 | int res = RedisModule_SetKeyMeta(class_id, key, (uint64_t)new_str); | ||
| 440 | |||
| 441 | if (res == REDISMODULE_OK) { | ||
| 442 | active_metadata_count++; /* New metadata instance created */ | ||
| 443 | } | ||
| 444 | |||
| 445 | RedisModule_CloseKey(key); | ||
| 446 | |||
| 447 | if (res == REDISMODULE_OK) { | ||
| 448 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 449 | } else { | ||
| 450 | free(new_str); | ||
| 451 | RedisModule_ReplyWithError(ctx, "ERR failed to set metadata"); | ||
| 452 | } | ||
| 453 | return REDISMODULE_OK; | ||
| 454 | } | ||
| 455 | |||
| 456 | /* KEYMETA.GET <9-byte-id> <key> */ | ||
| 457 | static int KeyMetaGet_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 458 | if (argc != 3) { | ||
| 459 | return RedisModule_WrongArity(ctx); | ||
| 460 | } | ||
| 461 | |||
| 462 | /* Parse arguments */ | ||
| 463 | const char *metaname = RedisModule_StringPtrLen(argv[1], NULL); | ||
| 464 | RedisModuleString *keyname = argv[2]; | ||
| 465 | |||
| 466 | /* Lookup the metadata class by name */ | ||
| 467 | RedisModuleKeyMetaClassId class_id = lookupClassId(metaname); | ||
| 468 | if (class_id < 0) { | ||
| 469 | RedisModule_ReplyWithError(ctx, "ERR metadata class not found"); | ||
| 470 | return REDISMODULE_OK; | ||
| 471 | } | ||
| 472 | |||
| 473 | /* Open the key for reading */ | ||
| 474 | RedisModuleKey *key = RedisModule_OpenKey(ctx, keyname, REDISMODULE_READ); | ||
| 475 | if (RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_EMPTY) { | ||
| 476 | RedisModule_ReplyWithNull(ctx); | ||
| 477 | RedisModule_CloseKey(key); | ||
| 478 | return REDISMODULE_OK; | ||
| 479 | } | ||
| 480 | |||
| 481 | /* Get the metadata */ | ||
| 482 | uint64_t meta = 0; | ||
| 483 | int result = RedisModule_GetKeyMeta(class_id, key, &meta); | ||
| 484 | |||
| 485 | RedisModule_CloseKey(key); | ||
| 486 | |||
| 487 | if (result == REDISMODULE_OK && meta != 0) { | ||
| 488 | char *str = (char *)meta; | ||
| 489 | RedisModule_ReplyWithCString(ctx, str); | ||
| 490 | } else { | ||
| 491 | RedisModule_ReplyWithNull(ctx); | ||
| 492 | } | ||
| 493 | |||
| 494 | return REDISMODULE_OK; | ||
| 495 | } | ||
| 496 | |||
| 497 | /* KEYMETA.UNREGISTER <9-byte-id> */ | ||
| 498 | static int KeyMetaUnregister_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 499 | if (argc != 2) { | ||
| 500 | return RedisModule_WrongArity(ctx); | ||
| 501 | } | ||
| 502 | |||
| 503 | /* Parse arguments */ | ||
| 504 | const char *metaname = RedisModule_StringPtrLen(argv[1], NULL); | ||
| 505 | |||
| 506 | /* Lookup the metadata class by name */ | ||
| 507 | RedisModuleKeyMetaClassId class_id = lookupClassId(metaname); | ||
| 508 | if (class_id < 0) { | ||
| 509 | RedisModule_ReplyWithError(ctx, "ERR metadata class not found"); | ||
| 510 | return REDISMODULE_OK; | ||
| 511 | } | ||
| 512 | |||
| 513 | /* Release the metadata class */ | ||
| 514 | int result = RedisModule_ReleaseKeyMetaClass(class_id); | ||
| 515 | |||
| 516 | if (result == REDISMODULE_OK) { | ||
| 517 | /* Remove the mapping */ | ||
| 518 | removeClassMapping(metaname); | ||
| 519 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 520 | } else { | ||
| 521 | RedisModule_ReplyWithError(ctx, "ERR failed to unregister class"); | ||
| 522 | } | ||
| 523 | return REDISMODULE_OK; | ||
| 524 | } | ||
| 525 | |||
| 526 | /* KEYMETA.ACTIVE | ||
| 527 | * Returns the total number of active metadata instances that haven't been freed yet. | ||
| 528 | * This is useful for testing to verify that metadata is properly cleaned up. */ | ||
| 529 | static int KeyMetaActive_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 530 | if (argc != 1) { | ||
| 531 | return RedisModule_WrongArity(ctx); | ||
| 532 | } | ||
| 533 | REDISMODULE_NOT_USED(argv); | ||
| 534 | |||
| 535 | RedisModule_ReplyWithLongLong(ctx, active_metadata_count); | ||
| 536 | return REDISMODULE_OK; | ||
| 537 | } | ||
| 538 | |||
| 539 | /* Module initialization */ | ||
| 540 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 541 | REDISMODULE_NOT_USED(argv); | ||
| 542 | REDISMODULE_NOT_USED(argc); | ||
| 543 | |||
| 544 | if (RedisModule_Init(ctx, "test_metakey", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) { | ||
| 545 | return REDISMODULE_ERR; | ||
| 546 | } | ||
| 547 | |||
| 548 | /* Register commands */ | ||
| 549 | if (RedisModule_CreateCommand(ctx, "keymeta.register", | ||
| 550 | KeyMetaRegister_RedisCommand, "write", 0, 0, 0) == REDISMODULE_ERR) { | ||
| 551 | return REDISMODULE_ERR; | ||
| 552 | } | ||
| 553 | |||
| 554 | if (RedisModule_CreateCommand(ctx, "keymeta.set", | ||
| 555 | KeyMetaSet_RedisCommand, "write deny-oom", 1, 1, 1) == REDISMODULE_ERR) { | ||
| 556 | return REDISMODULE_ERR; | ||
| 557 | } | ||
| 558 | |||
| 559 | if (RedisModule_CreateCommand(ctx, "keymeta.get", | ||
| 560 | KeyMetaGet_RedisCommand, "readonly", 1, 1, 1) == REDISMODULE_ERR) { | ||
| 561 | return REDISMODULE_ERR; | ||
| 562 | } | ||
| 563 | |||
| 564 | if (RedisModule_CreateCommand(ctx, "keymeta.unregister", | ||
| 565 | KeyMetaUnregister_RedisCommand, "write", 0, 0, 0) == REDISMODULE_ERR) { | ||
| 566 | return REDISMODULE_ERR; | ||
| 567 | } | ||
| 568 | |||
| 569 | if (RedisModule_CreateCommand(ctx, "keymeta.active", | ||
| 570 | KeyMetaActive_RedisCommand, "readonly fast", 0, 0, 0) == REDISMODULE_ERR) { | ||
| 571 | return REDISMODULE_ERR; | ||
| 572 | } | ||
| 573 | |||
| 574 | return REDISMODULE_OK; | ||
| 575 | } | ||
| 576 | |||
| 577 | int RedisModule_OnUnload(RedisModuleCtx *ctx) { | ||
| 578 | REDISMODULE_NOT_USED(ctx); | ||
| 579 | long unsigned int i; | ||
| 580 | for (i = 0 ; i < sizeof(class_ids) / sizeof(class_ids[0]); i++) { | ||
| 581 | if (class_ids[i] > 0) | ||
| 582 | RedisModule_ReleaseKeyMetaClass(class_ids[i]); | ||
| 583 | } | ||
| 584 | return REDISMODULE_OK; | ||
| 585 | } | ||
diff --git a/examples/redis-unstable/tests/modules/test_lazyfree.c b/examples/redis-unstable/tests/modules/test_lazyfree.c new file mode 100644 index 0000000..7ba213f --- /dev/null +++ b/examples/redis-unstable/tests/modules/test_lazyfree.c | |||
| @@ -0,0 +1,196 @@ | |||
| 1 | /* This module emulates a linked list for lazyfree testing of modules, which | ||
| 2 | is a simplified version of 'hellotype.c' | ||
| 3 | */ | ||
| 4 | #include "redismodule.h" | ||
| 5 | #include <stdio.h> | ||
| 6 | #include <stdlib.h> | ||
| 7 | #include <ctype.h> | ||
| 8 | #include <string.h> | ||
| 9 | #include <stdint.h> | ||
| 10 | |||
| 11 | static RedisModuleType *LazyFreeLinkType; | ||
| 12 | |||
| 13 | struct LazyFreeLinkNode { | ||
| 14 | int64_t value; | ||
| 15 | struct LazyFreeLinkNode *next; | ||
| 16 | }; | ||
| 17 | |||
| 18 | struct LazyFreeLinkObject { | ||
| 19 | struct LazyFreeLinkNode *head; | ||
| 20 | size_t len; /* Number of elements added. */ | ||
| 21 | }; | ||
| 22 | |||
| 23 | struct LazyFreeLinkObject *createLazyFreeLinkObject(void) { | ||
| 24 | struct LazyFreeLinkObject *o; | ||
| 25 | o = RedisModule_Alloc(sizeof(*o)); | ||
| 26 | o->head = NULL; | ||
| 27 | o->len = 0; | ||
| 28 | return o; | ||
| 29 | } | ||
| 30 | |||
| 31 | void LazyFreeLinkInsert(struct LazyFreeLinkObject *o, int64_t ele) { | ||
| 32 | struct LazyFreeLinkNode *next = o->head, *newnode, *prev = NULL; | ||
| 33 | |||
| 34 | while(next && next->value < ele) { | ||
| 35 | prev = next; | ||
| 36 | next = next->next; | ||
| 37 | } | ||
| 38 | newnode = RedisModule_Alloc(sizeof(*newnode)); | ||
| 39 | newnode->value = ele; | ||
| 40 | newnode->next = next; | ||
| 41 | if (prev) { | ||
| 42 | prev->next = newnode; | ||
| 43 | } else { | ||
| 44 | o->head = newnode; | ||
| 45 | } | ||
| 46 | o->len++; | ||
| 47 | } | ||
| 48 | |||
| 49 | void LazyFreeLinkReleaseObject(struct LazyFreeLinkObject *o) { | ||
| 50 | struct LazyFreeLinkNode *cur, *next; | ||
| 51 | cur = o->head; | ||
| 52 | while(cur) { | ||
| 53 | next = cur->next; | ||
| 54 | RedisModule_Free(cur); | ||
| 55 | cur = next; | ||
| 56 | } | ||
| 57 | RedisModule_Free(o); | ||
| 58 | } | ||
| 59 | |||
| 60 | /* LAZYFREELINK.INSERT key value */ | ||
| 61 | int LazyFreeLinkInsert_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 62 | RedisModule_AutoMemory(ctx); /* Use automatic memory management. */ | ||
| 63 | |||
| 64 | if (argc != 3) return RedisModule_WrongArity(ctx); | ||
| 65 | RedisModuleKey *key = RedisModule_OpenKey(ctx,argv[1], | ||
| 66 | REDISMODULE_READ|REDISMODULE_WRITE); | ||
| 67 | int type = RedisModule_KeyType(key); | ||
| 68 | if (type != REDISMODULE_KEYTYPE_EMPTY && | ||
| 69 | RedisModule_ModuleTypeGetType(key) != LazyFreeLinkType) | ||
| 70 | { | ||
| 71 | return RedisModule_ReplyWithError(ctx,REDISMODULE_ERRORMSG_WRONGTYPE); | ||
| 72 | } | ||
| 73 | |||
| 74 | long long value; | ||
| 75 | if ((RedisModule_StringToLongLong(argv[2],&value) != REDISMODULE_OK)) { | ||
| 76 | return RedisModule_ReplyWithError(ctx,"ERR invalid value: must be a signed 64 bit integer"); | ||
| 77 | } | ||
| 78 | |||
| 79 | struct LazyFreeLinkObject *hto; | ||
| 80 | if (type == REDISMODULE_KEYTYPE_EMPTY) { | ||
| 81 | hto = createLazyFreeLinkObject(); | ||
| 82 | RedisModule_ModuleTypeSetValue(key,LazyFreeLinkType,hto); | ||
| 83 | } else { | ||
| 84 | hto = RedisModule_ModuleTypeGetValue(key); | ||
| 85 | } | ||
| 86 | |||
| 87 | LazyFreeLinkInsert(hto,value); | ||
| 88 | RedisModule_SignalKeyAsReady(ctx,argv[1]); | ||
| 89 | |||
| 90 | RedisModule_ReplyWithLongLong(ctx,hto->len); | ||
| 91 | RedisModule_ReplicateVerbatim(ctx); | ||
| 92 | return REDISMODULE_OK; | ||
| 93 | } | ||
| 94 | |||
| 95 | /* LAZYFREELINK.LEN key */ | ||
| 96 | int LazyFreeLinkLen_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 97 | RedisModule_AutoMemory(ctx); /* Use automatic memory management. */ | ||
| 98 | |||
| 99 | if (argc != 2) return RedisModule_WrongArity(ctx); | ||
| 100 | RedisModuleKey *key = RedisModule_OpenKey(ctx,argv[1], | ||
| 101 | REDISMODULE_READ); | ||
| 102 | int type = RedisModule_KeyType(key); | ||
| 103 | if (type != REDISMODULE_KEYTYPE_EMPTY && | ||
| 104 | RedisModule_ModuleTypeGetType(key) != LazyFreeLinkType) | ||
| 105 | { | ||
| 106 | return RedisModule_ReplyWithError(ctx,REDISMODULE_ERRORMSG_WRONGTYPE); | ||
| 107 | } | ||
| 108 | |||
| 109 | struct LazyFreeLinkObject *hto = RedisModule_ModuleTypeGetValue(key); | ||
| 110 | RedisModule_ReplyWithLongLong(ctx,hto ? hto->len : 0); | ||
| 111 | return REDISMODULE_OK; | ||
| 112 | } | ||
| 113 | |||
| 114 | void *LazyFreeLinkRdbLoad(RedisModuleIO *rdb, int encver) { | ||
| 115 | if (encver != 0) { | ||
| 116 | return NULL; | ||
| 117 | } | ||
| 118 | uint64_t elements = RedisModule_LoadUnsigned(rdb); | ||
| 119 | struct LazyFreeLinkObject *hto = createLazyFreeLinkObject(); | ||
| 120 | while(elements--) { | ||
| 121 | int64_t ele = RedisModule_LoadSigned(rdb); | ||
| 122 | LazyFreeLinkInsert(hto,ele); | ||
| 123 | } | ||
| 124 | return hto; | ||
| 125 | } | ||
| 126 | |||
| 127 | void LazyFreeLinkRdbSave(RedisModuleIO *rdb, void *value) { | ||
| 128 | struct LazyFreeLinkObject *hto = value; | ||
| 129 | struct LazyFreeLinkNode *node = hto->head; | ||
| 130 | RedisModule_SaveUnsigned(rdb,hto->len); | ||
| 131 | while(node) { | ||
| 132 | RedisModule_SaveSigned(rdb,node->value); | ||
| 133 | node = node->next; | ||
| 134 | } | ||
| 135 | } | ||
| 136 | |||
| 137 | void LazyFreeLinkAofRewrite(RedisModuleIO *aof, RedisModuleString *key, void *value) { | ||
| 138 | struct LazyFreeLinkObject *hto = value; | ||
| 139 | struct LazyFreeLinkNode *node = hto->head; | ||
| 140 | while(node) { | ||
| 141 | RedisModule_EmitAOF(aof,"LAZYFREELINK.INSERT","sl",key,node->value); | ||
| 142 | node = node->next; | ||
| 143 | } | ||
| 144 | } | ||
| 145 | |||
| 146 | void LazyFreeLinkFree(void *value) { | ||
| 147 | LazyFreeLinkReleaseObject(value); | ||
| 148 | } | ||
| 149 | |||
| 150 | size_t LazyFreeLinkFreeEffort(RedisModuleString *key, const void *value) { | ||
| 151 | REDISMODULE_NOT_USED(key); | ||
| 152 | const struct LazyFreeLinkObject *hto = value; | ||
| 153 | return hto->len; | ||
| 154 | } | ||
| 155 | |||
| 156 | void LazyFreeLinkUnlink(RedisModuleString *key, const void *value) { | ||
| 157 | REDISMODULE_NOT_USED(key); | ||
| 158 | REDISMODULE_NOT_USED(value); | ||
| 159 | /* Here you can know which key and value is about to be freed. */ | ||
| 160 | } | ||
| 161 | |||
| 162 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 163 | REDISMODULE_NOT_USED(argv); | ||
| 164 | REDISMODULE_NOT_USED(argc); | ||
| 165 | |||
| 166 | if (RedisModule_Init(ctx,"lazyfreetest",1,REDISMODULE_APIVER_1) | ||
| 167 | == REDISMODULE_ERR) return REDISMODULE_ERR; | ||
| 168 | |||
| 169 | /* We only allow our module to be loaded when the redis core version is greater than the version of my module */ | ||
| 170 | if (RedisModule_GetTypeMethodVersion() < REDISMODULE_TYPE_METHOD_VERSION) { | ||
| 171 | return REDISMODULE_ERR; | ||
| 172 | } | ||
| 173 | |||
| 174 | RedisModuleTypeMethods tm = { | ||
| 175 | .version = REDISMODULE_TYPE_METHOD_VERSION, | ||
| 176 | .rdb_load = LazyFreeLinkRdbLoad, | ||
| 177 | .rdb_save = LazyFreeLinkRdbSave, | ||
| 178 | .aof_rewrite = LazyFreeLinkAofRewrite, | ||
| 179 | .free = LazyFreeLinkFree, | ||
| 180 | .free_effort = LazyFreeLinkFreeEffort, | ||
| 181 | .unlink = LazyFreeLinkUnlink, | ||
| 182 | }; | ||
| 183 | |||
| 184 | LazyFreeLinkType = RedisModule_CreateDataType(ctx,"test_lazy",0,&tm); | ||
| 185 | if (LazyFreeLinkType == NULL) return REDISMODULE_ERR; | ||
| 186 | |||
| 187 | if (RedisModule_CreateCommand(ctx,"lazyfreelink.insert", | ||
| 188 | LazyFreeLinkInsert_RedisCommand,"write deny-oom",1,1,1) == REDISMODULE_ERR) | ||
| 189 | return REDISMODULE_ERR; | ||
| 190 | |||
| 191 | if (RedisModule_CreateCommand(ctx,"lazyfreelink.len", | ||
| 192 | LazyFreeLinkLen_RedisCommand,"readonly",1,1,1) == REDISMODULE_ERR) | ||
| 193 | return REDISMODULE_ERR; | ||
| 194 | |||
| 195 | return REDISMODULE_OK; | ||
| 196 | } | ||
diff --git a/examples/redis-unstable/tests/modules/testrdb.c b/examples/redis-unstable/tests/modules/testrdb.c new file mode 100644 index 0000000..c31aebb --- /dev/null +++ b/examples/redis-unstable/tests/modules/testrdb.c | |||
| @@ -0,0 +1,405 @@ | |||
| 1 | #include "redismodule.h" | ||
| 2 | |||
| 3 | #include <string.h> | ||
| 4 | #include <assert.h> | ||
| 5 | |||
| 6 | /* Module configuration, save aux or not? */ | ||
| 7 | #define CONF_AUX_OPTION_NO_AUX 0 | ||
| 8 | #define CONF_AUX_OPTION_SAVE2 1 << 0 | ||
| 9 | #define CONF_AUX_OPTION_BEFORE_KEYSPACE 1 << 1 | ||
| 10 | #define CONF_AUX_OPTION_AFTER_KEYSPACE 1 << 2 | ||
| 11 | #define CONF_AUX_OPTION_NO_DATA 1 << 3 | ||
| 12 | long long conf_aux_count = 0; | ||
| 13 | |||
| 14 | /* Registered type */ | ||
| 15 | RedisModuleType *testrdb_type = NULL; | ||
| 16 | |||
| 17 | /* Global values to store and persist to aux */ | ||
| 18 | RedisModuleString *before_str = NULL; | ||
| 19 | RedisModuleString *after_str = NULL; | ||
| 20 | |||
| 21 | /* Global values used to keep aux from db being loaded (in case of async_loading) */ | ||
| 22 | RedisModuleString *before_str_temp = NULL; | ||
| 23 | RedisModuleString *after_str_temp = NULL; | ||
| 24 | |||
| 25 | /* Indicates whether there is an async replication in progress. | ||
| 26 | * We control this value from RedisModuleEvent_ReplAsyncLoad events. */ | ||
| 27 | int async_loading = 0; | ||
| 28 | |||
| 29 | int n_aux_load_called = 0; | ||
| 30 | |||
| 31 | void replAsyncLoadCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data) | ||
| 32 | { | ||
| 33 | REDISMODULE_NOT_USED(e); | ||
| 34 | REDISMODULE_NOT_USED(data); | ||
| 35 | |||
| 36 | switch (sub) { | ||
| 37 | case REDISMODULE_SUBEVENT_REPL_ASYNC_LOAD_STARTED: | ||
| 38 | assert(async_loading == 0); | ||
| 39 | async_loading = 1; | ||
| 40 | break; | ||
| 41 | case REDISMODULE_SUBEVENT_REPL_ASYNC_LOAD_ABORTED: | ||
| 42 | /* Discard temp aux */ | ||
| 43 | if (before_str_temp) | ||
| 44 | RedisModule_FreeString(ctx, before_str_temp); | ||
| 45 | if (after_str_temp) | ||
| 46 | RedisModule_FreeString(ctx, after_str_temp); | ||
| 47 | before_str_temp = NULL; | ||
| 48 | after_str_temp = NULL; | ||
| 49 | |||
| 50 | async_loading = 0; | ||
| 51 | break; | ||
| 52 | case REDISMODULE_SUBEVENT_REPL_ASYNC_LOAD_COMPLETED: | ||
| 53 | if (before_str) | ||
| 54 | RedisModule_FreeString(ctx, before_str); | ||
| 55 | if (after_str) | ||
| 56 | RedisModule_FreeString(ctx, after_str); | ||
| 57 | before_str = before_str_temp; | ||
| 58 | after_str = after_str_temp; | ||
| 59 | |||
| 60 | before_str_temp = NULL; | ||
| 61 | after_str_temp = NULL; | ||
| 62 | |||
| 63 | async_loading = 0; | ||
| 64 | break; | ||
| 65 | default: | ||
| 66 | assert(0); | ||
| 67 | } | ||
| 68 | } | ||
| 69 | |||
| 70 | void *testrdb_type_load(RedisModuleIO *rdb, int encver) { | ||
| 71 | int count = RedisModule_LoadSigned(rdb); | ||
| 72 | RedisModuleString *str = RedisModule_LoadString(rdb); | ||
| 73 | float f = RedisModule_LoadFloat(rdb); | ||
| 74 | long double ld = RedisModule_LoadLongDouble(rdb); | ||
| 75 | if (RedisModule_IsIOError(rdb)) { | ||
| 76 | RedisModuleCtx *ctx = RedisModule_GetContextFromIO(rdb); | ||
| 77 | if (str) | ||
| 78 | RedisModule_FreeString(ctx, str); | ||
| 79 | return NULL; | ||
| 80 | } | ||
| 81 | /* Using the values only after checking for io errors. */ | ||
| 82 | assert(count==1); | ||
| 83 | assert(encver==1); | ||
| 84 | assert(f==1.5f); | ||
| 85 | assert(ld==0.333333333333333333L); | ||
| 86 | return str; | ||
| 87 | } | ||
| 88 | |||
| 89 | void testrdb_type_save(RedisModuleIO *rdb, void *value) { | ||
| 90 | RedisModuleString *str = (RedisModuleString*)value; | ||
| 91 | RedisModule_SaveSigned(rdb, 1); | ||
| 92 | RedisModule_SaveString(rdb, str); | ||
| 93 | RedisModule_SaveFloat(rdb, 1.5); | ||
| 94 | RedisModule_SaveLongDouble(rdb, 0.333333333333333333L); | ||
| 95 | } | ||
| 96 | |||
| 97 | void testrdb_aux_save(RedisModuleIO *rdb, int when) { | ||
| 98 | if (!(conf_aux_count & CONF_AUX_OPTION_BEFORE_KEYSPACE)) assert(when == REDISMODULE_AUX_AFTER_RDB); | ||
| 99 | if (!(conf_aux_count & CONF_AUX_OPTION_AFTER_KEYSPACE)) assert(when == REDISMODULE_AUX_BEFORE_RDB); | ||
| 100 | assert(conf_aux_count!=CONF_AUX_OPTION_NO_AUX); | ||
| 101 | if (when == REDISMODULE_AUX_BEFORE_RDB) { | ||
| 102 | if (before_str) { | ||
| 103 | RedisModule_SaveSigned(rdb, 1); | ||
| 104 | RedisModule_SaveString(rdb, before_str); | ||
| 105 | } else { | ||
| 106 | RedisModule_SaveSigned(rdb, 0); | ||
| 107 | } | ||
| 108 | } else { | ||
| 109 | if (after_str) { | ||
| 110 | RedisModule_SaveSigned(rdb, 1); | ||
| 111 | RedisModule_SaveString(rdb, after_str); | ||
| 112 | } else { | ||
| 113 | RedisModule_SaveSigned(rdb, 0); | ||
| 114 | } | ||
| 115 | } | ||
| 116 | } | ||
| 117 | |||
| 118 | int testrdb_aux_load(RedisModuleIO *rdb, int encver, int when) { | ||
| 119 | assert(encver == 1); | ||
| 120 | if (!(conf_aux_count & CONF_AUX_OPTION_BEFORE_KEYSPACE)) assert(when == REDISMODULE_AUX_AFTER_RDB); | ||
| 121 | if (!(conf_aux_count & CONF_AUX_OPTION_AFTER_KEYSPACE)) assert(when == REDISMODULE_AUX_BEFORE_RDB); | ||
| 122 | assert(conf_aux_count!=CONF_AUX_OPTION_NO_AUX); | ||
| 123 | RedisModuleCtx *ctx = RedisModule_GetContextFromIO(rdb); | ||
| 124 | if (when == REDISMODULE_AUX_BEFORE_RDB) { | ||
| 125 | if (async_loading == 0) { | ||
| 126 | if (before_str) | ||
| 127 | RedisModule_FreeString(ctx, before_str); | ||
| 128 | before_str = NULL; | ||
| 129 | int count = RedisModule_LoadSigned(rdb); | ||
| 130 | if (RedisModule_IsIOError(rdb)) | ||
| 131 | return REDISMODULE_ERR; | ||
| 132 | if (count) | ||
| 133 | before_str = RedisModule_LoadString(rdb); | ||
| 134 | } else { | ||
| 135 | if (before_str_temp) | ||
| 136 | RedisModule_FreeString(ctx, before_str_temp); | ||
| 137 | before_str_temp = NULL; | ||
| 138 | int count = RedisModule_LoadSigned(rdb); | ||
| 139 | if (RedisModule_IsIOError(rdb)) | ||
| 140 | return REDISMODULE_ERR; | ||
| 141 | if (count) | ||
| 142 | before_str_temp = RedisModule_LoadString(rdb); | ||
| 143 | } | ||
| 144 | } else { | ||
| 145 | if (async_loading == 0) { | ||
| 146 | if (after_str) | ||
| 147 | RedisModule_FreeString(ctx, after_str); | ||
| 148 | after_str = NULL; | ||
| 149 | int count = RedisModule_LoadSigned(rdb); | ||
| 150 | if (RedisModule_IsIOError(rdb)) | ||
| 151 | return REDISMODULE_ERR; | ||
| 152 | if (count) | ||
| 153 | after_str = RedisModule_LoadString(rdb); | ||
| 154 | } else { | ||
| 155 | if (after_str_temp) | ||
| 156 | RedisModule_FreeString(ctx, after_str_temp); | ||
| 157 | after_str_temp = NULL; | ||
| 158 | int count = RedisModule_LoadSigned(rdb); | ||
| 159 | if (RedisModule_IsIOError(rdb)) | ||
| 160 | return REDISMODULE_ERR; | ||
| 161 | if (count) | ||
| 162 | after_str_temp = RedisModule_LoadString(rdb); | ||
| 163 | } | ||
| 164 | } | ||
| 165 | |||
| 166 | if (RedisModule_IsIOError(rdb)) | ||
| 167 | return REDISMODULE_ERR; | ||
| 168 | return REDISMODULE_OK; | ||
| 169 | } | ||
| 170 | |||
| 171 | void testrdb_type_free(void *value) { | ||
| 172 | if (value) | ||
| 173 | RedisModule_FreeString(NULL, (RedisModuleString*)value); | ||
| 174 | } | ||
| 175 | |||
| 176 | int testrdb_set_before(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 177 | { | ||
| 178 | if (argc != 2) { | ||
| 179 | RedisModule_WrongArity(ctx); | ||
| 180 | return REDISMODULE_OK; | ||
| 181 | } | ||
| 182 | |||
| 183 | if (before_str) | ||
| 184 | RedisModule_FreeString(ctx, before_str); | ||
| 185 | before_str = argv[1]; | ||
| 186 | RedisModule_RetainString(ctx, argv[1]); | ||
| 187 | RedisModule_ReplyWithLongLong(ctx, 1); | ||
| 188 | return REDISMODULE_OK; | ||
| 189 | } | ||
| 190 | |||
| 191 | int testrdb_get_before(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 192 | { | ||
| 193 | REDISMODULE_NOT_USED(argv); | ||
| 194 | if (argc != 1){ | ||
| 195 | RedisModule_WrongArity(ctx); | ||
| 196 | return REDISMODULE_OK; | ||
| 197 | } | ||
| 198 | if (before_str) | ||
| 199 | RedisModule_ReplyWithString(ctx, before_str); | ||
| 200 | else | ||
| 201 | RedisModule_ReplyWithStringBuffer(ctx, "", 0); | ||
| 202 | return REDISMODULE_OK; | ||
| 203 | } | ||
| 204 | |||
| 205 | /* For purpose of testing module events, expose variable state during async_loading. */ | ||
| 206 | int testrdb_async_loading_get_before(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 207 | { | ||
| 208 | REDISMODULE_NOT_USED(argv); | ||
| 209 | if (argc != 1){ | ||
| 210 | RedisModule_WrongArity(ctx); | ||
| 211 | return REDISMODULE_OK; | ||
| 212 | } | ||
| 213 | if (before_str_temp) | ||
| 214 | RedisModule_ReplyWithString(ctx, before_str_temp); | ||
| 215 | else | ||
| 216 | RedisModule_ReplyWithStringBuffer(ctx, "", 0); | ||
| 217 | return REDISMODULE_OK; | ||
| 218 | } | ||
| 219 | |||
| 220 | int testrdb_set_after(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 221 | { | ||
| 222 | if (argc != 2){ | ||
| 223 | RedisModule_WrongArity(ctx); | ||
| 224 | return REDISMODULE_OK; | ||
| 225 | } | ||
| 226 | |||
| 227 | if (after_str) | ||
| 228 | RedisModule_FreeString(ctx, after_str); | ||
| 229 | after_str = argv[1]; | ||
| 230 | RedisModule_RetainString(ctx, argv[1]); | ||
| 231 | RedisModule_ReplyWithLongLong(ctx, 1); | ||
| 232 | return REDISMODULE_OK; | ||
| 233 | } | ||
| 234 | |||
| 235 | int testrdb_get_after(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 236 | { | ||
| 237 | REDISMODULE_NOT_USED(argv); | ||
| 238 | if (argc != 1){ | ||
| 239 | RedisModule_WrongArity(ctx); | ||
| 240 | return REDISMODULE_OK; | ||
| 241 | } | ||
| 242 | if (after_str) | ||
| 243 | RedisModule_ReplyWithString(ctx, after_str); | ||
| 244 | else | ||
| 245 | RedisModule_ReplyWithStringBuffer(ctx, "", 0); | ||
| 246 | return REDISMODULE_OK; | ||
| 247 | } | ||
| 248 | |||
| 249 | int testrdb_set_key(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 250 | { | ||
| 251 | if (argc != 3){ | ||
| 252 | RedisModule_WrongArity(ctx); | ||
| 253 | return REDISMODULE_OK; | ||
| 254 | } | ||
| 255 | |||
| 256 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE); | ||
| 257 | RedisModuleString *str = RedisModule_ModuleTypeGetValue(key); | ||
| 258 | if (str) | ||
| 259 | RedisModule_FreeString(ctx, str); | ||
| 260 | RedisModule_ModuleTypeSetValue(key, testrdb_type, argv[2]); | ||
| 261 | RedisModule_RetainString(ctx, argv[2]); | ||
| 262 | RedisModule_CloseKey(key); | ||
| 263 | RedisModule_ReplyWithLongLong(ctx, 1); | ||
| 264 | return REDISMODULE_OK; | ||
| 265 | } | ||
| 266 | |||
| 267 | int testrdb_get_key(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 268 | { | ||
| 269 | if (argc != 2){ | ||
| 270 | RedisModule_WrongArity(ctx); | ||
| 271 | return REDISMODULE_OK; | ||
| 272 | } | ||
| 273 | |||
| 274 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ); | ||
| 275 | RedisModuleString *str = RedisModule_ModuleTypeGetValue(key); | ||
| 276 | RedisModule_CloseKey(key); | ||
| 277 | RedisModule_ReplyWithString(ctx, str); | ||
| 278 | return REDISMODULE_OK; | ||
| 279 | } | ||
| 280 | |||
| 281 | int testrdb_get_n_aux_load_called(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 282 | { | ||
| 283 | REDISMODULE_NOT_USED(ctx); | ||
| 284 | REDISMODULE_NOT_USED(argv); | ||
| 285 | REDISMODULE_NOT_USED(argc); | ||
| 286 | RedisModule_ReplyWithLongLong(ctx, n_aux_load_called); | ||
| 287 | return REDISMODULE_OK; | ||
| 288 | } | ||
| 289 | |||
| 290 | int test2rdb_aux_load(RedisModuleIO *rdb, int encver, int when) { | ||
| 291 | REDISMODULE_NOT_USED(rdb); | ||
| 292 | REDISMODULE_NOT_USED(encver); | ||
| 293 | REDISMODULE_NOT_USED(when); | ||
| 294 | n_aux_load_called++; | ||
| 295 | return REDISMODULE_OK; | ||
| 296 | } | ||
| 297 | |||
| 298 | void test2rdb_aux_save(RedisModuleIO *rdb, int when) { | ||
| 299 | REDISMODULE_NOT_USED(rdb); | ||
| 300 | REDISMODULE_NOT_USED(when); | ||
| 301 | } | ||
| 302 | |||
| 303 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 304 | REDISMODULE_NOT_USED(argv); | ||
| 305 | REDISMODULE_NOT_USED(argc); | ||
| 306 | if (RedisModule_Init(ctx,"testrdb",1,REDISMODULE_APIVER_1) == REDISMODULE_ERR) | ||
| 307 | return REDISMODULE_ERR; | ||
| 308 | |||
| 309 | RedisModule_SetModuleOptions(ctx, REDISMODULE_OPTIONS_HANDLE_IO_ERRORS | REDISMODULE_OPTIONS_HANDLE_REPL_ASYNC_LOAD); | ||
| 310 | |||
| 311 | if (argc > 0) | ||
| 312 | RedisModule_StringToLongLong(argv[0], &conf_aux_count); | ||
| 313 | |||
| 314 | if (conf_aux_count==CONF_AUX_OPTION_NO_AUX) { | ||
| 315 | RedisModuleTypeMethods datatype_methods = { | ||
| 316 | .version = 1, | ||
| 317 | .rdb_load = testrdb_type_load, | ||
| 318 | .rdb_save = testrdb_type_save, | ||
| 319 | .aof_rewrite = NULL, | ||
| 320 | .digest = NULL, | ||
| 321 | .free = testrdb_type_free, | ||
| 322 | }; | ||
| 323 | |||
| 324 | testrdb_type = RedisModule_CreateDataType(ctx, "test__rdb", 1, &datatype_methods); | ||
| 325 | if (testrdb_type == NULL) | ||
| 326 | return REDISMODULE_ERR; | ||
| 327 | } else if (!(conf_aux_count & CONF_AUX_OPTION_NO_DATA)) { | ||
| 328 | RedisModuleTypeMethods datatype_methods = { | ||
| 329 | .version = REDISMODULE_TYPE_METHOD_VERSION, | ||
| 330 | .rdb_load = testrdb_type_load, | ||
| 331 | .rdb_save = testrdb_type_save, | ||
| 332 | .aof_rewrite = NULL, | ||
| 333 | .digest = NULL, | ||
| 334 | .free = testrdb_type_free, | ||
| 335 | .aux_load = testrdb_aux_load, | ||
| 336 | .aux_save = testrdb_aux_save, | ||
| 337 | .aux_save_triggers = ((conf_aux_count & CONF_AUX_OPTION_BEFORE_KEYSPACE) ? REDISMODULE_AUX_BEFORE_RDB : 0) | | ||
| 338 | ((conf_aux_count & CONF_AUX_OPTION_AFTER_KEYSPACE) ? REDISMODULE_AUX_AFTER_RDB : 0) | ||
| 339 | }; | ||
| 340 | |||
| 341 | if (conf_aux_count & CONF_AUX_OPTION_SAVE2) { | ||
| 342 | datatype_methods.aux_save2 = testrdb_aux_save; | ||
| 343 | } | ||
| 344 | |||
| 345 | testrdb_type = RedisModule_CreateDataType(ctx, "test__rdb", 1, &datatype_methods); | ||
| 346 | if (testrdb_type == NULL) | ||
| 347 | return REDISMODULE_ERR; | ||
| 348 | } else { | ||
| 349 | |||
| 350 | /* Used to verify that aux_save2 api without any data, saves nothing to the RDB. */ | ||
| 351 | RedisModuleTypeMethods datatype_methods = { | ||
| 352 | .version = REDISMODULE_TYPE_METHOD_VERSION, | ||
| 353 | .aux_load = test2rdb_aux_load, | ||
| 354 | .aux_save = test2rdb_aux_save, | ||
| 355 | .aux_save_triggers = ((conf_aux_count & CONF_AUX_OPTION_BEFORE_KEYSPACE) ? REDISMODULE_AUX_BEFORE_RDB : 0) | | ||
| 356 | ((conf_aux_count & CONF_AUX_OPTION_AFTER_KEYSPACE) ? REDISMODULE_AUX_AFTER_RDB : 0) | ||
| 357 | }; | ||
| 358 | if (conf_aux_count & CONF_AUX_OPTION_SAVE2) { | ||
| 359 | datatype_methods.aux_save2 = test2rdb_aux_save; | ||
| 360 | } | ||
| 361 | |||
| 362 | RedisModule_CreateDataType(ctx, "test__rdb", 1, &datatype_methods); | ||
| 363 | } | ||
| 364 | |||
| 365 | if (RedisModule_CreateCommand(ctx,"testrdb.set.before", testrdb_set_before,"deny-oom",0,0,0) == REDISMODULE_ERR) | ||
| 366 | return REDISMODULE_ERR; | ||
| 367 | |||
| 368 | if (RedisModule_CreateCommand(ctx,"testrdb.get.before", testrdb_get_before,"",0,0,0) == REDISMODULE_ERR) | ||
| 369 | return REDISMODULE_ERR; | ||
| 370 | |||
| 371 | if (RedisModule_CreateCommand(ctx,"testrdb.async_loading.get.before", testrdb_async_loading_get_before,"",0,0,0) == REDISMODULE_ERR) | ||
| 372 | return REDISMODULE_ERR; | ||
| 373 | |||
| 374 | if (RedisModule_CreateCommand(ctx,"testrdb.set.after", testrdb_set_after,"deny-oom",0,0,0) == REDISMODULE_ERR) | ||
| 375 | return REDISMODULE_ERR; | ||
| 376 | |||
| 377 | if (RedisModule_CreateCommand(ctx,"testrdb.get.after", testrdb_get_after,"",0,0,0) == REDISMODULE_ERR) | ||
| 378 | return REDISMODULE_ERR; | ||
| 379 | |||
| 380 | if (RedisModule_CreateCommand(ctx,"testrdb.set.key", testrdb_set_key,"deny-oom",1,1,1) == REDISMODULE_ERR) | ||
| 381 | return REDISMODULE_ERR; | ||
| 382 | |||
| 383 | if (RedisModule_CreateCommand(ctx,"testrdb.get.key", testrdb_get_key,"",1,1,1) == REDISMODULE_ERR) | ||
| 384 | return REDISMODULE_ERR; | ||
| 385 | |||
| 386 | if (RedisModule_CreateCommand(ctx,"testrdb.get.n_aux_load_called", testrdb_get_n_aux_load_called,"",1,1,1) == REDISMODULE_ERR) | ||
| 387 | return REDISMODULE_ERR; | ||
| 388 | |||
| 389 | RedisModule_SubscribeToServerEvent(ctx, | ||
| 390 | RedisModuleEvent_ReplAsyncLoad, replAsyncLoadCallback); | ||
| 391 | |||
| 392 | return REDISMODULE_OK; | ||
| 393 | } | ||
| 394 | |||
| 395 | int RedisModule_OnUnload(RedisModuleCtx *ctx) { | ||
| 396 | if (before_str) | ||
| 397 | RedisModule_FreeString(ctx, before_str); | ||
| 398 | if (after_str) | ||
| 399 | RedisModule_FreeString(ctx, after_str); | ||
| 400 | if (before_str_temp) | ||
| 401 | RedisModule_FreeString(ctx, before_str_temp); | ||
| 402 | if (after_str_temp) | ||
| 403 | RedisModule_FreeString(ctx, after_str_temp); | ||
| 404 | return REDISMODULE_OK; | ||
| 405 | } | ||
diff --git a/examples/redis-unstable/tests/modules/timer.c b/examples/redis-unstable/tests/modules/timer.c new file mode 100644 index 0000000..c9bd636 --- /dev/null +++ b/examples/redis-unstable/tests/modules/timer.c | |||
| @@ -0,0 +1,102 @@ | |||
| 1 | |||
| 2 | #include "redismodule.h" | ||
| 3 | |||
| 4 | static void timer_callback(RedisModuleCtx *ctx, void *data) | ||
| 5 | { | ||
| 6 | RedisModuleString *keyname = data; | ||
| 7 | RedisModuleCallReply *reply; | ||
| 8 | |||
| 9 | reply = RedisModule_Call(ctx, "INCR", "s", keyname); | ||
| 10 | if (reply != NULL) | ||
| 11 | RedisModule_FreeCallReply(reply); | ||
| 12 | RedisModule_FreeString(ctx, keyname); | ||
| 13 | } | ||
| 14 | |||
| 15 | int test_createtimer(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 16 | { | ||
| 17 | if (argc != 3) { | ||
| 18 | RedisModule_WrongArity(ctx); | ||
| 19 | return REDISMODULE_OK; | ||
| 20 | } | ||
| 21 | |||
| 22 | long long period; | ||
| 23 | if (RedisModule_StringToLongLong(argv[1], &period) == REDISMODULE_ERR) { | ||
| 24 | RedisModule_ReplyWithError(ctx, "Invalid time specified."); | ||
| 25 | return REDISMODULE_OK; | ||
| 26 | } | ||
| 27 | |||
| 28 | RedisModuleString *keyname = argv[2]; | ||
| 29 | RedisModule_RetainString(ctx, keyname); | ||
| 30 | |||
| 31 | RedisModuleTimerID id = RedisModule_CreateTimer(ctx, period, timer_callback, keyname); | ||
| 32 | RedisModule_ReplyWithLongLong(ctx, id); | ||
| 33 | |||
| 34 | return REDISMODULE_OK; | ||
| 35 | } | ||
| 36 | |||
| 37 | int test_gettimer(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 38 | { | ||
| 39 | if (argc != 2) { | ||
| 40 | RedisModule_WrongArity(ctx); | ||
| 41 | return REDISMODULE_OK; | ||
| 42 | } | ||
| 43 | |||
| 44 | long long id; | ||
| 45 | if (RedisModule_StringToLongLong(argv[1], &id) == REDISMODULE_ERR) { | ||
| 46 | RedisModule_ReplyWithError(ctx, "Invalid id specified."); | ||
| 47 | return REDISMODULE_OK; | ||
| 48 | } | ||
| 49 | |||
| 50 | uint64_t remaining; | ||
| 51 | RedisModuleString *keyname; | ||
| 52 | if (RedisModule_GetTimerInfo(ctx, id, &remaining, (void **)&keyname) == REDISMODULE_ERR) { | ||
| 53 | RedisModule_ReplyWithNull(ctx); | ||
| 54 | } else { | ||
| 55 | RedisModule_ReplyWithArray(ctx, 2); | ||
| 56 | RedisModule_ReplyWithString(ctx, keyname); | ||
| 57 | RedisModule_ReplyWithLongLong(ctx, remaining); | ||
| 58 | } | ||
| 59 | |||
| 60 | return REDISMODULE_OK; | ||
| 61 | } | ||
| 62 | |||
| 63 | int test_stoptimer(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 64 | { | ||
| 65 | if (argc != 2) { | ||
| 66 | RedisModule_WrongArity(ctx); | ||
| 67 | return REDISMODULE_OK; | ||
| 68 | } | ||
| 69 | |||
| 70 | long long id; | ||
| 71 | if (RedisModule_StringToLongLong(argv[1], &id) == REDISMODULE_ERR) { | ||
| 72 | RedisModule_ReplyWithError(ctx, "Invalid id specified."); | ||
| 73 | return REDISMODULE_OK; | ||
| 74 | } | ||
| 75 | |||
| 76 | int ret = 0; | ||
| 77 | RedisModuleString *keyname; | ||
| 78 | if (RedisModule_StopTimer(ctx, id, (void **) &keyname) == REDISMODULE_OK) { | ||
| 79 | RedisModule_FreeString(ctx, keyname); | ||
| 80 | ret = 1; | ||
| 81 | } | ||
| 82 | |||
| 83 | RedisModule_ReplyWithLongLong(ctx, ret); | ||
| 84 | return REDISMODULE_OK; | ||
| 85 | } | ||
| 86 | |||
| 87 | |||
| 88 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 89 | REDISMODULE_NOT_USED(argv); | ||
| 90 | REDISMODULE_NOT_USED(argc); | ||
| 91 | if (RedisModule_Init(ctx,"timer",1,REDISMODULE_APIVER_1)== REDISMODULE_ERR) | ||
| 92 | return REDISMODULE_ERR; | ||
| 93 | |||
| 94 | if (RedisModule_CreateCommand(ctx,"test.createtimer", test_createtimer,"",0,0,0) == REDISMODULE_ERR) | ||
| 95 | return REDISMODULE_ERR; | ||
| 96 | if (RedisModule_CreateCommand(ctx,"test.gettimer", test_gettimer,"",0,0,0) == REDISMODULE_ERR) | ||
| 97 | return REDISMODULE_ERR; | ||
| 98 | if (RedisModule_CreateCommand(ctx,"test.stoptimer", test_stoptimer,"",0,0,0) == REDISMODULE_ERR) | ||
| 99 | return REDISMODULE_ERR; | ||
| 100 | |||
| 101 | return REDISMODULE_OK; | ||
| 102 | } | ||
diff --git a/examples/redis-unstable/tests/modules/usercall.c b/examples/redis-unstable/tests/modules/usercall.c new file mode 100644 index 0000000..cc28817 --- /dev/null +++ b/examples/redis-unstable/tests/modules/usercall.c | |||
| @@ -0,0 +1,230 @@ | |||
| 1 | #include "redismodule.h" | ||
| 2 | #include <pthread.h> | ||
| 3 | #include <assert.h> | ||
| 4 | |||
| 5 | #define UNUSED(V) ((void) V) | ||
| 6 | |||
| 7 | RedisModuleUser *user = NULL; | ||
| 8 | |||
| 9 | int call_without_user(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 10 | if (argc < 2) { | ||
| 11 | return RedisModule_WrongArity(ctx); | ||
| 12 | } | ||
| 13 | |||
| 14 | const char *cmd = RedisModule_StringPtrLen(argv[1], NULL); | ||
| 15 | |||
| 16 | RedisModuleCallReply *rep = RedisModule_Call(ctx, cmd, "Ev", argv + 2, (size_t)argc - 2); | ||
| 17 | if (!rep) { | ||
| 18 | RedisModule_ReplyWithError(ctx, "NULL reply returned"); | ||
| 19 | } else { | ||
| 20 | RedisModule_ReplyWithCallReply(ctx, rep); | ||
| 21 | RedisModule_FreeCallReply(rep); | ||
| 22 | } | ||
| 23 | return REDISMODULE_OK; | ||
| 24 | } | ||
| 25 | |||
| 26 | int call_with_user_flag(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 27 | if (argc < 3) { | ||
| 28 | return RedisModule_WrongArity(ctx); | ||
| 29 | } | ||
| 30 | |||
| 31 | RedisModule_SetContextUser(ctx, user); | ||
| 32 | |||
| 33 | /* Append Ev to the provided flags. */ | ||
| 34 | RedisModuleString *flags = RedisModule_CreateStringFromString(ctx, argv[1]); | ||
| 35 | RedisModule_StringAppendBuffer(ctx, flags, "Ev", 2); | ||
| 36 | |||
| 37 | const char* flg = RedisModule_StringPtrLen(flags, NULL); | ||
| 38 | const char* cmd = RedisModule_StringPtrLen(argv[2], NULL); | ||
| 39 | |||
| 40 | RedisModuleCallReply* rep = RedisModule_Call(ctx, cmd, flg, argv + 3, (size_t)argc - 3); | ||
| 41 | if (!rep) { | ||
| 42 | RedisModule_ReplyWithError(ctx, "NULL reply returned"); | ||
| 43 | } else { | ||
| 44 | RedisModule_ReplyWithCallReply(ctx, rep); | ||
| 45 | RedisModule_FreeCallReply(rep); | ||
| 46 | } | ||
| 47 | RedisModule_FreeString(ctx, flags); | ||
| 48 | |||
| 49 | return REDISMODULE_OK; | ||
| 50 | } | ||
| 51 | |||
| 52 | int add_to_acl(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 53 | if (argc != 2) { | ||
| 54 | return RedisModule_WrongArity(ctx); | ||
| 55 | } | ||
| 56 | |||
| 57 | size_t acl_len; | ||
| 58 | const char *acl = RedisModule_StringPtrLen(argv[1], &acl_len); | ||
| 59 | |||
| 60 | RedisModuleString *error; | ||
| 61 | int ret = RedisModule_SetModuleUserACLString(ctx, user, acl, &error); | ||
| 62 | if (ret) { | ||
| 63 | size_t len; | ||
| 64 | const char * e = RedisModule_StringPtrLen(error, &len); | ||
| 65 | RedisModule_ReplyWithError(ctx, e); | ||
| 66 | return REDISMODULE_OK; | ||
| 67 | } | ||
| 68 | |||
| 69 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 70 | |||
| 71 | return REDISMODULE_OK; | ||
| 72 | } | ||
| 73 | |||
| 74 | int get_acl(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 75 | REDISMODULE_NOT_USED(argv); | ||
| 76 | |||
| 77 | if (argc != 1) { | ||
| 78 | return RedisModule_WrongArity(ctx); | ||
| 79 | } | ||
| 80 | |||
| 81 | RedisModule_Assert(user != NULL); | ||
| 82 | |||
| 83 | RedisModuleString *acl = RedisModule_GetModuleUserACLString(user); | ||
| 84 | |||
| 85 | RedisModule_ReplyWithString(ctx, acl); | ||
| 86 | |||
| 87 | RedisModule_FreeString(NULL, acl); | ||
| 88 | |||
| 89 | return REDISMODULE_OK; | ||
| 90 | } | ||
| 91 | |||
| 92 | int reset_user(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 93 | REDISMODULE_NOT_USED(argv); | ||
| 94 | |||
| 95 | if (argc != 1) { | ||
| 96 | return RedisModule_WrongArity(ctx); | ||
| 97 | } | ||
| 98 | |||
| 99 | if (user != NULL) { | ||
| 100 | RedisModule_FreeModuleUser(user); | ||
| 101 | } | ||
| 102 | |||
| 103 | user = RedisModule_CreateModuleUser("module_user"); | ||
| 104 | |||
| 105 | RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 106 | |||
| 107 | return REDISMODULE_OK; | ||
| 108 | } | ||
| 109 | |||
| 110 | typedef struct { | ||
| 111 | RedisModuleString **argv; | ||
| 112 | int argc; | ||
| 113 | RedisModuleBlockedClient *bc; | ||
| 114 | } bg_call_data; | ||
| 115 | |||
| 116 | void *bg_call_worker(void *arg) { | ||
| 117 | bg_call_data *bg = arg; | ||
| 118 | RedisModuleBlockedClient *bc = bg->bc; | ||
| 119 | |||
| 120 | // Get Redis module context | ||
| 121 | RedisModuleCtx *ctx = RedisModule_GetThreadSafeContext(bg->bc); | ||
| 122 | |||
| 123 | // Acquire GIL | ||
| 124 | RedisModule_ThreadSafeContextLock(ctx); | ||
| 125 | |||
| 126 | // Set user | ||
| 127 | RedisModule_SetContextUser(ctx, user); | ||
| 128 | |||
| 129 | // Call the command | ||
| 130 | size_t format_len; | ||
| 131 | RedisModuleString *format_redis_str = RedisModule_CreateString(NULL, "v", 1); | ||
| 132 | const char *format = RedisModule_StringPtrLen(bg->argv[1], &format_len); | ||
| 133 | RedisModule_StringAppendBuffer(NULL, format_redis_str, format, format_len); | ||
| 134 | RedisModule_StringAppendBuffer(NULL, format_redis_str, "E", 1); | ||
| 135 | format = RedisModule_StringPtrLen(format_redis_str, NULL); | ||
| 136 | const char *cmd = RedisModule_StringPtrLen(bg->argv[2], NULL); | ||
| 137 | RedisModuleCallReply *rep = RedisModule_Call(ctx, cmd, format, bg->argv + 3, (size_t)bg->argc - 3); | ||
| 138 | RedisModule_FreeString(NULL, format_redis_str); | ||
| 139 | |||
| 140 | /* Free the arguments within GIL to prevent simultaneous freeing in main thread. */ | ||
| 141 | for (int i=0; i<bg->argc; i++) | ||
| 142 | RedisModule_FreeString(ctx, bg->argv[i]); | ||
| 143 | RedisModule_Free(bg->argv); | ||
| 144 | RedisModule_Free(bg); | ||
| 145 | |||
| 146 | // Release GIL | ||
| 147 | RedisModule_ThreadSafeContextUnlock(ctx); | ||
| 148 | |||
| 149 | // Reply to client | ||
| 150 | if (!rep) { | ||
| 151 | RedisModule_ReplyWithError(ctx, "NULL reply returned"); | ||
| 152 | } else { | ||
| 153 | RedisModule_ReplyWithCallReply(ctx, rep); | ||
| 154 | RedisModule_FreeCallReply(rep); | ||
| 155 | } | ||
| 156 | |||
| 157 | // Unblock client | ||
| 158 | RedisModule_UnblockClient(bc, NULL); | ||
| 159 | |||
| 160 | // Free the Redis module context | ||
| 161 | RedisModule_FreeThreadSafeContext(ctx); | ||
| 162 | |||
| 163 | return NULL; | ||
| 164 | } | ||
| 165 | |||
| 166 | int call_with_user_bg(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) | ||
| 167 | { | ||
| 168 | UNUSED(argv); | ||
| 169 | UNUSED(argc); | ||
| 170 | |||
| 171 | /* Make sure we're not trying to block a client when we shouldn't */ | ||
| 172 | int flags = RedisModule_GetContextFlags(ctx); | ||
| 173 | int allFlags = RedisModule_GetContextFlagsAll(); | ||
| 174 | if ((allFlags & REDISMODULE_CTX_FLAGS_MULTI) && | ||
| 175 | (flags & REDISMODULE_CTX_FLAGS_MULTI)) { | ||
| 176 | RedisModule_ReplyWithSimpleString(ctx, "Blocked client is not supported inside multi"); | ||
| 177 | return REDISMODULE_OK; | ||
| 178 | } | ||
| 179 | if ((allFlags & REDISMODULE_CTX_FLAGS_DENY_BLOCKING) && | ||
| 180 | (flags & REDISMODULE_CTX_FLAGS_DENY_BLOCKING)) { | ||
| 181 | RedisModule_ReplyWithSimpleString(ctx, "Blocked client is not allowed"); | ||
| 182 | return REDISMODULE_OK; | ||
| 183 | } | ||
| 184 | |||
| 185 | /* Make a copy of the arguments and pass them to the thread. */ | ||
| 186 | bg_call_data *bg = RedisModule_Alloc(sizeof(bg_call_data)); | ||
| 187 | bg->argv = RedisModule_Alloc(sizeof(RedisModuleString*)*argc); | ||
| 188 | bg->argc = argc; | ||
| 189 | for (int i=0; i<argc; i++) | ||
| 190 | bg->argv[i] = RedisModule_HoldString(ctx, argv[i]); | ||
| 191 | |||
| 192 | /* Block the client */ | ||
| 193 | bg->bc = RedisModule_BlockClient(ctx, NULL, NULL, NULL, 0); | ||
| 194 | |||
| 195 | /* Start a thread to handle the request */ | ||
| 196 | pthread_t tid; | ||
| 197 | int res = pthread_create(&tid, NULL, bg_call_worker, bg); | ||
| 198 | assert(res == 0); | ||
| 199 | pthread_detach(tid); | ||
| 200 | |||
| 201 | return REDISMODULE_OK; | ||
| 202 | } | ||
| 203 | |||
| 204 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 205 | REDISMODULE_NOT_USED(argv); | ||
| 206 | REDISMODULE_NOT_USED(argc); | ||
| 207 | |||
| 208 | if (RedisModule_Init(ctx,"usercall",1,REDISMODULE_APIVER_1)== REDISMODULE_ERR) | ||
| 209 | return REDISMODULE_ERR; | ||
| 210 | |||
| 211 | if (RedisModule_CreateCommand(ctx,"usercall.call_without_user", call_without_user,"write",0,0,0) == REDISMODULE_ERR) | ||
| 212 | return REDISMODULE_ERR; | ||
| 213 | |||
| 214 | if (RedisModule_CreateCommand(ctx,"usercall.call_with_user_flag", call_with_user_flag,"write",0,0,0) == REDISMODULE_ERR) | ||
| 215 | return REDISMODULE_ERR; | ||
| 216 | |||
| 217 | if (RedisModule_CreateCommand(ctx, "usercall.call_with_user_bg", call_with_user_bg, "write", 0, 0, 0) == REDISMODULE_ERR) | ||
| 218 | return REDISMODULE_ERR; | ||
| 219 | |||
| 220 | if (RedisModule_CreateCommand(ctx, "usercall.add_to_acl", add_to_acl, "write",0,0,0) == REDISMODULE_ERR) | ||
| 221 | return REDISMODULE_ERR; | ||
| 222 | |||
| 223 | if (RedisModule_CreateCommand(ctx,"usercall.reset_user", reset_user,"write",0,0,0) == REDISMODULE_ERR) | ||
| 224 | return REDISMODULE_ERR; | ||
| 225 | |||
| 226 | if (RedisModule_CreateCommand(ctx,"usercall.get_acl", get_acl,"write",0,0,0) == REDISMODULE_ERR) | ||
| 227 | return REDISMODULE_ERR; | ||
| 228 | |||
| 229 | return REDISMODULE_OK; | ||
| 230 | } | ||
diff --git a/examples/redis-unstable/tests/modules/zset.c b/examples/redis-unstable/tests/modules/zset.c new file mode 100644 index 0000000..adb318d --- /dev/null +++ b/examples/redis-unstable/tests/modules/zset.c | |||
| @@ -0,0 +1,180 @@ | |||
| 1 | #include "redismodule.h" | ||
| 2 | #include <math.h> | ||
| 3 | #include <errno.h> | ||
| 4 | |||
| 5 | /* ZSET.REM key element | ||
| 6 | * | ||
| 7 | * Removes an occurrence of an element from a sorted set. Replies with the | ||
| 8 | * number of removed elements (0 or 1). | ||
| 9 | */ | ||
| 10 | int zset_rem(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 11 | if (argc != 3) return RedisModule_WrongArity(ctx); | ||
| 12 | RedisModule_AutoMemory(ctx); | ||
| 13 | int keymode = REDISMODULE_READ | REDISMODULE_WRITE; | ||
| 14 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], keymode); | ||
| 15 | int deleted; | ||
| 16 | if (RedisModule_ZsetRem(key, argv[2], &deleted) == REDISMODULE_OK) | ||
| 17 | return RedisModule_ReplyWithLongLong(ctx, deleted); | ||
| 18 | else | ||
| 19 | return RedisModule_ReplyWithError(ctx, "ERR ZsetRem failed"); | ||
| 20 | } | ||
| 21 | |||
| 22 | /* ZSET.ADD key score member | ||
| 23 | * | ||
| 24 | * Adds a specified member with the specified score to the sorted | ||
| 25 | * set stored at key. | ||
| 26 | */ | ||
| 27 | int zset_add(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 28 | if (argc != 4) return RedisModule_WrongArity(ctx); | ||
| 29 | RedisModule_AutoMemory(ctx); | ||
| 30 | int keymode = REDISMODULE_READ | REDISMODULE_WRITE; | ||
| 31 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], keymode); | ||
| 32 | |||
| 33 | size_t len; | ||
| 34 | double score; | ||
| 35 | char *endptr; | ||
| 36 | const char *str = RedisModule_StringPtrLen(argv[2], &len); | ||
| 37 | score = strtod(str, &endptr); | ||
| 38 | if (*endptr != '\0' || errno == ERANGE) | ||
| 39 | return RedisModule_ReplyWithError(ctx, "value is not a valid float"); | ||
| 40 | |||
| 41 | if (RedisModule_ZsetAdd(key, score, argv[3], NULL) == REDISMODULE_OK) | ||
| 42 | return RedisModule_ReplyWithSimpleString(ctx, "OK"); | ||
| 43 | else | ||
| 44 | return RedisModule_ReplyWithError(ctx, "ERR ZsetAdd failed"); | ||
| 45 | } | ||
| 46 | |||
| 47 | /* ZSET.INCRBY key member increment | ||
| 48 | * | ||
| 49 | * Increments the score stored at member in the sorted set stored at key by increment. | ||
| 50 | * Replies with the new score of this element. | ||
| 51 | */ | ||
| 52 | int zset_incrby(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 53 | if (argc != 4) return RedisModule_WrongArity(ctx); | ||
| 54 | RedisModule_AutoMemory(ctx); | ||
| 55 | int keymode = REDISMODULE_READ | REDISMODULE_WRITE; | ||
| 56 | RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], keymode); | ||
| 57 | |||
| 58 | size_t len; | ||
| 59 | double score, newscore; | ||
| 60 | char *endptr; | ||
| 61 | const char *str = RedisModule_StringPtrLen(argv[3], &len); | ||
| 62 | score = strtod(str, &endptr); | ||
| 63 | if (*endptr != '\0' || errno == ERANGE) | ||
| 64 | return RedisModule_ReplyWithError(ctx, "value is not a valid float"); | ||
| 65 | |||
| 66 | if (RedisModule_ZsetIncrby(key, score, argv[2], NULL, &newscore) == REDISMODULE_OK) | ||
| 67 | return RedisModule_ReplyWithDouble(ctx, newscore); | ||
| 68 | else | ||
| 69 | return RedisModule_ReplyWithError(ctx, "ERR ZsetIncrby failed"); | ||
| 70 | } | ||
| 71 | |||
| 72 | /* Structure to hold data for the delall scan callback */ | ||
| 73 | typedef struct { | ||
| 74 | RedisModuleCtx *ctx; | ||
| 75 | RedisModuleString **keys_to_delete; | ||
| 76 | size_t keys_capacity; | ||
| 77 | size_t keys_count; | ||
| 78 | } zset_delall_data; | ||
| 79 | |||
| 80 | /* Callback function for scanning keys and collecting zset keys to delete */ | ||
| 81 | void zset_delall_callback(RedisModuleCtx *ctx, RedisModuleString *keyname, RedisModuleKey *key, void *privdata) { | ||
| 82 | zset_delall_data *data = privdata; | ||
| 83 | int was_opened = 0; | ||
| 84 | |||
| 85 | /* Open the key if it wasn't already opened */ | ||
| 86 | if (!key) { | ||
| 87 | key = RedisModule_OpenKey(ctx, keyname, REDISMODULE_READ); | ||
| 88 | was_opened = 1; | ||
| 89 | } | ||
| 90 | |||
| 91 | /* Check if the key is a zset and add it to the list */ | ||
| 92 | if (RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_ZSET) { | ||
| 93 | /* Expand the array if needed */ | ||
| 94 | if (data->keys_count >= data->keys_capacity) { | ||
| 95 | data->keys_capacity = data->keys_capacity ? data->keys_capacity * 2 : 16; | ||
| 96 | data->keys_to_delete = RedisModule_Realloc(data->keys_to_delete, | ||
| 97 | data->keys_capacity * sizeof(RedisModuleString*)); | ||
| 98 | } | ||
| 99 | |||
| 100 | /* Store the key name (retain it so it doesn't get freed) */ | ||
| 101 | data->keys_to_delete[data->keys_count] = keyname; | ||
| 102 | RedisModule_RetainString(ctx, keyname); | ||
| 103 | data->keys_count++; | ||
| 104 | } | ||
| 105 | |||
| 106 | /* Close the key if we opened it */ | ||
| 107 | if (was_opened) { | ||
| 108 | RedisModule_CloseKey(key); | ||
| 109 | } | ||
| 110 | } | ||
| 111 | |||
| 112 | /* ZSET.DELALL | ||
| 113 | * | ||
| 114 | * Iterates through the keyspace and deletes all keys of type "zset". | ||
| 115 | * Returns the number of deleted keys. | ||
| 116 | */ | ||
| 117 | int zset_delall(RedisModuleCtx *ctx, REDISMODULE_ATTR_UNUSED RedisModuleString **argv, int argc) { | ||
| 118 | if (argc != 1) return RedisModule_WrongArity(ctx); | ||
| 119 | RedisModule_AutoMemory(ctx); | ||
| 120 | |||
| 121 | zset_delall_data data = { | ||
| 122 | .ctx = ctx, | ||
| 123 | .keys_to_delete = NULL, | ||
| 124 | .keys_capacity = 0, | ||
| 125 | .keys_count = 0 | ||
| 126 | }; | ||
| 127 | |||
| 128 | /* Create a scan cursor and iterate through all keys */ | ||
| 129 | RedisModuleScanCursor *cursor = RedisModule_ScanCursorCreate(); | ||
| 130 | while (RedisModule_Scan(ctx, cursor, zset_delall_callback, &data)); | ||
| 131 | RedisModule_ScanCursorDestroy(cursor); | ||
| 132 | |||
| 133 | /* Delete all the collected zset keys after scan is complete */ | ||
| 134 | size_t deleted_count = 0; | ||
| 135 | for (size_t i = 0; i < data.keys_count; i++) { | ||
| 136 | RedisModuleCallReply *reply = RedisModule_Call(ctx, "DEL", "s!", data.keys_to_delete[i]); | ||
| 137 | if (reply && RedisModule_CallReplyType(reply) == REDISMODULE_REPLY_INTEGER) { | ||
| 138 | long long del_result = RedisModule_CallReplyInteger(reply); | ||
| 139 | if (del_result > 0) { | ||
| 140 | deleted_count++; | ||
| 141 | } | ||
| 142 | } | ||
| 143 | if (reply) { | ||
| 144 | RedisModule_FreeCallReply(reply); | ||
| 145 | } | ||
| 146 | RedisModule_FreeString(ctx, data.keys_to_delete[i]); | ||
| 147 | } | ||
| 148 | |||
| 149 | /* Free the keys array */ | ||
| 150 | if (data.keys_to_delete) { | ||
| 151 | RedisModule_Free(data.keys_to_delete); | ||
| 152 | } | ||
| 153 | |||
| 154 | return RedisModule_ReplyWithLongLong(ctx, deleted_count); | ||
| 155 | } | ||
| 156 | |||
| 157 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | ||
| 158 | REDISMODULE_NOT_USED(argv); | ||
| 159 | REDISMODULE_NOT_USED(argc); | ||
| 160 | if (RedisModule_Init(ctx, "zset", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) | ||
| 161 | return REDISMODULE_ERR; | ||
| 162 | |||
| 163 | if (RedisModule_CreateCommand(ctx, "zset.rem", zset_rem, "write", | ||
| 164 | 1, 1, 1) == REDISMODULE_ERR) | ||
| 165 | return REDISMODULE_ERR; | ||
| 166 | |||
| 167 | if (RedisModule_CreateCommand(ctx, "zset.add", zset_add, "write", | ||
| 168 | 1, 1, 1) == REDISMODULE_ERR) | ||
| 169 | return REDISMODULE_ERR; | ||
| 170 | |||
| 171 | if (RedisModule_CreateCommand(ctx, "zset.incrby", zset_incrby, "write", | ||
| 172 | 1, 1, 1) == REDISMODULE_ERR) | ||
| 173 | return REDISMODULE_ERR; | ||
| 174 | |||
| 175 | if (RedisModule_CreateCommand(ctx, "zset.delall", zset_delall, "write touches-arbitrary-keys", | ||
| 176 | 0, 0, 0) == REDISMODULE_ERR) | ||
| 177 | return REDISMODULE_ERR; | ||
| 178 | |||
| 179 | return REDISMODULE_OK; | ||
| 180 | } | ||
