diff --git a/00-RELEASENOTES b/00-RELEASENOTES index 33adfac6..bffd7a12 100644 --- a/00-RELEASENOTES +++ b/00-RELEASENOTES @@ -10,9 +10,45 @@ 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 3.0.3 ] Release date: 17 Jul 2015 + +Upgrade urgency: LOW for Redis and Sentinel. + +* [FIX] Fix blocking operations timeout precision when HZ is at its default + value (not increased) and there are thousands of clients connected + at the same time. This bug affected Sidekiq users that experienced + a very long delay for BLPOP and similar commands to return for + timeout. Check commit b029ff1 for more info. (Salvatore Sanfilippo) +* [FIX] MIGRATE "creating socket: Invalid argument" error fix. Check + issues #2609 and #2612 for more info. (Salvatore Sanfilippo) +* [FIX] Be able to connect to the master even when the slave is bound to + just the loopback interface and has no valid public address in the + network the master is reacahble. (Salvatore Sanfilippo) +* [FIX] ZADD with options encoding promotion fixed. (linfangrong) +* [FIX] Reset aof_delayed_fsync on CONFIG RESETSTATS. (Tom Kiemes) +* [FIX] PFCOUNT key parsing in cluster fixed. (MOON_CLJ) +* [FIX] Fix Solaris compilation of Redis 3.0. (Jan-Erik Rediger) + +* [NEW] Variadic EXISTS command. Now the command accepts multiple arguments + and returns the total count of existing keys. + +--[ Redis 3.0.2 ] Release date: 4 Jun 2015 + +Upgrade urgency: HIGH for Redis because of a security issue. + LOW for Sentinel. + +* [FIX] Critical security issue fix by Ben Murphy: http://t.co/LpGTyZmfS7 +* [FIX] SMOVE reply fixed when src and dst keys are the same. (Glenn Nethercutt) +* [FIX] Lua cmsgpack lib updated to support str8 type. (Sebastian Waisbrot) + +* [NEW] ZADD support for options: NX, XX, CH. See new doc at redis.io. + (Salvatore Sanfilippo) +* [NEW] Senitnel: CKQUORUM and FLUSHCONFIG commands back ported. + (Salvatore Sanfilippo and Bill Anderson) + --[ Redis 3.0.1 ] Release date: 5 May 2015 -Upgrade urgency: LOW for Redis, Sentinel, Cluster. +Upgrade urgency: LOW for Redis and Cluster, MODERATE for Sentinel. * [FIX] Sentinel memory leak due to hiredis fixed. (Salvatore Sanfilippo) * [FIX] Sentinel memory leak on duplicated instance. (Charsyam) @@ -32,7 +68,7 @@ Upgrade urgency: LOW for Redis, Sentinel, Cluster. * WAIT command to block waiting for a write to be transmitted to the specified number of slaves. * MIGRATE connection caching. Much faster keys migraitons. -* MIGARTE new options COPY and REPLACE. +* MIGRATE new options COPY and REPLACE. * CLIENT PAUSE command: stop processing client requests for a specified amount of time. * BITCOUNT performance improvements. @@ -594,6 +630,11 @@ non-backward compatible changes introduced in the 3.0 release: '?' is actually the role of the instance. M for master, S for slave, C if this process is a saving child (for RDB/AOF), and X for Sentinel. +* The default maxmemory policy in Redis 3.0 is no longer "volatile-lru" as + it used to be in 2.8, but "noeviction". The policies behavior is the same + (but LRU eviction is much more precise in 3.0), so only the default value + changed. Just make sure to specify in your redis.conf what you mean. + -------------------------------------------------------------------------------- Credits: Where not specified the implementation and design is done by diff --git a/src/anet.c b/src/anet.c index 94d67f4f..e1873df4 100644 --- a/src/anet.c +++ b/src/anet.c @@ -291,7 +291,7 @@ static int anetCreateSocket(char *err, int domain) { #define ANET_CONNECT_NONE 0 #define ANET_CONNECT_NONBLOCK 1 - +#define ANET_CONNECT_BE_BINDING 2 /* Best effort binding. */ #ifdef _WIN32 static int anetTcpGenericConnect(char *err, char *addr, int port, char *source_addr, int flags) { int fd; @@ -349,7 +349,7 @@ static int anetTcpGenericConnect(char *err, char *addr, int port, if ((rv = getaddrinfo(source_addr, NULL, &hints, &bservinfo)) != 0) { anetSetError(err, "%s", gai_strerror(rv)); - goto end; + goto error; } for (b = bservinfo; b != NULL; b = b->ai_next) { if (bind(s,b->ai_addr,b->ai_addrlen) != -1) { @@ -360,7 +360,7 @@ static int anetTcpGenericConnect(char *err, char *addr, int port, freeaddrinfo(bservinfo); if (!bound) { anetSetError(err, "bind: %s", strerror(errno)); - goto end; + goto error; } } if (connect(s,p->ai_addr,p->ai_addrlen) == -1) { @@ -385,9 +385,17 @@ error: close(s); s = ANET_ERR; } + end: freeaddrinfo(servinfo); - return s; + + /* Handle best effort binding: if a binding address was used, but it is + * not possible to create a socket, try again without a binding address. */ + if (s == ANET_ERR && source_addr && (flags & ANET_CONNECT_BE_BINDING)) { + return anetTcpGenericConnect(err,addr,port,NULL,flags); + } else { + return s; + } } #endif @@ -401,9 +409,18 @@ int anetTcpNonBlockConnect(char *err, char *addr, int port) return anetTcpGenericConnect(err,addr,port,NULL,ANET_CONNECT_NONBLOCK); } -int anetTcpNonBlockBindConnect(char *err, char *addr, int port, char *source_addr) +int anetTcpNonBlockBindConnect(char *err, char *addr, int port, + char *source_addr) { - return anetTcpGenericConnect(err,addr,port,source_addr,ANET_CONNECT_NONBLOCK); + return anetTcpGenericConnect(err,addr,port,source_addr, + ANET_CONNECT_NONBLOCK); +} + +int anetTcpNonBlockBestEffortBindConnect(char *err, char *addr, int port, + char *source_addr) +{ + return anetTcpGenericConnect(err,addr,port,source_addr, + ANET_CONNECT_NONBLOCK|ANET_CONNECT_BE_BINDING); } int anetUnixGenericConnect(char *err, char *path, int flags) diff --git a/src/anet.h b/src/anet.h index dfea9d1f..28391244 100644 --- a/src/anet.h +++ b/src/anet.h @@ -50,6 +50,7 @@ int anetTcpConnect(char *err, char *addr, int port); int anetTcpNonBlockConnect(char *err, char *addr, int port); int anetTcpNonBlockBindConnect(char *err, char *addr, int port, char *source_addr); +int anetTcpNonBlockBestEffortBindConnect(char *err, char *addr, int port, char *source_addr); int anetUnixConnect(char *err, char *path); int anetUnixNonBlockConnect(char *err, char *path); int anetRead(int fd, char *buf, int count); diff --git a/src/aof.c b/src/aof.c index 205bb024..a44a06f6 100644 --- a/src/aof.c +++ b/src/aof.c @@ -1182,9 +1182,9 @@ int rewriteAppendOnlyFile(char *filename) { return REDIS_OK; werr: + redisLog(REDIS_WARNING,"Write error writing append only file on disk: %s", strerror(errno)); fclose(fp); unlink(tmpfile); - redisLog(REDIS_WARNING,"Write error writing append only file on disk: %s", strerror(errno)); if (di) dictReleaseIterator(di); return REDIS_ERR; } @@ -1616,4 +1616,4 @@ void aofProcessDiffRewriteEvents(aeEventLoop* eventLoop) aofChildPipeReadable(eventLoop, server.aof_pipe_read_ack_from_child, NULL, 0); } } -} \ No newline at end of file +} diff --git a/src/cluster.c b/src/cluster.c index a36f4614..261c98fe 100644 --- a/src/cluster.c +++ b/src/cluster.c @@ -358,6 +358,11 @@ void clusterSaveConfigOrDie(int do_fsync) { * On success REDIS_OK is returned, otherwise an error is logged and * the function returns REDIS_ERR to signal a lock was not acquired. */ int clusterLockConfig(char *filename) { +/* flock() does not exist on Solaris + * and a fcntl-based solution won't help, as we constantly re-open that file, + * which will release _all_ locks anyway + */ +#if !defined(__sun) /* To lock it, we need to open the file in a way it is created if * it does not exist, otherwise there is a race condition with other * processes. */ @@ -400,6 +405,8 @@ int clusterLockConfig(char *filename) { } /* Lock acquired: leak the 'fd' by not closing it, so that we'll retain the * lock to the file as long as the process exists. */ +#endif /* __sun */ + return REDIS_OK; } @@ -4505,8 +4512,8 @@ migrateCachedSocket* migrateGetSocket(redisClient *c, robj *host, robj *port, PO } /* Create the socket */ - fd = anetTcpNonBlockBindConnect(server.neterr,c->argv[1]->ptr, - atoi(c->argv[2]->ptr),REDIS_BIND_ADDR); + fd = anetTcpNonBlockConnect(server.neterr,c->argv[1]->ptr, + atoi(c->argv[2]->ptr)); if (fd == -1) { sdsfree(name); addReplyErrorFormat(c,"Can't connect to target node: %s", diff --git a/src/db.c b/src/db.c index dc6041ad..c62d57e5 100644 --- a/src/db.c +++ b/src/db.c @@ -297,13 +297,17 @@ void delCommand(redisClient *c) { addReplyLongLong(c,deleted); } +/* EXISTS key1 key2 ... key_N. + * Return value is the number of keys existing. */ void existsCommand(redisClient *c) { - expireIfNeeded(c->db,c->argv[1]); - if (dbExists(c->db,c->argv[1])) { - addReply(c, shared.cone); - } else { - addReply(c, shared.czero); + PORT_LONGLONG count = 0; + int j; + + for (j = 1; j < c->argc; j++) { + expireIfNeeded(c->db,c->argv[j]); + if (dbExists(c->db,c->argv[j])) count++; } + addReplyLongLong(c,count); } void selectCommand(redisClient *c) { diff --git a/src/networking.c b/src/networking.c index 08f08bcd..5328f9f0 100644 --- a/src/networking.c +++ b/src/networking.c @@ -1104,7 +1104,7 @@ int processInlineBuffer(redisClient *c) { /* Helper function. Trims query buffer to make the function that processes * multi bulk requests idempotent. */ static void setProtocolError(redisClient *c, int pos) { - if (server.verbosity >= REDIS_VERBOSE) { + if (server.verbosity <= REDIS_VERBOSE) { sds client = catClientInfoString(sdsempty(),c); redisLog(REDIS_VERBOSE, "Protocol error from client: %s", client); diff --git a/src/rdb.c b/src/rdb.c index e75fd67f..a9fe73c1 100644 --- a/src/rdb.c +++ b/src/rdb.c @@ -778,9 +778,9 @@ int rdbSave(char *filename) { return REDIS_OK; werr: + redisLog(REDIS_WARNING,"Write error saving DB on disk: %s", strerror(errno)); fclose(fp); unlink(tmpfile); - redisLog(REDIS_WARNING,"Write error saving DB on disk: %s", strerror(errno)); return REDIS_ERR; } diff --git a/src/redis.c b/src/redis.c index 60ab5bad..81b8e4a3 100644 --- a/src/redis.c +++ b/src/redis.c @@ -136,7 +136,7 @@ struct redisCommand redisCommandTable[] = { {"append",appendCommand,3,"wm",0,NULL,1,1,1,0,0}, {"strlen",strlenCommand,2,"rF",0,NULL,1,1,1,0,0}, {"del",delCommand,-2,"w",0,NULL,1,-1,1,0,0}, - {"exists",existsCommand,2,"rF",0,NULL,1,1,1,0,0}, + {"exists",existsCommand,-2,"rF",0,NULL,1,-1,1,0,0}, {"setbit",setbitCommand,4,"wm",0,NULL,1,1,1,0,0}, {"getbit",getbitCommand,3,"rF",0,NULL,1,1,1,0,0}, {"setrange",setrangeCommand,4,"wm",0,NULL,1,1,1,0,0}, @@ -287,7 +287,7 @@ struct redisCommand redisCommandTable[] = { {"command",commandCommand,0,"rlt",0,NULL,0,0,0,0,0}, {"pfselftest",pfselftestCommand,1,"r",0,NULL,0,0,0,0,0}, {"pfadd",pfaddCommand,-2,"wmF",0,NULL,1,1,1,0,0}, - {"pfcount",pfcountCommand,-2,"r",0,NULL,1,1,1,0,0}, + {"pfcount",pfcountCommand,-2,"r",0,NULL,1,-1,1,0,0}, {"pfmerge",pfmergeCommand,-2,"wm",0,NULL,1,-1,1,0,0}, {"pfdebug",pfdebugCommand,-3,"w",0,NULL,0,0,0,0,0}, {"latency",latencyCommand,-2,"arslt",0,NULL,0,0,0,0,0} @@ -911,9 +911,12 @@ PORT_LONGLONG getInstantaneousMetric(int metric) { return sum / REDIS_METRIC_SAMPLES; } -/* Check for timeouts. Returns non-zero if the client was terminated */ -int clientsCronHandleTimeout(redisClient *c) { - time_t now = server.unixtime; +/* Check for timeouts. Returns non-zero if the client was terminated. + * The function gets the current time in milliseconds as argument since + * it gets called multiple times in a loop, so calling gettimeofday() for + * each iteration would be costly without any actual gain. */ +int clientsCronHandleTimeout(redisClient *c, mstime_t now_ms) { + time_t now = now_ms/1000; if (server.maxidletime && !(c->flags & REDIS_SLAVE) && /* no timeout for slaves */ @@ -929,7 +932,6 @@ int clientsCronHandleTimeout(redisClient *c) { /* Blocked OPS timeout is handled with milliseconds resolution. * However note that the actual resolution is limited by * server.hz. */ - mstime_t now_ms = mstime(); if (c->bpop.timeout != 0 && c->bpop.timeout < now_ms) { /* Handle blocking operation specific timeout. */ @@ -971,17 +973,23 @@ int clientsCronResizeQueryBuffer(redisClient *c) { return 0; } +#define CLIENTS_CRON_MIN_ITERATIONS 5 void clientsCron(void) { - /* Make sure to process at least 1/(server.hz*10) of clients per call. - * Since this function is called server.hz times per second we are sure that - * in the worst case we process all the clients in 10 seconds. - * In normal conditions (a reasonable number of clients) we process - * all the clients in a shorter time. */ + /* Make sure to process at least numclients/server.hz of clients + * per call. Since this function is called server.hz times per second + * we are sure that in the worst case we process all the clients in 1 + * second. */ int numclients = (int)listLength(server.clients); WIN_PORT_FIX /* cast (int) */ - int iterations = numclients/(server.hz*10); + int iterations = numclients/server.hz; + mstime_t now = mstime(); + + /* Process at least a few clients while we are at it, even if we need + * to process less than CLIENTS_CRON_MIN_ITERATIONS to meet our contract + * of processing each client once per second. */ + if (iterations < CLIENTS_CRON_MIN_ITERATIONS) + iterations = (numclients < CLIENTS_CRON_MIN_ITERATIONS) ? + numclients : CLIENTS_CRON_MIN_ITERATIONS; - if (iterations < 50) - iterations = (numclients < 50) ? numclients : 50; while(listLength(server.clients) && iterations--) { redisClient *c; listNode *head; @@ -995,7 +1003,7 @@ void clientsCron(void) { /* The following functions do different service checks on the client. * The protocol is that they return non-zero if the client was * terminated. */ - if (clientsCronHandleTimeout(c)) continue; + if (clientsCronHandleTimeout(c,now)) continue; if (clientsCronResizeQueryBuffer(c)) continue; } } @@ -1778,6 +1786,7 @@ void resetServerStats(void) { } server.stat_net_input_bytes = 0; server.stat_net_output_bytes = 0; + server.aof_delayed_fsync = 0; } void initServer(void) { diff --git a/src/replication.c b/src/replication.c index 28060b35..abef922d 100644 --- a/src/replication.c +++ b/src/replication.c @@ -1517,7 +1517,7 @@ error: int connectWithMaster(void) { int fd; - fd = anetTcpNonBlockBindConnect(NULL, + fd = anetTcpNonBlockBestEffortBindConnect(NULL, server.masterhost,server.masterport,REDIS_BIND_ADDR); if (fd == -1) { redisLog(REDIS_WARNING,"Unable to connect to MASTER: %s", diff --git a/src/scripting.c b/src/scripting.c index 46d3e201..6e6d451b 100644 --- a/src/scripting.c +++ b/src/scripting.c @@ -615,11 +615,12 @@ void scriptingEnableGlobalsProtection(lua_State *lua) { /* strict.lua from: http://metalua.luaforge.net/src/lib/strict.lua.html. * Modified to be adapted to Redis. */ + s[j++]="local dbg=debug\n"; s[j++]="local mt = {}\n"; s[j++]="setmetatable(_G, mt)\n"; s[j++]="mt.__newindex = function (t, n, v)\n"; - s[j++]=" if debug.getinfo(2) then\n"; - s[j++]=" local w = debug.getinfo(2, \"S\").what\n"; + s[j++]=" if dbg.getinfo(2) then\n"; + s[j++]=" local w = dbg.getinfo(2, \"S\").what\n"; s[j++]=" if w ~= \"main\" and w ~= \"C\" then\n"; s[j++]=" error(\"Script attempted to create global variable '\"..tostring(n)..\"'\", 2)\n"; s[j++]=" end\n"; @@ -627,11 +628,12 @@ void scriptingEnableGlobalsProtection(lua_State *lua) { s[j++]=" rawset(t, n, v)\n"; s[j++]="end\n"; s[j++]="mt.__index = function (t, n)\n"; - s[j++]=" if debug.getinfo(2) and debug.getinfo(2, \"S\").what ~= \"C\" then\n"; + s[j++]=" if dbg.getinfo(2) and dbg.getinfo(2, \"S\").what ~= \"C\" then\n"; s[j++]=" error(\"Script attempted to access unexisting global variable '\"..tostring(n)..\"'\", 2)\n"; s[j++]=" end\n"; s[j++]=" return rawget(t, n)\n"; s[j++]="end\n"; + s[j++]="debug = nil\n"; s[j++]=NULL; for (j = 0; s[j] != NULL; j++) code = sdscatlen(code,s[j],strlen(s[j])); @@ -735,10 +737,11 @@ void scriptingInit(void) { * information about the caller, that's what makes sense from the point * of view of the user debugging a script. */ { - char *errh_func = "function __redis__err__handler(err)\n" - " local i = debug.getinfo(2,'nSl')\n" + char *errh_func = "local dbg = debug\n" + "function __redis__err__handler(err)\n" + " local i = dbg.getinfo(2,'nSl')\n" " if i and i.what == 'C' then\n" - " i = debug.getinfo(3,'nSl')\n" + " i = dbg.getinfo(3,'nSl')\n" " end\n" " if i then\n" " return i.source .. ':' .. i.currentline .. ': ' .. err\n" diff --git a/src/sentinel.c b/src/sentinel.c index ae8f55a4..3dee84e3 100644 --- a/src/sentinel.c +++ b/src/sentinel.c @@ -2744,6 +2744,31 @@ sentinelRedisInstance *sentinelGetMasterByNameOrReplyError(redisClient *c, return ri; } +#define SENTINEL_ISQR_OK 0 +#define SENTINEL_ISQR_NOQUORUM (1<<0) +#define SENTINEL_ISQR_NOAUTH (1<<1) +int sentinelIsQuorumReachable(sentinelRedisInstance *master, int *usableptr) { + dictIterator *di; + dictEntry *de; + int usable = 1; /* Number of usable Sentinels. Init to 1 to count myself. */ + int result = SENTINEL_ISQR_OK; + int voters = dictSize(master->sentinels)+1; /* Known Sentinels + myself. */ + + di = dictGetIterator(master->sentinels); + while((de = dictNext(di)) != NULL) { + sentinelRedisInstance *ri = dictGetVal(de); + + if (ri->flags & (SRI_S_DOWN|SRI_O_DOWN)) continue; + usable++; + } + dictReleaseIterator(di); + + if (usable < (int)master->quorum) result |= SENTINEL_ISQR_NOQUORUM; + if (usable < voters/2+1) result |= SENTINEL_ISQR_NOAUTH; + if (usableptr) *usableptr = usable; + return result; +} + void sentinelCommand(redisClient *c) { if (!strcasecmp(c->argv[1]->ptr,"masters")) { /* SENTINEL MASTERS */ @@ -2894,6 +2919,10 @@ void sentinelCommand(redisClient *c) { sentinelEvent(REDIS_WARNING,"+monitor",ri,"%@ quorum %d",ri->quorum); addReply(c,shared.ok); } + } else if (!strcasecmp(c->argv[1]->ptr,"flushconfig")) { + sentinelFlushConfig(); + addReply(c,shared.ok); + return; } else if (!strcasecmp(c->argv[1]->ptr,"remove")) { /* SENTINEL REMOVE */ sentinelRedisInstance *ri; @@ -2904,6 +2933,32 @@ void sentinelCommand(redisClient *c) { dictDelete(sentinel.masters,c->argv[2]->ptr); sentinelFlushConfig(); addReply(c,shared.ok); + } else if (!strcasecmp(c->argv[1]->ptr,"ckquorum")) { + /* SENTINEL CKQUORUM */ + sentinelRedisInstance *ri; + int usable; + + if ((ri = sentinelGetMasterByNameOrReplyError(c,c->argv[2])) + == NULL) return; + int result = sentinelIsQuorumReachable(ri,&usable); + if (result == SENTINEL_ISQR_OK) { + addReplySds(c, sdscatfmt(sdsempty(), + "+OK %i usable Sentinels. Quorum and failover authorization " + "can be reached\r\n",usable)); + } else { + sds e = sdscatfmt(sdsempty(), + "-NOQUORUM %i usable Sentinels. ",usable); + if (result & SENTINEL_ISQR_NOQUORUM) + e = sdscat(e,"Not enough available Sentinels to reach the" + " specified quorum for this master"); + if (result & SENTINEL_ISQR_NOAUTH) { + if (result & SENTINEL_ISQR_NOQUORUM) e = sdscat(e,". "); + e = sdscat(e, "Not enough available Sentinels to reach the" + " majority and authorize a failover"); + } + e = sdscat(e,"\r\n"); + addReplySds(c,e); + } } else if (!strcasecmp(c->argv[1]->ptr,"set")) { if (c->argc < 3 || c->argc % 2 == 0) goto numargserr; sentinelSetCommand(c); diff --git a/src/sort.c b/src/sort.c index e71b2e9c..c981bf1e 100644 --- a/src/sort.c +++ b/src/sort.c @@ -209,7 +209,7 @@ void sortCommand(redisClient *c) { } /* Create a list of operations to perform for every sorted element. - * Operations can be GET/DEL/INCR/DECR */ + * Operations can be GET */ operations = listCreate(); listSetFreeMethod(operations,zfree); j = 2; /* options start at argv[2] */ diff --git a/src/t_set.c b/src/t_set.c index c404cf55..c6a00fd8 100644 --- a/src/t_set.c +++ b/src/t_set.c @@ -321,7 +321,7 @@ void smoveCommand(redisClient *c) { /* If srcset and dstset are equal, SMOVE is a no-op */ if (srcset == dstset) { - addReply(c,shared.cone); + addReply(c,setTypeIsMember(srcset,ele) ? shared.cone : shared.czero); return; } diff --git a/src/t_zset.c b/src/t_zset.c index 688bbb27..e83a2ce3 100644 --- a/src/t_zset.c +++ b/src/t_zset.c @@ -1175,35 +1175,84 @@ void zsetConvert(robj *zobj, int encoding) { *----------------------------------------------------------------------------*/ /* This generic command implements both ZADD and ZINCRBY. */ -void zaddGenericCommand(redisClient *c, int incr) { +#define ZADD_NONE 0 +#define ZADD_INCR (1<<0) /* Increment the score instead of setting it. */ +#define ZADD_NX (1<<1) /* Don't touch elements not already existing. */ +#define ZADD_XX (1<<2) /* Only touch elements already exisitng. */ +#define ZADD_CH (1<<3) /* Return num of elements added or updated. */ +void zaddGenericCommand(redisClient *c, int flags) { static char *nanerr = "resulting score is not a number (NaN)"; robj *key = c->argv[1]; robj *ele; robj *zobj; robj *curobj; double score = 0, *scores = NULL, curscore = 0.0; - int j, elements = (c->argc-2)/2; - int added = 0, updated = 0; + int j, elements; + int scoreidx = 0; + /* The following vars are used in order to track what the command actually + * did during the execution, to reply to the client and to trigger the + * notification of keyspace change. */ + int added = 0; /* Number of new elements added. */ + int updated = 0; /* Number of elements with updated score. */ + int processed = 0; /* Number of elements processed, may remain zero with + options like XX. */ - if (c->argc % 2) { + /* Parse options. At the end 'scoreidx' is set to the argument position + * of the score of the first score-element pair. */ + scoreidx = 2; + while(scoreidx < c->argc) { + char *opt = c->argv[scoreidx]->ptr; + if (!strcasecmp(opt,"nx")) flags |= ZADD_NX; + else if (!strcasecmp(opt,"xx")) flags |= ZADD_XX; + else if (!strcasecmp(opt,"ch")) flags |= ZADD_CH; + else if (!strcasecmp(opt,"incr")) flags |= ZADD_INCR; + else break; + scoreidx++; + } + + /* Turn options into simple to check vars. */ + int incr = (flags & ZADD_INCR) != 0; + int nx = (flags & ZADD_NX) != 0; + int xx = (flags & ZADD_XX) != 0; + int ch = (flags & ZADD_CH) != 0; + + /* After the options, we expect to have an even number of args, since + * we expect any number of score-element pairs. */ + elements = c->argc-scoreidx; + if (elements % 2) { addReply(c,shared.syntaxerr); return; } + elements /= 2; /* Now this holds the number of score-element pairs. */ + + /* Check for incompatible options. */ + if (nx && xx) { + addReplyError(c, + "XX and NX options at the same time are not compatible"); + return; + } + + if (incr && elements > 1) { + addReplyError(c, + "INCR option supports a single increment-element pair"); + return; + } /* Start parsing all the scores, we need to emit any syntax error * before executing additions to the sorted set, as the command should * either execute fully or nothing at all. */ scores = zmalloc(sizeof(double)*elements); for (j = 0; j < elements; j++) { - if (getDoubleFromObjectOrReply(c,c->argv[2+j*2],&scores[j],NULL) + if (getDoubleFromObjectOrReply(c,c->argv[scoreidx+j*2],&scores[j],NULL) != REDIS_OK) goto cleanup; } /* Lookup the key and create the sorted set if does not exist. */ zobj = lookupKeyWrite(c->db,key); if (zobj == NULL) { + if (xx) goto reply_to_client; /* No key + XX option: nothing to do. */ if (server.zset_max_ziplist_entries == 0 || - server.zset_max_ziplist_value < sdslen(c->argv[3]->ptr)) + server.zset_max_ziplist_value < sdslen(c->argv[scoreidx+1]->ptr)) { zobj = createZsetObject(); } else { @@ -1224,8 +1273,9 @@ void zaddGenericCommand(redisClient *c, int incr) { unsigned char *eptr; /* Prefer non-encoded element when dealing with ziplists. */ - ele = c->argv[3+j*2]; + ele = c->argv[scoreidx+1+j*2]; if ((eptr = zzlFind(zobj->ptr,ele,&curscore)) != NULL) { + if (nx) continue; if (incr) { score += curscore; if (isnan(score)) { @@ -1241,7 +1291,8 @@ void zaddGenericCommand(redisClient *c, int incr) { server.dirty++; updated++; } - } else { + processed++; + } else if (!xx) { /* Optimize: check if the element is too large or the list * becomes too long *before* executing zzlInsert. */ zobj->ptr = zzlInsert(zobj->ptr,ele,score); @@ -1251,15 +1302,18 @@ void zaddGenericCommand(redisClient *c, int incr) { zsetConvert(zobj,REDIS_ENCODING_SKIPLIST); server.dirty++; added++; + processed++; } } else if (zobj->encoding == REDIS_ENCODING_SKIPLIST) { zset *zs = zobj->ptr; zskiplistNode *znode; dictEntry *de; - ele = c->argv[3+j*2] = tryObjectEncoding(c->argv[3+j*2]); + ele = c->argv[scoreidx+1+j*2] = + tryObjectEncoding(c->argv[scoreidx+1+j*2]); de = dictFind(zs->dict,ele); if (de != NULL) { + if (nx) continue; curobj = dictGetKey(de); curscore = *(double*)dictGetVal(de); @@ -1284,22 +1338,30 @@ void zaddGenericCommand(redisClient *c, int incr) { server.dirty++; updated++; } - } else { + processed++; + } else if (!xx) { znode = zslInsert(zs->zsl,score,ele); incrRefCount(ele); /* Inserted in skiplist. */ redisAssertWithInfo(c,NULL,dictAdd(zs->dict,ele,&znode->score) == DICT_OK); incrRefCount(ele); /* Added to dictionary. */ server.dirty++; added++; + processed++; } } else { redisPanic("Unknown sorted set encoding"); } } - if (incr) /* ZINCRBY */ - addReplyDouble(c,score); - else /* ZADD */ - addReplyLongLong(c,added); + +reply_to_client: + if (incr) { /* ZINCRBY or INCR option. */ + if (processed) + addReplyDouble(c,score); + else + addReply(c,shared.nullbulk); + } else { /* ZADD. */ + addReplyLongLong(c,ch ? added+updated : added); + } cleanup: zfree(scores); @@ -1311,11 +1373,11 @@ cleanup: } void zaddCommand(redisClient *c) { - zaddGenericCommand(c,0); + zaddGenericCommand(c,ZADD_NONE); } void zincrbyCommand(redisClient *c) { - zaddGenericCommand(c,1); + zaddGenericCommand(c,ZADD_INCR); } void zremCommand(redisClient *c) { diff --git a/src/version.h b/src/version.h index 466d9ae1..ba55bbb1 100644 --- a/src/version.h +++ b/src/version.h @@ -1 +1 @@ -#define REDIS_VERSION "3.0.100-alpha1" +#define REDIS_VERSION "3.0.300-alpha1" diff --git a/tests/sentinel/tests/06-ckquorum.tcl b/tests/sentinel/tests/06-ckquorum.tcl new file mode 100644 index 00000000..31e5fa2f --- /dev/null +++ b/tests/sentinel/tests/06-ckquorum.tcl @@ -0,0 +1,34 @@ +# Test for the SENTINEL CKQUORUM command + +source "../tests/includes/init-tests.tcl" +set num_sentinels [llength $::sentinel_instances] + +test "CKQUORUM reports OK and the right amount of Sentinels" { + foreach_sentinel_id id { + assert_match "*OK $num_sentinels usable*" [S $id SENTINEL CKQUORUM mymaster] + } +} + +test "CKQUORUM detects quorum cannot be reached" { + set orig_quorum [expr {$num_sentinels/2+1}] + S 0 SENTINEL SET mymaster quorum [expr {$num_sentinels+1}] + catch {[S 0 SENTINEL CKQUORUM mymaster]} err + assert_match "*NOQUORUM*" $err + S 0 SENTINEL SET mymaster quorum $orig_quorum +} + +test "CKQUORUM detects failover authorization cannot be reached" { + set orig_quorum [expr {$num_sentinels/2+1}] + S 0 SENTINEL SET mymaster quorum 1 + kill_instance sentinel 1 + kill_instance sentinel 2 + kill_instance sentinel 3 + after 5000 + catch {[S 0 SENTINEL CKQUORUM mymaster]} err + assert_match "*NOQUORUM*" $err + S 0 SENTINEL SET mymaster quorum $orig_quorum + restart_instance sentinel 1 + restart_instance sentinel 2 + restart_instance sentinel 3 +} + diff --git a/tests/unit/type/set.tcl b/tests/unit/type/set.tcl index 162de0af..f294c01f 100644 --- a/tests/unit/type/set.tcl +++ b/tests/unit/type/set.tcl @@ -450,6 +450,7 @@ start_server { test "SMOVE non existing key" { setup_move assert_equal 0 [r smove myset1 myset2 foo] + assert_equal 0 [r smove myset1 myset1 foo] assert_equal {1 a b} [lsort [r smembers myset1]] assert_equal {2 3 4} [lsort [r smembers myset2]] } diff --git a/tests/unit/type/zset.tcl b/tests/unit/type/zset.tcl index 20ec5a7b..85a87741 100644 --- a/tests/unit/type/zset.tcl +++ b/tests/unit/type/zset.tcl @@ -43,6 +43,84 @@ start_server {tags {"zset"}} { assert_error "*not*float*" {r zadd myzset nan abc} } + test "ZADD with options syntax error with incomplete pair" { + r del ztmp + catch {r zadd ztmp xx 10 x 20} err + set err + } {ERR*} + + test "ZADD XX option without key - $encoding" { + r del ztmp + assert {[r zadd ztmp xx 10 x] == 0} + assert {[r type ztmp] eq {none}} + } + + test "ZADD XX existing key - $encoding" { + r del ztmp + r zadd ztmp 10 x + assert {[r zadd ztmp xx 20 y] == 0} + assert {[r zcard ztmp] == 1} + } + + test "ZADD XX returns the number of elements actually added" { + r del ztmp + r zadd ztmp 10 x + set retval [r zadd ztmp 10 x 20 y 30 z] + assert {$retval == 2} + } + + test "ZADD XX updates existing elements score" { + r del ztmp + r zadd ztmp 10 x 20 y 30 z + r zadd ztmp xx 5 foo 11 x 21 y 40 zap + assert {[r zcard ztmp] == 3} + assert {[r zscore ztmp x] == 11} + assert {[r zscore ztmp y] == 21} + } + + test "ZADD XX and NX are not compatible" { + r del ztmp + catch {r zadd ztmp xx nx 10 x} err + set err + } {ERR*} + + test "ZADD NX with non exisitng key" { + r del ztmp + r zadd ztmp nx 10 x 20 y 30 z + assert {[r zcard ztmp] == 3} + } + + test "ZADD NX only add new elements without updating old ones" { + r del ztmp + r zadd ztmp 10 x 20 y 30 z + assert {[r zadd ztmp nx 11 x 21 y 100 a 200 b] == 2} + assert {[r zscore ztmp x] == 10} + assert {[r zscore ztmp y] == 20} + assert {[r zscore ztmp a] == 100} + assert {[r zscore ztmp b] == 200} + } + + test "ZADD INCR works like ZINCRBY" { + r del ztmp + r zadd ztmp 10 x 20 y 30 z + r zadd ztmp INCR 15 x + assert {[r zscore ztmp x] == 25} + } + + test "ZADD INCR works with a single score-elemenet pair" { + r del ztmp + r zadd ztmp 10 x 20 y 30 z + catch {r zadd ztmp INCR 15 x 10 y} err + set err + } {ERR*} + + test "ZADD CH option changes return value to all changed elements" { + r del ztmp + r zadd ztmp 10 x 20 y 30 z + assert {[r zadd ztmp 11 x 21 y 30 z] == 0} + assert {[r zadd ztmp ch 12 x 22 y 30 z] == 2} + } + test "ZINCRBY calls leading to NaN result in error" { r zincrby myzset +inf abc assert_error "*NaN*" {r zincrby myzset -inf abc} @@ -77,6 +155,8 @@ start_server {tags {"zset"}} { } test "ZCARD basics - $encoding" { + r del ztmp + r zadd ztmp 10 a 20 b 30 c assert_equal 3 [r zcard ztmp] assert_equal 0 [r zcard zdoesntexist] }