diff options
-rw-r--r-- | src/Makefile | 18 | ||||
-rw-r--r-- | src/anet.c | 10 | ||||
-rw-r--r-- | src/anet.h | 1 | ||||
-rw-r--r-- | src/aof.c | 8 | ||||
-rw-r--r-- | src/endian.c | 63 | ||||
-rw-r--r-- | src/endian.h | 20 | ||||
-rw-r--r-- | src/intset.c | 5 | ||||
-rw-r--r-- | src/intset.h | 1 | ||||
-rw-r--r-- | src/networking.c | 89 | ||||
-rw-r--r-- | src/object.c | 1 | ||||
-rw-r--r-- | src/rdb.c | 150 | ||||
-rw-r--r-- | src/redis-cli.c | 10 | ||||
-rw-r--r-- | src/redis.c | 21 | ||||
-rw-r--r-- | src/redis.h | 9 | ||||
-rw-r--r-- | src/t_hash.c | 17 | ||||
-rw-r--r-- | src/t_set.c | 32 | ||||
-rw-r--r-- | src/util.c | 5 | ||||
-rw-r--r-- | src/vm.c | 12 | ||||
-rw-r--r-- | src/ziplist.c | 69 | ||||
-rw-r--r-- | src/ziplist.h | 2 | ||||
-rw-r--r-- | src/zipmap.c | 16 | ||||
-rw-r--r-- | src/zipmap.h | 1 | ||||
-rw-r--r-- | tests/integration/aof.tcl | 72 | ||||
-rw-r--r-- | tests/unit/type/hash.tcl | 9 | ||||
-rw-r--r-- | tests/unit/type/set.tcl | 8 |
25 files changed, 464 insertions, 185 deletions
diff --git a/src/Makefile b/src/Makefile index 48422f3b1..40ee7e6ff 100644 --- a/src/Makefile +++ b/src/Makefile @@ -65,7 +65,6 @@ ae_select.o: ae_select.c anet.o: anet.c fmacros.h anet.h aof.o: aof.c redis.h fmacros.h config.h ae.h sds.h dict.h adlist.h \ zmalloc.h anet.h zipmap.h ziplist.h intset.h version.h -chprgname.o: chprgname.c config.o: config.c redis.h fmacros.h config.h ae.h sds.h dict.h adlist.h \ zmalloc.h anet.h zipmap.h ziplist.h intset.h version.h db.o: db.c redis.h fmacros.h config.h ae.h sds.h dict.h adlist.h \ @@ -73,6 +72,7 @@ db.o: db.c redis.h fmacros.h config.h ae.h sds.h dict.h adlist.h \ debug.o: debug.c redis.h fmacros.h config.h ae.h sds.h dict.h adlist.h \ zmalloc.h anet.h zipmap.h ziplist.h intset.h version.h sha1.h dict.o: dict.c fmacros.h dict.h zmalloc.h +endian.o: endian.c intset.o: intset.c intset.h zmalloc.h lzf_c.o: lzf_c.c lzfP.h lzf_d.o: lzf_d.c lzfP.h @@ -87,11 +87,12 @@ pubsub.o: pubsub.c redis.h fmacros.h config.h ae.h sds.h dict.h adlist.h \ zmalloc.h anet.h zipmap.h ziplist.h intset.h version.h rdb.o: rdb.c redis.h fmacros.h config.h ae.h sds.h dict.h adlist.h \ zmalloc.h anet.h zipmap.h ziplist.h intset.h version.h lzf.h -redis-benchmark.o: redis-benchmark.c fmacros.h ae.h anet.h sds.h adlist.h \ - zmalloc.h +redis-benchmark.o: redis-benchmark.c fmacros.h ae.h \ + ../deps/hiredis/hiredis.h sds.h adlist.h zmalloc.h redis-check-aof.o: redis-check-aof.c fmacros.h config.h redis-check-dump.o: redis-check-dump.c lzf.h -redis-cli.o: redis-cli.c fmacros.h version.h sds.h adlist.h zmalloc.h +redis-cli.o: redis-cli.c fmacros.h version.h ../deps/hiredis/hiredis.h \ + sds.h zmalloc.h ../deps/linenoise/linenoise.h help.h redis.o: redis.c redis.h fmacros.h config.h ae.h sds.h dict.h adlist.h \ zmalloc.h anet.h zipmap.h ziplist.h intset.h version.h release.o: release.c release.h @@ -101,7 +102,8 @@ sds.o: sds.c sds.h zmalloc.h sha1.o: sha1.c sha1.h sort.o: sort.c redis.h fmacros.h config.h ae.h sds.h dict.h adlist.h \ zmalloc.h anet.h zipmap.h ziplist.h intset.h version.h pqsort.h -syncio.o: syncio.c +syncio.o: syncio.c redis.h fmacros.h config.h ae.h sds.h dict.h adlist.h \ + zmalloc.h anet.h zipmap.h ziplist.h intset.h version.h t_hash.o: t_hash.c redis.h fmacros.h config.h ae.h sds.h dict.h adlist.h \ zmalloc.h anet.h zipmap.h ziplist.h intset.h version.h t_list.o: t_list.c redis.h fmacros.h config.h ae.h sds.h dict.h adlist.h \ @@ -116,8 +118,8 @@ util.o: util.c util.h vm.o: vm.c redis.h fmacros.h config.h ae.h sds.h dict.h adlist.h \ zmalloc.h anet.h zipmap.h ziplist.h intset.h version.h ziplist.o: ziplist.c zmalloc.h ziplist.h -zipmap.o: zipmap.c zmalloc.h -zmalloc.o: zmalloc.c config.h +zipmap.o: zipmap.c zmalloc.h endian.h +zmalloc.o: zmalloc.c config.h zmalloc.h dependencies: cd ../deps/hiredis && $(MAKE) static ARCH="$(ARCH)" @@ -152,7 +154,7 @@ clean: rm -rf $(PRGNAME) $(BENCHPRGNAME) $(CLIPRGNAME) $(CHECKDUMPPRGNAME) $(CHECKAOFPRGNAME) *.o *.gcda *.gcno *.gcov dep: - $(CC) -MM *.c + $(CC) -I../deps/hiredis -I../deps/linenoise -MM *.c test: redis-server (cd ..; tclsh8.5 tests/test_helper.tcl --tags "${TAGS}" --file "${FILE}") diff --git a/src/anet.c b/src/anet.c index 4e16f2e4c..692cef194 100644 --- a/src/anet.c +++ b/src/anet.c @@ -345,3 +345,13 @@ int anetUnixAccept(char *err, int s) { return fd; } + +int anetPeerToString(int fd, char *ip, int *port) { + struct sockaddr_in sa; + socklen_t salen = sizeof(sa); + + if (getpeername(fd,(struct sockaddr*)&sa,&salen) == -1) return -1; + if (ip) strcpy(ip,inet_ntoa(sa.sin_addr)); + if (port) *port = ntohs(sa.sin_port); + return 0; +} diff --git a/src/anet.h b/src/anet.h index 118b4ddac..2b2dea456 100644 --- a/src/anet.h +++ b/src/anet.h @@ -53,5 +53,6 @@ int anetWrite(int fd, char *buf, int count); int anetNonBlock(char *err, int fd); int anetTcpNoDelay(char *err, int fd); int anetTcpKeepAlive(char *err, int fd); +int anetPeerToString(int fd, char *ip, int *port); #endif @@ -285,9 +285,11 @@ int loadAppendOnlyFile(char *filename) { /* The fake client should not have a reply */ redisAssert(fakeClient->bufpos == 0 && listLength(fakeClient->reply) == 0); - /* Clean up, ready for the next command */ - for (j = 0; j < argc; j++) decrRefCount(argv[j]); - zfree(argv); + /* Clean up. Command code may have changed argv/argc so we use the + * argv/argc of the client instead of the local variables. */ + for (j = 0; j < fakeClient->argc; j++) + decrRefCount(fakeClient->argv[j]); + zfree(fakeClient->argv); /* Handle swapping while loading big datasets when VM is on */ force_swapout = 0; diff --git a/src/endian.c b/src/endian.c new file mode 100644 index 000000000..aff2425a6 --- /dev/null +++ b/src/endian.c @@ -0,0 +1,63 @@ +/* Toggle the 16 bit unsigned integer pointed by *p from little endian to + * big endian */ +void memrev16(void *p) { + unsigned char *x = p, t; + + t = x[0]; + x[0] = x[1]; + x[1] = t; +} + +/* Toggle the 32 bit unsigned integer pointed by *p from little endian to + * big endian */ +void memrev32(void *p) { + unsigned char *x = p, t; + + t = x[0]; + x[0] = x[3]; + x[3] = t; + t = x[1]; + x[1] = x[2]; + x[2] = t; +} + +/* Toggle the 64 bit unsigned integer pointed by *p from little endian to + * big endian */ +void memrev64(void *p) { + unsigned char *x = p, t; + + t = x[0]; + x[0] = x[7]; + x[7] = t; + t = x[1]; + x[1] = x[6]; + x[6] = t; + t = x[2]; + x[2] = x[5]; + x[5] = t; + t = x[3]; + x[3] = x[4]; + x[4] = t; +} + +#ifdef TESTMAIN +#include <stdio.h> + +int main(void) { + char buf[32]; + + sprintf(buf,"ciaoroma"); + memrev16(buf); + printf("%s\n", buf); + + sprintf(buf,"ciaoroma"); + memrev32(buf); + printf("%s\n", buf); + + sprintf(buf,"ciaoroma"); + memrev64(buf); + printf("%s\n", buf); + + return 0; +} +#endif diff --git a/src/endian.h b/src/endian.h new file mode 100644 index 000000000..bef822727 --- /dev/null +++ b/src/endian.h @@ -0,0 +1,20 @@ +#ifndef __ENDIAN_H +#define __ENDIAN_H + +void memrev16(void *p); +void memrev32(void *p); +void memrev64(void *p); + +/* variants of the function doing the actual convertion only if the target + * host is big endian */ +#if (BYTE_ORDER == LITTLE_ENDIAN) +#define memrev16ifbe(p) +#define memrev32ifbe(p) +#define memrev64ifbe(p) +#else +#define memrev16ifbe(p) memrev16(p) +#define memrev32ifbe(p) memrev32(p) +#define memrev64ifbe(p) memrev64(p) +#endif + +#endif diff --git a/src/intset.c b/src/intset.c index bfd3307d2..13bd220e7 100644 --- a/src/intset.c +++ b/src/intset.c @@ -222,6 +222,11 @@ uint32_t intsetLen(intset *is) { return is->length; } +/* Return intset blob size in bytes. */ +size_t intsetBlobLen(intset *is) { + return sizeof(intset)+is->length*is->encoding; +} + #ifdef INTSET_TEST_MAIN #include <sys/time.h> diff --git a/src/intset.h b/src/intset.h index 10d49d2e0..ee4b91fa9 100644 --- a/src/intset.h +++ b/src/intset.h @@ -15,5 +15,6 @@ uint8_t intsetFind(intset *is, int64_t value); int64_t intsetRandom(intset *is); uint8_t intsetGet(intset *is, uint32_t pos, int64_t *value); uint32_t intsetLen(intset *is); +size_t intsetBlobLen(intset *is); #endif // __INTSET_H diff --git a/src/networking.c b/src/networking.c index 7a57031cc..258d63fda 100644 --- a/src/networking.c +++ b/src/networking.c @@ -16,7 +16,6 @@ redisClient *createClient(int fd) { anetNonBlock(NULL,fd); anetTcpNoDelay(NULL,fd); - if (!c) return NULL; if (aeCreateFileEvent(server.el,fd,AE_READABLE, readQueryFromClient, c) == AE_ERR) { @@ -322,7 +321,12 @@ void _addReplyLongLong(redisClient *c, long long ll, char prefix) { } void addReplyLongLong(redisClient *c, long long ll) { - _addReplyLongLong(c,ll,':'); + if (ll == 0) + addReply(c,shared.czero); + else if (ll == 1) + addReply(c,shared.cone); + else + _addReplyLongLong(c,ll,':'); } void addReplyMultiBulkLen(redisClient *c, long length) { @@ -523,10 +527,16 @@ void freeClient(redisClient *c) { * close the connection with all our slaves if we have any, so * when we'll resync with the master the other slaves will sync again * with us as well. Note that also when the slave is not connected - * to the master it will keep refusing connections by other slaves. */ - while (listLength(server.slaves)) { - ln = listFirst(server.slaves); - freeClient((redisClient*)ln->value); + * to the master it will keep refusing connections by other slaves. + * + * We do this only if server.masterhost != NULL. If it is NULL this + * means the user called SLAVEOF NO ONE and we are freeing our + * link with the master, so no need to close link with slaves. */ + if (server.masterhost != NULL) { + while (listLength(server.slaves)) { + ln = listFirst(server.slaves); + freeClient((redisClient*)ln->value); + } } } /* Release memory */ @@ -870,3 +880,70 @@ void getClientsMaxBuffers(unsigned long *longest_output_list, *biggest_input_buffer = bib; } +void clientCommand(redisClient *c) { + listNode *ln; + listIter li; + redisClient *client; + + if (!strcasecmp(c->argv[1]->ptr,"list") && c->argc == 2) { + sds o = sdsempty(); + time_t now = time(NULL); + + listRewind(server.clients,&li); + while ((ln = listNext(&li)) != NULL) { + char ip[32], flags[16], *p; + int port; + + client = listNodeValue(ln); + if (anetPeerToString(client->fd,ip,&port) == -1) continue; + p = flags; + if (client->flags & REDIS_SLAVE) { + if (client->flags & REDIS_MONITOR) + *p++ = 'O'; + else + *p++ = 'S'; + } + if (client->flags & REDIS_MASTER) *p++ = 'M'; + if (p == flags) *p++ = 'N'; + if (client->flags & REDIS_MULTI) *p++ = 'x'; + if (client->flags & REDIS_BLOCKED) *p++ = 'b'; + if (client->flags & REDIS_IO_WAIT) *p++ = 'i'; + if (client->flags & REDIS_DIRTY_CAS) *p++ = 'd'; + if (client->flags & REDIS_CLOSE_AFTER_REPLY) *p++ = 'c'; + if (client->flags & REDIS_UNBLOCKED) *p++ = 'u'; + *p++ = '\0'; + o = sdscatprintf(o, + "addr=%s:%d fd=%d idle=%ld flags=%s db=%d sub=%d psub=%d\n", + ip,port,client->fd, + (long)(now - client->lastinteraction), + flags, + client->db->id, + (int) dictSize(client->pubsub_channels), + (int) listLength(client->pubsub_patterns)); + } + addReplyBulkCBuffer(c,o,sdslen(o)); + sdsfree(o); + } else if (!strcasecmp(c->argv[1]->ptr,"kill") && c->argc == 3) { + listRewind(server.clients,&li); + while ((ln = listNext(&li)) != NULL) { + char ip[32], addr[64]; + int port; + + client = listNodeValue(ln); + if (anetPeerToString(client->fd,ip,&port) == -1) continue; + snprintf(addr,sizeof(addr),"%s:%d",ip,port); + if (strcmp(addr,c->argv[2]->ptr) == 0) { + addReply(c,shared.ok); + if (c == client) { + client->flags |= REDIS_CLOSE_AFTER_REPLY; + } else { + freeClient(client); + } + return; + } + } + addReplyError(c,"No such client"); + } else { + addReplyError(c, "Syntax error, try CLIENT (LIST | KILL ip:port)"); + } +} diff --git a/src/object.c b/src/object.c index 2f99e1414..e521e5dab 100644 --- a/src/object.c +++ b/src/object.c @@ -475,6 +475,7 @@ unsigned long estimateObjectIdleTime(robj *o) { robj *objectCommandLookup(redisClient *c, robj *key) { dictEntry *de; + if (server.vm_enabled) lookupKeyRead(c->db,key); if ((de = dictFind(c->db->dict,key->ptr)) == NULL) return NULL; return (robj*) dictGetEntryVal(de); } @@ -139,7 +139,7 @@ writeerr: } /* Save a string objet as [len][data] on disk. If the object is a string - * representation of an integer value we try to safe it in a special form */ + * representation of an integer value we try to save it in a special form */ int rdbSaveRawString(FILE *fp, unsigned char *s, size_t len) { int enclen; int n, nwritten = 0; @@ -256,27 +256,10 @@ int rdbSaveObject(FILE *fp, robj *o) { } else if (o->type == REDIS_LIST) { /* Save a list value */ if (o->encoding == REDIS_ENCODING_ZIPLIST) { - unsigned char *p; - unsigned char *vstr; - unsigned int vlen; - long long vlong; + size_t l = ziplistBlobLen((unsigned char*)o->ptr); - if ((n = rdbSaveLen(fp,ziplistLen(o->ptr))) == -1) return -1; + if ((n = rdbSaveRawString(fp,o->ptr,l)) == -1) return -1; nwritten += n; - - p = ziplistIndex(o->ptr,0); - while(ziplistGet(p,&vstr,&vlen,&vlong)) { - if (vstr) { - if ((n = rdbSaveRawString(fp,vstr,vlen)) == -1) - return -1; - nwritten += n; - } else { - if ((n = rdbSaveLongLongAsStringObject(fp,vlong)) == -1) - return -1; - nwritten += n; - } - p = ziplistNext(o->ptr,p); - } } else if (o->encoding == REDIS_ENCODING_LINKEDLIST) { list *list = o->ptr; listIter li; @@ -311,56 +294,20 @@ int rdbSaveObject(FILE *fp, robj *o) { } dictReleaseIterator(di); } else if (o->encoding == REDIS_ENCODING_INTSET) { - intset *is = o->ptr; - int64_t llval; - int i = 0; + size_t l = intsetBlobLen((intset*)o->ptr); - if ((n = rdbSaveLen(fp,intsetLen(is))) == -1) return -1; + if ((n = rdbSaveRawString(fp,o->ptr,l)) == -1) return -1; nwritten += n; - - while(intsetGet(is,i++,&llval)) { - if ((n = rdbSaveLongLongAsStringObject(fp,llval)) == -1) return -1; - nwritten += n; - } } else { redisPanic("Unknown set encoding"); } } else if (o->type == REDIS_ZSET) { /* Save a sorted set value */ if (o->encoding == REDIS_ENCODING_ZIPLIST) { - unsigned char *zl = o->ptr; - unsigned char *eptr, *sptr; - unsigned char *vstr; - unsigned int vlen; - long long vlong; - double score; + size_t l = ziplistBlobLen((unsigned char*)o->ptr); - if ((n = rdbSaveLen(fp,zsetLength(o))) == -1) return -1; + if ((n = rdbSaveRawString(fp,o->ptr,l)) == -1) return -1; nwritten += n; - - eptr = ziplistIndex(zl,0); - redisAssert(eptr != NULL); - sptr = ziplistNext(zl,eptr); - redisAssert(sptr != NULL); - - while (eptr != NULL) { - redisAssert(ziplistGet(eptr,&vstr,&vlen,&vlong)); - if (vstr) { - if ((n = rdbSaveRawString(fp,vstr,vlen)) == -1) - return -1; - nwritten += n; - } else { - if ((n = rdbSaveLongLongAsStringObject(fp,vlong)) == -1) - return -1; - nwritten += n; - } - - score = zzlGetScore(sptr); - if ((n = rdbSaveDoubleValue(fp,score)) == -1) return -1; - nwritten += n; - - zzlNext(zl,&eptr,&sptr); - } } else if (o->encoding == REDIS_ENCODING_SKIPLIST) { zset *zs = o->ptr; dictIterator *di = dictGetIterator(zs->dict); @@ -385,20 +332,10 @@ int rdbSaveObject(FILE *fp, robj *o) { } else if (o->type == REDIS_HASH) { /* Save a hash value */ if (o->encoding == REDIS_ENCODING_ZIPMAP) { - unsigned char *p = zipmapRewind(o->ptr); - unsigned int count = zipmapLen(o->ptr); - unsigned char *key, *val; - unsigned int klen, vlen; + size_t l = zipmapBlobLen((unsigned char*)o->ptr); - if ((n = rdbSaveLen(fp,count)) == -1) return -1; + if ((n = rdbSaveRawString(fp,o->ptr,l)) == -1) return -1; nwritten += n; - - while((p = zipmapNext(p,&key,&klen,&val,&vlen)) != NULL) { - if ((n = rdbSaveRawString(fp,key,klen)) == -1) return -1; - nwritten += n; - if ((n = rdbSaveRawString(fp,val,vlen)) == -1) return -1; - nwritten += n; - } } else { dictIterator *di = dictGetIterator(o->ptr); dictEntry *de; @@ -439,6 +376,20 @@ off_t rdbSavedObjectPages(robj *o) { return (bytes+(server.vm_page_size-1))/server.vm_page_size; } +int getObjectSaveType(robj *o) { + /* Fix the type id for specially encoded data types */ + if (o->type == REDIS_HASH && o->encoding == REDIS_ENCODING_ZIPMAP) + return REDIS_HASH_ZIPMAP; + else if (o->type == REDIS_LIST && o->encoding == REDIS_ENCODING_ZIPLIST) + return REDIS_LIST_ZIPLIST; + else if (o->type == REDIS_SET && o->encoding == REDIS_ENCODING_INTSET) + return REDIS_SET_INTSET; + else if (o->type == REDIS_ZSET && o->encoding == REDIS_ENCODING_ZIPLIST) + return REDIS_ZSET_ZIPLIST; + else + return o->type; +} + /* Save the DB on disk. Return REDIS_ERR on error, REDIS_OK on success */ int rdbSave(char *filename) { dictIterator *di = NULL; @@ -495,8 +446,10 @@ int rdbSave(char *filename) { * handling if the value is swapped out. */ if (!server.vm_enabled || o->storage == REDIS_VM_MEMORY || o->storage == REDIS_VM_SWAPPING) { + int otype = getObjectSaveType(o); + /* Save type, key, value */ - if (rdbSaveType(fp,o->type) == -1) goto werr; + if (rdbSaveType(fp,otype) == -1) goto werr; if (rdbSaveStringObject(fp,&key) == -1) goto werr; if (rdbSaveObject(fp,o) == -1) goto werr; } else { @@ -505,7 +458,8 @@ int rdbSave(char *filename) { /* Get a preview of the object in memory */ po = vmPreviewObject(o); /* Save type, key, value */ - if (rdbSaveType(fp,po->type) == -1) goto werr; + if (rdbSaveType(fp,getObjectSaveType(po)) == -1) + goto werr; if (rdbSaveStringObject(fp,&key) == -1) goto werr; if (rdbSaveObject(fp,po) == -1) goto werr; /* Remove the loaded object from memory */ @@ -890,6 +844,54 @@ robj *rdbLoadObject(int type, FILE *fp) { dictAdd((dict*)o->ptr,key,val); } } + } else if (type == REDIS_HASH_ZIPMAP || + type == REDIS_LIST_ZIPLIST || + type == REDIS_SET_INTSET || + type == REDIS_ZSET_ZIPLIST) + { + robj *aux = rdbLoadStringObject(fp); + + if (aux == NULL) return NULL; + o = createObject(REDIS_STRING,NULL); /* string is just placeholder */ + o->ptr = zmalloc(sdslen(aux->ptr)); + memcpy(o->ptr,aux->ptr,sdslen(aux->ptr)); + decrRefCount(aux); + + /* Fix the object encoding, and make sure to convert the encoded + * data type into the base type if accordingly to the current + * configuration there are too many elements in the encoded data + * type. Note that we only check the length and not max element + * size as this is an O(N) scan. Eventually everything will get + * converted. */ + switch(type) { + case REDIS_HASH_ZIPMAP: + o->type = REDIS_HASH; + o->encoding = REDIS_ENCODING_ZIPMAP; + if (zipmapLen(o->ptr) > server.hash_max_zipmap_entries) + convertToRealHash(o); + break; + case REDIS_LIST_ZIPLIST: + o->type = REDIS_LIST; + o->encoding = REDIS_ENCODING_ZIPLIST; + if (ziplistLen(o->ptr) > server.list_max_ziplist_entries) + listTypeConvert(o,REDIS_ENCODING_LINKEDLIST); + break; + case REDIS_SET_INTSET: + o->type = REDIS_SET; + o->encoding = REDIS_ENCODING_INTSET; + if (intsetLen(o->ptr) > server.set_max_intset_entries) + setTypeConvert(o,REDIS_ENCODING_HT); + break; + case REDIS_ZSET_ZIPLIST: + o->type = REDIS_ZSET; + o->encoding = REDIS_ENCODING_ZIPLIST; + if (zsetLength(o) > server.zset_max_ziplist_entries) + zsetConvert(o,REDIS_ENCODING_SKIPLIST); + break; + default: + redisPanic("Unknown encoding"); + break; + } } else { redisPanic("Unknown object type"); } diff --git a/src/redis-cli.c b/src/redis-cli.c index f0c712e74..9e497b4bf 100644 --- a/src/redis-cli.c +++ b/src/redis-cli.c @@ -465,7 +465,15 @@ static int cliSendCommand(int argc, char **argv, int repeat) { return REDIS_OK; } - output_raw = !strcasecmp(command,"info"); + output_raw = 0; + if (!strcasecmp(command,"info") || + (argc == 2 && !strcasecmp(command,"client") && + !strcasecmp(argv[1],"list"))) + + { + output_raw = 1; + } + if (!strcasecmp(command,"help") || !strcasecmp(command,"?")) { cliOutputHelp(--argc, ++argv); return REDIS_OK; diff --git a/src/redis.c b/src/redis.c index 87d46e019..e089cefae 100644 --- a/src/redis.c +++ b/src/redis.c @@ -102,8 +102,8 @@ struct redisCommand readonlyCommandTable[] = { {"ltrim",ltrimCommand,4,0,NULL,1,1,1}, {"lrem",lremCommand,4,0,NULL,1,1,1}, {"rpoplpush",rpoplpushCommand,3,REDIS_CMD_DENYOOM,NULL,1,2,1}, - {"sadd",saddCommand,3,REDIS_CMD_DENYOOM,NULL,1,1,1}, - {"srem",sremCommand,3,0,NULL,1,1,1}, + {"sadd",saddCommand,-3,REDIS_CMD_DENYOOM,NULL,1,1,1}, + {"srem",sremCommand,-3,0,NULL,1,1,1}, {"smove",smoveCommand,4,0,NULL,1,2,1}, {"sismember",sismemberCommand,3,0,NULL,1,1,1}, {"scard",scardCommand,2,0,NULL,1,1,1}, @@ -138,7 +138,7 @@ struct redisCommand readonlyCommandTable[] = { {"hmset",hmsetCommand,-4,REDIS_CMD_DENYOOM,NULL,1,1,1}, {"hmget",hmgetCommand,-3,0,NULL,1,1,1}, {"hincrby",hincrbyCommand,4,REDIS_CMD_DENYOOM,NULL,1,1,1}, - {"hdel",hdelCommand,3,0,NULL,1,1,1}, + {"hdel",hdelCommand,-3,0,NULL,1,1,1}, {"hlen",hlenCommand,2,0,NULL,1,1,1}, {"hkeys",hkeysCommand,2,0,NULL,1,1,1}, {"hvals",hvalsCommand,2,0,NULL,1,1,1}, @@ -188,7 +188,8 @@ struct redisCommand readonlyCommandTable[] = { {"publish",publishCommand,3,REDIS_CMD_FORCE_REPLICATION,NULL,0,0,0}, {"watch",watchCommand,-2,0,NULL,0,0,0}, {"unwatch",unwatchCommand,1,0,NULL,0,0,0}, - {"object",objectCommand,-2,0,NULL,0,0,0} + {"object",objectCommand,-2,0,NULL,0,0,0}, + {"client",clientCommand,-2,0,NULL,0,0,0} }; /*============================ Utility functions ============================ */ @@ -541,6 +542,10 @@ int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { */ updateLRUClock(); + /* Record the max memory used since the server was started. */ + if (zmalloc_used_memory() > server.stat_peak_memory) + server.stat_peak_memory = zmalloc_used_memory(); + /* We received a SIGTERM, shutting down here in a safe way, as it is * not ok doing so inside the signal handler. */ if (server.shutdown_asap) { @@ -902,6 +907,7 @@ void initServer() { server.stat_starttime = time(NULL); server.stat_keyspace_misses = 0; server.stat_keyspace_hits = 0; + server.stat_peak_memory = 0; server.unixtime = time(NULL); aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL); if (server.ipfd > 0 && aeCreateFileEvent(server.el,server.ipfd,AE_READABLE, @@ -1148,7 +1154,7 @@ sds genRedisInfoString(void) { sds info; time_t uptime = time(NULL)-server.stat_starttime; int j; - char hmem[64]; + char hmem[64], peak_hmem[64]; struct rusage self_ru, c_ru; unsigned long lol, bib; @@ -1157,6 +1163,7 @@ sds genRedisInfoString(void) { getClientsMaxBuffers(&lol,&bib); bytesToHuman(hmem,zmalloc_used_memory()); + bytesToHuman(peak_hmem,server.stat_peak_memory); info = sdscatprintf(sdsempty(), "redis_version:%s\r\n" "redis_git_sha1:%s\r\n" @@ -1179,6 +1186,8 @@ sds genRedisInfoString(void) { "used_memory:%zu\r\n" "used_memory_human:%s\r\n" "used_memory_rss:%zu\r\n" + "used_memory_peak:%zu\r\n" + "used_memory_peak_human:%s\r\n" "mem_fragmentation_ratio:%.2f\r\n" "mem_allocator:%s\r\n" "loading:%d\r\n" @@ -1219,6 +1228,8 @@ sds genRedisInfoString(void) { zmalloc_used_memory(), hmem, zmalloc_get_rss(), + server.stat_peak_memory, + peak_hmem, zmalloc_get_fragmentation_ratio(), REDIS_MALLOC, server.loading, diff --git a/src/redis.h b/src/redis.h index dfc0a41bb..a425aada3 100644 --- a/src/redis.h +++ b/src/redis.h @@ -72,6 +72,12 @@ #define REDIS_HASH 4 #define REDIS_VMPOINTER 8 +/* Object types only used for persistence in .rdb files */ +#define REDIS_HASH_ZIPMAP 9 +#define REDIS_LIST_ZIPLIST 10 +#define REDIS_SET_INTSET 11 +#define REDIS_ZSET_ZIPLIST 12 + /* Objects encoding. Some kind of objects like Strings and Hashes can be * internally represented in multiple ways. The 'encoding' field of the object * is set to one of this fields for this object. */ @@ -391,6 +397,7 @@ struct redisServer { long long stat_evictedkeys; /* number of evicted keys (maxmemory) */ long long stat_keyspace_hits; /* number of successful lookups of keys */ long long stat_keyspace_misses; /* number of failed lookups of keys */ + size_t stat_peak_memory; /* max used memory record */ /* Configuration */ int verbosity; int maxidletime; @@ -766,6 +773,7 @@ off_t rdbSavedObjectLen(robj *o); off_t rdbSavedObjectPages(robj *o); robj *rdbLoadObject(int type, FILE *fp); void backgroundSaveDoneHandler(int statloc); +int getObjectSaveType(robj *o); /* AOF persistence */ void flushAppendOnlyFile(void); @@ -1013,6 +1021,7 @@ void publishCommand(redisClient *c); void watchCommand(redisClient *c); void unwatchCommand(redisClient *c); void objectCommand(redisClient *c); +void clientCommand(redisClient *c); #if defined(__GNUC__) void *calloc(size_t count, size_t size) __attribute__ ((deprecated)); diff --git a/src/t_hash.c b/src/t_hash.c index 488bf6b7a..a5324b7eb 100644 --- a/src/t_hash.c +++ b/src/t_hash.c @@ -396,17 +396,22 @@ void hmgetCommand(redisClient *c) { void hdelCommand(redisClient *c) { robj *o; + int j, deleted = 0; + if ((o = lookupKeyWriteOrReply(c,c->argv[1],shared.czero)) == NULL || checkType(c,o,REDIS_HASH)) return; - if (hashTypeDelete(o,c->argv[2])) { - if (hashTypeLength(o) == 0) dbDelete(c->db,c->argv[1]); - addReply(c,shared.cone); + for (j = 2; j < c->argc; j++) { + if (hashTypeDelete(o,c->argv[j])) { + if (hashTypeLength(o) == 0) dbDelete(c->db,c->argv[1]); + deleted++; + } + } + if (deleted) { touchWatchedKey(c->db,c->argv[1]); - server.dirty++; - } else { - addReply(c,shared.czero); + server.dirty += deleted; } + addReplyLongLong(c,deleted); } void hlenCommand(redisClient *c) { diff --git a/src/t_set.c b/src/t_set.c index 9bff7c626..26f70f646 100644 --- a/src/t_set.c +++ b/src/t_set.c @@ -218,9 +218,9 @@ void setTypeConvert(robj *setobj, int enc) { void saddCommand(redisClient *c) { robj *set; + int j, added = 0; set = lookupKeyWrite(c->db,c->argv[1]); - c->argv[2] = tryObjectEncoding(c->argv[2]); if (set == NULL) { set = setTypeCreate(c->argv[2]); dbAdd(c->db,c->argv[1],set); @@ -230,30 +230,34 @@ void saddCommand(redisClient *c) { return; } } - if (setTypeAdd(set,c->argv[2])) { - touchWatchedKey(c->db,c->argv[1]); - server.dirty++; - addReply(c,shared.cone); - } else { - addReply(c,shared.czero); + + for (j = 2; j < c->argc; j++) { + c->argv[j] = tryObjectEncoding(c->argv[j]); + if (setTypeAdd(set,c->argv[j])) added++; } + if (added) touchWatchedKey(c->db,c->argv[1]); + server.dirty += added; + addReplyLongLong(c,added); } void sremCommand(redisClient *c) { robj *set; + int j, deleted = 0; if ((set = lookupKeyWriteOrReply(c,c->argv[1],shared.czero)) == NULL || checkType(c,set,REDIS_SET)) return; - c->argv[2] = tryObjectEncoding(c->argv[2]); - if (setTypeRemove(set,c->argv[2])) { - if (setTypeSize(set) == 0) dbDelete(c->db,c->argv[1]); + for (j = 2; j < c->argc; j++) { + if (setTypeRemove(set,c->argv[j])) { + if (setTypeSize(set) == 0) dbDelete(c->db,c->argv[1]); + deleted++; + } + } + if (deleted) { touchWatchedKey(c->db,c->argv[1]); - server.dirty++; - addReply(c,shared.cone); - } else { - addReply(c,shared.czero); + server.dirty += deleted; } + addReplyLongLong(c,deleted); } void smoveCommand(redisClient *c) { diff --git a/src/util.c b/src/util.c index 7dc5d4a60..e83dbeddc 100644 --- a/src/util.c +++ b/src/util.c @@ -231,10 +231,13 @@ int string2ll(char *s, size_t slen, long long *value) { return 0; } - /* First digit should be 1-9. */ + /* First digit should be 1-9, otherwise the string should just be 0. */ if (p[0] >= '1' && p[0] <= '9') { v = p[0]-'0'; p++; plen++; + } else if (p[0] == '0' && slen == 1) { + *value = 0; + return 1; } else { return 0; } @@ -30,12 +30,12 @@ /* Create a VM pointer object. This kind of objects are used in place of * values in the key -> value hash table, for swapped out objects. */ -vmpointer *createVmPointer(int vtype) { +vmpointer *createVmPointer(robj *o) { vmpointer *vp = zmalloc(sizeof(vmpointer)); vp->type = REDIS_VMPOINTER; vp->storage = REDIS_VM_SWAPPED; - vp->vtype = vtype; + vp->vtype = getObjectSaveType(o); return vp; } @@ -272,7 +272,7 @@ vmpointer *vmSwapObjectBlocking(robj *val) { if (vmFindContiguousPages(&page,pages) == REDIS_ERR) return NULL; if (vmWriteObjectOnSwap(val,page) == REDIS_ERR) return NULL; - vp = createVmPointer(val->type); + vp = createVmPointer(val); vp->page = page; vp->usedpages = pages; decrRefCount(val); /* Deallocate the object from memory. */ @@ -380,7 +380,7 @@ double computeObjectSwappability(robj *o) { break; case REDIS_LIST: if (o->encoding == REDIS_ENCODING_ZIPLIST) { - asize = sizeof(*o)+ziplistSize(o->ptr); + asize = sizeof(*o)+ziplistBlobLen(o->ptr); } else { l = o->ptr; ln = listFirst(l); @@ -411,7 +411,7 @@ double computeObjectSwappability(robj *o) { break; case REDIS_ZSET: if (o->encoding == REDIS_ENCODING_ZIPLIST) { - asize = sizeof(*o)+(ziplistSize(o->ptr) / 2); + asize = sizeof(*o)+(ziplistBlobLen(o->ptr) / 2); } else { d = ((zset*)o->ptr)->dict; asize = sizeof(zset)+(sizeof(struct dictEntry*)*dictSlots(d)); @@ -663,7 +663,7 @@ void vmThreadedIOCompletedJob(aeEventLoop *el, int fd, void *privdata, printf("val->ptr: %s\n",(char*)j->val->ptr); } redisAssert(j->val->storage == REDIS_VM_SWAPPING); - vp = createVmPointer(j->val->type); + vp = createVmPointer(j->val); vp->page = j->page; vp->usedpages = j->pages; dictGetEntryVal(de) = vp; diff --git a/src/ziplist.c b/src/ziplist.c index b491bf09f..e775b77f7 100644 --- a/src/ziplist.c +++ b/src/ziplist.c @@ -384,12 +384,17 @@ static unsigned char *__ziplistCascadeUpdate(unsigned char *zl, unsigned char *p offset = p-zl; extra = rawlensize-next.prevrawlensize; zl = ziplistResize(zl,curlen+extra); - ZIPLIST_TAIL_OFFSET(zl) += extra; p = zl+offset; - /* Move the tail to the back. */ + /* Current pointer and offset for next element. */ np = p+rawlen; noffset = np-zl; + + /* Update tail offset when next element is not the tail element. */ + if ((zl+ZIPLIST_TAIL_OFFSET(zl)) != np) + ZIPLIST_TAIL_OFFSET(zl) += extra; + + /* Move the tail to the back. */ memmove(np+rawlensize, np+next.prevrawlensize, curlen-noffset-next.prevrawlensize-1); @@ -718,8 +723,8 @@ unsigned int ziplistLen(unsigned char *zl) { return len; } -/* Return size in bytes of ziplist. */ -unsigned int ziplistSize(unsigned char *zl) { +/* Return ziplist blob size in bytes. */ +size_t ziplistBlobLen(unsigned char *zl) { return ZIPLIST_BYTES(zl); } @@ -865,7 +870,7 @@ void pop(unsigned char *zl, int where) { } } -void randstring(char *target, unsigned int min, unsigned int max) { +int randstring(char *target, unsigned int min, unsigned int max) { int p, len = min+rand()%(max-min+1); int minval, maxval; switch(rand() % 3) { @@ -887,10 +892,9 @@ void randstring(char *target, unsigned int min, unsigned int max) { while(p < len) target[p++] = minval+rand()%(maxval-minval+1); - return; + return len; } - int main(int argc, char **argv) { unsigned char *zl, *p; unsigned char *entry; @@ -1223,6 +1227,7 @@ int main(int argc, char **argv) { int i,j,len,where; unsigned char *p; char buf[1024]; + int buflen; list *ref; listNode *refnode; @@ -1231,10 +1236,6 @@ int main(int argc, char **argv) { unsigned int slen; long long sval; - /* In the regression for the cascade bug, it was triggered - * with a random seed of 2. */ - srand(2); - for (i = 0; i < 20000; i++) { zl = ziplistNew(); ref = listCreate(); @@ -1244,31 +1245,32 @@ int main(int argc, char **argv) { /* Create lists */ for (j = 0; j < len; j++) { where = (rand() & 1) ? ZIPLIST_HEAD : ZIPLIST_TAIL; - switch(rand() % 4) { - case 0: - sprintf(buf,"%lld",(0LL + rand()) >> 20); - break; - case 1: - sprintf(buf,"%lld",(0LL + rand())); - break; - case 2: - sprintf(buf,"%lld",(0LL + rand()) << 20); - break; - case 3: - randstring(buf,0,256); - break; - default: - assert(NULL); + if (rand() % 2) { + buflen = randstring(buf,1,sizeof(buf)-1); + } else { + switch(rand() % 3) { + case 0: + buflen = sprintf(buf,"%lld",(0LL + rand()) >> 20); + break; + case 1: + buflen = sprintf(buf,"%lld",(0LL + rand())); + break; + case 2: + buflen = sprintf(buf,"%lld",(0LL + rand()) << 20); + break; + default: + assert(NULL); + } } /* Add to ziplist */ - zl = ziplistPush(zl, (unsigned char*)buf, strlen(buf), where); + zl = ziplistPush(zl, (unsigned char*)buf, buflen, where); /* Add to reference list */ if (where == ZIPLIST_HEAD) { - listAddNodeHead(ref,sdsnew(buf)); + listAddNodeHead(ref,sdsnewlen(buf, buflen)); } else if (where == ZIPLIST_TAIL) { - listAddNodeTail(ref,sdsnew(buf)); + listAddNodeTail(ref,sdsnewlen(buf, buflen)); } else { assert(NULL); } @@ -1283,12 +1285,13 @@ int main(int argc, char **argv) { assert(ziplistGet(p,&sstr,&slen,&sval)); if (sstr == NULL) { - sprintf(buf,"%lld",sval); + buflen = sprintf(buf,"%lld",sval); } else { - memcpy(buf,sstr,slen); - buf[slen] = '\0'; + buflen = slen; + memcpy(buf,sstr,buflen); + buf[buflen] = '\0'; } - assert(strcmp(buf,listNodeValue(refnode)) == 0); + assert(memcmp(buf,listNodeValue(refnode),buflen) == 0); } zfree(zl); listRelease(ref); diff --git a/src/ziplist.h b/src/ziplist.h index 311257256..a07b84404 100644 --- a/src/ziplist.h +++ b/src/ziplist.h @@ -12,4 +12,4 @@ unsigned char *ziplistDelete(unsigned char *zl, unsigned char **p); unsigned char *ziplistDeleteRange(unsigned char *zl, unsigned int index, unsigned int num); unsigned int ziplistCompare(unsigned char *p, unsigned char *s, unsigned int slen); unsigned int ziplistLen(unsigned char *zl); -unsigned int ziplistSize(unsigned char *zl); +size_t ziplistBlobLen(unsigned char *zl); diff --git a/src/zipmap.c b/src/zipmap.c index be780a828..9f0fc7181 100644 --- a/src/zipmap.c +++ b/src/zipmap.c @@ -80,6 +80,7 @@ #include <string.h> #include <assert.h> #include "zmalloc.h" +#include "endian.h" #define ZIPMAP_BIGLEN 254 #define ZIPMAP_END 255 @@ -108,6 +109,7 @@ static unsigned int zipmapDecodeLength(unsigned char *p) { if (len < ZIPMAP_BIGLEN) return len; memcpy(&len,p+1,sizeof(unsigned int)); + memrev32ifbe(&len); return len; } @@ -123,6 +125,7 @@ static unsigned int zipmapEncodeLength(unsigned char *p, unsigned int len) { } else { p[0] = ZIPMAP_BIGLEN; memcpy(p+1,&len,sizeof(len)); + memrev32ifbe(p+1); return 1+sizeof(len); } } @@ -144,7 +147,7 @@ static unsigned char *zipmapLookupRaw(unsigned char *zm, unsigned char *key, uns /* Match or skip the key */ l = zipmapDecodeLength(p); llen = zipmapEncodeLength(NULL,l); - if (k == NULL && l == klen && !memcmp(p+llen,key,l)) { + if (key != NULL && k == NULL && l == klen && !memcmp(p+llen,key,l)) { /* Only return when the user doesn't care * for the total length of the zipmap. */ if (totlen != NULL) { @@ -360,6 +363,16 @@ unsigned int zipmapLen(unsigned char *zm) { return len; } +/* Return the raw size in bytes of a zipmap, so that we can serialize + * the zipmap on disk (or everywhere is needed) just writing the returned + * amount of bytes of the C array starting at the zipmap pointer. */ +size_t zipmapBlobLen(unsigned char *zm) { + unsigned int totlen; + zipmapLookupRaw(zm,NULL,0,&totlen); + return totlen; +} + +#ifdef ZIPMAP_TEST_MAIN void zipmapRepr(unsigned char *p) { unsigned int l; @@ -393,7 +406,6 @@ void zipmapRepr(unsigned char *p) { printf("\n"); } -#ifdef ZIPMAP_TEST_MAIN int main(void) { unsigned char *zm; diff --git a/src/zipmap.h b/src/zipmap.h index e5f6c9f28..acb25d67a 100644 --- a/src/zipmap.h +++ b/src/zipmap.h @@ -43,6 +43,7 @@ unsigned char *zipmapNext(unsigned char *zm, unsigned char **key, unsigned int * int zipmapGet(unsigned char *zm, unsigned char *key, unsigned int klen, unsigned char **value, unsigned int *vlen); int zipmapExists(unsigned char *zm, unsigned char *key, unsigned int klen); unsigned int zipmapLen(unsigned char *zm); +size_t zipmapBlobLen(unsigned char *zm); void zipmapRepr(unsigned char *p); #endif diff --git a/tests/integration/aof.tcl b/tests/integration/aof.tcl index 4cbe6eaae..927969b62 100644 --- a/tests/integration/aof.tcl +++ b/tests/integration/aof.tcl @@ -31,13 +31,14 @@ tags {"aof"} { } start_server_aof [list dir $server_path] { - test {Unfinished MULTI: Server should not have been started} { - is_alive $srv - } {0} + test "Unfinished MULTI: Server should not have been started" { + assert_equal 0 [is_alive $srv] + } - test {Unfinished MULTI: Server should have logged an error} { - exec cat [dict get $srv stdout] | tail -n1 - } {*Unexpected end of file reading the append only file*} + test "Unfinished MULTI: Server should have logged an error" { + set result [exec cat [dict get $srv stdout] | tail -n1] + assert_match "*Unexpected end of file reading the append only file*" $result + } } ## Test that the server exits when the AOF contains a short read @@ -47,36 +48,57 @@ tags {"aof"} { } start_server_aof [list dir $server_path] { - test {Short read: Server should not have been started} { - is_alive $srv - } {0} + test "Short read: Server should not have been started" { + assert_equal 0 [is_alive $srv] + } - test {Short read: Server should have logged an error} { - exec cat [dict get $srv stdout] | tail -n1 - } {*Bad file format reading the append only file*} + test "Short read: Server should have logged an error" { + set result [exec cat [dict get $srv stdout] | tail -n1] + assert_match "*Bad file format reading the append only file*" $result + } } ## Test that redis-check-aof indeed sees this AOF is not valid - test {Short read: Utility should confirm the AOF is not valid} { + test "Short read: Utility should confirm the AOF is not valid" { catch { exec src/redis-check-aof $aof_path - } str - set _ $str - } {*not valid*} + } result + assert_match "*not valid*" $result + } - test {Short read: Utility should be able to fix the AOF} { - exec echo y | src/redis-check-aof --fix $aof_path - } {*Successfully truncated AOF*} + test "Short read: Utility should be able to fix the AOF" { + set result [exec echo y | src/redis-check-aof --fix $aof_path] + assert_match "*Successfully truncated AOF*" $result + } ## Test that the server can be started using the truncated AOF start_server_aof [list dir $server_path] { - test {Fixed AOF: Server should have been started} { - is_alive $srv - } {1} + test "Fixed AOF: Server should have been started" { + assert_equal 1 [is_alive $srv] + } + + test "Fixed AOF: Keyspace should contain values that were parsable" { + set client [redis [dict get $srv host] [dict get $srv port]] + assert_equal "hello" [$client get foo] + assert_equal "" [$client get bar] + } + } + + ## Test that SPOP (that modifies the client its argc/argv) is correctly free'd + create_aof { + append_to_aof [formatCommand sadd set foo] + append_to_aof [formatCommand sadd set bar] + append_to_aof [formatCommand spop set] + } + + start_server_aof [list dir $server_path] { + test "AOF+SPOP: Server should have been started" { + assert_equal 1 [is_alive $srv] + } - test {Fixed AOF: Keyspace should contain values that were parsable} { + test "AOF+SPOP: Set should have 1 member" { set client [redis [dict get $srv host] [dict get $srv port]] - list [$client get foo] [$client get bar] - } {hello {}} + assert_equal 1 [$client scard set] + } } } diff --git a/tests/unit/type/hash.tcl b/tests/unit/type/hash.tcl index 8559dc3c3..9b043d3f3 100644 --- a/tests/unit/type/hash.tcl +++ b/tests/unit/type/hash.tcl @@ -226,6 +226,15 @@ start_server {tags {"hash"}} { set _ $rv } {0 0 1 0 {} 1 0 {}} + test {HDEL - more than a single value} { + set rv {} + r del myhash + r hmset myhash a 1 b 2 c 3 + assert_equal 0 [r hdel myhash x y] + assert_equal 2 [r hdel myhash a c f] + r hgetall myhash + } {b 2} + test {HEXISTS} { set rv {} set k [lindex [array names smallhash *] 0] diff --git a/tests/unit/type/set.tcl b/tests/unit/type/set.tcl index 5608a6480..a6d6875bc 100644 --- a/tests/unit/type/set.tcl +++ b/tests/unit/type/set.tcl @@ -90,6 +90,14 @@ start_server { assert_equal {3 5} [lsort [r smembers myset]] } + test {SREM with multiple arguments} { + r del myset + r sadd myset a b c d + assert_equal 0 [r srem myset k k k] + assert_equal 2 [r srem myset b d x y] + lsort [r smembers myset] + } {a c} + foreach {type} {hashtable intset} { for {set i 1} {$i <= 5} {incr i} { r del [format "set%d" $i] |