Skip to content

Commit

Permalink
More tests with simulated Redis traffic (MOVED redirects, async API)
Browse files Browse the repository at this point in the history
Add a test case for MOVED redirects and run simulated traffic tests
with both the sync and the async API.
  • Loading branch information
zuiderkwast committed Dec 7, 2020
1 parent 0f2922a commit abb920a
Show file tree
Hide file tree
Showing 7 changed files with 242 additions and 29 deletions.
30 changes: 26 additions & 4 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,31 @@ endif()
# Tests using simulated redis node
add_executable(clusterclient clusterclient.c)
target_link_libraries(clusterclient hiredis_cluster hiredis ${SSL_LIBRARY})
add_executable(clusterclient_async clusterclient_async.c)
target_link_libraries(clusterclient_async hiredis_cluster hiredis ${SSL_LIBRARY} ${EVENT_LIBRARY})
add_test(NAME set-get-test
COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/set-get-test.sh" "$<TARGET_FILE:clusterclient>"
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/")
COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/set-get-test.sh"
"$<TARGET_FILE:clusterclient>"
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/")
add_test(NAME set-get-test-async
COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/set-get-test.sh"
"$<TARGET_FILE:clusterclient_async>"
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/")
add_test(NAME ask-redirect-test
COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/ask-redirect-test.sh" "$<TARGET_FILE:clusterclient>"
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/")
COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/ask-redirect-test.sh"
"$<TARGET_FILE:clusterclient>"
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/")
add_test(NAME ask-redirect-test-async
COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/ask-redirect-test.sh"
"$<TARGET_FILE:clusterclient_async>"
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/")
add_test(NAME moved-redirect-test
COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/moved-redirect-test.sh"
"$<TARGET_FILE:clusterclient>"
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/")
# The test "moved-redirect-test-async" below triggers a warning when running
# with santitizers. TODO: Fix this problem and uncomment the test case below.
# add_test(NAME moved-redirect-test-async
# COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/moved-redirect-test.sh"
# "$<TARGET_FILE:clusterclient_async>"
# WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/")
2 changes: 1 addition & 1 deletion tests/clusterclient.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
#include <string.h>

int main(int argc, char **argv) {
if (argc < 1) {
if (argc <= 1) {
fprintf(stderr, "Usage: clusterclient HOST:PORT\n");
exit(1);
}
Expand Down
114 changes: 114 additions & 0 deletions tests/clusterclient_async.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* This program connects to a cluster and then reads commands from stdin, such
* as "SET foo bar", one per line and prints the results to stdout.
*
* The behaviour is the same as that of clusterclient.c, but the asynchronous
* API of the library is used rather than the synchronous API.
*/

#include "adapters/libevent.h"
#include "hircluster.h"
#include "test_utils.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int num_running = 0;

/*
void printReply(redisReply *reply) {
switch (reply->type) {
case REDIS_REPLY_INTEGER: printf("%lld", reply->integer); break;
case REDIS_REPLY_DOUBLE: printf("%s", reply->str); break;
case REDIS_REPLY_ERROR: printf("-%s", reply->str); break;
// TODO: Escape special chars in strings
case REDIS_REPLY_STRING: printf("\"%s\"", reply->str); break;
case REDIS_REPLY_ARRAY:
printf("[");
for (size_t i = 0; i < reply->elements; i++) {
printReply(reply->element[i]);
if (i < reply->elements - 1)
printf(", ");
}
printf("]");
break;
default:
printf("UNKNOWN TYPE %d", reply->type);
}
}
*/

void replyCallback(redisClusterAsyncContext *acc, void *r, void *privdata) {
UNUSED(privdata);
redisReply *reply = (redisReply *)r;
ASSERT_MSG(reply != NULL, acc->errstr);

/* printReply(reply); */
/* printf("\n"); */
printf("%s\n", reply->str);

if (--num_running == 0) {
// Disconnect after receiving all replies
redisClusterAsyncDisconnect(acc);
}
}

void connectCallback(const redisAsyncContext *ac, int status) {
ASSERT_MSG(status == REDIS_OK, ac->errstr);
// printf("Connected to %s:%d\n", ac->c.tcp.host, ac->c.tcp.port);
}

void disconnectCallback(const redisAsyncContext *ac, int status) {
ASSERT_MSG(status == REDIS_OK, ac->errstr);
// printf("Disconnected from %s:%d\n", ac->c.tcp.host, ac->c.tcp.port);
}

int main(int argc, char **argv) {
if (argc <= 1) {
fprintf(stderr, "Usage: clusterclient_async HOST:PORT\n");
exit(1);
}
const char *initnode = argv[1];

redisClusterAsyncContext *acc = redisClusterAsyncContextInit();
assert(acc);
redisClusterAsyncSetConnectCallback(acc, connectCallback);
redisClusterAsyncSetDisconnectCallback(acc, disconnectCallback);
redisClusterSetOptionAddNodes(acc->cc, initnode);
redisClusterSetOptionRouteUseSlots(acc->cc);
redisClusterConnect2(acc->cc);
if (acc->err) {
printf("Connect error: %s\n", acc->errstr);
exit(-1);
}

int status;
struct event_base *base = event_base_new();
status = redisClusterLibeventAttach(acc, base);
assert(status == REDIS_OK);

// Forward commands from stdin to redis cluster
char command[256];

// Make sure num_running doesn't reach 0 in replyCallback() before all
// commands have been sent.
num_running++;

while (fgets(command, 256, stdin)) {
size_t len = strlen(command);
if (command[len - 1] == '\n') // Chop trailing line break
command[len - 1] = '\0';
status =
redisClusterAsyncCommand(acc, replyCallback, (char *)"ID", command);
ASSERT_MSG(status == REDIS_OK, acc->errstr);
num_running++;
}
num_running--; // all commands sent

event_base_dispatch(base);

redisClusterAsyncFree(acc);
event_base_free(base);
return 0;
}
5 changes: 4 additions & 1 deletion tests/scripts/ask-redirect-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ syncpid2=$!;

# Start simulated redis node #1
timeout 5s ./simulated-redis.pl -p 7401 -d --sigcont $syncpid1 <<'EOF' &
EXPECT CONNECT
EXPECT ["CLUSTER", "SLOTS"]
SEND [[0, 16383, ["127.0.0.1", 7401, "nodeid123"]]]
EXPECT RECONNECT
EXPECT CLOSE
EXPECT CONNECT
EXPECT ["GET", "foo"]
SEND -ASK 12182 127.0.0.1:7402
EXPECT CLOSE
Expand All @@ -24,6 +26,7 @@ server1=$!

# Start simulated redis node #2
timeout 5s ./simulated-redis.pl -p 7402 -d --sigcont $syncpid2 <<'EOF' &
EXPECT CONNECT
EXPECT ["ASKING"]
SEND +OK
EXPECT ["GET", "foo"]
Expand Down
69 changes: 69 additions & 0 deletions tests/scripts/moved-redirect-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/bin/sh

# Usage: $0 /path/to/clusterclient-binary

clientprog=${1:-./clusterclient}
testname=moved-redirect-test

# Sync processes waiting for CONT signals.
perl -we 'use sigtrap "handler", sub{exit}, "CONT"; sleep 1; die "timeout"' &
syncpid1=$!;
perl -we 'use sigtrap "handler", sub{exit}, "CONT"; sleep 1; die "timeout"' &
syncpid2=$!;

# Start simulated redis node #1
timeout 5s ./simulated-redis.pl -p 7403 -d --sigcont $syncpid1 <<'EOF' &
EXPECT CONNECT
EXPECT ["CLUSTER", "SLOTS"]
SEND [[0, 16383, ["127.0.0.1", 7403, "nodeid7403"]]]
EXPECT CLOSE
EXPECT CONNECT
EXPECT ["GET", "foo"]
SEND -MOVED 12182 127.0.0.1:7404
EXPECT CONNECT
EXPECT ["CLUSTER", "SLOTS"]
SEND [[0, 16383, ["127.0.0.1", 7404, "nodeid7404"]]]
EXPECT CLOSE
EXPECT CLOSE
EOF
server1=$!

# Start simulated redis node #2
timeout 5s ./simulated-redis.pl -p 7404 -d --sigcont $syncpid2 <<'EOF' &
EXPECT CONNECT
EXPECT ["GET", "foo"]
SEND "bar"
EXPECT CLOSE
EOF
server2=$!

# Wait until both nodes are ready to accept client connections
wait $syncpid1 $syncpid2;

# Run client
echo 'GET foo' | timeout 3s "$clientprog" 127.0.0.1:7403 > "$testname.out"
clientexit=$?

# Wait for servers to exit
wait $server1; server1exit=$?
wait $server2; server2exit=$?

# Check exit statuses
if [ $server1exit -ne 0 ]; then
echo "Simulated server #1 exited with status $server1exit"
exit $server1exit
fi
if [ $server2exit -ne 0 ]; then
echo "Simulated server #2 exited with status $server2exit"
exit $server2exit
fi
if [ $clientexit -ne 0 ]; then
echo "$clientprog exited with status $clientexit"
exit $clientexit
fi

# Check the output from clusterclient
echo 'bar' | cmp "$testname.out" - || exit 99

# Clean up
rm "$testname.out"
4 changes: 3 additions & 1 deletion tests/scripts/set-get-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ syncpid=$!

# Start simulated server
timeout 5s ./simulated-redis.pl -p 7400 -d --sigcont $syncpid <<'EOF' &
EXPECT CONNECT
EXPECT ["CLUSTER", "SLOTS"]
SEND [[0, 16383, ["127.0.0.1", 7400, "nodeid123"]]]
EXPECT RECONNECT
EXPECT CLOSE
EXPECT CONNECT
EXPECT ["SET", "foo", "bar"]
SEND +OK
EXPECT ["GET", "foo"]
Expand Down
47 changes: 25 additions & 22 deletions tests/scripts/simulated-redis.pl
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@
map { print "$_\n" }
"Usage: $0 [ OPTIONS ]",
"",
"Acts as a Redis node, communicating with a single client according to",
"expected traffic provided as events on stdin.",
"Acts as a Redis node, communicating with clients according to",
"expected traffic provided as events on stdin. Multiple connections",
"are accepted, but data can only be sent to and received from the",
"last accepted client, referred to as \"the client\" below.",
"",
"Options:",
"",
Expand All @@ -46,11 +48,11 @@
"Expected traffic is to be provided on stdin, one event per line,",
"where the following events are accepted:",
"",
" SEND response Send response to the client.",
" EXPECT CONNECT Wait for a client to connect.",
" EXPECT CLOSE Wait for the client to close the connection.",
" EXPECT command Receive expected command from the client.",
" SEND response Send response to the client.",
" CLOSE Close the connection to the client.",
" EXPECT CLOSE Wait for the client to close the connection.",
" EXPECT RECONNECT Wait for the client to close and reconnect.",
" SLEEP n Sleep n seconds.",
"",
"The command and response in the events above is provided in a subset",
Expand All @@ -72,9 +74,8 @@
}
}

my $listener; # Listener socket
my $connection; # Connection socket

# Listener socket. Close it on SIGTERM, etc. and at normal exit.
my $listener;
END {
close $listener if $listener;
}
Expand All @@ -89,15 +90,17 @@ END
or die "listen: $!\n";
print "(port $port) Listening.\n" if $debug;
kill $sig, $sig_pid if $sig_pid;
my $peer_addr = accept($connection, $listener);
my($client_port, $client_addr) = sockaddr_in($peer_addr);
my $name = gethostbyaddr($client_addr, AF_INET);
print "(port $port) Connection from $name [", inet_ntoa($client_addr), "]",
" on client port $client_port.\n" if $debug;

# Accept multiple connections, but only the last one is used.
my @connections = ();
my $connection; # Active connection = the last element of @connections

# Loop over events on stdin.
while (<>) {
next if /^#/; # skip commented-out events
s/#.*//; # trim trailing comments
s/^\s+//; # trim leading whitespace
s/\s+$//; # trim trailing whitespace
next if /^$/; # skip empty lines
print "(port $port) $_" if $debug;
if (/^SEND (.*)/) {
my $data = $1;
Expand All @@ -114,24 +117,24 @@ END
flush $connection;
} elsif (/^CLOSE$/) {
close $connection;
pop @connections;
$connection = $connections[-1];
} elsif (/^EXPECT CLOSE$/) {
my $buffer;
my $bytes_read = read $connection, $buffer, 1;
die "(port $port) Data received from peer when close is expected.\n"
if $bytes_read;
print "(port $port) Client disconnected.\n" if $debug;
close $connection;
} elsif (/^EXPECT RECONNECT$/) {
my $buffer;
my $bytes_read = read $connection, $buffer, 1;
die "(port $port) Data received from peer when reconnect is expected.\n"
if $bytes_read;
close $connection;
print "(port $port) Client disconnected.\n" if $debug;
pop @connections;
$connection = $connections[-1];
} elsif (/^EXPECT CONNECT$/) {
undef $connection;
my $peer_addr = accept($connection, $listener);
push @connections, $connection;
my($client_port, $client_addr) = sockaddr_in($peer_addr);
my $name = gethostbyaddr($client_addr, AF_INET);
print "(port $port) Reconnect from $name [", inet_ntoa($client_addr),
print "(port $port) Connection from $name [", inet_ntoa($client_addr),
"] on client port $client_port.\n" if $debug;
} elsif (/^EXPECT ([\[\"].*)/) {
my $expected = eval $1;
Expand Down

0 comments on commit abb920a

Please sign in to comment.