Commit 72935b9d authored by Vitaly Arbuzov's avatar Vitaly Arbuzov
Browse files

Merge branch 'unstable' into dict-split-by-slot

parents 6baf20af 6948daca
......@@ -180,6 +180,8 @@ start_server {} {
}
exec kill -SIGCONT $replica2_pid
}
# speed up termination
$master config set shutdown-timeout 0
}
}
}
......@@ -227,3 +229,68 @@ test {Partial resynchronization is successful even client-output-buffer-limit is
}
}
}
# This test was added to make sure big keys added to the backlog do not trigger psync loop.
test {Replica client-output-buffer size is limited to backlog_limit/16 when no replication data is pending} {
proc client_field {r type f} {
set client [$r client list type $type]
if {![regexp $f=(\[a-zA-Z0-9-\]+) $client - res]} {
error "field $f not found for in $client"
}
return $res
}
start_server {tags {"repl external:skip"}} {
start_server {} {
set replica [srv -1 client]
set replica_host [srv -1 host]
set replica_port [srv -1 port]
set master [srv 0 client]
set master_host [srv 0 host]
set master_port [srv 0 port]
$master config set repl-backlog-size 16384
$master config set client-output-buffer-limit "replica 32768 32768 60"
# Key has has to be larger than replica client-output-buffer limit.
set keysize [expr 256*1024]
$replica replicaof $master_host $master_port
wait_for_condition 50 100 {
[lindex [$replica role] 0] eq {slave} &&
[string match {*master_link_status:up*} [$replica info replication]]
} else {
fail "Can't turn the instance into a replica"
}
set _v [prepare_value $keysize]
$master set key $_v
wait_for_ofs_sync $master $replica
# Write another key to force the test to wait for another event loop iteration
# to give the serverCron a chance to disconnect replicas with COB size exceeding the limits
$master set key1 "1"
wait_for_ofs_sync $master $replica
assert {[status $master connected_slaves] == 1}
wait_for_condition 50 100 {
[client_field $master replica tot-mem] < $keysize
} else {
fail "replica client-output-buffer usage is higher than expected."
}
assert {[status $master sync_partial_ok] == 0}
# Before this fix (#11905), the test would trigger an assertion in 'o->used >= c->ref_block_pos'
test {The update of replBufBlock's repl_offset is ok - Regression test for #11666} {
set rd [redis_deferring_client]
set replid [status $master master_replid]
set offset [status $master repl_backlog_first_byte_offset]
$rd psync $replid $offset
assert_equal {PONG} [$master ping] ;# Make sure the master doesn't crash.
$rd close
}
}
}
}
......@@ -60,7 +60,8 @@ TEST_MODULES = \
moduleconfigstwo.so \
publish.so \
usercall.so \
postnotifications.so
postnotifications.so \
moduleauthtwo.so
.PHONY: all
......
......@@ -183,6 +183,37 @@ int rm_call_aclcheck(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){
return REDISMODULE_OK;
}
int module_test_acl_category(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
REDISMODULE_NOT_USED(argv);
REDISMODULE_NOT_USED(argc);
RedisModule_ReplyWithSimpleString(ctx, "OK");
return REDISMODULE_OK;
}
int commandBlockCheck(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
REDISMODULE_NOT_USED(argv);
REDISMODULE_NOT_USED(argc);
int response_ok = 0;
int result = RedisModule_CreateCommand(ctx,"command.that.should.fail", module_test_acl_category, "", 0, 0, 0);
response_ok |= (result == REDISMODULE_OK);
RedisModuleCommand *parent = RedisModule_GetCommand(ctx,"block.commands.outside.onload");
result = RedisModule_SetCommandACLCategories(parent, "write");
response_ok |= (result == REDISMODULE_OK);
result = RedisModule_CreateSubcommand(parent,"subcommand.that.should.fail",module_test_acl_category,"",0,0,0);
response_ok |= (result == REDISMODULE_OK);
/* This validates that it's not possible to create commands outside OnLoad,
* thus returns an error if they succeed. */
if (response_ok) {
RedisModule_ReplyWithError(ctx, "UNEXPECTEDOK");
} else {
RedisModule_ReplyWithSimpleString(ctx, "OK");
}
return REDISMODULE_OK;
}
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
REDISMODULE_NOT_USED(argv);
REDISMODULE_NOT_USED(argc);
......@@ -193,6 +224,30 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
if (RedisModule_CreateCommand(ctx,"aclcheck.set.check.key", set_aclcheck_key,"write",0,0,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx,"block.commands.outside.onload", commandBlockCheck,"write",0,0,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx,"aclcheck.module.command.aclcategories.write", module_test_acl_category,"write",0,0,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
RedisModuleCommand *aclcategories_write = RedisModule_GetCommand(ctx,"aclcheck.module.command.aclcategories.write");
if (RedisModule_SetCommandACLCategories(aclcategories_write, "write") == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx,"aclcheck.module.command.aclcategories.write.function.read.category", module_test_acl_category,"write",0,0,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
RedisModuleCommand *read_category = RedisModule_GetCommand(ctx,"aclcheck.module.command.aclcategories.write.function.read.category");
if (RedisModule_SetCommandACLCategories(read_category, "read") == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx,"aclcheck.module.command.aclcategories.read.only.category", module_test_acl_category,"",0,0,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
RedisModuleCommand *read_only_category = RedisModule_GetCommand(ctx,"aclcheck.module.command.aclcategories.read.only.category");
if (RedisModule_SetCommandACLCategories(read_only_category, "read") == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx,"aclcheck.publish.check.channel", publish_aclcheck_channel,"",0,0,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
......
/* define macros for having usleep */
#define _BSD_SOURCE
#define _DEFAULT_SOURCE
#include "redismodule.h"
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#define UNUSED(V) ((void) V)
// A simple global user
......@@ -72,6 +80,146 @@ int Auth_ChangeCount(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
return RedisModule_ReplyWithLongLong(ctx, result);
}
/* The Module functionality below validates that module authentication callbacks can be registered
* to support both non-blocking and blocking module based authentication. */
/* Non Blocking Module Auth callback / implementation. */
int auth_cb(RedisModuleCtx *ctx, RedisModuleString *username, RedisModuleString *password, RedisModuleString **err) {
const char *user = RedisModule_StringPtrLen(username, NULL);
const char *pwd = RedisModule_StringPtrLen(password, NULL);
if (!strcmp(user,"foo") && !strcmp(pwd,"allow")) {
RedisModule_AuthenticateClientWithACLUser(ctx, "foo", 3, NULL, NULL, NULL);
return REDISMODULE_AUTH_HANDLED;
}
else if (!strcmp(user,"foo") && !strcmp(pwd,"deny")) {
RedisModuleString *log = RedisModule_CreateString(ctx, "Module Auth", 11);
RedisModule_ACLAddLogEntryByUserName(ctx, username, log, REDISMODULE_ACL_LOG_AUTH);
RedisModule_FreeString(ctx, log);
const char *err_msg = "Auth denied by Misc Module.";
*err = RedisModule_CreateString(ctx, err_msg, strlen(err_msg));
return REDISMODULE_AUTH_HANDLED;
}
return REDISMODULE_AUTH_NOT_HANDLED;
}
int test_rm_register_auth_cb(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
REDISMODULE_NOT_USED(argv);
REDISMODULE_NOT_USED(argc);
RedisModule_RegisterAuthCallback(ctx, auth_cb);
RedisModule_ReplyWithSimpleString(ctx, "OK");
return REDISMODULE_OK;
}
/*
* The thread entry point that actually executes the blocking part of the AUTH command.
* This function sleeps for 0.5 seconds and then unblocks the client which will later call
* `AuthBlock_Reply`.
* `arg` is expected to contain the RedisModuleBlockedClient, username, and password.
*/
void *AuthBlock_ThreadMain(void *arg) {
usleep(500000);
void **targ = arg;
RedisModuleBlockedClient *bc = targ[0];
int result = 2;
const char *user = RedisModule_StringPtrLen(targ[1], NULL);
const char *pwd = RedisModule_StringPtrLen(targ[2], NULL);
if (!strcmp(user,"foo") && !strcmp(pwd,"block_allow")) {
result = 1;
}
else if (!strcmp(user,"foo") && !strcmp(pwd,"block_deny")) {
result = 0;
}
else if (!strcmp(user,"foo") && !strcmp(pwd,"block_abort")) {
RedisModule_BlockedClientMeasureTimeEnd(bc);
RedisModule_AbortBlock(bc);
goto cleanup;
}
/* Provide the result to the blocking reply cb. */
void **replyarg = RedisModule_Alloc(sizeof(void*));
replyarg[0] = (void *) (uintptr_t) result;
RedisModule_BlockedClientMeasureTimeEnd(bc);
RedisModule_UnblockClient(bc, replyarg);
cleanup:
/* Free the username and password and thread / arg data. */
RedisModule_FreeString(NULL, targ[1]);
RedisModule_FreeString(NULL, targ[2]);
RedisModule_Free(targ);
return NULL;
}
/*
* Reply callback for a blocking AUTH command. This is called when the client is unblocked.
*/
int AuthBlock_Reply(RedisModuleCtx *ctx, RedisModuleString *username, RedisModuleString *password, RedisModuleString **err) {
REDISMODULE_NOT_USED(password);
void **targ = RedisModule_GetBlockedClientPrivateData(ctx);
int result = (uintptr_t) targ[0];
size_t userlen = 0;
const char *user = RedisModule_StringPtrLen(username, &userlen);
/* Handle the success case by authenticating. */
if (result == 1) {
RedisModule_AuthenticateClientWithACLUser(ctx, user, userlen, NULL, NULL, NULL);
return REDISMODULE_AUTH_HANDLED;
}
/* Handle the Error case by denying auth */
else if (result == 0) {
RedisModuleString *log = RedisModule_CreateString(ctx, "Module Auth", 11);
RedisModule_ACLAddLogEntryByUserName(ctx, username, log, REDISMODULE_ACL_LOG_AUTH);
RedisModule_FreeString(ctx, log);
const char *err_msg = "Auth denied by Misc Module.";
*err = RedisModule_CreateString(ctx, err_msg, strlen(err_msg));
return REDISMODULE_AUTH_HANDLED;
}
/* "Skip" Authentication */
return REDISMODULE_AUTH_NOT_HANDLED;
}
/* Private data freeing callback for Module Auth. */
void AuthBlock_FreeData(RedisModuleCtx *ctx, void *privdata) {
REDISMODULE_NOT_USED(ctx);
RedisModule_Free(privdata);
}
/* Callback triggered when the engine attempts module auth
* Return code here is one of the following: Auth succeeded, Auth denied,
* Auth not handled, Auth blocked.
* The Module can have auth succeed / denied here itself, but this is an example
* of blocking module auth.
*/
int blocking_auth_cb(RedisModuleCtx *ctx, RedisModuleString *username, RedisModuleString *password, RedisModuleString **err) {
REDISMODULE_NOT_USED(username);
REDISMODULE_NOT_USED(password);
REDISMODULE_NOT_USED(err);
/* Block the client from the Module. */
RedisModuleBlockedClient *bc = RedisModule_BlockClientOnAuth(ctx, AuthBlock_Reply, AuthBlock_FreeData);
int ctx_flags = RedisModule_GetContextFlags(ctx);
if (ctx_flags & REDISMODULE_CTX_FLAGS_MULTI || ctx_flags & REDISMODULE_CTX_FLAGS_LUA) {
/* Clean up by using RedisModule_UnblockClient since we attempted blocking the client. */
RedisModule_UnblockClient(bc, NULL);
return REDISMODULE_AUTH_HANDLED;
}
RedisModule_BlockedClientMeasureTimeStart(bc);
pthread_t tid;
/* Allocate memory for information needed. */
void **targ = RedisModule_Alloc(sizeof(void*)*3);
targ[0] = bc;
targ[1] = RedisModule_CreateStringFromString(NULL, username);
targ[2] = RedisModule_CreateStringFromString(NULL, password);
/* Create bg thread and pass the blockedclient, username and password to it. */
if (pthread_create(&tid, NULL, AuthBlock_ThreadMain, targ) != 0) {
RedisModule_AbortBlock(bc);
}
return REDISMODULE_AUTH_HANDLED;
}
int test_rm_register_blocking_auth_cb(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
REDISMODULE_NOT_USED(argv);
REDISMODULE_NOT_USED(argc);
RedisModule_RegisterAuthCallback(ctx, blocking_auth_cb);
RedisModule_ReplyWithSimpleString(ctx, "OK");
return REDISMODULE_OK;
}
/* This function must be present on each Redis module. It is used in order to
* register the commands into the Redis server. */
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
......@@ -101,6 +249,14 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
Auth_RedactedAPI,"",0,0,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx,"testmoduleone.rm_register_auth_cb",
test_rm_register_auth_cb,"",0,0,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx,"testmoduleone.rm_register_blocking_auth_cb",
test_rm_register_blocking_auth_cb,"",0,0,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
return REDISMODULE_OK;
}
......
......@@ -219,6 +219,257 @@ int do_rm_call(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){
return REDISMODULE_OK;
}
static void rm_call_async_send_reply(RedisModuleCtx *ctx, RedisModuleCallReply *reply) {
RedisModule_ReplyWithCallReply(ctx, reply);
RedisModule_FreeCallReply(reply);
}
/* Called when the command that was blocked on 'RM_Call' gets unblocked
* and send the reply to the blocked client. */
static void rm_call_async_on_unblocked(RedisModuleCtx *ctx, RedisModuleCallReply *reply, void *private_data) {
UNUSED(ctx);
RedisModuleBlockedClient *bc = private_data;
RedisModuleCtx *bctx = RedisModule_GetThreadSafeContext(bc);
rm_call_async_send_reply(bctx, reply);
RedisModule_FreeThreadSafeContext(bctx);
RedisModule_UnblockClient(bc, RedisModule_BlockClientGetPrivateData(bc));
}
int do_rm_call_async_fire_and_forget(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){
UNUSED(argv);
UNUSED(argc);
if(argc < 2){
return RedisModule_WrongArity(ctx);
}
const char* cmd = RedisModule_StringPtrLen(argv[1], NULL);
RedisModuleCallReply* rep = RedisModule_Call(ctx, cmd, "!KEv", argv + 2, argc - 2);
if(RedisModule_CallReplyType(rep) != REDISMODULE_REPLY_PROMISE) {
RedisModule_ReplyWithCallReply(ctx, rep);
} else {
RedisModule_ReplyWithSimpleString(ctx, "Blocked");
}
RedisModule_FreeCallReply(rep);
return REDISMODULE_OK;
}
static void do_rm_call_async_free_pd(RedisModuleCtx * ctx, void *pd) {
UNUSED(ctx);
RedisModule_FreeCallReply(pd);
}
static void do_rm_call_async_disconnect(RedisModuleCtx *ctx, struct RedisModuleBlockedClient *bc) {
UNUSED(ctx);
RedisModuleCallReply* rep = RedisModule_BlockClientGetPrivateData(bc);
RedisModule_CallReplyPromiseAbort(rep, NULL);
RedisModule_FreeCallReply(rep);
RedisModule_AbortBlock(bc);
}
/*
* Callback for do_rm_call_async / do_rm_call_async_script_mode
* Gets the command to invoke as the first argument to the command and runs it,
* passing the rest of the arguments to the command invocation.
* If the command got blocked, blocks the client and unblock it when the command gets unblocked,
* this allows check the K (allow blocking) argument to RM_Call.
*/
int do_rm_call_async(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){
UNUSED(argv);
UNUSED(argc);
if(argc < 2){
return RedisModule_WrongArity(ctx);
}
size_t format_len = 0;
char format[6] = {0};
if (!(RedisModule_GetContextFlags(ctx) & REDISMODULE_CTX_FLAGS_DENY_BLOCKING)) {
/* We are allowed to block the client so we can allow RM_Call to also block us */
format[format_len++] = 'K';
}
const char* invoked_cmd = RedisModule_StringPtrLen(argv[0], NULL);
if (strcasecmp(invoked_cmd, "do_rm_call_async_script_mode") == 0) {
format[format_len++] = 'S';
}
format[format_len++] = 'E';
format[format_len++] = 'v';
if (strcasecmp(invoked_cmd, "do_rm_call_async_no_replicate") != 0) {
/* Notice, without the '!' flag we will have inconsistency between master and replica.
* This is used only to check '!' flag correctness on blocked commands. */
format[format_len++] = '!';
}
const char* cmd = RedisModule_StringPtrLen(argv[1], NULL);
RedisModuleCallReply* rep = RedisModule_Call(ctx, cmd, format, argv + 2, argc - 2);
if(RedisModule_CallReplyType(rep) != REDISMODULE_REPLY_PROMISE) {
rm_call_async_send_reply(ctx, rep);
} else {
RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx, NULL, NULL, do_rm_call_async_free_pd, 0);
RedisModule_SetDisconnectCallback(bc, do_rm_call_async_disconnect);
RedisModule_BlockClientSetPrivateData(bc, rep);
RedisModule_CallReplyPromiseSetUnblockHandler(rep, rm_call_async_on_unblocked, bc);
}
return REDISMODULE_OK;
}
/* Private data for wait_and_do_rm_call_async that holds information about:
* 1. the block client, to unblock when done.
* 2. the arguments, contains the command to run using RM_Call */
typedef struct WaitAndDoRMCallCtx {
RedisModuleBlockedClient *bc;
RedisModuleString **argv;
int argc;
} WaitAndDoRMCallCtx;
/*
* This callback will be called when the 'wait' command invoke on 'wait_and_do_rm_call_async' will finish.
* This callback will continue the execution flow just like 'do_rm_call_async' command.
*/
static void wait_and_do_rm_call_async_on_unblocked(RedisModuleCtx *ctx, RedisModuleCallReply *reply, void *private_data) {
WaitAndDoRMCallCtx *wctx = private_data;
if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_INTEGER) {
goto done;
}
if (RedisModule_CallReplyInteger(reply) != 1) {
goto done;
}
RedisModule_FreeCallReply(reply);
reply = NULL;
const char* cmd = RedisModule_StringPtrLen(wctx->argv[0], NULL);
reply = RedisModule_Call(ctx, cmd, "!EKv", wctx->argv + 1, wctx->argc - 1);
done:
if(RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_PROMISE) {
RedisModuleCtx *bctx = RedisModule_GetThreadSafeContext(wctx->bc);
rm_call_async_send_reply(bctx, reply);
RedisModule_FreeThreadSafeContext(bctx);
RedisModule_UnblockClient(wctx->bc, NULL);
} else {
RedisModule_CallReplyPromiseSetUnblockHandler(reply, rm_call_async_on_unblocked, wctx->bc);
RedisModule_FreeCallReply(reply);
}
for (int i = 0 ; i < wctx->argc ; ++i) {
RedisModule_FreeString(NULL, wctx->argv[i]);
}
RedisModule_Free(wctx->argv);
RedisModule_Free(wctx);
}
/*
* Callback for wait_and_do_rm_call
* Gets the command to invoke as the first argument, runs 'wait'
* command (using the K flag to RM_Call). Once the wait finished, runs the
* command that was given (just like 'do_rm_call_async').
*/
int wait_and_do_rm_call_async(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
UNUSED(argv);
UNUSED(argc);
if(argc < 2){
return RedisModule_WrongArity(ctx);
}
int flags = RedisModule_GetContextFlags(ctx);
if (flags & REDISMODULE_CTX_FLAGS_DENY_BLOCKING) {
return RedisModule_ReplyWithError(ctx, "Err can not run wait, blocking is not allowed.");
}
RedisModuleCallReply* rep = RedisModule_Call(ctx, "wait", "!EKcc", "1", "0");
if(RedisModule_CallReplyType(rep) != REDISMODULE_REPLY_PROMISE) {
rm_call_async_send_reply(ctx, rep);
} else {
RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx, NULL, NULL, NULL, 0);
WaitAndDoRMCallCtx *wctx = RedisModule_Alloc(sizeof(*wctx));
*wctx = (WaitAndDoRMCallCtx){
.bc = bc,
.argv = RedisModule_Alloc((argc - 1) * sizeof(RedisModuleString*)),
.argc = argc - 1,
};
for (int i = 1 ; i < argc ; ++i) {
wctx->argv[i - 1] = RedisModule_HoldString(NULL, argv[i]);
}
RedisModule_CallReplyPromiseSetUnblockHandler(rep, wait_and_do_rm_call_async_on_unblocked, wctx);
RedisModule_FreeCallReply(rep);
}
return REDISMODULE_OK;
}
static void blpop_and_set_multiple_keys_on_unblocked(RedisModuleCtx *ctx, RedisModuleCallReply *reply, void *private_data) {
/* ignore the reply */
RedisModule_FreeCallReply(reply);
WaitAndDoRMCallCtx *wctx = private_data;
for (int i = 0 ; i < wctx->argc ; i += 2) {
RedisModuleCallReply* rep = RedisModule_Call(ctx, "set", "!ss", wctx->argv[i], wctx->argv[i + 1]);
RedisModule_FreeCallReply(rep);
}
RedisModuleCtx *bctx = RedisModule_GetThreadSafeContext(wctx->bc);
RedisModule_ReplyWithSimpleString(bctx, "OK");
RedisModule_FreeThreadSafeContext(bctx);
RedisModule_UnblockClient(wctx->bc, NULL);
for (int i = 0 ; i < wctx->argc ; ++i) {
RedisModule_FreeString(NULL, wctx->argv[i]);
}
RedisModule_Free(wctx->argv);
RedisModule_Free(wctx);
}
/*
* Performs a blpop command on a given list and when unblocked set multiple string keys.
* This command allows checking that the unblock callback is performed as a unit
* and its effect are replicated to the replica and AOF wrapped with multi exec.
*/
int blpop_and_set_multiple_keys(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
UNUSED(argv);
UNUSED(argc);
if(argc < 2 || argc % 2 != 0){
return RedisModule_WrongArity(ctx);
}
int flags = RedisModule_GetContextFlags(ctx);
if (flags & REDISMODULE_CTX_FLAGS_DENY_BLOCKING) {
return RedisModule_ReplyWithError(ctx, "Err can not run wait, blocking is not allowed.");
}
RedisModuleCallReply* rep = RedisModule_Call(ctx, "blpop", "!EKsc", argv[1], "0");
if(RedisModule_CallReplyType(rep) != REDISMODULE_REPLY_PROMISE) {
rm_call_async_send_reply(ctx, rep);
} else {
RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx, NULL, NULL, NULL, 0);
WaitAndDoRMCallCtx *wctx = RedisModule_Alloc(sizeof(*wctx));
*wctx = (WaitAndDoRMCallCtx){
.bc = bc,
.argv = RedisModule_Alloc((argc - 2) * sizeof(RedisModuleString*)),
.argc = argc - 2,
};
for (int i = 0 ; i < argc - 2 ; ++i) {
wctx->argv[i] = RedisModule_HoldString(NULL, argv[i + 2]);
}
RedisModule_CallReplyPromiseSetUnblockHandler(rep, blpop_and_set_multiple_keys_on_unblocked, wctx);
RedisModule_FreeCallReply(rep);
}
return REDISMODULE_OK;
}
/* simulate a blocked client replying to a thread safe context without creating a thread */
int do_fake_bg_true(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
UNUSED(argv);
......@@ -316,6 +567,30 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
"write", 0, 0, 0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx, "do_rm_call_async", do_rm_call_async,
"write", 0, 0, 0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx, "do_rm_call_async_script_mode", do_rm_call_async,
"write", 0, 0, 0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx, "do_rm_call_async_no_replicate", do_rm_call_async,
"write", 0, 0, 0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx, "do_rm_call_fire_and_forget", do_rm_call_async_fire_and_forget,
"write", 0, 0, 0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx, "wait_and_do_rm_call", wait_and_do_rm_call_async,
"write", 0, 0, 0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx, "blpop_and_set_multiple_keys", blpop_and_set_multiple_keys,
"write", 0, 0, 0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx, "do_bg_rm_call", do_bg_rm_call, "", 0, 0, 0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
......
......@@ -89,6 +89,7 @@ int get_fsl(RedisModuleCtx *ctx, RedisModuleString *keyname, int mode, int creat
create = 0; /* No need to create, key exists in its basic state */
} else {
RedisModule_DeleteKey(key);
*fsl = NULL;
}
} else {
/* Key exists, and has elements in it - no need to create anything */
......@@ -435,6 +436,10 @@ int blockonkeys_blpopn_reply_callback(RedisModuleCtx *ctx, RedisModuleString **a
result = REDISMODULE_OK;
} else if (RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_LIST ||
RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_EMPTY) {
const char *module_cmd = RedisModule_StringPtrLen(argv[0], NULL);
if (!strcasecmp(module_cmd, "blockonkeys.blpopn_or_unblock"))
RedisModule_UnblockClient(RedisModule_GetBlockedClientHandle(ctx), NULL);
/* continue blocking */
result = REDISMODULE_ERR;
} else {
......@@ -450,6 +455,12 @@ int blockonkeys_blpopn_timeout_callback(RedisModuleCtx *ctx, RedisModuleString *
return RedisModule_ReplyWithError(ctx, "ERR Timeout");
}
int blockonkeys_blpopn_abort_callback(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
REDISMODULE_NOT_USED(argv);
REDISMODULE_NOT_USED(argc);
return RedisModule_ReplyWithSimpleString(ctx, "Action aborted");
}
/* BLOCKONKEYS.BLPOPN key N
*
* Blocks until key has N elements and then pops them or fails after 3 seconds.
......@@ -457,11 +468,16 @@ int blockonkeys_blpopn_timeout_callback(RedisModuleCtx *ctx, RedisModuleString *
int blockonkeys_blpopn(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (argc < 3) return RedisModule_WrongArity(ctx);
long long n;
long long n, timeout = 3000LL;
if (RedisModule_StringToLongLong(argv[2], &n) != REDISMODULE_OK) {
return RedisModule_ReplyWithError(ctx, "ERR Invalid N");
}
if (argc > 3 ) {
if (RedisModule_StringToLongLong(argv[3], &timeout) != REDISMODULE_OK) {
return RedisModule_ReplyWithError(ctx, "ERR Invalid timeout value");
}
}
RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE);
int keytype = RedisModule_KeyType(key);
if (keytype != REDISMODULE_KEYTYPE_EMPTY &&
......@@ -477,8 +493,8 @@ int blockonkeys_blpopn(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
}
} else {
RedisModule_BlockClientOnKeys(ctx, blockonkeys_blpopn_reply_callback,
blockonkeys_blpopn_timeout_callback,
NULL, 3000, &argv[1], 1, NULL);
timeout ? blockonkeys_blpopn_timeout_callback : blockonkeys_blpopn_abort_callback,
NULL, timeout, &argv[1], 1, NULL);
}
RedisModule_CloseKey(key);
return REDISMODULE_OK;
......@@ -536,5 +552,8 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
"write", 1, 1, 1) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx, "blockonkeys.blpopn_or_unblock", blockonkeys_blpopn,
"write", 1, 1, 1) == REDISMODULE_ERR)
return REDISMODULE_ERR;
return REDISMODULE_OK;
}
......@@ -10,7 +10,7 @@ static const char retained_command_name[] = "commandfilter.retained";
static const char unregister_command_name[] = "commandfilter.unregister";
static int in_log_command = 0;
static RedisModuleCommandFilter *filter;
static RedisModuleCommandFilter *filter, *filter1;
static RedisModuleString *retained;
int CommandFilter_UnregisterCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
......@@ -89,6 +89,32 @@ int CommandFilter_LogCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int
return REDISMODULE_OK;
}
/* Filter to protect against Bug #11894 reappearing
*
* ensures that the filter is only run the first time through, and not on reprocessing
*/
void CommandFilter_BlmoveSwap(RedisModuleCommandFilterCtx *filter)
{
if (RedisModule_CommandFilterArgsCount(filter) != 6)
return;
RedisModuleString *arg = RedisModule_CommandFilterArgGet(filter, 0);
size_t arg_len;
const char *arg_str = RedisModule_StringPtrLen(arg, &arg_len);
if (arg_len != 6 || strncmp(arg_str, "blmove", 6))
return;
/*
* Swapping directional args (right/left) from source and destination.
* need to hold here, can't push into the ArgReplace func, as it will cause other to freed -> use after free
*/
RedisModuleString *dir1 = RedisModule_HoldString(NULL, RedisModule_CommandFilterArgGet(filter, 3));
RedisModuleString *dir2 = RedisModule_HoldString(NULL, RedisModule_CommandFilterArgGet(filter, 4));
RedisModule_CommandFilterArgReplace(filter, 3, dir2);
RedisModule_CommandFilterArgReplace(filter, 4, dir1);
}
void CommandFilter_CommandFilter(RedisModuleCommandFilterCtx *filter)
{
if (in_log_command) return; /* don't process our own RM_Call() from CommandFilter_LogCommand() */
......@@ -170,6 +196,9 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
noself ? REDISMODULE_CMDFILTER_NOSELF : 0))
== NULL) return REDISMODULE_ERR;
if ((filter1 = RedisModule_RegisterCommandFilter(ctx, CommandFilter_BlmoveSwap, 0)) == NULL)
return REDISMODULE_ERR;
return REDISMODULE_OK;
}
......
......@@ -234,6 +234,31 @@ static int datatype_is_in_slow_loading(RedisModuleCtx *ctx, RedisModuleString **
return REDISMODULE_OK;
}
int createDataTypeBlockCheck(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
REDISMODULE_NOT_USED(argv);
REDISMODULE_NOT_USED(argc);
static RedisModuleType *datatype_outside_onload = NULL;
RedisModuleTypeMethods datatype_methods = {
.version = REDISMODULE_TYPE_METHOD_VERSION,
.rdb_load = datatype_load,
.rdb_save = datatype_save,
.free = datatype_free,
.copy = datatype_copy
};
datatype_outside_onload = RedisModule_CreateDataType(ctx, "test_dt_outside_onload", 1, &datatype_methods);
/* This validates that it's not possible to create datatype outside OnLoad,
* thus returns an error if it succeeds. */
if (datatype_outside_onload == NULL) {
RedisModule_ReplyWithSimpleString(ctx, "OK");
} else {
RedisModule_ReplyWithError(ctx, "UNEXPECTEDOK");
}
return REDISMODULE_OK;
}
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
REDISMODULE_NOT_USED(argv);
REDISMODULE_NOT_USED(argc);
......@@ -241,6 +266,10 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
if (RedisModule_Init(ctx,"datatype",DATATYPE_ENC_VER,REDISMODULE_APIVER_1) == REDISMODULE_ERR)
return REDISMODULE_ERR;
/* Creates a command which creates a datatype outside OnLoad() function. */
if (RedisModule_CreateCommand(ctx,"block.create.datatype.outside.onload", createDataTypeBlockCheck, "write", 0, 0, 0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
RedisModule_SetModuleOptions(ctx, REDISMODULE_OPTIONS_HANDLE_IO_ERRORS);
RedisModuleTypeMethods datatype_methods = {
......
#include "redismodule.h"
#include <string.h>
/* This is a second sample module to validate that module authentication callbacks can be registered
* from multiple modules. */
/* Non Blocking Module Auth callback / implementation. */
int auth_cb(RedisModuleCtx *ctx, RedisModuleString *username, RedisModuleString *password, RedisModuleString **err) {
const char *user = RedisModule_StringPtrLen(username, NULL);
const char *pwd = RedisModule_StringPtrLen(password, NULL);
if (!strcmp(user,"foo") && !strcmp(pwd,"allow_two")) {
RedisModule_AuthenticateClientWithACLUser(ctx, "foo", 3, NULL, NULL, NULL);
return REDISMODULE_AUTH_HANDLED;
}
else if (!strcmp(user,"foo") && !strcmp(pwd,"deny_two")) {
RedisModuleString *log = RedisModule_CreateString(ctx, "Module Auth", 11);
RedisModule_ACLAddLogEntryByUserName(ctx, username, log, REDISMODULE_ACL_LOG_AUTH);
RedisModule_FreeString(ctx, log);
const char *err_msg = "Auth denied by Misc Module.";
*err = RedisModule_CreateString(ctx, err_msg, strlen(err_msg));
return REDISMODULE_AUTH_HANDLED;
}
return REDISMODULE_AUTH_NOT_HANDLED;
}
int test_rm_register_auth_cb(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
REDISMODULE_NOT_USED(argv);
REDISMODULE_NOT_USED(argc);
RedisModule_RegisterAuthCallback(ctx, auth_cb);
RedisModule_ReplyWithSimpleString(ctx, "OK");
return REDISMODULE_OK;
}
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
REDISMODULE_NOT_USED(argv);
REDISMODULE_NOT_USED(argc);
if (RedisModule_Init(ctx,"moduleauthtwo",1,REDISMODULE_APIVER_1)== REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx,"testmoduletwo.rm_register_auth_cb", test_rm_register_auth_cb,"",0,0,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
return REDISMODULE_OK;
}
\ No newline at end of file
......@@ -103,6 +103,37 @@ int longlongApplyFunc(RedisModuleCtx *ctx, void *privdata, RedisModuleString **e
return REDISMODULE_OK;
}
int registerBlockCheck(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
REDISMODULE_NOT_USED(argv);
REDISMODULE_NOT_USED(argc);
int response_ok = 0;
int result = RedisModule_RegisterBoolConfig(ctx, "mutable_bool", 1, REDISMODULE_CONFIG_DEFAULT, getBoolConfigCommand, setBoolConfigCommand, boolApplyFunc, &mutable_bool_val);
response_ok |= (result == REDISMODULE_OK);
result = RedisModule_RegisterStringConfig(ctx, "string", "secret password", REDISMODULE_CONFIG_DEFAULT, getStringConfigCommand, setStringConfigCommand, NULL, NULL);
response_ok |= (result == REDISMODULE_OK);
const char *enum_vals[] = {"none", "five", "one", "two", "four"};
const int int_vals[] = {0, 5, 1, 2, 4};
result = RedisModule_RegisterEnumConfig(ctx, "enum", 1, REDISMODULE_CONFIG_DEFAULT, enum_vals, int_vals, 5, getEnumConfigCommand, setEnumConfigCommand, NULL, NULL);
response_ok |= (result == REDISMODULE_OK);
result = RedisModule_RegisterNumericConfig(ctx, "numeric", -1, REDISMODULE_CONFIG_DEFAULT, -5, 2000, getNumericConfigCommand, setNumericConfigCommand, longlongApplyFunc, &longval);
response_ok |= (result == REDISMODULE_OK);
result = RedisModule_LoadConfigs(ctx);
response_ok |= (result == REDISMODULE_OK);
/* This validates that it's not possible to register/load configs outside OnLoad,
* thus returns an error if they succeed. */
if (response_ok) {
RedisModule_ReplyWithError(ctx, "UNEXPECTEDOK");
} else {
RedisModule_ReplyWithSimpleString(ctx, "OK");
}
return REDISMODULE_OK;
}
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
REDISMODULE_NOT_USED(argv);
REDISMODULE_NOT_USED(argc);
......@@ -147,6 +178,10 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
}
return REDISMODULE_ERR;
}
/* Creates a command which registers configs outside OnLoad() function. */
if (RedisModule_CreateCommand(ctx,"block.register.configs.outside.onload", registerBlockCheck, "write", 0, 0, 0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
return REDISMODULE_OK;
}
......
......@@ -12,8 +12,21 @@ test "Manual failover works" {
set old_port [RPort $master_id]
set addr [S 0 SENTINEL GET-MASTER-ADDR-BY-NAME mymaster]
assert {[lindex $addr 1] == $old_port}
# Since we reduced the info-period (default 10000) above immediately,
# sentinel - replica may not have enough time to exchange INFO and update
# the replica's info-period, so the test may get a NOGOODSLAVE.
wait_for_condition 300 50 {
[catch {S 0 SENTINEL FAILOVER mymaster}] == 0
} else {
catch {S 0 SENTINEL FAILOVER mymaster} reply
puts [S 0 SENTINEL REPLICAS mymaster]
fail "Sentinel manual failover did not work, got: $reply"
}
catch {S 0 SENTINEL FAILOVER mymaster} reply
assert {$reply eq "OK"}
assert_match {*INPROG*} $reply ;# Failover already in progress
foreach_sentinel_id id {
wait_for_condition 1000 50 {
[lindex [S $id SENTINEL GET-MASTER-ADDR-BY-NAME mymaster] 1] != $old_port
......
......@@ -72,6 +72,7 @@ test "SDOWN is triggered by masters advertising as slaves" {
ensure_master_up
}
if {!$::log_req_res} { # this test changes 'dir' config to '/' and logreqres.c cannot open protocol dump file under the root directory.
test "SDOWN is triggered by misconfigured instance replying with errors" {
ensure_master_up
set orig_dir [lindex [R 0 config get dir] 1]
......@@ -90,6 +91,7 @@ test "SDOWN is triggered by misconfigured instance replying with errors" {
R 0 bgsave
ensure_master_up
}
}
# We use this test setup to also test command renaming, as a side
# effect of the master going down if we send PONG instead of PING
......
......@@ -28,6 +28,8 @@
package require Tcl 8.5
package provide redis 0.1
source [file join [file dirname [info script]] "response_transformers.tcl"]
namespace eval redis {}
set ::redis::id 0
array set ::redis::fd {}
......@@ -41,6 +43,11 @@ array set ::redis::tls {}
array set ::redis::callback {}
array set ::redis::state {} ;# State in non-blocking reply reading
array set ::redis::statestack {} ;# Stack of states, for nested mbulks
array set ::redis::curr_argv {} ;# Remember the current argv, to be used in response_transformers.tcl
array set ::redis::testing_resp3 {} ;# Indicating if the current client is using RESP3 (only if the test is trying to test RESP3 specific behavior. It won't be on in case of force_resp3)
set ::force_resp3 0
set ::log_req_res 0
proc redis {{server 127.0.0.1} {port 6379} {defer 0} {tls 0} {tlsoptions {}} {readraw 0}} {
if {$tls} {
......@@ -62,6 +69,8 @@ proc redis {{server 127.0.0.1} {port 6379} {defer 0} {tls 0} {tlsoptions {}} {re
set ::redis::deferred($id) $defer
set ::redis::readraw($id) $readraw
set ::redis::reconnect($id) 0
set ::redis::curr_argv($id) 0
set ::redis::testing_resp3($id) 0
set ::redis::tls($id) $tls
::redis::redis_reset_state $id
interp alias {} ::redis::redisHandle$id {} ::redis::__dispatch__ $id
......@@ -123,6 +132,20 @@ proc ::redis::__dispatch__raw__ {id method argv} {
set fd $::redis::fd($id)
}
# Transform HELLO 2 to HELLO 3 if force_resp3
# All set the connection var testing_resp3 in case of HELLO 3
if {[llength $argv] > 0 && [string compare -nocase $method "HELLO"] == 0} {
if {[lindex $argv 0] == 3} {
set ::redis::testing_resp3($id) 1
} else {
set ::redis::testing_resp3($id) 0
if {$::force_resp3} {
# If we are in force_resp3 we run HELLO 3 instead of HELLO 2
lset argv 0 3
}
}
}
set blocking $::redis::blocking($id)
set deferred $::redis::deferred($id)
if {$blocking == 0} {
......@@ -146,6 +169,7 @@ proc ::redis::__dispatch__raw__ {id method argv} {
return -code error "I/O error reading reply"
}
set ::redis::curr_argv($id) [concat $method $argv]
if {!$deferred} {
if {$blocking} {
::redis::redis_read_reply $id $fd
......@@ -200,6 +224,8 @@ proc ::redis::__method__close {id fd} {
catch {unset ::redis::state($id)}
catch {unset ::redis::statestack($id)}
catch {unset ::redis::callback($id)}
catch {unset ::redis::curr_argv($id)}
catch {unset ::redis::testing_resp3($id)}
catch {interp alias {} ::redis::redisHandle$id {}}
}
......@@ -253,7 +279,7 @@ proc ::redis::redis_multi_bulk_read {id fd} {
set err {}
for {set i 0} {$i < $count} {incr i} {
if {[catch {
lappend l [redis_read_reply $id $fd]
lappend l [redis_read_reply_logic $id $fd]
} e] && $err eq {}} {
set err $e
}
......@@ -269,8 +295,8 @@ proc ::redis::redis_read_map {id fd} {
set err {}
for {set i 0} {$i < $count} {incr i} {
if {[catch {
set k [redis_read_reply $id $fd] ; # key
set v [redis_read_reply $id $fd] ; # value
set k [redis_read_reply_logic $id $fd] ; # key
set v [redis_read_reply_logic $id $fd] ; # value
dict set d $k $v
} e] && $err eq {}} {
set err $e
......@@ -296,13 +322,25 @@ proc ::redis::redis_read_bool fd {
return -code error "Bad protocol, '$v' as bool type"
}
proc ::redis::redis_read_double {id fd} {
set v [redis_read_line $fd]
# unlike many other DTs, there is a textual difference between double and a string with the same value,
# so we need to transform to double if we are testing RESP3 (i.e. some tests check that a
# double reply is "1.0" and not "1")
if {[should_transform_to_resp2 $id]} {
return $v
} else {
return [expr {double($v)}]
}
}
proc ::redis::redis_read_verbatim_str fd {
set v [redis_bulk_read $fd]
# strip the first 4 chars ("txt:")
return [string range $v 4 end]
}
proc ::redis::redis_read_reply {id fd} {
proc ::redis::redis_read_reply_logic {id fd} {
if {$::redis::readraw($id)} {
return [redis_read_line $fd]
}
......@@ -314,7 +352,7 @@ proc ::redis::redis_read_reply {id fd} {
: -
( -
+ {return [redis_read_line $fd]}
, {return [expr {double([redis_read_line $fd])}]}
, {return [redis_read_double $id $fd]}
# {return [redis_read_bool $fd]}
= {return [redis_read_verbatim_str $fd]}
- {return -code error [redis_read_line $fd]}
......@@ -340,6 +378,11 @@ proc ::redis::redis_read_reply {id fd} {
}
}
proc ::redis::redis_read_reply {id fd} {
set response [redis_read_reply_logic $id $fd]
::response_transformers::transform_response_if_needed $id $::redis::curr_argv($id) $response
}
proc ::redis::redis_reset_state id {
set ::redis::state($id) [dict create buf {} mbulk -1 bulk -1 reply {}]
set ::redis::statestack($id) {}
......@@ -416,3 +459,8 @@ proc ::redis::redis_readable {fd id} {
}
}
}
# when forcing resp3 some tests that rely on resp2 can fail, so we have to translate the resp3 response to resp2
proc ::redis::should_transform_to_resp2 {id} {
return [expr {$::force_resp3 && !$::redis::testing_resp3($id)}]
}
# Tcl client library - used by the Redis test
# Copyright (C) 2009-2023 Redis Ltd.
# Released under the BSD license like Redis itself
#
# This file contains a bunch of commands whose purpose is to transform
# a RESP3 response to RESP2
# Why is it needed?
# When writing the reply_schema part in COMMAND DOCS we decided to use
# the existing tests in order to verify the schemas (see logreqres.c)
# The problem was that many tests were relying on the RESP2 structure
# of the response (e.g. HRANDFIELD WITHVALUES in RESP2: {f1 v1 f2 v2}
# vs. RESP3: {{f1 v1} {f2 v2}}).
# Instead of adjusting the tests to expect RESP3 responses (a lot of
# changes in many files) we decided to transform the response to RESP2
# when running with --force-resp3
package require Tcl 8.5
namespace eval response_transformers {}
# Transform a map response into an array of tuples (tuple = array with 2 elements)
# Used for XREAD[GROUP]
proc transfrom_map_to_tupple_array {argv response} {
set tuparray {}
foreach {key val} $response {
set tmp {}
lappend tmp $key
lappend tmp $val
lappend tuparray $tmp
}
return $tuparray
}
# Transform an array of tuples to a flat array
proc transfrom_tuple_array_to_flat_array {argv response} {
set flatarray {}
foreach pair $response {
lappend flatarray {*}$pair
}
return $flatarray
}
# With HRANDFIELD, we only need to transform the response if the request had WITHVALUES
# (otherwise the returned response is a flat array in both RESPs)
proc transfrom_hrandfield_command {argv response} {
foreach ele $argv {
if {[string compare -nocase $ele "WITHVALUES"] == 0} {
return [transfrom_tuple_array_to_flat_array $argv $response]
}
}
return $response
}
# With some zset commands, we only need to transform the response if the request had WITHSCORES
# (otherwise the returned response is a flat array in both RESPs)
proc transfrom_zset_withscores_command {argv response} {
foreach ele $argv {
if {[string compare -nocase $ele "WITHSCORES"] == 0} {
return [transfrom_tuple_array_to_flat_array $argv $response]
}
}
return $response
}
# With ZPOPMIN/ZPOPMAX, we only need to transform the response if the request had COUNT (3rd arg)
# (otherwise the returned response is a flat array in both RESPs)
proc transfrom_zpopmin_zpopmax {argv response} {
if {[llength $argv] == 3} {
return [transfrom_tuple_array_to_flat_array $argv $response]
}
return $response
}
set ::trasformer_funcs {
XREAD transfrom_map_to_tupple_array
XREADGROUP transfrom_map_to_tupple_array
HRANDFIELD transfrom_hrandfield_command
ZRANDMEMBER transfrom_zset_withscores_command
ZRANGE transfrom_zset_withscores_command
ZRANGEBYSCORE transfrom_zset_withscores_command
ZRANGEBYLEX transfrom_zset_withscores_command
ZREVRANGE transfrom_zset_withscores_command
ZREVRANGEBYSCORE transfrom_zset_withscores_command
ZREVRANGEBYLEX transfrom_zset_withscores_command
ZUNION transfrom_zset_withscores_command
ZDIFF transfrom_zset_withscores_command
ZINTER transfrom_zset_withscores_command
ZPOPMIN transfrom_zpopmin_zpopmax
ZPOPMAX transfrom_zpopmin_zpopmax
}
proc ::response_transformers::transform_response_if_needed {id argv response} {
if {![::redis::should_transform_to_resp2 $id] || $::redis::readraw($id)} {
return $response
}
set key [string toupper [lindex $argv 0]]
if {![dict exists $::trasformer_funcs $key]} {
return $response
}
set transform [dict get $::trasformer_funcs $key]
return [$transform $argv $response]
}
......@@ -207,6 +207,12 @@ proc tags_acceptable {tags err_return} {
}
}
# some units mess with the client output buffer so we can't really use the req-res logging mechanism.
if {$::log_req_res && [lsearch $tags "logreqres:skip"] >= 0} {
set err "Not supported when running in log-req-res mode"
return 0
}
if {$::external && [lsearch $tags "external:skip"] >= 0} {
set err "Not supported on external server"
return 0
......@@ -511,6 +517,14 @@ proc start_server {options {code undefined}} {
dict unset config $directive
}
if {$::log_req_res} {
dict set config "req-res-logfile" "stdout.reqres"
}
if {$::force_resp3} {
dict set config "client-default-resp" "3"
}
# write new configuration to temporary file
set config_file [tmpfile redis.conf]
create_server_config_file $config_file $config $config_lines
......
......@@ -24,9 +24,11 @@ proc assert_no_match {pattern value} {
}
}
proc assert_match {pattern value {detail ""}} {
proc assert_match {pattern value {detail ""} {context ""}} {
if {![string match $pattern $value]} {
set context "(context: [info frame -1])"
if {$context eq ""} {
set context "(context: [info frame -1])"
}
error "assertion:Expected '$value' to match '$pattern' $context $detail"
}
}
......
......@@ -878,17 +878,17 @@ proc debug_digest {{level 0}} {
r $level debug digest
}
proc wait_for_blocked_client {} {
proc wait_for_blocked_client {{idx 0}} {
wait_for_condition 50 100 {
[s blocked_clients] ne 0
[s $idx blocked_clients] ne 0
} else {
fail "no blocked clients"
}
}
proc wait_for_blocked_clients_count {count {maxtries 100} {delay 10}} {
proc wait_for_blocked_clients_count {count {maxtries 100} {delay 10} {idx 0}} {
wait_for_condition $maxtries $delay {
[s blocked_clients] == $count
[s $idx blocked_clients] == $count
} else {
fail "Timeout waiting for blocked clients"
}
......
......@@ -100,6 +100,7 @@ set ::all_tests {
unit/cluster/hostnames
unit/cluster/multi-slot-operations
unit/cluster/slot-ownership
unit/cluster/links
}
# Index to the next test to run in the ::all_tests list.
set ::next_test 0
......@@ -134,6 +135,7 @@ set ::timeout 1200; # 20 minutes without progresses will quit the test.
set ::last_progress [clock seconds]
set ::active_servers {} ; # Pids of active Redis instances.
set ::dont_clean 0
set ::dont_pre_clean 0
set ::wait_server 0
set ::stop_on_failure 0
set ::dump_logs 0
......@@ -144,6 +146,8 @@ set ::cluster_mode 0
set ::ignoreencoding 0
set ::ignoredigest 0
set ::large_memory 0
set ::log_req_res 0
set ::force_resp3 0
# Set to 1 when we are running in client mode. The Redis test uses a
# server-client model to run tests simultaneously. The server instance
......@@ -319,7 +323,7 @@ proc cleanup {} {
}
proc test_server_main {} {
cleanup
if {!$::dont_pre_clean} cleanup
set tclsh [info nameofexecutable]
# Open a listening socket, trying different ports in order to find a
# non busy one.
......@@ -612,6 +616,7 @@ proc print_help_screen {} {
"--skiptest <test> Test name or regexp pattern (if <test> starts with '/') to skip. This option can be repeated."
"--tags <tags> Run only tests having specified tags or not having '-' prefixed tags."
"--dont-clean Don't delete redis log files after the run."
"--dont-pre-clean Don't delete existing redis log files before the run."
"--no-latency Skip latency measurements and validation by some tests."
"--stop Blocks once the first test fails."
"--loop Execute the specified set of tests forever."
......@@ -650,6 +655,10 @@ for {set j 0} {$j < [llength $argv]} {incr j} {
lappend ::global_overrides $arg
lappend ::global_overrides $arg2
incr j 2
} elseif {$opt eq {--log-req-res}} {
set ::log_req_res 1
} elseif {$opt eq {--force-resp3}} {
set ::force_resp3 1
} elseif {$opt eq {--skipfile}} {
incr j
set fp [open $arg r]
......@@ -724,6 +733,8 @@ for {set j 0} {$j < [llength $argv]} {incr j} {
set ::durable 1
} elseif {$opt eq {--dont-clean}} {
set ::dont_clean 1
} elseif {$opt eq {--dont-pre-clean}} {
set ::dont_pre_clean 1
} elseif {$opt eq {--no-latency}} {
set ::no_latency 1
} elseif {$opt eq {--wait-server}} {
......@@ -857,9 +868,22 @@ proc read_from_replication_stream {s} {
}
proc assert_replication_stream {s patterns} {
set errors 0
set values_list {}
set patterns_list {}
for {set j 0} {$j < [llength $patterns]} {incr j} {
assert_match [lindex $patterns $j] [read_from_replication_stream $s]
set pattern [lindex $patterns $j]
lappend patterns_list $pattern
set value [read_from_replication_stream $s]
lappend values_list $value
if {![string match $pattern $value]} { incr errors }
}
if {$errors == 0} { return }
set context [info frame -1]
close_replication_stream $s ;# for fast exit
assert_match $patterns_list $values_list "" $context
}
proc close_replication_stream {s} {
......
......@@ -7,6 +7,10 @@ start_server {tags {"acl external:skip"}} {
r ACL setuser newuser
}
test {Coverage: ACL USERS} {
r ACL USERS
} {default newuser}
test {Usernames can not contain spaces or null characters} {
catch {r ACL setuser "a a"} err
set err
......@@ -789,6 +793,31 @@ start_server {tags {"acl external:skip"}} {
r AUTH default ""
}
test {When an authentication chain is used in the HELLO cmd, the last auth cmd has precedence} {
r ACL setuser secure-user1 >supass on +@all
r ACL setuser secure-user2 >supass on +@all
r HELLO 2 AUTH secure-user pass AUTH secure-user2 supass AUTH secure-user1 supass
assert {[r ACL whoami] eq {secure-user1}}
catch {r HELLO 2 AUTH secure-user supass AUTH secure-user2 supass AUTH secure-user pass} e
assert_match "WRONGPASS invalid username-password pair or user is disabled." $e
assert {[r ACL whoami] eq {secure-user1}}
}
test {When a setname chain is used in the HELLO cmd, the last setname cmd has precedence} {
r HELLO 2 setname client1 setname client2 setname client3 setname client4
assert {[r client getname] eq {client4}}
catch {r HELLO 2 setname client5 setname client6 setname "client name"} e
assert_match "ERR Client names cannot contain spaces, newlines or special characters." $e
assert {[r client getname] eq {client4}}
}
test {When authentication fails in the HELLO cmd, the client setname should not be applied} {
r client setname client0
catch {r HELLO 2 AUTH user pass setname client1} e
assert_match "WRONGPASS invalid username-password pair or user is disabled." $e
assert {[r client getname] eq {client0}}
}
test {ACL HELP should not have unexpected options} {
catch {r ACL help xxx} e
assert_match "*wrong number of arguments for 'acl|help' command" $e
......
start_server {tags {"aofrw external:skip"}} {
# This unit has the potential to create huge .reqres files, causing log-req-res-validator.py to run for a very long time...
# Since this unit doesn't do anything worth validating, reply_schema-wise, we decided to skip it
start_server {tags {"aofrw external:skip logreqres:skip"}} {
# Enable the AOF
r config set appendonly yes
r config set auto-aof-rewrite-percentage 0 ; # Disable auto-rewrite.
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment