diff --git a/src/module.c b/src/module.c index 776d74b65..ade541497 100644 --- a/src/module.c +++ b/src/module.c @@ -357,6 +357,7 @@ typedef struct RedisModuleServerInfoData { #define REDISMODULE_ARGV_NO_WRITES (1<<7) #define REDISMODULE_ARGV_CALL_REPLIES_AS_ERRORS (1<<8) #define REDISMODULE_ARGV_RESPECT_DENY_OOM (1<<9) +#define REDISMODULE_ARGV_DRY_RUN (1<<10) /* Determine whether Redis should signalModifiedKey implicitly. * In case 'ctx' has no 'module' member (and therefore no module->options), @@ -5703,6 +5704,8 @@ robj **moduleCreateArgvFromUserFormat(const char *cmdname, const char *fmt, int if (flags) (*flags) |= REDISMODULE_ARGV_RESPECT_DENY_OOM; } else if (*p == 'E') { if (flags) (*flags) |= REDISMODULE_ARGV_CALL_REPLIES_AS_ERRORS; + } else if (*p == 'D') { + if (flags) (*flags) |= (REDISMODULE_ARGV_DRY_RUN | REDISMODULE_ARGV_CALL_REPLIES_AS_ERRORS); } else { goto fmterr; } @@ -5754,6 +5757,10 @@ fmterr: * invoking the command, the error is returned using errno mechanism. * This flag allows to get the error also as an error CallReply with * relevant error message. + * * 'D' -- A "Dry Run" mode. Return before executing the underlying call(). + * If everything succeeded, it will return with a NULL, otherwise it will + * return with a CallReply object denoting the error, as if it was called with + * the 'E' code. * * **...**: The actual arguments to the Redis command. * * On success a RedisModuleCallReply object is returned, otherwise @@ -6007,6 +6014,10 @@ RedisModuleCallReply *RM_Call(RedisModuleCtx *ctx, const char *cmdname, const ch } } + if (flags & REDISMODULE_ARGV_DRY_RUN) { + goto cleanup; + } + /* We need to use a global replication_allowed flag in order to prevent * replication of nested RM_Calls. Example: * 1. module1.foo does RM_Call of module2.bar without replication (i.e. no '!') diff --git a/tests/unit/moduleapi/misc.tcl b/tests/unit/moduleapi/misc.tcl index 04618ab89..fafb82be8 100644 --- a/tests/unit/moduleapi/misc.tcl +++ b/tests/unit/moduleapi/misc.tcl @@ -389,8 +389,42 @@ start_server {tags {"modules"}} { # server is writable again r set x y } {OK} +} + +start_server {tags {"modules"}} { + r module load $testmodule + + test {test Dry Run - OK OOM/ACL} { + set x 5 + r set x $x + catch {r test.rm_call_flags DMC set x 10} e + assert_match {*NULL reply returned*} $e + assert_equal [r get x] 5 + } + + test {test Dry Run - Fail OOM} { + set x 5 + r set x $x + r config set maxmemory 1 + catch {r test.rm_call_flags DM set x 10} e + assert_match {*OOM*} $e + assert_equal [r get x] $x + r config set maxmemory 0 + } {OK} {needs:config-maxmemory} + + test {test Dry Run - Fail ACL} { + set x 5 + r set x $x + # deny all permissions besides the dryrun command + r acl setuser default resetkeys + + catch {r test.rm_call_flags DC set x 10} e + assert_match {*ERR acl verification failed, can't access at least one of the keys*} $e + r acl setuser default +@all ~* + assert_equal [r get x] $x + } test "Unload the module - misc" { assert_equal {OK} [r module unload misc] } -} +} \ No newline at end of file