Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Imported Upstream version 2.8.7

  • Loading branch information...
commit b3f9e62f6a54f2d9c6c6e7bbdcdb40411cd7a5fb 1 parent e68ab31
@lamby authored
View
46 00-RELEASENOTES
@@ -14,6 +14,52 @@ HIGH: There is a critical bug that may affect a subset of users. Upgrade!
CRITICAL: There is a critical bug affecting MOST USERS. Upgrade ASAP.
--------------------------------------------------------------------------------
+--[ Redis 2.8.7 ] Release date: 5 Mar 2014
+
+# UPGRADE URGENCY: LOW for Redis, LOW for Sentinel. However this release adds
+ new features so users may want to upgrade in order to
+ exploit the new functionalities.
+
+* [FIX] Sometimes the absolute config file path was obtained in a wrong way.
+ This happened when there was a "dir" directive inside the config file
+ and at the same time the configuration file was given as a relative
+ path to redis-server or redis-sentinel executables.
+* [FIX] redis-cli: Automatically enter --slave mode when SYNC or PSYNC are
+ called during an interactive session.
+* [FIX] Sentinel "IDONTKNOW" error removed as it does not made sense with the
+ new Sentinel design. This error was actually a fix for a design error
+ in the first implementation of Sentinel.
+* [FIX] Sentinel: added a missing exit() call to abort after config file
+ checks at startup. This error was introduced with an improvement in
+ a previous 2.8 release.
+* [FIX] BITCOUNT: fixed unaligned access causing issues in sparc and other
+ archs not capable of dealing with unaligned accesses. This also makes
+ the code faster in archs where unaligned accesses are allowed.
+* [FIX] Sentinel: better nodes fail over start time desynchronization to avoid
+ split-brain during the voting process needed to get authorization to
+ fail over. This means the system is less likely to need to retry
+ and will fail over faster. No changes in behavior / correctness.
+* [FIX] Force INFO used_memory_peak to match peak memory. This generated some
+ confusion among users even if it was not an actual bug.
+
+* [NEW] Sentinel unit tests and framework. More tests needed and units must
+ be improved in order to have less false positives, but it is a start
+ and features a debugging console that is useful to fix tests or to
+ inspect bugs causing tests failures.
+* [NEW] New Sentinel events: +/-monitor and +set used to monitor when an
+ instance to monitor is added or removed, or when a configuration
+ is modified via SENTINEL SET.
+* [NEW] Redis-cli updated to use SCAN instead of random sampling via
+ RANDOMKEY in order to implement --bigkeys feature. Moreover the
+ implementation now supports pipelining and reports more information
+ at the end of the scan. Much faster, much better. A special thank
+ you to Michael Grunder for this improvement.
+* [NEW] redis-cli now supports a new --intrinsic-latency mode that is able
+ to meter the latency of a system due to kernel / hypervisor.
+ How to use it is explained at http://redis.io/topics/latency.
+* [NEW] New command BITPOS: find first bit set or clear in a bitmap.
+* [NEW] CONFIG REWRITE calls are now logged.
+
--[ Redis 2.8.6 ] Release date: 13 Feb 2014
# UPGRADE URGENCY: HIGH for Redis, LOW for Sentinel. Redis users using Lua
View
14 runtest-sentinel
@@ -0,0 +1,14 @@
+#!/bin/sh
+TCL_VERSIONS="8.5 8.6"
+TCLSH=""
+
+for VERSION in $TCL_VERSIONS; do
+ TCL=`which tclsh$VERSION 2>/dev/null` && TCLSH=$TCL
+done
+
+if [ -z $TCLSH ]
+then
+ echo "You need tcl 8.5 or newer in order to run the Redis Sentinel test"
+ exit 1
+fi
+$TCLSH tests/sentinel.tcl $*
View
3  src/Makefile
@@ -204,6 +204,9 @@ distclean: clean
test: $(REDIS_SERVER_NAME) $(REDIS_CHECK_AOF_NAME)
@(cd ..; ./runtest)
+test-sentinel: $(REDIS_SENTINEL_NAME)
+ @(cd ..; ./runtest-sentinel)
+
check: test
lcov:
View
187 src/bitops.c
@@ -60,11 +60,18 @@ static int getBitOffsetFromArgument(redisClient *c, robj *o, size_t *offset) {
* work with a input string length up to 512 MB. */
size_t redisPopcount(void *s, long count) {
size_t bits = 0;
- unsigned char *p;
- uint32_t *p4 = s;
+ unsigned char *p = s;
+ uint32_t *p4;
static const unsigned char bitsinbyte[256] = {0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,4,5,5,6,5,6,6,7,5,6,6,7,6,7,7,8};
+ /* Count initial bytes not aligned to 32 bit. */
+ while((unsigned long)p & 3 && count) {
+ bits += bitsinbyte[*p++];
+ count--;
+ }
+
/* Count bits 16 bytes at a time */
+ p4 = (uint32_t*)p;
while(count>=16) {
uint32_t aux1, aux2, aux3, aux4;
@@ -87,12 +94,99 @@ size_t redisPopcount(void *s, long count) {
((((aux3 + (aux3 >> 4)) & 0x0F0F0F0F) * 0x01010101) >> 24) +
((((aux4 + (aux4 >> 4)) & 0x0F0F0F0F) * 0x01010101) >> 24);
}
- /* Count the remaining bytes */
+ /* Count the remaining bytes. */
p = (unsigned char*)p4;
while(count--) bits += bitsinbyte[*p++];
return bits;
}
+/* Return the position of the first bit set to one (if 'bit' is 1) or
+ * zero (if 'bit' is 0) in the bitmap starting at 's' and long 'count' bytes.
+ *
+ * The function is guaranteed to return a value >= 0 if 'bit' is 0 since if
+ * no zero bit is found, it returns count*8 assuming the string is zero
+ * padded on the right. However if 'bit' is 1 it is possible that there is
+ * not a single set bit in the bitmap. In this special case -1 is returned. */
+long redisBitpos(void *s, long count, int bit) {
+ unsigned long *l;
+ unsigned char *c;
+ unsigned long skipval, word = 0, one;
+ long pos = 0; /* Position of bit, to return to the caller. */
+ int j;
+
+ /* Process whole words first, seeking for first word that is not
+ * all ones or all zeros respectively if we are lookig for zeros
+ * or ones. This is much faster with large strings having contiguous
+ * blocks of 1 or 0 bits compared to the vanilla bit per bit processing.
+ *
+ * Note that if we start from an address that is not aligned
+ * to sizeof(unsigned long) we consume it byte by byte until it is
+ * aligned. */
+
+ /* Skip initial bits not aligned to sizeof(unsigned long) byte by byte. */
+ skipval = bit ? 0 : UCHAR_MAX;
+ c = (unsigned char*) s;
+ while((unsigned long)c & (sizeof(*l)-1) && count) {
+ if (*c != skipval) break;
+ c++;
+ count--;
+ pos += 8;
+ }
+
+ /* Skip bits with full word step. */
+ skipval = bit ? 0 : ULONG_MAX;
+ l = (unsigned long*) c;
+ while (count >= sizeof(*l)) {
+ if (*l != skipval) break;
+ l++;
+ count -= sizeof(*l);
+ pos += sizeof(*l)*8;
+ }
+
+ /* Load bytes into "word" considering the first byte as the most significant
+ * (we basically consider it as written in big endian, since we consider the
+ * string as a set of bits from left to right, with the first bit at position
+ * zero.
+ *
+ * Note that the loading is designed to work even when the bytes left
+ * (count) are less than a full word. We pad it with zero on the right. */
+ c = (unsigned char*)l;
+ for (j = 0; j < sizeof(*l); j++) {
+ word <<= 8;
+ if (count) {
+ word |= *c;
+ c++;
+ count--;
+ }
+ }
+
+ /* Special case:
+ * If bits in the string are all zero and we are looking for one,
+ * return -1 to signal that there is not a single "1" in the whole
+ * string. This can't happen when we are looking for "0" as we assume
+ * that the right of the string is zero padded. */
+ if (bit == 1 && word == 0) return -1;
+
+ /* Last word left, scan bit by bit. The first thing we need is to
+ * have a single "1" set in the most significant position in an
+ * unsigned long. We don't know the size of the long so we use a
+ * simple trick. */
+ one = ULONG_MAX; /* All bits set to 1.*/
+ one >>= 1; /* All bits set to 1 but the MSB. */
+ one = ~one; /* All bits set to 0 but the MSB. */
+
+ while(one) {
+ if (((one & word) != 0) == bit) return pos;
+ pos++;
+ one >>= 1;
+ }
+
+ /* If we reached this point, there is a bug in the algorithm, since
+ * the case of no match is handled as a special case before. */
+ redisPanic("End of redisBitpos() reached.");
+ return 0; /* Just to avoid warnings. */
+}
+
/* -----------------------------------------------------------------------------
* Bits related string commands: GETBIT, SETBIT, BITCOUNT, BITOP.
* -------------------------------------------------------------------------- */
@@ -410,3 +504,90 @@ void bitcountCommand(redisClient *c) {
addReplyLongLong(c,redisPopcount(p+start,bytes));
}
}
+
+/* BITPOS key bit [start [end]] */
+void bitposCommand(redisClient *c) {
+ robj *o;
+ long bit, start, end, strlen;
+ unsigned char *p;
+ char llbuf[32];
+ int end_given = 0;
+
+ /* Parse the bit argument to understand what we are looking for, set
+ * or clear bits. */
+ if (getLongFromObjectOrReply(c,c->argv[2],&bit,NULL) != REDIS_OK)
+ return;
+ if (bit != 0 && bit != 1) {
+ addReplyError(c, "The bit argument must be 1 or 0.");
+ return;
+ }
+
+ /* If the key does not exist, from our point of view it is an infinite
+ * array of 0 bits. If the user is looking for the fist clear bit return 0,
+ * If the user is looking for the first set bit, return -1. */
+ if ((o = lookupKeyRead(c->db,c->argv[1])) == NULL) {
+ addReplyLongLong(c, bit ? -1 : 0);
+ return;
+ }
+ if (checkType(c,o,REDIS_STRING)) return;
+
+ /* Set the 'p' pointer to the string, that can be just a stack allocated
+ * array if our string was integer encoded. */
+ if (o->encoding == REDIS_ENCODING_INT) {
+ p = (unsigned char*) llbuf;
+ strlen = ll2string(llbuf,sizeof(llbuf),(long)o->ptr);
+ } else {
+ p = (unsigned char*) o->ptr;
+ strlen = sdslen(o->ptr);
+ }
+
+ /* Parse start/end range if any. */
+ if (c->argc == 4 || c->argc == 5) {
+ if (getLongFromObjectOrReply(c,c->argv[3],&start,NULL) != REDIS_OK)
+ return;
+ if (c->argc == 5) {
+ if (getLongFromObjectOrReply(c,c->argv[4],&end,NULL) != REDIS_OK)
+ return;
+ end_given = 1;
+ } else {
+ end = strlen-1;
+ }
+ /* Convert negative indexes */
+ if (start < 0) start = strlen+start;
+ if (end < 0) end = strlen+end;
+ if (start < 0) start = 0;
+ if (end < 0) end = 0;
+ if (end >= strlen) end = strlen-1;
+ } else if (c->argc == 3) {
+ /* The whole string. */
+ start = 0;
+ end = strlen-1;
+ } else {
+ /* Syntax error. */
+ addReply(c,shared.syntaxerr);
+ return;
+ }
+
+ /* For empty ranges (start > end) we return -1 as an empty range does
+ * not contain a 0 nor a 1. */
+ if (start > end) {
+ addReplyLongLong(c, -1);
+ } else {
+ long bytes = end-start+1;
+ long pos = redisBitpos(p+start,bytes,bit);
+
+ /* If we are looking for clear bits, and the user specified an exact
+ * range with start-end, we can't consider the right of the range as
+ * zero padded (as we do when no explicit end is given).
+ *
+ * So if redisBitpos() returns the first bit outside the range,
+ * we return -1 to the caller, to mean, in the specified range there
+ * is not a single "0" bit. */
+ if (end_given && bit == 0 && pos == bytes*8) {
+ addReplyLongLong(c,-1);
+ return;
+ }
+ if (pos != -1) pos += start*8; /* Adjust for the bytes we skipped. */
+ addReplyLongLong(c,pos);
+ }
+}
View
3  src/config.c
@@ -1419,7 +1419,7 @@ void rewriteConfigSaveOption(struct rewriteConfigState *state) {
* resulting into no RDB persistence as expected. */
for (j = 0; j < server.saveparamslen; j++) {
line = sdscatprintf(sdsempty(),"save %ld %d",
- server.saveparams[j].seconds, server.saveparams[j].changes);
+ (long) server.saveparams[j].seconds, server.saveparams[j].changes);
rewriteConfigRewriteLine(state,"save",line,1);
}
/* Mark "save" as processed in case server.saveparamslen is zero. */
@@ -1780,6 +1780,7 @@ void configCommand(redisClient *c) {
redisLog(REDIS_WARNING,"CONFIG REWRITE failed: %s", strerror(errno));
addReplyErrorFormat(c,"Rewriting config file: %s", strerror(errno));
} else {
+ redisLog(REDIS_WARNING,"CONFIG REWRITE executed with success.");
addReply(c,shared.ok);
}
} else {
View
453 src/redis-cli.c
@@ -58,7 +58,7 @@
#define OUTPUT_RAW 1
#define OUTPUT_CSV 2
#define REDIS_CLI_KEEPALIVE_INTERVAL 15 /* seconds */
-#define REDIS_DEFAULT_PIPE_TIMEOUT 30 /* seconds */
+#define REDIS_CLI_DEFAULT_PIPE_TIMEOUT 30 /* seconds */
static redisContext *context;
static struct config {
@@ -82,6 +82,8 @@ static struct config {
int getrdb_mode;
int stat_mode;
int scan_mode;
+ int intrinsic_latency_mode;
+ int intrinsic_latency_duration;
char *pattern;
char *rdb_filename;
int bigkeys;
@@ -94,6 +96,7 @@ static struct config {
} config;
static void usage();
+static void slaveMode(void);
char *redisGitSHA1(void);
char *redisGitDirty(void);
@@ -101,14 +104,18 @@ char *redisGitDirty(void);
* Utility functions
*--------------------------------------------------------------------------- */
-static long long mstime(void) {
+static long long ustime(void) {
struct timeval tv;
- long long mst;
+ long long ust;
gettimeofday(&tv, NULL);
- mst = ((long long)tv.tv_sec)*1000;
- mst += tv.tv_usec/1000;
- return mst;
+ ust = ((long long)tv.tv_sec)*1000000;
+ ust += tv.tv_usec;
+ return ust;
+}
+
+static long long mstime(void) {
+ return ustime()/1000;
}
static void cliRefreshPrompt(void) {
@@ -599,6 +606,8 @@ static int cliSendCommand(int argc, char **argv, int repeat) {
if (!strcasecmp(command,"monitor")) config.monitor_mode = 1;
if (!strcasecmp(command,"subscribe") ||
!strcasecmp(command,"psubscribe")) config.pubsub_mode = 1;
+ if (!strcasecmp(command,"sync") ||
+ !strcasecmp(command,"psync")) config.slave_mode = 1;
/* Setup argument length */
argvlen = malloc(argc*sizeof(size_t));
@@ -620,6 +629,13 @@ static int cliSendCommand(int argc, char **argv, int repeat) {
}
}
+ if (config.slave_mode) {
+ printf("Entering slave output mode... (press Ctrl-C to quit)\n");
+ slaveMode();
+ config.slave_mode = 0;
+ return REDIS_ERR; /* Error = slaveMode lost connection to master */
+ }
+
if (cliReadReply(output_raw) != REDIS_OK) {
free(argvlen);
return REDIS_ERR;
@@ -715,8 +731,11 @@ static int parseOptions(int argc, char **argv) {
config.stat_mode = 1;
} else if (!strcmp(argv[i],"--scan")) {
config.scan_mode = 1;
- } else if (!strcmp(argv[i],"--pattern")) {
+ } else if (!strcmp(argv[i],"--pattern") && !lastarg) {
config.pattern = argv[++i];
+ } else if (!strcmp(argv[i],"--intrinsic-latency") && !lastarg) {
+ config.intrinsic_latency_mode = 1;
+ config.intrinsic_latency_duration = atoi(argv[++i]);
} else if (!strcmp(argv[i],"--rdb") && !lastarg) {
config.getrdb_mode = 1;
config.rdb_filename = argv[++i];
@@ -802,6 +821,8 @@ static void usage() {
" --bigkeys Sample Redis keys looking for big keys.\n"
" --scan List all keys using the SCAN command.\n"
" --pattern <pat> Useful with --scan to specify a SCAN pattern.\n"
+" --intrinsic-latency <sec> Run a test to measure intrinsic system latency.\n"
+" The test will run for the specified amount of seconds.\n"
" --eval <file> Send an EVAL command using the Lua script at <file>.\n"
" --help Output this help and exit.\n"
" --version Output version and exit.\n"
@@ -819,7 +840,7 @@ static void usage() {
"When no command is given, redis-cli starts in interactive mode.\n"
"Type \"help\" in interactive mode for information on available commands.\n"
"\n",
- version, REDIS_DEFAULT_PIPE_TIMEOUT);
+ version, REDIS_CLI_DEFAULT_PIPE_TIMEOUT);
sdsfree(version);
exit(1);
}
@@ -939,6 +960,10 @@ static int noninteractive(int argc, char **argv) {
return retval;
}
+/*------------------------------------------------------------------------------
+ * Eval mode
+ *--------------------------------------------------------------------------- */
+
static int evalMode(int argc, char **argv) {
sds script = sdsempty();
FILE *fp;
@@ -977,6 +1002,10 @@ static int evalMode(int argc, char **argv) {
return cliSendCommand(argc+3-got_comma, argv2, config.repeat);
}
+/*------------------------------------------------------------------------------
+ * Latency and latency history modes
+ *--------------------------------------------------------------------------- */
+
#define LATENCY_SAMPLE_RATE 10 /* milliseconds. */
#define LATENCY_HISTORY_DEFAULT_INTERVAL 15000 /* milliseconds. */
static void latencyMode(void) {
@@ -1021,6 +1050,10 @@ static void latencyMode(void) {
}
}
+/*------------------------------------------------------------------------------
+ * Slave mode
+ *--------------------------------------------------------------------------- */
+
/* Sends SYNC and reads the number of bytes in the payload. Used both by
* slaveMode() and getRDB(). */
unsigned long long sendSync(int fd) {
@@ -1060,6 +1093,7 @@ static void slaveMode(void) {
int fd = context->fd;
unsigned long long payload = sendSync(fd);
char buf[1024];
+ int original_output = config.output;
fprintf(stderr,"SYNC with master, discarding %llu "
"bytes of bulk transfer...\n", payload);
@@ -1080,8 +1114,13 @@ static void slaveMode(void) {
/* Now we can use hiredis to read the incoming protocol. */
config.output = OUTPUT_CSV;
while (cliReadReply(0) == REDIS_OK);
+ config.output = original_output;
}
+/*------------------------------------------------------------------------------
+ * RDB transfer mode
+ *--------------------------------------------------------------------------- */
+
/* This function implements --rdb, so it uses the replication protocol in order
* to fetch the RDB file from a remote server. */
static void getRDB(void) {
@@ -1127,6 +1166,10 @@ static void getRDB(void) {
exit(0);
}
+/*------------------------------------------------------------------------------
+ * Bulk import (pipe) mode
+ *--------------------------------------------------------------------------- */
+
static void pipeMode(void) {
int fd = context->fd;
long long errors = 0, replies = 0, obuf_len = 0, obuf_pos = 0;
@@ -1278,92 +1321,287 @@ static void pipeMode(void) {
exit(0);
}
+/*------------------------------------------------------------------------------
+ * Find big keys
+ *--------------------------------------------------------------------------- */
+
#define TYPE_STRING 0
#define TYPE_LIST 1
#define TYPE_SET 2
#define TYPE_HASH 3
#define TYPE_ZSET 4
+#define TYPE_NONE 5
-static void findBigKeys(void) {
- unsigned long long biggest[5] = {0,0,0,0,0};
- unsigned long long samples = 0;
- redisReply *reply1, *reply2, *reply3 = NULL;
- char *sizecmd, *typename[] = {"string","list","set","hash","zset"};
- char *typeunit[] = {"bytes","items","members","fields","members"};
- int type;
+static redisReply *sendScan(unsigned long long *it) {
+ redisReply *reply = redisCommand(context, "SCAN %llu", *it);
- printf("\n# Press ctrl+c when you have had enough of it... :)\n");
- printf("# You can use -i 0.1 to sleep 0.1 sec every 100 sampled keys\n");
- printf("# in order to reduce server load (usually not needed).\n\n");
- while(1) {
- /* Sample with RANDOMKEY */
- reply1 = redisCommand(context,"RANDOMKEY");
- if (reply1 == NULL) {
- fprintf(stderr,"\nI/O error\n");
- exit(1);
- } else if (reply1->type == REDIS_REPLY_ERROR) {
- fprintf(stderr, "RANDOMKEY error: %s\n",
- reply1->str);
+ /* Handle any error conditions */
+ if(reply == NULL) {
+ fprintf(stderr, "\nI/O error\n");
+ exit(1);
+ } else if(reply->type == REDIS_REPLY_ERROR) {
+ fprintf(stderr, "SCAN error: %s\n", reply->str);
+ exit(1);
+ } else if(reply->type != REDIS_REPLY_ARRAY) {
+ fprintf(stderr, "Non ARRAY response from SCAN!\n");
+ exit(1);
+ } else if(reply->elements != 2) {
+ fprintf(stderr, "Invalid element count from SCAN!\n");
+ exit(1);
+ }
+
+ /* Validate our types are correct */
+ assert(reply->element[0]->type == REDIS_REPLY_STRING);
+ assert(reply->element[1]->type == REDIS_REPLY_ARRAY);
+
+ /* Update iterator */
+ *it = atoi(reply->element[0]->str);
+
+ return reply;
+}
+
+static int getDbSize(void) {
+ redisReply *reply;
+ int size;
+
+ reply = redisCommand(context, "DBSIZE");
+
+ if(reply == NULL || reply->type != REDIS_REPLY_INTEGER) {
+ fprintf(stderr, "Couldn't determine DBSIZE!\n");
+ exit(1);
+ }
+
+ /* Grab the number of keys and free our reply */
+ size = reply->integer;
+ freeReplyObject(reply);
+
+ return size;
+}
+
+static int toIntType(char *key, char *type) {
+ if(!strcmp(type, "string")) {
+ return TYPE_STRING;
+ } else if(!strcmp(type, "list")) {
+ return TYPE_LIST;
+ } else if(!strcmp(type, "set")) {
+ return TYPE_SET;
+ } else if(!strcmp(type, "hash")) {
+ return TYPE_HASH;
+ } else if(!strcmp(type, "zset")) {
+ return TYPE_ZSET;
+ } else if(!strcmp(type, "none")) {
+ return TYPE_NONE;
+ } else {
+ fprintf(stderr, "Unknown type '%s' for key '%s'\n", type, key);
+ exit(1);
+ }
+}
+
+static void getKeyTypes(redisReply *keys, int *types) {
+ redisReply *reply;
+ int i;
+
+ /* Pipeline TYPE commands */
+ for(i=0;i<keys->elements;i++) {
+ redisAppendCommand(context, "TYPE %s", keys->element[i]->str);
+ }
+
+ /* Retrieve types */
+ for(i=0;i<keys->elements;i++) {
+ if(redisGetReply(context, (void**)&reply)!=REDIS_OK) {
+ fprintf(stderr, "Error getting type for key '%s' (%d: %s)\n",
+ keys->element[i]->str, context->err, context->errstr);
exit(1);
- } else if (reply1->type == REDIS_REPLY_NIL) {
- fprintf(stderr, "It looks like the database is empty!\n");
+ } else if(reply->type != REDIS_REPLY_STATUS) {
+ fprintf(stderr, "Invalid reply type (%d) for TYPE on key '%s'!\n",
+ reply->type, keys->element[i]->str);
exit(1);
}
- /* Get the key type */
- reply2 = redisCommand(context,"TYPE %s",reply1->str);
- assert(reply2 && reply2->type == REDIS_REPLY_STATUS);
- samples++;
-
- /* Get the key "size" */
- if (!strcmp(reply2->str,"string")) {
- sizecmd = "STRLEN";
- type = TYPE_STRING;
- } else if (!strcmp(reply2->str,"list")) {
- sizecmd = "LLEN";
- type = TYPE_LIST;
- } else if (!strcmp(reply2->str,"set")) {
- sizecmd = "SCARD";
- type = TYPE_SET;
- } else if (!strcmp(reply2->str,"hash")) {
- sizecmd = "HLEN";
- type = TYPE_HASH;
- } else if (!strcmp(reply2->str,"zset")) {
- sizecmd = "ZCARD";
- type = TYPE_ZSET;
- } else if (!strcmp(reply2->str,"none")) {
- freeReplyObject(reply1);
- freeReplyObject(reply2);
+ types[i] = toIntType(keys->element[i]->str, reply->str);
+ freeReplyObject(reply);
+ }
+}
+
+static void getKeySizes(redisReply *keys, int *types,
+ unsigned long long *sizes)
+{
+ redisReply *reply;
+ char *sizecmds[] = {"STRLEN","LLEN","SCARD","HLEN","ZCARD"};
+ int i;
+
+ /* Pipeline size commands */
+ for(i=0;i<keys->elements;i++) {
+ /* Skip keys that were deleted */
+ if(types[i]==TYPE_NONE)
continue;
+
+ redisAppendCommand(context, "%s %s", sizecmds[types[i]],
+ keys->element[i]->str);
+ }
+
+ /* Retreive sizes */
+ for(i=0;i<keys->elements;i++) {
+ /* Skip keys that dissapeared between SCAN and TYPE */
+ if(types[i] == TYPE_NONE) {
+ sizes[i] = 0;
+ continue;
+ }
+
+ /* Retreive size */
+ if(redisGetReply(context, (void**)&reply)!=REDIS_OK) {
+ fprintf(stderr, "Error getting size for key '%s' (%d: %s)\n",
+ keys->element[i]->str, context->err, context->errstr);
+ exit(1);
+ } else if(reply->type != REDIS_REPLY_INTEGER) {
+ /* Theoretically the key could have been removed and
+ * added as a different type between TYPE and SIZE */
+ fprintf(stderr,
+ "Warning: %s on '%s' failed (may have changed type)\n",
+ sizecmds[types[i]], keys->element[i]->str);
+ sizes[i] = 0;
} else {
- fprintf(stderr, "Unknown key type '%s' for key '%s'\n",
- reply2->str, reply1->str);
+ sizes[i] = reply->integer;
+ }
+
+ freeReplyObject(reply);
+ }
+}
+
+static void findBigKeys(void) {
+ unsigned long long biggest[5] = {0}, counts[5] = {0}, totalsize[5] = {0};
+ unsigned long long sampled = 0, total_keys, totlen=0, *sizes=NULL, it=0;
+ sds maxkeys[5] = {0};
+ char *typename[] = {"string","list","set","hash","zset"};
+ char *typeunit[] = {"bytes","items","members","fields","members"};
+ redisReply *reply, *keys;
+ int type, *types=NULL, arrsize=0, i;
+ double pct;
+
+ /* Total keys pre scanning */
+ total_keys = getDbSize();
+
+ /* Status message */
+ printf("\n# Scanning the entire keyspace to find biggest keys as well as\n");
+ printf("# average sizes per key type. You can use -i 0.1 to sleep 0.1 sec\n");
+ printf("# per 100 SCAN commands (not usually needed).\n\n");
+
+ /* New up sds strings to keep track of overall biggest per type */
+ for(i=0;i<TYPE_NONE; i++) {
+ maxkeys[i] = sdsempty();
+ if(!maxkeys[i]) {
+ fprintf(stderr, "Failed to allocate memory for largest key names!");
exit(1);
}
+ }
+
+ /* SCAN loop */
+ do {
+ /* Calculate approximate percentage completion */
+ pct = 100 * (double)sampled/total_keys;
+
+ /* Grab some keys and point to the keys array */
+ reply = sendScan(&it);
+ keys = reply->element[1];
- reply3 = redisCommand(context,"%s %s", sizecmd, reply1->str);
- if (reply3 && reply3->type == REDIS_REPLY_INTEGER) {
- if (biggest[type] < reply3->integer) {
- printf("Biggest %-6s found so far '%s' with %llu %s.\n",
- typename[type], reply1->str,
- (unsigned long long) reply3->integer,
- typeunit[type]);
- biggest[type] = reply3->integer;
+ /* Reallocate our type and size array if we need to */
+ if(keys->elements > arrsize) {
+ types = zrealloc(types, sizeof(int)*keys->elements);
+ sizes = zrealloc(sizes, sizeof(unsigned long long)*keys->elements);
+
+ if(!types || !sizes) {
+ fprintf(stderr, "Failed to allocate storage for keys!\n");
+ exit(1);
}
+
+ arrsize = keys->elements;
}
- if ((samples % 1000000) == 0)
- printf("(%llu keys sampled)\n", samples);
+ /* Retreive types and then sizes */
+ getKeyTypes(keys, types);
+ getKeySizes(keys, types, sizes);
+
+ /* Now update our stats */
+ for(i=0;i<keys->elements;i++) {
+ if((type = types[i]) == TYPE_NONE)
+ continue;
+
+ totalsize[type] += sizes[i];
+ counts[type]++;
+ totlen += keys->element[i]->len;
+ sampled++;
+
+ if(biggest[type]<sizes[i]) {
+ printf(
+ "[%05.2f%%] Biggest %-6s found so far '%s' with %llu %s\n",
+ pct, typename[type], keys->element[i]->str, sizes[i],
+ typeunit[type]);
+
+ /* Keep track of biggest key name for this type */
+ maxkeys[type] = sdscpy(maxkeys[type], keys->element[i]->str);
+ if(!maxkeys[type]) {
+ fprintf(stderr, "Failed to allocate memory for key!\n");
+ exit(1);
+ }
- if ((samples % 100) == 0 && config.interval)
+ /* Keep track of the biggest size for this type */
+ biggest[type] = sizes[i];
+ }
+
+ /* Update overall progress */
+ if(sampled % 1000000 == 0) {
+ printf("[%05.2f%%] Sampled %llu keys so far\n", pct, sampled);
+ }
+ }
+
+ /* Sleep if we've been directed to do so */
+ if(sampled && (sampled %100) == 0 && config.interval) {
usleep(config.interval);
+ }
+
+ freeReplyObject(reply);
+ } while(it != 0);
+
+ if(types) zfree(types);
+ if(sizes) zfree(sizes);
+
+ /* We're done */
+ printf("\n-------- summary -------\n\n");
+
+ printf("Sampled %llu keys in the keyspace!\n", sampled);
+ printf("Total key length in bytes is %llu (avg len %.2f)\n\n",
+ totlen, totlen ? (double)totlen/sampled : 0);
+
+ /* Output the biggest keys we found, for types we did find */
+ for(i=0;i<TYPE_NONE;i++) {
+ if(sdslen(maxkeys[i])>0) {
+ printf("Biggest %6s found '%s' has %llu %s\n", typename[i], maxkeys[i],
+ biggest[i], typeunit[i]);
+ }
+ }
+
+ printf("\n");
+
+ for(i=0;i<TYPE_NONE;i++) {
+ printf("%llu %ss with %llu %s (%05.2f%% of keys, avg size %.2f)\n",
+ counts[i], typename[i], totalsize[i], typeunit[i],
+ sampled ? 100 * (double)counts[i]/sampled : 0,
+ counts[i] ? (double)totalsize[i]/counts[i] : 0);
+ }
- freeReplyObject(reply1);
- freeReplyObject(reply2);
- if (reply3) freeReplyObject(reply3);
+ /* Free sds strings containing max keys */
+ for(i=0;i<TYPE_NONE;i++) {
+ sdsfree(maxkeys[i]);
}
+
+ /* Success! */
+ exit(0);
}
+/*------------------------------------------------------------------------------
+ * Stats mode
+ *--------------------------------------------------------------------------- */
+
/* Return the specified INFO field from the INFO command output "info".
* A new buffer is allocated for the result, that needs to be free'd.
* If the field is not found NULL is returned. */
@@ -1503,6 +1741,10 @@ static void statMode() {
}
}
+/*------------------------------------------------------------------------------
+ * Scan mode
+ *--------------------------------------------------------------------------- */
+
static void scanMode() {
redisReply *reply;
unsigned long long cur = 0;
@@ -1532,6 +1774,73 @@ static void scanMode() {
exit(0);
}
+/*------------------------------------------------------------------------------
+ * Intrisic latency mode.
+ *
+ * Measure max latency of a running process that does not result from
+ * syscalls. Basically this software should provide an hint about how much
+ * time the kernel leaves the process without a chance to run.
+ *--------------------------------------------------------------------------- */
+
+/* This is just some computation the compiler can't optimize out.
+ * Should run in less than 100-200 microseconds even using very
+ * slow hardware. Runs in less than 10 microseconds in modern HW. */
+unsigned long compute_something_fast(void) {
+ unsigned char s[256], i, j, t;
+ int count = 1000, k;
+ unsigned long output = 0;
+
+ for (k = 0; k < 256; k++) s[k] = k;
+
+ i = 0;
+ j = 0;
+ while(count--) {
+ i++;
+ j = j + s[i];
+ t = s[i];
+ s[i] = s[j];
+ s[j] = t;
+ output += s[(s[i]+s[j])&255];
+ }
+ return output;
+}
+
+static void intrinsicLatencyMode(void) {
+ long long test_end, run_time, max_latency = 0, runs = 0;
+
+ run_time = config.intrinsic_latency_duration*1000000;
+ test_end = ustime() + run_time;
+
+ while(1) {
+ long long start, end, latency;
+
+ start = ustime();
+ compute_something_fast();
+ end = ustime();
+ latency = end-start;
+ runs++;
+ if (latency <= 0) continue;
+
+ /* Reporting */
+ if (latency > max_latency) {
+ max_latency = latency;
+ printf("Max latency so far: %lld microseconds.\n", max_latency);
+ }
+
+ if (end > test_end) {
+ printf("\n%lld total runs (avg %lld microseconds per run).\n",
+ runs, run_time/runs);
+ printf("Worst run took %.02fx times the avarege.\n",
+ (double) max_latency / (run_time/runs));
+ exit(0);
+ }
+ }
+}
+
+/*------------------------------------------------------------------------------
+ * Program main()
+ *--------------------------------------------------------------------------- */
+
int main(int argc, char **argv) {
int firstarg;
@@ -1552,10 +1861,11 @@ int main(int argc, char **argv) {
config.getrdb_mode = 0;
config.stat_mode = 0;
config.scan_mode = 0;
+ config.intrinsic_latency_mode = 0;
config.pattern = NULL;
config.rdb_filename = NULL;
config.pipe_mode = 0;
- config.pipe_timeout = REDIS_DEFAULT_PIPE_TIMEOUT;
+ config.pipe_timeout = REDIS_CLI_DEFAULT_PIPE_TIMEOUT;
config.bigkeys = 0;
config.stdinarg = 0;
config.auth = NULL;
@@ -1614,6 +1924,9 @@ int main(int argc, char **argv) {
scanMode();
}
+ /* Intrinsic latency mode */
+ if (config.intrinsic_latency_mode) intrinsicLatencyMode();
+
/* Start interactive mode when no command is provided */
if (argc == 0 && !config.eval) {
/* Note that in repl mode we don't abort on connection error.
View
17 src/redis.c
@@ -257,7 +257,8 @@ struct redisCommand redisCommandTable[] = {
{"script",scriptCommand,-2,"ras",0,NULL,0,0,0,0,0},
{"time",timeCommand,1,"rR",0,NULL,0,0,0,0,0},
{"bitop",bitopCommand,-4,"wm",0,NULL,2,-1,1,0,0},
- {"bitcount",bitcountCommand,-2,"r",0,NULL,1,1,1,0,0}
+ {"bitcount",bitcountCommand,-2,"r",0,NULL,1,1,1,0,0},
+ {"bitpos",bitposCommand,-3,"r",0,NULL,1,1,1,0,0}
};
/*============================ Utility functions ============================ */
@@ -2295,8 +2296,16 @@ sds genRedisInfoString(char *section) {
if (allsections || defsections || !strcasecmp(section,"memory")) {
char hmem[64];
char peak_hmem[64];
+ size_t zmalloc_used = zmalloc_used_memory();
- bytesToHuman(hmem,zmalloc_used_memory());
+ /* Peak memory is updated from time to time by serverCron() so it
+ * may happen that the instantaneous value is slightly bigger than
+ * the peak value. This may confuse users, so we update the peak
+ * if found smaller than the current memory usage. */
+ if (zmalloc_used > server.stat_peak_memory)
+ server.stat_peak_memory = zmalloc_used;
+
+ bytesToHuman(hmem,zmalloc_used);
bytesToHuman(peak_hmem,server.stat_peak_memory);
if (sections++) info = sdscat(info,"\r\n");
info = sdscatprintf(info,
@@ -2309,7 +2318,7 @@ sds genRedisInfoString(char *section) {
"used_memory_lua:%lld\r\n"
"mem_fragmentation_ratio:%.2f\r\n"
"mem_allocator:%s\r\n",
- zmalloc_used_memory(),
+ zmalloc_used,
hmem,
zmalloc_get_rss(),
server.stat_peak_memory,
@@ -3030,10 +3039,10 @@ int main(int argc, char **argv) {
}
j++;
}
+ if (configfile) server.configfile = getAbsolutePath(configfile);
resetServerSaveParams();
loadServerConfig(configfile,options);
sdsfree(options);
- if (configfile) server.configfile = getAbsolutePath(configfile);
} else {
redisLog(REDIS_WARNING, "Warning: no config file specified, using the default config. In order to specify a config file use %s /path/to/%s.conf", argv[0], server.sentinel_mode ? "sentinel" : "redis");
}
View
1  src/redis.h
@@ -1346,6 +1346,7 @@ void scriptCommand(redisClient *c);
void timeCommand(redisClient *c);
void bitopCommand(redisClient *c);
void bitcountCommand(redisClient *c);
+void bitposCommand(redisClient *c);
void replconfCommand(redisClient *c);
#if defined(__GNUC__)
View
69 src/sentinel.c
@@ -78,12 +78,13 @@ typedef struct sentinelAddr {
#define SENTINEL_TILT_TRIGGER 2000
#define SENTINEL_TILT_PERIOD (SENTINEL_PING_PERIOD*30)
#define SENTINEL_DEFAULT_SLAVE_PRIORITY 100
-#define SENTINEL_SLAVE_RECONF_RETRY_PERIOD 10000
+#define SENTINEL_SLAVE_RECONF_TIMEOUT 10000
#define SENTINEL_DEFAULT_PARALLEL_SYNCS 1
#define SENTINEL_MIN_LINK_RECONNECT_PERIOD 15000
#define SENTINEL_DEFAULT_FAILOVER_TIMEOUT (60*3*1000)
#define SENTINEL_MAX_PENDING_COMMANDS 100
#define SENTINEL_ELECTION_TIMEOUT 10000
+#define SENTINEL_MAX_DESYNC 1000
/* Failover machine different states. */
#define SENTINEL_FAILOVER_STATE_NONE 0 /* No failover in progress. */
@@ -327,6 +328,7 @@ void sentinelDiscardReplyCallback(redisAsyncContext *c, void *reply, void *privd
int sentinelSendSlaveOf(sentinelRedisInstance *ri, char *host, int port);
char *sentinelVoteLeader(sentinelRedisInstance *master, uint64_t req_epoch, char *req_runid, uint64_t *leader_epoch);
void sentinelFlushConfig(void);
+void sentinelGenerateInitialMonitorEvents(void);
/* ========================= Dictionary types =============================== */
@@ -417,10 +419,20 @@ void initSentinel(void) {
void sentinelIsRunning(void) {
redisLog(REDIS_WARNING,"Sentinel runid is %s", server.runid);
- if (server.configfile == NULL || access(server.configfile,W_OK) == -1) {
- redisLog(REDIS_WARNING,"Sentinel started without a config file, or config file not writable. Exiting...");
+ if (server.configfile == NULL) {
+ redisLog(REDIS_WARNING,
+ "Sentinel started without a config file. Exiting...");
+ exit(1);
+ } else if (access(server.configfile,W_OK) == -1) {
+ redisLog(REDIS_WARNING,
+ "Sentinel config file %s is not writable: %s. Exiting...",
+ server.configfile,strerror(errno));
exit(1);
}
+
+ /* We want to generate a +monitor event for every configured master
+ * at startup. */
+ sentinelGenerateInitialMonitorEvents();
}
/* ============================== sentinelAddr ============================== */
@@ -552,6 +564,22 @@ void sentinelEvent(int level, char *type, sentinelRedisInstance *ri,
}
}
+/* This function is called only at startup and is used to generate a
+ * +monitor event for every configured master. The same events are also
+ * generated when a master to monitor is added at runtime via the
+ * SENTINEL MONITOR command. */
+void sentinelGenerateInitialMonitorEvents(void) {
+ dictIterator *di;
+ dictEntry *de;
+
+ di = dictGetIterator(sentinel.masters);
+ while((de = dictNext(di)) != NULL) {
+ sentinelRedisInstance *ri = dictGetVal(de);
+ sentinelEvent(REDIS_WARNING,"+monitor",ri,"%@ quorum %d",ri->quorum);
+ }
+ dictReleaseIterator(di);
+}
+
/* ============================ script execution ============================ */
/* Release a script job structure and all the associated data. */
@@ -1765,6 +1793,14 @@ void sentinelRefreshInstanceInfo(sentinelRedisInstance *ri, const char *info) {
ri->role_reported_time = mstime();
ri->role_reported = role;
if (role == SRI_SLAVE) ri->slave_conf_change_time = mstime();
+ /* Log the event with +role-change if the new role is coherent or
+ * with -role-change if there is a mismatch with the current config. */
+ sentinelEvent(REDIS_VERBOSE,
+ ((ri->flags & (SRI_MASTER|SRI_SLAVE)) == role) ?
+ "+role-change" : "-role-change",
+ ri, "%@ new reported role is %s",
+ role == SRI_MASTER ? "master" : "slave",
+ ri->flags & SRI_MASTER ? "master" : "slave");
}
/* Handle master -> slave role switch. */
@@ -2455,8 +2491,6 @@ void sentinelCommand(redisClient *c) {
ri = sentinelGetMasterByName(c->argv[2]->ptr);
if (ri == NULL) {
addReply(c,shared.nullmultibulk);
- } else if (ri->info_refresh == 0) {
- addReplySds(c,sdsnew("-IDONTKNOW I have not enough information to reply. Please ask another Sentinel.\r\n"));
} else {
sentinelAddr *addr = sentinelGetCurrentMasterAddress(ri);
@@ -2491,6 +2525,7 @@ void sentinelCommand(redisClient *c) {
sentinelPendingScriptsCommand(c);
} else if (!strcasecmp(c->argv[1]->ptr,"monitor")) {
/* SENTINEL MONITOR <name> <ip> <port> <quorum> */
+ sentinelRedisInstance *ri;
long quorum, port;
char buf[32];
@@ -2506,9 +2541,11 @@ void sentinelCommand(redisClient *c) {
addReplyError(c,"Invalid IP address specified");
return;
}
- if (createSentinelRedisInstance(c->argv[2]->ptr,SRI_MASTER,
- c->argv[3]->ptr,port,quorum,NULL) == NULL)
- {
+
+ /* Parameters are valid. Try to create the master instance. */
+ ri = createSentinelRedisInstance(c->argv[2]->ptr,SRI_MASTER,
+ c->argv[3]->ptr,port,quorum,NULL);
+ if (ri == NULL) {
switch(errno) {
case EBUSY:
addReplyError(c,"Duplicated master name");
@@ -2522,6 +2559,7 @@ void sentinelCommand(redisClient *c) {
}
} else {
sentinelFlushConfig();
+ sentinelEvent(REDIS_WARNING,"+monitor",ri,"%@ quorum %d",ri->quorum);
addReply(c,shared.ok);
}
} else if (!strcasecmp(c->argv[1]->ptr,"remove")) {
@@ -2530,6 +2568,7 @@ void sentinelCommand(redisClient *c) {
if ((ri = sentinelGetMasterByNameOrReplyError(c,c->argv[2]))
== NULL) return;
+ sentinelEvent(REDIS_WARNING,"-monitor",ri,"%@");
dictDelete(sentinel.masters,c->argv[2]->ptr);
sentinelFlushConfig();
addReply(c,shared.ok);
@@ -2681,6 +2720,7 @@ void sentinelSetCommand(redisClient *c) {
if (changes) sentinelFlushConfig();
return;
}
+ sentinelEvent(REDIS_WARNING,"+set",ri,"%@ %s %s",option,value);
}
if (changes) sentinelFlushConfig();
@@ -2904,7 +2944,7 @@ char *sentinelVoteLeader(sentinelRedisInstance *master, uint64_t req_epoch, char
* time to now, in order to force a delay before we can start a
* failover for the same master. */
if (strcasecmp(master->leader,server.runid))
- master->failover_start_time = mstime();
+ master->failover_start_time = mstime()+rand()%SENTINEL_MAX_DESYNC;
}
*leader_epoch = master->leader_epoch;
@@ -3049,7 +3089,7 @@ void sentinelStartFailover(sentinelRedisInstance *master) {
sentinelEvent(REDIS_WARNING,"+new-epoch",master,"%llu",
(unsigned long long) sentinel.current_epoch);
sentinelEvent(REDIS_WARNING,"+try-failover",master,"%@");
- master->failover_start_time = mstime();
+ master->failover_start_time = mstime()+rand()%SENTINEL_MAX_DESYNC;
master->failover_state_change_time = mstime();
}
@@ -3359,14 +3399,17 @@ void sentinelFailoverReconfNextSlave(sentinelRedisInstance *master) {
/* Skip the promoted slave, and already configured slaves. */
if (slave->flags & (SRI_PROMOTED|SRI_RECONF_DONE)) continue;
- /* Clear the SRI_RECONF_SENT flag if too much time elapsed without
- * the slave moving forward to the next state. */
+ /* If too much time elapsed without the slave moving forward to
+ * the next state, consider it reconfigured even if it is not.
+ * Sentinels will detect the slave as misconfigured and fix its
+ * configuration later. */
if ((slave->flags & SRI_RECONF_SENT) &&
(mstime() - slave->slave_reconf_sent_time) >
- SENTINEL_SLAVE_RECONF_RETRY_PERIOD)
+ SENTINEL_SLAVE_RECONF_TIMEOUT)
{
sentinelEvent(REDIS_NOTICE,"-slave-reconf-sent-timeout",slave,"%@");
slave->flags &= ~SRI_RECONF_SENT;
+ slave->flags |= SRI_RECONF_DONE;
}
/* Nothing to do for instances that are disconnected or already
View
2  src/version.h
@@ -1 +1 @@
-#define REDIS_VERSION "2.8.6"
+#define REDIS_VERSION "2.8.7"
View
126 tests/sentinel-tests/00-base.tcl
@@ -0,0 +1,126 @@
+# Check the basic monitoring and failover capabilities.
+
+source "../sentinel-tests/includes/init-tests.tcl"
+
+if {$::simulate_error} {
+ test "This test will fail" {
+ fail "Simulated error"
+ }
+}
+
+test "Basic failover works if the master is down" {
+ set old_port [RI $master_id tcp_port]
+ set addr [S 0 SENTINEL GET-MASTER-ADDR-BY-NAME mymaster]
+ assert {[lindex $addr 1] == $old_port}
+ kill_instance redis $master_id
+ foreach_sentinel_id id {
+ wait_for_condition 1000 50 {
+ [lindex [S $id SENTINEL GET-MASTER-ADDR-BY-NAME mymaster] 1] != $old_port
+ } else {
+ fail "At least one Sentinel did not received failover info"
+ }
+ }
+ restart_instance redis $master_id
+ set addr [S 0 SENTINEL GET-MASTER-ADDR-BY-NAME mymaster]
+ set master_id [get_instance_id_by_port redis [lindex $addr 1]]
+}
+
+test "New master [join $addr {:}] role matches" {
+ assert {[RI $master_id role] eq {master}}
+}
+
+test "All the other slaves now point to the new master" {
+ foreach_redis_id id {
+ if {$id != $master_id && $id != 0} {
+ wait_for_condition 1000 50 {
+ [RI $id master_port] == [lindex $addr 1]
+ } else {
+ fail "Redis ID $id not configured to replicate with new master"
+ }
+ }
+ }
+}
+
+test "The old master eventually gets reconfigured as a slave" {
+ wait_for_condition 1000 50 {
+ [RI 0 master_port] == [lindex $addr 1]
+ } else {
+ fail "Old master not reconfigured as slave of new master"
+ }
+}
+
+test "ODOWN is not possible without N (quorum) Sentinels reports" {
+ foreach_sentinel_id id {
+ S $id SENTINEL SET mymaster quorum [expr $sentinels+1]
+ }
+ set old_port [RI $master_id tcp_port]
+ set addr [S 0 SENTINEL GET-MASTER-ADDR-BY-NAME mymaster]
+ assert {[lindex $addr 1] == $old_port}
+ kill_instance redis $master_id
+
+ # Make sure failover did not happened.
+ set addr [S 0 SENTINEL GET-MASTER-ADDR-BY-NAME mymaster]
+ assert {[lindex $addr 1] == $old_port}
+ restart_instance redis $master_id
+}
+
+test "Failover is not possible without majority agreement" {
+ foreach_sentinel_id id {
+ S $id SENTINEL SET mymaster quorum $quorum
+ }
+
+ # Crash majority of sentinels
+ for {set id 0} {$id < $quorum} {incr id} {
+ kill_instance sentinel $id
+ }
+
+ # Kill the current master
+ kill_instance redis $master_id
+
+ # Make sure failover did not happened.
+ set addr [S $quorum SENTINEL GET-MASTER-ADDR-BY-NAME mymaster]
+ assert {[lindex $addr 1] == $old_port}
+ restart_instance redis $master_id
+
+ # Cleanup: restart Sentinels to monitor the master.
+ for {set id 0} {$id < $quorum} {incr id} {
+ restart_instance sentinel $id
+ }
+}
+
+test "Failover works if we configure for absolute agreement" {
+ foreach_sentinel_id id {
+ S $id SENTINEL SET mymaster quorum $sentinels
+ }
+
+ # Wait for Sentinels to monitor the master again
+ foreach_sentinel_id id {
+ wait_for_condition 1000 50 {
+ [dict get [S $id SENTINEL MASTER mymaster] info-refresh] < 100000
+ } else {
+ fail "At least one Sentinel is not monitoring the master"
+ }
+ }
+
+ kill_instance redis $master_id
+
+ foreach_sentinel_id id {
+ wait_for_condition 1000 50 {
+ [lindex [S $id SENTINEL GET-MASTER-ADDR-BY-NAME mymaster] 1] != $old_port
+ } else {
+ fail "At least one Sentinel did not received failover info"
+ }
+ }
+ restart_instance redis $master_id
+ set addr [S 0 SENTINEL GET-MASTER-ADDR-BY-NAME mymaster]
+ set master_id [get_instance_id_by_port redis [lindex $addr 1]]
+
+ # Set the min ODOWN agreement back to strict majority.
+ foreach_sentinel_id id {
+ S $id SENTINEL SET mymaster quorum $quorum
+ }
+}
+
+test "New master [join $addr {:}] role matches" {
+ assert {[RI $master_id role] eq {master}}
+}
View
39 tests/sentinel-tests/01-conf-update.tcl
@@ -0,0 +1,39 @@
+# Test Sentinel configuration consistency after partitions heal.
+
+source "../sentinel-tests/includes/init-tests.tcl"
+
+test "We can failover with Sentinel 1 crashed" {
+ set old_port [RI $master_id tcp_port]
+ set addr [S 0 SENTINEL GET-MASTER-ADDR-BY-NAME mymaster]
+ assert {[lindex $addr 1] == $old_port}
+
+ # Crash Sentinel 1
+ kill_instance sentinel 1
+
+ kill_instance redis $master_id
+ foreach_sentinel_id id {
+ if {$id != 1} {
+ wait_for_condition 1000 50 {
+ [lindex [S $id SENTINEL GET-MASTER-ADDR-BY-NAME mymaster] 1] != $old_port
+ } else {
+ fail "Sentinel $id did not received failover info"
+ }
+ }
+ }
+ restart_instance redis $master_id
+ set addr [S 0 SENTINEL GET-MASTER-ADDR-BY-NAME mymaster]
+ set master_id [get_instance_id_by_port redis [lindex $addr 1]]
+}
+
+test "After Sentinel 1 is restarted, its config gets updated" {
+ restart_instance sentinel 1
+ wait_for_condition 1000 50 {
+ [lindex [S 1 SENTINEL GET-MASTER-ADDR-BY-NAME mymaster] 1] != $old_port
+ } else {
+ fail "Restarted Sentinel did not received failover info"
+ }
+}
+
+test "New master [join $addr {:}] role matches" {
+ assert {[RI $master_id role] eq {master}}
+}
View
45 tests/sentinel-tests/02-slaves-reconf.tcl
@@ -0,0 +1,45 @@
+# Check that slaves are reconfigured at a latter time if they are partitioned.
+#
+# Here we should test:
+# 1) That slaves point to the new master after failover.
+# 2) That partitioned slaves point to new master when they are partitioned
+# away during failover and return at a latter time.
+
+source "../sentinel-tests/includes/init-tests.tcl"
+
+proc 03_test_slaves_replication {} {
+ uplevel 1 {
+ test "Check that slaves replicate from current master" {
+ set master_port [RI $master_id tcp_port]
+ foreach_redis_id id {
+ if {$id == $master_id} continue
+ wait_for_condition 1000 50 {
+ [RI $id master_port] == $master_port
+ } else {
+ fail "Redis slave $id is replicating from wrong master"
+ }
+ }
+ }
+ }
+}
+
+03_test_slaves_replication
+
+test "Crash the master and force a failover" {
+ set old_port [RI $master_id tcp_port]
+ set addr [S 0 SENTINEL GET-MASTER-ADDR-BY-NAME mymaster]
+ assert {[lindex $addr 1] == $old_port}
+ kill_instance redis $master_id
+ foreach_sentinel_id id {
+ wait_for_condition 1000 50 {
+ [lindex [S $id SENTINEL GET-MASTER-ADDR-BY-NAME mymaster] 1] != $old_port
+ } else {
+ fail "At least one Sentinel did not received failover info"
+ }
+ }
+ restart_instance redis $master_id
+ set addr [S 0 SENTINEL GET-MASTER-ADDR-BY-NAME mymaster]
+ set master_id [get_instance_id_by_port redis [lindex $addr 1]]
+}
+
+03_test_slaves_replication
View
1  tests/sentinel-tests/03-runtime-reconf.tcl
@@ -0,0 +1 @@
+# Test runtime reconfiguration command SENTINEL SET.
View
5 tests/sentinel-tests/04-slave-selection.tcl
@@ -0,0 +1,5 @@
+# Test slave selection algorithm.
+#
+# This unit should test:
+# 1) That when there are no suitable slaves no failover is performed.
+# 2) That among the available slaves, the one with better offset is picked.
View
67 tests/sentinel-tests/includes/init-tests.tcl
@@ -0,0 +1,67 @@
+# Initialization tests -- most units will start including this.
+
+test "(init) Restart killed instances" {
+ foreach type {redis sentinel} {
+ foreach_${type}_id id {
+ if {[get_instance_attrib $type $id pid] == -1} {
+ puts -nonewline "$type/$id "
+ flush stdout
+ restart_instance $type $id
+ }
+ }
+ }
+}
+
+set redis_slaves 4
+test "(init) Create a master-slaves cluster of [expr $redis_slaves+1] instances" {
+ create_redis_master_slave_cluster [expr {$redis_slaves+1}]
+}
+set master_id 0
+
+test "(init) Sentinels can start monitoring a master" {
+ set sentinels [llength $::sentinel_instances]
+ set quorum [expr {$sentinels/2+1}]
+ foreach_sentinel_id id {
+ catch {S $id SENTINEL REMOVE mymaster}
+ S $id SENTINEL MONITOR mymaster \
+ [get_instance_attrib redis $master_id host] \
+ [get_instance_attrib redis $master_id port] $quorum
+ }
+ foreach_sentinel_id id {
+ assert {[S $id sentinel master mymaster] ne {}}
+ S $id SENTINEL SET mymaster down-after-milliseconds 2000
+ S $id SENTINEL SET mymaster failover-timeout 20000
+ S $id SENTINEL SET mymaster parallel-syncs 10
+ }
+}
+
+test "(init) Sentinels can talk with the master" {
+ foreach_sentinel_id id {
+ wait_for_condition 100 50 {
+ [catch {S $id SENTINEL GET-MASTER-ADDR-BY-NAME mymaster}] == 0
+ } else {
+ fail "Sentinel $id can't talk with the master."
+ }
+ }
+}
+
+test "(init) Sentinels are able to auto-discover other sentinels" {
+ set sentinels [llength $::sentinel_instances]
+ foreach_sentinel_id id {
+ wait_for_condition 100 50 {
+ [dict get [S $id SENTINEL MASTER mymaster] num-other-sentinels] == ($sentinels-1)
+ } else {
+ fail "At least some sentinel can't detect some other sentinel"
+ }
+ }
+}
+
+test "(init) Sentinels are able to auto-discover slaves" {
+ foreach_sentinel_id id {
+ wait_for_condition 100 50 {
+ [dict get [S $id SENTINEL MASTER mymaster] num-slaves] == $redis_slaves
+ } else {
+ fail "At least some sentinel can't detect some slave"
+ }
+ }
+}
View
2  tests/sentinel-tmp/.gitignore
@@ -0,0 +1,2 @@
+redis_*
+sentinel_*
View
388 tests/sentinel.tcl
@@ -0,0 +1,388 @@
+# Sentinel test suite. Copyright (C) 2014 Salvatore Sanfilippo antirez@gmail.com
+# This softare is released under the BSD License. See the COPYING file for
+# more information.
+
+package require Tcl 8.5
+
+set tcl_precision 17
+source tests/support/redis.tcl
+source tests/support/util.tcl
+source tests/support/server.tcl
+source tests/support/test.tcl
+
+set ::verbose 0
+set ::pause_on_error 0
+set ::simulate_error 0
+set ::sentinel_instances {}
+set ::redis_instances {}
+set ::sentinel_base_port 20000
+set ::redis_base_port 30000
+set ::instances_count 5 ; # How many Sentinels / Instances we use at max
+set ::pids {} ; # We kill everything at exit
+set ::dirs {} ; # We remove all the temp dirs at exit
+set ::run_matching {} ; # If non empty, only tests matching pattern are run.
+
+if {[catch {cd tests/sentinel-tmp}]} {
+ puts "tests/sentinel-tmp directory not found."
+ puts "Please run this test from the Redis source root."
+ exit 1
+}
+
+# Spawn a redis or sentinel instance, depending on 'type'.
+proc spawn_instance {type base_port count} {
+ for {set j 0} {$j < $count} {incr j} {
+ set port [find_available_port $base_port]
+ incr base_port
+ puts "Starting $type #$j at port $port"
+
+ # Create a directory for this Sentinel.
+ set dirname "${type}_${j}"
+ lappend ::dirs $dirname
+ catch {exec rm -rf $dirname}
+ file mkdir $dirname
+
+ # Write the Sentinel config file.
+ set cfgfile [file join $dirname $type.conf]
+ set cfg [open $cfgfile w]
+ puts $cfg "port $port"
+ puts $cfg "dir ./$dirname"
+ puts $cfg "logfile log.txt"
+ close $cfg
+
+ # Finally exec it and remember the pid for later cleanup.
+ if {$type eq "redis"} {
+ set prgname redis-server
+ } else {
+ set prgname redis-sentinel
+ }
+ set pid [exec ../../src/${prgname} $cfgfile &]
+ lappend ::pids $pid
+
+ # Check availability
+ if {[server_is_up 127.0.0.1 $port 100] == 0} {
+ abort_sentinel_test "Problems starting $type #$j: ping timeout"
+ }
+
+ # Push the instance into the right list
+ lappend ::${type}_instances [list \
+ pid $pid \
+ host 127.0.0.1 \
+ port $port \
+ link [redis 127.0.0.1 $port] \
+ ]
+ }
+}
+
+proc cleanup {} {
+ puts "Cleaning up..."
+ foreach pid $::pids {
+ catch {exec kill -9 $pid}
+ }
+ foreach dir $::dirs {
+ catch {exec rm -rf $dir}
+ }
+}
+
+proc abort_sentinel_test msg {
+ puts "WARNING: Aborting the test."
+ puts ">>>>>>>> $msg"
+ cleanup
+ exit 1
+}
+
+proc parse_options {} {
+ for {set j 0} {$j < [llength $::argv]} {incr j} {
+ set opt [lindex $::argv $j]
+ set val [lindex $::argv [expr $j+1]]
+ if {$opt eq "--single"} {
+ incr j
+ set ::run_matching "*${val}*"
+ } elseif {$opt eq "--pause-on-error"} {
+ set ::pause_on_error 1
+ } elseif {$opt eq "--fail"} {
+ set ::simulate_error 1
+ } elseif {$opt eq "--help"} {
+ puts "Hello, I'm sentinel.tcl and I run Sentinel unit tests."
+ puts "\nOptions:"
+ puts "--single <pattern> Only runs tests specified by pattern."
+ puts "--pause-on-error Pause for manual inspection on error."
+ puts "--fail Simulate a test failure."
+ puts "--help Shows this help."
+ exit 0
+ } else {
+ puts "Unknown option $opt"
+ exit 1
+ }
+ }
+}
+
+proc main {} {
+ parse_options
+ spawn_instance sentinel $::sentinel_base_port $::instances_count
+ spawn_instance redis $::redis_base_port $::instances_count
+ run_tests
+ cleanup
+}
+
+# If --pause-on-error option was passed at startup this function is called
+# on error in order to give the developer a chance to understand more about
+# the error condition while the instances are still running.
+proc pause_on_error {} {
+ puts ""
+ puts [colorstr yellow "*** Please inspect the error now ***"]
+ puts "\nType \"continue\" to resume the test, \"help\" for help screen.\n"
+ while 1 {
+ puts -nonewline "> "
+ flush stdout
+ set line [gets stdin]
+ set argv [split $line " "]
+ set cmd [lindex $argv 0]
+ if {$cmd eq {continue}} {
+ break
+ } elseif {$cmd eq {show-sentinel-logs}} {
+ set count 10
+ if {[lindex $argv 1] ne {}} {set count [lindex $argv 1]}
+ foreach_sentinel_id id {
+ puts "=== SENTINEL $id ===="
+ puts [exec tail -$count sentinel_$id/log.txt]
+ puts "---------------------\n"
+ }
+ } elseif {$cmd eq {ls}} {
+ foreach_redis_id id {
+ puts -nonewline "Redis $id"
+ set errcode [catch {
+ set str {}
+ append str "@[RI $id tcp_port]: "
+ append str "[RI $id role] "
+ if {[RI $id role] eq {slave}} {
+ append str "[RI $id master_host]:[RI $id master_port]"
+ }
+ set str
+ } retval]
+ if {$errcode} {
+ puts " -- $retval"
+ } else {
+ puts $retval
+ }
+ }
+ foreach_sentinel_id id {
+ puts -nonewline "Sentinel $id"
+ set errcode [catch {
+ set str {}
+ append str "@[SI $id tcp_port]: "
+ append str "[join [S $id sentinel get-master-addr-by-name mymaster]]"
+ set str
+ } retval]
+ if {$errcode} {
+ puts " -- $retval"
+ } else {
+ puts $retval
+ }
+ }
+ } elseif {$cmd eq {help}} {
+ puts "ls List Sentinel and Redis instances."
+ puts "show-sentinel-logs \[N\] Show latest N lines of logs."
+ puts "S <id> cmd ... arg Call command in Sentinel <id>."
+ puts "R <id> cmd ... arg Call command in Redis <id>."
+ puts "SI <id> <field> Show Sentinel <id> INFO <field>."
+ puts "RI <id> <field> Show Sentinel <id> INFO <field>."
+ puts "continue Resume test."
+ } else {
+ set errcode [catch {eval $line} retval]
+ if {$retval ne {}} {puts "$retval"}
+ }
+ }
+}
+
+# We redefine 'test' as for Sentinel we don't use the server-client
+# architecture for the test, everything is sequential.
+proc test {descr code} {
+ set ts [clock format [clock seconds] -format %H:%M:%S]
+ puts -nonewline "$ts> $descr: "
+ flush stdout
+
+ if {[catch {set retval [uplevel 1 $code]} error]} {
+ if {[string match "assertion:*" $error]} {
+ set msg [string range $error 10 end]
+ puts [colorstr red $msg]
+ if {$::pause_on_error} pause_on_error
+ puts "(Jumping to next unit after error)"
+ return -code continue
+ } else {
+ # Re-raise, let handler up the stack take care of this.
+ error $error $::errorInfo
+ }
+ } else {
+ puts [colorstr green OK]
+ }
+}
+
+proc run_tests {} {
+ set tests [lsort [glob ../sentinel-tests/*]]
+ foreach test $tests {
+ if {$::run_matching ne {} && [string match $::run_matching $test] == 0} {
+ continue
+ }
+ if {[file isdirectory $test]} continue
+ puts [colorstr yellow "Testing unit: [lindex [file split $test] end]"]
+ source $test
+ }
+}
+
+# The "S" command is used to interact with the N-th Sentinel.
+# The general form is:
+#
+# S <sentinel-id> command arg arg arg ...
+#
+# Example to ping the Sentinel 0 (first instance): S 0 PING
+proc S {n args} {
+ set s [lindex $::sentinel_instances $n]
+ [dict get $s link] {*}$args
+}
+
+# Like R but to chat with Redis instances.
+proc R {n args} {
+ set r [lindex $::redis_instances $n]
+ [dict get $r link] {*}$args
+}
+
+proc get_info_field {info field} {
+ set fl [string length $field]
+ append field :
+ foreach line [split $info "\n"] {
+ set line [string trim $line "\r\n "]
+ if {[string range $line 0 $fl] eq $field} {
+ return [string range $line [expr {$fl+1}] end]
+ }
+ }
+ return {}
+}
+
+proc SI {n field} {
+ get_info_field [S $n info] $field
+}
+
+proc RI {n field} {
+ get_info_field [R $n info] $field
+}
+
+# Iterate over IDs of sentinel or redis instances.
+proc foreach_instance_id {instances idvar code} {
+ upvar 1 $idvar id
+ for {set id 0} {$id < [llength $instances]} {incr id} {
+ set errcode [catch {uplevel 1 $code} result]
+ if {$errcode == 1} {
+ error $result $::errorInfo $::errorCode
+ } elseif {$errcode == 4} {
+ continue
+ } elseif {$errcode != 0} {
+ return -code $errcode $result
+ }
+ }
+}
+
+proc foreach_sentinel_id {idvar code} {
+ set errcode [catch {uplevel 1 [list foreach_instance_id $::sentinel_instances $idvar $code]} result]
+ return -code $errcode $result
+}
+
+proc foreach_redis_id {idvar code} {
+ set errcode [catch {uplevel 1 [list foreach_instance_id $::redis_instances $idvar $code]} result]
+ return -code $errcode $result
+}
+
+# Get the specific attribute of the specified instance type, id.
+proc get_instance_attrib {type id attrib} {
+ dict get [lindex [set ::${type}_instances] $id] $attrib
+}
+
+# Set the specific attribute of the specified instance type, id.
+proc set_instance_attrib {type id attrib newval} {
+ set d [lindex [set ::${type}_instances] $id]
+ dict set d $attrib $newval
+ lset ::${type}_instances $id $d
+}
+
+# Create a master-slave cluster of the given number of total instances.
+# The first instance "0" is the master, all others are configured as
+# slaves.
+proc create_redis_master_slave_cluster n {
+ foreach_redis_id id {
+ if {$id == 0} {
+ # Our master.
+ R $id slaveof no one
+ R $id flushall
+ } elseif {$id < $n} {
+ R $id slaveof [get_instance_attrib redis 0 host] \
+ [get_instance_attrib redis 0 port]
+ } else {
+ # Instances not part of the cluster.
+ R $id slaveof no one
+ }
+ }
+ # Wait for all the slaves to sync.
+ wait_for_condition 1000 50 {
+ [RI 0 connected_slaves] == ($n-1)
+ } else {
+ fail "Unable to create a master-slaves cluster."
+ }
+}
+
+proc get_instance_id_by_port {type port} {
+ foreach_${type}_id id {
+ if {[get_instance_attrib $type $id port] == $port} {
+ return $id
+ }
+ }
+ fail "Instance $type port $port not found."
+}
+
+# Kill an instance of the specified type/id with SIGKILL.
+# This function will mark the instance PID as -1 to remember that this instance
+# is no longer running and will remove its PID from the list of pids that
+# we kill at cleanup.
+#
+# The instance can be restarted with restart-instance.
+proc kill_instance {type id} {
+ set pid [get_instance_attrib $type $id pid]
+ if {$pid == -1} {
+ error "You tried to kill $type $id twice."
+ }
+ exec kill -9 $pid
+ set_instance_attrib $type $id pid -1
+ set_instance_attrib $type $id link you_tried_to_talk_with_killed_instance
+
+ # Remove the PID from the list of pids to kill at exit.
+ set ::pids [lsearch -all -inline -not -exact $::pids $pid]
+}
+
+# Restart an instance previously killed by kill_instance
+proc restart_instance {type id} {
+ set dirname "${type}_${id}"
+ set cfgfile [file join $dirname $type.conf]
+ set port [get_instance_attrib $type $id port]
+
+ # Execute the instance with its old setup and append the new pid
+ # file for cleanup.
+ if {$type eq "redis"} {
+ set prgname redis-server
+ } else {
+ set prgname redis-sentinel
+ }
+ set pid [exec ../../src/${prgname} $cfgfile &]
+ set_instance_attrib $type $id pid $pid
+ lappend ::pids $pid
+
+ # Check that the instance is running
+ if {[server_is_up 127.0.0.1 $port 100] == 0} {
+ abort_sentinel_test "Problems starting $type #$j: ping timeout"
+ }
+
+ # Connect with it with a fresh link
+ set_instance_attrib $type $id link [redis 127.0.0.1 $port]
+}
+
+if {[catch main e]} {
+ puts $::errorInfo
+ cleanup
+}
View
30 tests/support/server.tcl
@@ -79,7 +79,7 @@ proc is_alive config {
proc ping_server {host port} {
set retval 0
if {[catch {
- set fd [socket $::host $::port]
+ set fd [socket $host $port]
fconfigure $fd -translation binary
puts $fd "PING\r\n"
flush $fd
@@ -101,6 +101,22 @@ proc ping_server {host port} {
return $retval
}
+# Return 1 if the server at the specified addr is reachable by PING, otherwise
+# returns 0. Performs a try every 50 milliseconds for the specified number
+# of retries.
+proc server_is_up {host port retrynum} {
+ after 10 ;# Use a small delay to make likely a first-try success.
+ set retval 0
+ while {[incr retrynum -1]} {
+ if {[catch {ping_server $host $port} ping]} {
+ set ping 0
+ }
+ if {$ping} {return 1}
+ after 50
+ }
+ return 0
+}
+
# doesn't really belong here, but highly coupled to code in start_server
proc tags {tags code} {
set ::tags [concat $::tags $tags]
@@ -191,23 +207,13 @@ proc start_server {options {code undefined}} {
# check that the server actually started
# ugly but tries to be as fast as possible...
if {$::valgrind} {set retrynum 1000} else {set retrynum 100}
- set serverisup 0
if {$::verbose} {
puts -nonewline "=== ($tags) Starting server ${::host}:${::port} "
}
- after 10
if {$code ne "undefined"} {
- while {[incr retrynum -1]} {
- catch {
- if {[ping_server $::host $::port]} {
- set serverisup 1
- }
- }
- if {$serverisup} break
- after 50
- }
+ set serverisup [server_is_up $::host $::port $retrynum]
} else {
set serverisup 1
}
View
40 tests/support/test.tcl
@@ -53,41 +53,17 @@ proc assert_type {type key} {
# executed.
proc wait_for_condition {maxtries delay e _else_ elsescript} {
while {[incr maxtries -1] >= 0} {
- if {[uplevel 1 [list expr $e]]} break
+ set errcode [catch {uplevel 1 [list expr $e]} result]
+ if {$errcode == 0} {
+ if {$result} break
+ } else {
+ return -code $errcode $result
+ }
after $delay
}
if {$maxtries == -1} {
- uplevel 1 $elsescript
- }
-}
-
-# Test if TERM looks like to support colors
-proc color_term {} {
- expr {[info exists ::env(TERM)] && [string match *xterm* $::env(TERM)]}
-}
-
-proc colorstr {color str} {
- if {[color_term]} {
- set b 0
- if {[string range $color 0 4] eq {bold-}} {
- set b 1
- set color [string range $color 5 end]
- }
- switch $color {
- red {set colorcode {31}}
- green {set colorcode {32}}
- yellow {set colorcode {33}}
- blue {set colorcode {34}}
- magenta {set colorcode {35}}
- cyan {set colorcode {36}}
- white {set colorcode {37}}
- default {set colorcode {37}}
- }
- if {$colorcode ne {}} {
- return "\033\[$b;${colorcode};40m$str\033\[0m"
- }
- } else {
- return $str
+ set errcode [catch [uplevel 1 $elsescript] result]
+ return -code $errcode $result
}
}
View
45 tests/support/util.tcl
@@ -312,3 +312,48 @@ proc csvstring s {
proc roundFloat f {
format "%.10g" $f
}
+
+proc find_available_port start {
+ for {set j $start} {$j < $start+1024} {incr j} {
+ if {[catch {
+ set fd [socket 127.0.0.1 $j]
+ }]} {
+ return $j
+ } else {
+ close $fd
+ }
+ }
+ if {$j == $start+1024} {
+ error "Can't find a non busy port in the $start-[expr {$start+1023}] range."
+ }
+}
+
+# Test if TERM looks like to support colors
+proc color_term {} {
+ expr {[info exists ::env(TERM)] && [string match *xterm* $::env(TERM)]}
+}
+
+proc colorstr {color str} {
+ if {[color_term]} {
+ set b 0
+ if {[string range $color 0 4] eq {bold-}} {
+ set b 1
+ set color [string range $color 5 end]
+ }
+ switch $color {
+ red {set colorcode {31}}
+ green {set colorcode {32}}
+ yellow {set colorcode {33}}
+ blue {set colorcode {34}}
+ magenta {set colorcode {35}}
+ cyan {set colorcode {36}}
+ white {set colorcode {37}}
+ default {set colorcode {37}}
+ }
+ if {$colorcode ne {}} {
+ return "\033\[$b;${colorcode};40m$str\033\[0m"
+ }
+ } else {
+ return $str
+ }
+}
View
15 tests/test_helper.tcl
@@ -164,21 +164,6 @@ proc cleanup {} {
if {!$::quiet} {puts "OK"}
}
-proc find_available_port start {
- for {set j $start} {$j < $start+1024} {incr j} {
- if {[catch {
- set fd [socket 127.0.0.1 $j]
- }]} {
- return $j
- } else {
- close $fd
- }
- }
- if {$j == $start+1024} {
- error "Can't find a non busy port in the $start-[expr {$start+1023}] range."
- }
-}
-
proc test_server_main {} {
cleanup
set tclsh [info nameofexecutable]
View
163 tests/unit/bitops.tcl
@@ -52,7 +52,7 @@ start_server {tags {"bitops"}} {
}
}
- test {BITCOUNT fuzzing} {
+ test {BITCOUNT fuzzing without start/end} {
for {set j 0} {$j < 100} {incr j} {
set str [randstring 0 3000]
r set str $str
@@ -60,6 +60,20 @@ start_server {tags {"bitops"}} {
}
}
+ test {BITCOUNT fuzzing with start/end} {
+ for {set j 0} {$j < 100} {incr j} {
+ set str [randstring 0 3000]
+ r set str $str
+ set l [string length $str]
+ set start [randomInt $l]
+ set end [randomInt $l]
+ if {$start > $end} {
+ lassign [list $end $start] start end
+ }
+ assert {[r bitcount str $start $end] == [count_bits [string range $str $start $end]]}
+ }
+ }
+
test {BITCOUNT with start, end} {
r set s "foobar"
assert_equal [r bitcount s 0 -1] [count_bits "foobar"]
@@ -84,6 +98,18 @@ start_server {tags {"bitops"}} {
}
} {1}
+ test {BITCOUNT misaligned prefix} {
+ r del str
+ r set str ab
+ r bitcount str 1 -1
+ } {3}
+
+ test {BITCOUNT misaligned prefix + full words + remainder} {
+ r del str
+ r set str __PPxxxxxxxxxxxxxxxxRR__
+ r bitcount str 2 -3
+ } {74}
+
test {BITOP NOT (empty string)} {
r set s ""
r bitop not dest s
@@ -177,4 +203,139 @@ start_server {tags {"bitops"}} {
r set a "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
r bitop or x a b
} {32}
+
+ test {BITPOS bit=0 with empty key returns 0} {
+ r del str
+ r bitpos str 0
+ } {0}
+
+ test {BITPOS bit=1 with empty key returns -1} {
+ r del str
+ r bitpos str 1
+ } {-1}
+
+ test {BITPOS bit=0 with string less than 1 word works} {
+ r set str "\xff\xf0\x00"
+ r bitpos str 0
+ } {12}
+
+ test {BITPOS bit=1 with string less than 1 word works} {
+ r set str "\x00\x0f\x00"
+ r bitpos str 1
+ } {12}
+
+ test {BITPOS bit=0 starting at unaligned address} {
+ r set str "\xff\xf0\x00"
+ r bitpos str 0 1
+ } {12}
+
+ test {BITPOS bit=1 starting at unaligned address} {
+ r set str "\x00\x0f\xff"
+ r bitpos str 1 1
+ } {12}
+
+ test {BITPOS bit=0 unaligned+full word+reminder} {
+ r del str
+ r set str "\xff\xff\xff" ; # Prefix
+ # Followed by two (or four in 32 bit systems) full words
+ r append str "\xff\xff\xff\xff\xff\xff\xff\xff"
+ r append str "\xff\xff\xff\xff\xff\xff\xff\xff"
+ r append str "\xff\xff\xff\xff\xff\xff\xff\xff"
+ # First zero bit.
+ r append str "\x0f"
+ assert {[r bitpos str 0] == 216}
+ assert {[r bitpos str 0 1] == 216}
+ assert {[r bitpos str 0 2] == 216}
+ assert {[r bitpos str 0 3] == 216}
+ assert {[r bitpos str 0 4] == 216}
+ assert {[r bitpos str 0 5] == 216}
+ assert {[r bitpos str 0 6] == 216}
+ assert {[r bitpos str 0 7] == 216}
+ assert {[r bitpos str 0 8] == 216}
+ }
+
+ test {BITPOS bit=1 unaligned+full word+reminder} {
+ r del str
+ r set str "\x00\x00\x00" ; # Prefix
+ # Followed by two (or four in 32 bit systems) full words
+ r append str "\x00\x00\x00\x00\x00\x00\x00\x00"
+ r append str "\x00\x00\x00\x00\x00\x00\x00\x00"
+ r append str "\x00\x00\x00\x00\x00\x00\x00\x00"
+ # First zero bit.
+ r append str "\xf0"
+ assert {[r bitpos str 1] == 216}
+ assert {[r bitpos str 1 1] == 216}
+ assert {[r bitpos str 1 2] == 216}
+ assert {[r bitpos str 1 3] == 216}
+ assert {[r bitpos str 1 4] == 216}
+ assert {[r bitpos str 1 5] == 216}
+ assert {[r bitpos str 1 6] == 216}
+ assert {[r bitpos str 1 7] == 216}
+ assert {[r bitpos str 1 8] == 216}
+ }
+
+ test {BITPOS bit=1 returns -1 if string is all 0 bits} {
+ r set str ""
+ for {set j 0} {$j < 20} {incr j} {
+ assert {[r bitpos str 1] == -1}
+ r append str "\x00"
+ }
+ }
+
+ test {BITPOS bit=0 works with intervals} {
+ r set str "\x00\xff\x00"
+ assert {[r bitpos str 0 0 -1] == 0}
+ assert {[r bitpos str 0 1 -1] == 16}
+ assert {[r bitpos str 0 2 -1] == 16}
+ assert {[r bitpos str 0 2 200] == 16}
+ assert {[r bitpos str 0 1 1] == -1}
+ }
+
+ test {BITPOS bit=1 works with intervals} {
+ r set str "\x00\xff\x00"
+ assert {[r bitpos str 1 0 -1] == 8}
+ assert {[r bitpos str 1 1 -1] == 8}
+ assert {[r bitpos str 1 2 -1] == -1}
+ assert {[r bitpos str 1 2 200] == -1}
+ assert {[r bitpos str 1 1 1] == 8}
+ }
+
+ test {BITPOS bit=0 changes behavior if end is given} {
+ r set str "\xff\xff\xff"
+ assert {[r bitpos str 0] == 24}
+ assert {[r bitpos str 0 0] == 24}
+ assert {[r bitpos str 0 0 -1] == -1}
+ }
+
+ test {BITPOS bit=1 fuzzy testing using SETBIT} {
+ r del str
+ set max 524288; # 64k
+ set first_one_pos -1
+ for {set j 0} {$j < 1000} {incr j} {
+ assert {[r bitpos str 1] == $first_one_pos}
+ set pos [randomInt $max]
+ r setbit str $pos 1
+ if {$first_one_pos == -1 || $first_one_pos > $pos} {
+ # Update the position of the first 1 bit in the array
+ # if the bit we set is on the left of the previous one.
+ set first_one_pos $pos
+ }
+ }
+ }
+
+ test {BITPOS bit=0 fuzzy testing using SETBIT} {
+ set max 524288; # 64k
+ set first_zero_pos $max
+ r set str [string repeat "\xff" [expr $max/8]]
+ for {set j 0} {$j < 1000} {incr j} {
+ assert {[r bitpos str 0] == $first_zero_pos}
+ set pos [randomInt $max]
+ r setbit str $pos 0
+ if {$first_zero_pos > $pos} {
+ # Update the position of the first 0 bit in the array
+ # if the bit we clear is on the left of the previous one.
+ set first_zero_pos $pos
+ }
+ }
+ }
}