Compare commits
89
Commits
2.2-alpha0
...
2.2-alpha1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbebd395eb | ||
|
|
ec96ef47a6 | ||
|
|
f85202c3dc | ||
|
|
d320764706 | ||
|
|
f7f12a606c | ||
|
|
fc09b9f449 | ||
|
|
08f55b786b | ||
|
|
7b30cc3a7b | ||
|
|
1eb13e4913 | ||
|
|
fb92ecece7 | ||
|
|
8079656a8e | ||
|
|
93b2a7718e | ||
|
|
e0e1c19520 | ||
|
|
8fedd04dcc | ||
|
|
2f6b31c3bb | ||
|
|
e5f257c2b2 | ||
|
|
c1ae36aea8 | ||
|
|
09252fc4f3 | ||
|
|
357d36733d | ||
|
|
2df84b7269 | ||
|
|
ec7e138926 | ||
|
|
23c64fe50d | ||
|
|
588cd980e9 | ||
|
|
452ccf7a41 | ||
|
|
e4ecc93119 | ||
|
|
4e0d34089a | ||
|
|
94364d53b4 | ||
|
|
e59a64b8d3 | ||
|
|
2929ca9786 | ||
|
|
bad7d097e9 | ||
|
|
5f19e8a4a5 | ||
|
|
740eee1cc6 | ||
|
|
674492bceb | ||
|
|
acc75bfd4f | ||
|
|
f9d5c4e33c | ||
|
|
76864d5626 | ||
|
|
b4b62c34db | ||
|
|
f791d66e20 | ||
|
|
f9b252613b | ||
|
|
ae77016e57 | ||
|
|
5d15b5207d | ||
|
|
4b93e5e267 | ||
|
|
c0b3d42372 | ||
|
|
b37ca6edb1 | ||
|
|
695fe87456 | ||
|
|
b91d605a35 | ||
|
|
778b2210a9 | ||
|
|
e452436a07 | ||
|
|
01daeecee7 | ||
|
|
a679185aa5 | ||
|
|
e193873025 | ||
|
|
c91abdcd07 | ||
|
|
cb72d0f155 | ||
|
|
029e5577ff | ||
|
|
2b9a59471f | ||
|
|
ced6709cb9 | ||
|
|
5d4f3a8c85 | ||
|
|
a53ebb4c8e | ||
|
|
87c74dfaa8 | ||
|
|
aaada3f962 | ||
|
|
cbce517145 | ||
|
|
abb731e5b8 | ||
|
|
cf0c6b78f1 | ||
|
|
3a51bff035 | ||
|
|
123a10f7a5 | ||
|
|
07242c0ccf | ||
|
|
0439d792c4 | ||
|
|
f2dd4769dd | ||
|
|
1fb4e8def7 | ||
|
|
a539d29ac5 | ||
|
|
6146329f1f | ||
|
|
23d7298b15 | ||
|
|
2c572622fb | ||
|
|
0cf5b7b57c | ||
|
|
c25a5d3b10 | ||
|
|
bcf2995c98 | ||
|
|
6825491928 | ||
|
|
2767f1c0c6 | ||
|
|
96ffb2fe97 | ||
|
|
400aea2b13 | ||
|
|
273f616930 | ||
|
|
ab37269c38 | ||
|
|
70ff3511bc | ||
|
|
b978abbf02 | ||
|
|
d0b58d5300 | ||
|
|
e24d93762f | ||
|
|
35cabcb505 | ||
|
|
3ab98cef4e | ||
|
|
144b0094c3 |
@@ -1 +1,68 @@
|
||||
Check the 'doc' directory. doc/README.html is a good starting point :)
|
||||
Where to find complete Redis documentation?
|
||||
-------------------------------------------
|
||||
|
||||
This README is just a fast "quick start" document. You can find more detailed
|
||||
documentation here:
|
||||
|
||||
1) http://code.google.com/p/redis
|
||||
2) Check the 'doc' directory. doc/README.html is a good starting point :)
|
||||
|
||||
Building Redis
|
||||
--------------
|
||||
|
||||
It is as simple as:
|
||||
|
||||
% make
|
||||
|
||||
Redis is just a single binary, but if you want to install it you can use
|
||||
the "make install" target that will copy the binary in /usr/local/bin
|
||||
for default.
|
||||
|
||||
You can run a 32 bit Redis binary using:
|
||||
|
||||
% make 32bit
|
||||
|
||||
After you build Redis is a good idea to test it, using:
|
||||
|
||||
% make test
|
||||
|
||||
Running Redis
|
||||
-------------
|
||||
|
||||
To run Redis with the default configuration just type:
|
||||
|
||||
% cd src
|
||||
% ./redis-server
|
||||
|
||||
If you want to provide your redis.conf, you have to run it using an additional
|
||||
parameter (the path of the configuration file):
|
||||
|
||||
% cd src
|
||||
% ./redis-server /path/to/redis.conf
|
||||
|
||||
Playing with Redis
|
||||
------------------
|
||||
|
||||
You can use redis-cli to play with Redis. Start a redis-server instance,
|
||||
then in another terminal try the following:
|
||||
|
||||
% cd src
|
||||
% ./redis-cli
|
||||
redis> ping
|
||||
PONG
|
||||
redis> set foo bar
|
||||
OK
|
||||
redis> get foo
|
||||
"bar"
|
||||
redis> incr mycounter
|
||||
(integer) 1
|
||||
redis> incr mycounter
|
||||
(integer) 2
|
||||
redis>
|
||||
|
||||
You can find the list of all the available commands here:
|
||||
|
||||
http://code.google.com/p/redis/wiki/CommandReference
|
||||
|
||||
Enjoy!
|
||||
|
||||
|
||||
+43
-5
@@ -19,7 +19,7 @@ INSTALL_TOP= /usr/local
|
||||
INSTALL_BIN= $(INSTALL_TOP)/bin
|
||||
INSTALL= cp -p
|
||||
|
||||
OBJ = adlist.o ae.o anet.o dict.o redis.o sds.o zmalloc.o lzf_c.o lzf_d.o pqsort.o zipmap.o sha1.o ziplist.o release.o networking.o util.o object.o db.o replication.o rdb.o t_string.o t_list.o t_set.o t_zset.o t_hash.o config.o aof.o vm.o pubsub.o multi.o debug.o sort.o
|
||||
OBJ = adlist.o ae.o anet.o dict.o redis.o sds.o zmalloc.o lzf_c.o lzf_d.o pqsort.o zipmap.o sha1.o ziplist.o release.o networking.o util.o object.o db.o replication.o rdb.o t_string.o t_list.o t_set.o t_zset.o t_hash.o config.o aof.o vm.o pubsub.o multi.o debug.o sort.o intset.o
|
||||
BENCHOBJ = ae.o anet.o redis-benchmark.o sds.o adlist.o zmalloc.o
|
||||
CLIOBJ = anet.o sds.o adlist.o redis-cli.o zmalloc.o linenoise.o
|
||||
CHECKDUMPOBJ = redis-check-dump.o lzf_c.o lzf_d.o
|
||||
@@ -33,6 +33,7 @@ CHECKAOFPRGNAME = redis-check-aof
|
||||
|
||||
all: redis-server redis-benchmark redis-cli redis-check-dump redis-check-aof
|
||||
|
||||
|
||||
# Deps (use make dep to generate this)
|
||||
adlist.o: adlist.c adlist.h zmalloc.h
|
||||
ae.o: ae.c ae.h zmalloc.h config.h ae_kqueue.c
|
||||
@@ -40,22 +41,59 @@ ae_epoll.o: ae_epoll.c
|
||||
ae_kqueue.o: ae_kqueue.c
|
||||
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
|
||||
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 \
|
||||
zmalloc.h anet.h zipmap.h ziplist.h intset.h version.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
|
||||
intset.o: intset.c intset.h zmalloc.h
|
||||
linenoise.o: linenoise.c fmacros.h
|
||||
lzf_c.o: lzf_c.c lzfP.h
|
||||
lzf_d.o: lzf_d.c lzfP.h
|
||||
multi.o: multi.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
|
||||
networking.o: networking.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
|
||||
object.o: object.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.o: pqsort.c
|
||||
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-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 anet.h sds.h adlist.h zmalloc.h \
|
||||
linenoise.h
|
||||
redis.o: redis.c fmacros.h config.h redis.h ae.h sds.h anet.h dict.h \
|
||||
adlist.h zmalloc.h lzf.h pqsort.h zipmap.h ziplist.h sha1.h
|
||||
redis-cli.o: redis-cli.c fmacros.h version.h anet.h sds.h adlist.h \
|
||||
zmalloc.h linenoise.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
|
||||
replication.o: replication.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
|
||||
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
|
||||
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 \
|
||||
zmalloc.h anet.h zipmap.h ziplist.h intset.h version.h
|
||||
t_set.o: t_set.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_string.o: t_string.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_zset.o: t_zset.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
|
||||
util.o: util.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
|
||||
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
|
||||
|
||||
@@ -463,20 +463,30 @@ int rewriteAppendOnlyFile(char *filename) {
|
||||
redisPanic("Unknown list encoding");
|
||||
}
|
||||
} else if (o->type == REDIS_SET) {
|
||||
char cmd[]="*3\r\n$4\r\nSADD\r\n";
|
||||
|
||||
/* Emit the SADDs needed to rebuild the set */
|
||||
dict *set = o->ptr;
|
||||
dictIterator *di = dictGetIterator(set);
|
||||
dictEntry *de;
|
||||
|
||||
while((de = dictNext(di)) != NULL) {
|
||||
char cmd[]="*3\r\n$4\r\nSADD\r\n";
|
||||
robj *eleobj = dictGetEntryKey(de);
|
||||
|
||||
if (fwrite(cmd,sizeof(cmd)-1,1,fp) == 0) goto werr;
|
||||
if (fwriteBulkObject(fp,&key) == 0) goto werr;
|
||||
if (fwriteBulkObject(fp,eleobj) == 0) goto werr;
|
||||
if (o->encoding == REDIS_ENCODING_INTSET) {
|
||||
int ii = 0;
|
||||
int64_t llval;
|
||||
while(intsetGet(o->ptr,ii++,&llval)) {
|
||||
if (fwrite(cmd,sizeof(cmd)-1,1,fp) == 0) goto werr;
|
||||
if (fwriteBulkObject(fp,&key) == 0) goto werr;
|
||||
if (fwriteBulkLongLong(fp,llval) == 0) goto werr;
|
||||
}
|
||||
} else if (o->encoding == REDIS_ENCODING_HT) {
|
||||
dictIterator *di = dictGetIterator(o->ptr);
|
||||
dictEntry *de;
|
||||
while((de = dictNext(di)) != NULL) {
|
||||
robj *eleobj = dictGetEntryKey(de);
|
||||
if (fwrite(cmd,sizeof(cmd)-1,1,fp) == 0) goto werr;
|
||||
if (fwriteBulkObject(fp,&key) == 0) goto werr;
|
||||
if (fwriteBulkObject(fp,eleobj) == 0) goto werr;
|
||||
}
|
||||
dictReleaseIterator(di);
|
||||
} else {
|
||||
redisPanic("Unknown set encoding");
|
||||
}
|
||||
dictReleaseIterator(di);
|
||||
} else if (o->type == REDIS_ZSET) {
|
||||
/* Emit the ZADDs needed to rebuild the sorted set */
|
||||
zset *zs = o->ptr;
|
||||
|
||||
@@ -199,6 +199,8 @@ void loadServerConfig(char *filename) {
|
||||
server.list_max_ziplist_entries = memtoll(argv[1], NULL);
|
||||
} else if (!strcasecmp(argv[0],"list-max-ziplist-value") && argc == 2){
|
||||
server.list_max_ziplist_value = memtoll(argv[1], NULL);
|
||||
} else if (!strcasecmp(argv[0],"set-max-intset-entries") && argc == 2){
|
||||
server.set_max_intset_entries = memtoll(argv[1], NULL);
|
||||
} else {
|
||||
err = "Bad directive or wrong number of arguments"; goto loaderr;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ robj *lookupKeyRead(redisDb *db, robj *key) {
|
||||
}
|
||||
|
||||
robj *lookupKeyWrite(redisDb *db, robj *key) {
|
||||
deleteIfVolatile(db,key);
|
||||
expireIfNeeded(db,key);
|
||||
return lookupKey(db,key);
|
||||
}
|
||||
|
||||
@@ -221,19 +221,19 @@ void keysCommand(redisClient *c) {
|
||||
dictIterator *di;
|
||||
dictEntry *de;
|
||||
sds pattern = c->argv[1]->ptr;
|
||||
int plen = sdslen(pattern);
|
||||
int plen = sdslen(pattern), allkeys;
|
||||
unsigned long numkeys = 0;
|
||||
robj *lenobj = createObject(REDIS_STRING,NULL);
|
||||
|
||||
di = dictGetIterator(c->db->dict);
|
||||
addReply(c,lenobj);
|
||||
decrRefCount(lenobj);
|
||||
allkeys = (pattern[0] == '*' && pattern[1] == '\0');
|
||||
while((de = dictNext(di)) != NULL) {
|
||||
sds key = dictGetEntryKey(de);
|
||||
robj *keyobj;
|
||||
|
||||
if ((pattern[0] == '*' && pattern[1] == '\0') ||
|
||||
stringmatchlen(pattern,plen,key,sdslen(key),0)) {
|
||||
if (allkeys || stringmatchlen(pattern,plen,key,sdslen(key),0)) {
|
||||
keyobj = createStringObject(key,sdslen(key));
|
||||
if (expireIfNeeded(c->db,keyobj) == 0) {
|
||||
addReplyBulk(c,keyobj);
|
||||
@@ -321,7 +321,6 @@ void renameGenericCommand(redisClient *c, int nx) {
|
||||
return;
|
||||
|
||||
incrRefCount(o);
|
||||
deleteIfVolatile(c->db,c->argv[2]);
|
||||
if (dbAdd(c->db,c->argv[2],o) == REDIS_ERR) {
|
||||
if (nx) {
|
||||
decrRefCount(o);
|
||||
@@ -375,7 +374,6 @@ void moveCommand(redisClient *c) {
|
||||
}
|
||||
|
||||
/* Try to add the element to the target DB */
|
||||
deleteIfVolatile(dst,c->argv[1]);
|
||||
if (dbAdd(dst,c->argv[1],o) == REDIS_ERR) {
|
||||
addReply(c,shared.czero);
|
||||
return;
|
||||
@@ -396,23 +394,16 @@ int removeExpire(redisDb *db, robj *key) {
|
||||
/* An expire may only be removed if there is a corresponding entry in the
|
||||
* main dict. Otherwise, the key will never be freed. */
|
||||
redisAssert(dictFind(db->dict,key->ptr) != NULL);
|
||||
if (dictDelete(db->expires,key->ptr) == DICT_OK) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
return dictDelete(db->expires,key->ptr) == DICT_OK;
|
||||
}
|
||||
|
||||
int setExpire(redisDb *db, robj *key, time_t when) {
|
||||
void setExpire(redisDb *db, robj *key, time_t when) {
|
||||
dictEntry *de;
|
||||
|
||||
/* Reuse the sds from the main dict in the expire dict */
|
||||
redisAssert((de = dictFind(db->dict,key->ptr)) != NULL);
|
||||
if (dictAdd(db->expires,dictGetEntryKey(de),(void*)when) == DICT_ERR) {
|
||||
return 0;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
de = dictFind(db->dict,key->ptr);
|
||||
redisAssert(de != NULL);
|
||||
dictReplace(db->expires,dictGetEntryKey(de),(void*)when);
|
||||
}
|
||||
|
||||
/* Return the expire time of the specified key, or -1 if no expire
|
||||
@@ -430,8 +421,46 @@ time_t getExpire(redisDb *db, robj *key) {
|
||||
return (time_t) dictGetEntryVal(de);
|
||||
}
|
||||
|
||||
/* Propagate expires into slaves and the AOF file.
|
||||
* When a key expires in the master, a DEL operation for this key is sent
|
||||
* to all the slaves and the AOF file if enabled.
|
||||
*
|
||||
* This way the key expiry is centralized in one place, and since both
|
||||
* AOF and the master->slave link guarantee operation ordering, everything
|
||||
* will be consistent even if we allow write operations against expiring
|
||||
* keys. */
|
||||
void propagateExpire(redisDb *db, robj *key) {
|
||||
struct redisCommand *cmd;
|
||||
robj *argv[2];
|
||||
|
||||
cmd = lookupCommand("del");
|
||||
argv[0] = createStringObject("DEL",3);
|
||||
argv[1] = key;
|
||||
incrRefCount(key);
|
||||
|
||||
if (server.appendonly)
|
||||
feedAppendOnlyFile(cmd,db->id,argv,2);
|
||||
if (listLength(server.slaves))
|
||||
replicationFeedSlaves(server.slaves,db->id,argv,2);
|
||||
|
||||
decrRefCount(argv[0]);
|
||||
decrRefCount(argv[1]);
|
||||
}
|
||||
|
||||
int expireIfNeeded(redisDb *db, robj *key) {
|
||||
time_t when = getExpire(db,key);
|
||||
|
||||
/* If we are running in the context of a slave, return ASAP:
|
||||
* the slave key expiration is controlled by the master that will
|
||||
* send us synthesized DEL operations for expired keys.
|
||||
*
|
||||
* Still we try to return the right information to the caller,
|
||||
* that is, 0 if we think the key should be still valid, 1 if
|
||||
* we think the key is expired at this time. */
|
||||
if (server.masterhost != NULL) {
|
||||
return time(NULL) > when;
|
||||
}
|
||||
|
||||
if (when < 0) return 0;
|
||||
|
||||
/* Return when this key has not expired */
|
||||
@@ -440,15 +469,7 @@ int expireIfNeeded(redisDb *db, robj *key) {
|
||||
/* Delete the key */
|
||||
server.stat_expiredkeys++;
|
||||
server.dirty++;
|
||||
return dbDelete(db,key);
|
||||
}
|
||||
|
||||
int deleteIfVolatile(redisDb *db, robj *key) {
|
||||
if (getExpire(db,key) < 0) return 0;
|
||||
|
||||
/* Delete the key */
|
||||
server.stat_expiredkeys++;
|
||||
server.dirty++;
|
||||
propagateExpire(db,key);
|
||||
return dbDelete(db,key);
|
||||
}
|
||||
|
||||
@@ -476,13 +497,10 @@ void expireGenericCommand(redisClient *c, robj *key, robj *param, long offset) {
|
||||
return;
|
||||
} else {
|
||||
time_t when = time(NULL)+seconds;
|
||||
if (setExpire(c->db,key,when)) {
|
||||
addReply(c,shared.cone);
|
||||
touchWatchedKey(c->db,key);
|
||||
server.dirty++;
|
||||
} else {
|
||||
addReply(c,shared.czero);
|
||||
}
|
||||
setExpire(c->db,key,when);
|
||||
addReply(c,shared.cone);
|
||||
touchWatchedKey(c->db,key);
|
||||
server.dirty++;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -496,13 +514,28 @@ void expireatCommand(redisClient *c) {
|
||||
}
|
||||
|
||||
void ttlCommand(redisClient *c) {
|
||||
time_t expire;
|
||||
int ttl = -1;
|
||||
time_t expire, ttl = -1;
|
||||
|
||||
expire = getExpire(c->db,c->argv[1]);
|
||||
if (expire != -1) {
|
||||
ttl = (int) (expire-time(NULL));
|
||||
ttl = (expire-time(NULL));
|
||||
if (ttl < 0) ttl = -1;
|
||||
}
|
||||
addReplySds(c,sdscatprintf(sdsempty(),":%d\r\n",ttl));
|
||||
addReplyLongLong(c,(long long)ttl);
|
||||
}
|
||||
|
||||
void persistCommand(redisClient *c) {
|
||||
dictEntry *de;
|
||||
|
||||
de = dictFind(c->db->dict,c->argv[1]->ptr);
|
||||
if (de == NULL) {
|
||||
addReply(c,shared.czero);
|
||||
} else {
|
||||
if (removeExpire(c->db,c->argv[1])) {
|
||||
addReply(c,shared.cone);
|
||||
server.dirty++;
|
||||
} else {
|
||||
addReply(c,shared.czero);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-9
@@ -119,16 +119,13 @@ void computeDatasetDigest(unsigned char *final) {
|
||||
}
|
||||
listTypeReleaseIterator(li);
|
||||
} else if (o->type == REDIS_SET) {
|
||||
dict *set = o->ptr;
|
||||
dictIterator *di = dictGetIterator(set);
|
||||
dictEntry *de;
|
||||
|
||||
while((de = dictNext(di)) != NULL) {
|
||||
robj *eleobj = dictGetEntryKey(de);
|
||||
|
||||
xorObjectDigest(digest,eleobj);
|
||||
setTypeIterator *si = setTypeInitIterator(o);
|
||||
robj *ele;
|
||||
while((ele = setTypeNext(si)) != NULL) {
|
||||
xorObjectDigest(digest,ele);
|
||||
decrRefCount(ele);
|
||||
}
|
||||
dictReleaseIterator(di);
|
||||
setTypeReleaseIterator(si);
|
||||
} else if (o->type == REDIS_ZSET) {
|
||||
zset *zs = o->ptr;
|
||||
dictIterator *di = dictGetIterator(zs->dict);
|
||||
|
||||
+422
@@ -0,0 +1,422 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "intset.h"
|
||||
#include "zmalloc.h"
|
||||
|
||||
/* Note that these encodings are ordered, so:
|
||||
* INTSET_ENC_INT16 < INTSET_ENC_INT32 < INTSET_ENC_INT64. */
|
||||
#define INTSET_ENC_INT16 (sizeof(int16_t))
|
||||
#define INTSET_ENC_INT32 (sizeof(int32_t))
|
||||
#define INTSET_ENC_INT64 (sizeof(int64_t))
|
||||
|
||||
/* Return the required encoding for the provided value. */
|
||||
static uint8_t _intsetValueEncoding(int64_t v) {
|
||||
if (v < INT32_MIN || v > INT32_MAX)
|
||||
return INTSET_ENC_INT64;
|
||||
else if (v < INT16_MIN || v > INT16_MAX)
|
||||
return INTSET_ENC_INT32;
|
||||
return INTSET_ENC_INT16;
|
||||
}
|
||||
|
||||
/* Return the value at pos, given an encoding. */
|
||||
static int64_t _intsetGetEncoded(intset *is, int pos, uint8_t enc) {
|
||||
if (enc == INTSET_ENC_INT64)
|
||||
return ((int64_t*)is->contents)[pos];
|
||||
else if (enc == INTSET_ENC_INT32)
|
||||
return ((int32_t*)is->contents)[pos];
|
||||
return ((int16_t*)is->contents)[pos];
|
||||
}
|
||||
|
||||
/* Return the value at pos, using the configured encoding. */
|
||||
static int64_t _intsetGet(intset *is, int pos) {
|
||||
return _intsetGetEncoded(is,pos,is->encoding);
|
||||
}
|
||||
|
||||
/* Set the value at pos, using the configured encoding. */
|
||||
static void _intsetSet(intset *is, int pos, int64_t value) {
|
||||
if (is->encoding == INTSET_ENC_INT64)
|
||||
((int64_t*)is->contents)[pos] = value;
|
||||
else if (is->encoding == INTSET_ENC_INT32)
|
||||
((int32_t*)is->contents)[pos] = value;
|
||||
else
|
||||
((int16_t*)is->contents)[pos] = value;
|
||||
}
|
||||
|
||||
/* Create an empty intset. */
|
||||
intset *intsetNew(void) {
|
||||
intset *is = zmalloc(sizeof(intset));
|
||||
is->encoding = INTSET_ENC_INT16;
|
||||
is->length = 0;
|
||||
return is;
|
||||
}
|
||||
|
||||
/* Resize the intset */
|
||||
static intset *intsetResize(intset *is, uint32_t len) {
|
||||
uint32_t size = len*is->encoding;
|
||||
is = zrealloc(is,sizeof(intset)+size);
|
||||
return is;
|
||||
}
|
||||
|
||||
/* Search for the position of "value". Return 1 when the value was found and
|
||||
* sets "pos" to the position of the value within the intset. Return 0 when
|
||||
* the value is not present in the intset and sets "pos" to the position
|
||||
* where "value" can be inserted. */
|
||||
static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {
|
||||
int min = 0, max = is->length-1, mid = -1;
|
||||
int64_t cur = -1;
|
||||
|
||||
/* The value can never be found when the set is empty */
|
||||
if (is->length == 0) {
|
||||
if (pos) *pos = 0;
|
||||
return 0;
|
||||
} else {
|
||||
/* Check for the case where we know we cannot find the value,
|
||||
* but do know the insert position. */
|
||||
if (value > _intsetGet(is,is->length-1)) {
|
||||
if (pos) *pos = is->length;
|
||||
return 0;
|
||||
} else if (value < _intsetGet(is,0)) {
|
||||
if (pos) *pos = 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
while(max >= min) {
|
||||
mid = (min+max)/2;
|
||||
cur = _intsetGet(is,mid);
|
||||
if (value > cur) {
|
||||
min = mid+1;
|
||||
} else if (value < cur) {
|
||||
max = mid-1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (value == cur) {
|
||||
if (pos) *pos = mid;
|
||||
return 1;
|
||||
} else {
|
||||
if (pos) *pos = min;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Upgrades the intset to a larger encoding and inserts the given integer. */
|
||||
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
|
||||
uint8_t curenc = is->encoding;
|
||||
uint8_t newenc = _intsetValueEncoding(value);
|
||||
int length = is->length;
|
||||
int prepend = value < 0 ? 1 : 0;
|
||||
|
||||
/* First set new encoding and resize */
|
||||
is->encoding = newenc;
|
||||
is = intsetResize(is,is->length+1);
|
||||
|
||||
/* Upgrade back-to-front so we don't overwrite values.
|
||||
* Note that the "prepend" variable is used to make sure we have an empty
|
||||
* space at either the beginning or the end of the intset. */
|
||||
while(length--)
|
||||
_intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
|
||||
|
||||
/* Set the value at the beginning or the end. */
|
||||
if (prepend)
|
||||
_intsetSet(is,0,value);
|
||||
else
|
||||
_intsetSet(is,is->length,value);
|
||||
is->length++;
|
||||
return is;
|
||||
}
|
||||
|
||||
static void intsetMoveTail(intset *is, uint32_t from, uint32_t to) {
|
||||
void *src, *dst;
|
||||
uint32_t bytes = is->length-from;
|
||||
if (is->encoding == INTSET_ENC_INT64) {
|
||||
src = (int64_t*)is->contents+from;
|
||||
dst = (int64_t*)is->contents+to;
|
||||
bytes *= sizeof(int64_t);
|
||||
} else if (is->encoding == INTSET_ENC_INT32) {
|
||||
src = (int32_t*)is->contents+from;
|
||||
dst = (int32_t*)is->contents+to;
|
||||
bytes *= sizeof(int32_t);
|
||||
} else {
|
||||
src = (int16_t*)is->contents+from;
|
||||
dst = (int16_t*)is->contents+to;
|
||||
bytes *= sizeof(int16_t);
|
||||
}
|
||||
memmove(dst,src,bytes);
|
||||
}
|
||||
|
||||
/* Insert an integer in the intset */
|
||||
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
|
||||
uint8_t valenc = _intsetValueEncoding(value);
|
||||
uint32_t pos;
|
||||
if (success) *success = 1;
|
||||
|
||||
/* Upgrade encoding if necessary. If we need to upgrade, we know that
|
||||
* this value should be either appended (if > 0) or prepended (if < 0),
|
||||
* because it lies outside the range of existing values. */
|
||||
if (valenc > is->encoding) {
|
||||
/* This always succeeds, so we don't need to curry *success. */
|
||||
return intsetUpgradeAndAdd(is,value);
|
||||
} else {
|
||||
/* Abort if the value is already present in the set.
|
||||
* This call will populate "pos" with the right position to insert
|
||||
* the value when it cannot be found. */
|
||||
if (intsetSearch(is,value,&pos)) {
|
||||
if (success) *success = 0;
|
||||
return is;
|
||||
}
|
||||
|
||||
is = intsetResize(is,is->length+1);
|
||||
if (pos < is->length) intsetMoveTail(is,pos,pos+1);
|
||||
}
|
||||
|
||||
_intsetSet(is,pos,value);
|
||||
is->length++;
|
||||
return is;
|
||||
}
|
||||
|
||||
/* Delete integer from intset */
|
||||
intset *intsetRemove(intset *is, int64_t value, uint8_t *success) {
|
||||
uint8_t valenc = _intsetValueEncoding(value);
|
||||
uint32_t pos;
|
||||
if (success) *success = 0;
|
||||
|
||||
if (valenc <= is->encoding && intsetSearch(is,value,&pos)) {
|
||||
/* We know we can delete */
|
||||
if (success) *success = 1;
|
||||
|
||||
/* Overwrite value with tail and update length */
|
||||
if (pos < (is->length-1)) intsetMoveTail(is,pos+1,pos);
|
||||
is = intsetResize(is,is->length-1);
|
||||
is->length--;
|
||||
}
|
||||
return is;
|
||||
}
|
||||
|
||||
/* Determine whether a value belongs to this set */
|
||||
uint8_t intsetFind(intset *is, int64_t value) {
|
||||
uint8_t valenc = _intsetValueEncoding(value);
|
||||
return valenc <= is->encoding && intsetSearch(is,value,NULL);
|
||||
}
|
||||
|
||||
/* Return random member */
|
||||
int64_t intsetRandom(intset *is) {
|
||||
return _intsetGet(is,rand()%is->length);
|
||||
}
|
||||
|
||||
/* Sets the value to the value at the given position. When this position is
|
||||
* out of range the function returns 0, when in range it returns 1. */
|
||||
uint8_t intsetGet(intset *is, uint32_t pos, int64_t *value) {
|
||||
if (pos < is->length) {
|
||||
*value = _intsetGet(is,pos);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Return intset length */
|
||||
uint32_t intsetLen(intset *is) {
|
||||
return is->length;
|
||||
}
|
||||
|
||||
#ifdef INTSET_TEST_MAIN
|
||||
#include <sys/time.h>
|
||||
|
||||
void intsetRepr(intset *is) {
|
||||
int i;
|
||||
for (i = 0; i < is->length; i++) {
|
||||
printf("%lld\n", (uint64_t)_intsetGet(is,i));
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
void error(char *err) {
|
||||
printf("%s\n", err);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
void ok(void) {
|
||||
printf("OK\n");
|
||||
}
|
||||
|
||||
long long usec(void) {
|
||||
struct timeval tv;
|
||||
gettimeofday(&tv,NULL);
|
||||
return (((long long)tv.tv_sec)*1000000)+tv.tv_usec;
|
||||
}
|
||||
|
||||
#define assert(_e) ((_e)?(void)0:(_assert(#_e,__FILE__,__LINE__),exit(1)))
|
||||
void _assert(char *estr, char *file, int line) {
|
||||
printf("\n\n=== ASSERTION FAILED ===\n");
|
||||
printf("==> %s:%d '%s' is not true\n",file,line,estr);
|
||||
}
|
||||
|
||||
intset *createSet(int bits, int size) {
|
||||
uint64_t mask = (1<<bits)-1;
|
||||
uint64_t i, value;
|
||||
intset *is = intsetNew();
|
||||
|
||||
for (i = 0; i < size; i++) {
|
||||
if (bits > 32) {
|
||||
value = (rand()*rand()) & mask;
|
||||
} else {
|
||||
value = rand() & mask;
|
||||
}
|
||||
is = intsetAdd(is,value,NULL);
|
||||
}
|
||||
return is;
|
||||
}
|
||||
|
||||
void checkConsistency(intset *is) {
|
||||
int i;
|
||||
|
||||
for (i = 0; i < (is->length-1); i++) {
|
||||
if (is->encoding == INTSET_ENC_INT16) {
|
||||
int16_t *i16 = (int16_t*)is->contents;
|
||||
assert(i16[i] < i16[i+1]);
|
||||
} else if (is->encoding == INTSET_ENC_INT32) {
|
||||
int32_t *i32 = (int32_t*)is->contents;
|
||||
assert(i32[i] < i32[i+1]);
|
||||
} else {
|
||||
int64_t *i64 = (int64_t*)is->contents;
|
||||
assert(i64[i] < i64[i+1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
uint8_t success;
|
||||
int i;
|
||||
intset *is;
|
||||
sranddev();
|
||||
|
||||
printf("Value encodings: "); {
|
||||
assert(_intsetValueEncoding(-32768) == INTSET_ENC_INT16);
|
||||
assert(_intsetValueEncoding(+32767) == INTSET_ENC_INT16);
|
||||
assert(_intsetValueEncoding(-32769) == INTSET_ENC_INT32);
|
||||
assert(_intsetValueEncoding(+32768) == INTSET_ENC_INT32);
|
||||
assert(_intsetValueEncoding(-2147483648) == INTSET_ENC_INT32);
|
||||
assert(_intsetValueEncoding(+2147483647) == INTSET_ENC_INT32);
|
||||
assert(_intsetValueEncoding(-2147483649) == INTSET_ENC_INT64);
|
||||
assert(_intsetValueEncoding(+2147483648) == INTSET_ENC_INT64);
|
||||
assert(_intsetValueEncoding(-9223372036854775808ull) == INTSET_ENC_INT64);
|
||||
assert(_intsetValueEncoding(+9223372036854775807ull) == INTSET_ENC_INT64);
|
||||
ok();
|
||||
}
|
||||
|
||||
printf("Basic adding: "); {
|
||||
is = intsetNew();
|
||||
is = intsetAdd(is,5,&success); assert(success);
|
||||
is = intsetAdd(is,6,&success); assert(success);
|
||||
is = intsetAdd(is,4,&success); assert(success);
|
||||
is = intsetAdd(is,4,&success); assert(!success);
|
||||
ok();
|
||||
}
|
||||
|
||||
printf("Large number of random adds: "); {
|
||||
int inserts = 0;
|
||||
is = intsetNew();
|
||||
for (i = 0; i < 1024; i++) {
|
||||
is = intsetAdd(is,rand()%0x800,&success);
|
||||
if (success) inserts++;
|
||||
}
|
||||
assert(is->length == inserts);
|
||||
checkConsistency(is);
|
||||
ok();
|
||||
}
|
||||
|
||||
printf("Upgrade from int16 to int32: "); {
|
||||
is = intsetNew();
|
||||
is = intsetAdd(is,32,NULL);
|
||||
assert(is->encoding == INTSET_ENC_INT16);
|
||||
is = intsetAdd(is,65535,NULL);
|
||||
assert(is->encoding == INTSET_ENC_INT32);
|
||||
assert(intsetFind(is,32));
|
||||
assert(intsetFind(is,65535));
|
||||
checkConsistency(is);
|
||||
|
||||
is = intsetNew();
|
||||
is = intsetAdd(is,32,NULL);
|
||||
assert(is->encoding == INTSET_ENC_INT16);
|
||||
is = intsetAdd(is,-65535,NULL);
|
||||
assert(is->encoding == INTSET_ENC_INT32);
|
||||
assert(intsetFind(is,32));
|
||||
assert(intsetFind(is,-65535));
|
||||
checkConsistency(is);
|
||||
ok();
|
||||
}
|
||||
|
||||
printf("Upgrade from int16 to int64: "); {
|
||||
is = intsetNew();
|
||||
is = intsetAdd(is,32,NULL);
|
||||
assert(is->encoding == INTSET_ENC_INT16);
|
||||
is = intsetAdd(is,4294967295,NULL);
|
||||
assert(is->encoding == INTSET_ENC_INT64);
|
||||
assert(intsetFind(is,32));
|
||||
assert(intsetFind(is,4294967295));
|
||||
checkConsistency(is);
|
||||
|
||||
is = intsetNew();
|
||||
is = intsetAdd(is,32,NULL);
|
||||
assert(is->encoding == INTSET_ENC_INT16);
|
||||
is = intsetAdd(is,-4294967295,NULL);
|
||||
assert(is->encoding == INTSET_ENC_INT64);
|
||||
assert(intsetFind(is,32));
|
||||
assert(intsetFind(is,-4294967295));
|
||||
checkConsistency(is);
|
||||
ok();
|
||||
}
|
||||
|
||||
printf("Upgrade from int32 to int64: "); {
|
||||
is = intsetNew();
|
||||
is = intsetAdd(is,65535,NULL);
|
||||
assert(is->encoding == INTSET_ENC_INT32);
|
||||
is = intsetAdd(is,4294967295,NULL);
|
||||
assert(is->encoding == INTSET_ENC_INT64);
|
||||
assert(intsetFind(is,65535));
|
||||
assert(intsetFind(is,4294967295));
|
||||
checkConsistency(is);
|
||||
|
||||
is = intsetNew();
|
||||
is = intsetAdd(is,65535,NULL);
|
||||
assert(is->encoding == INTSET_ENC_INT32);
|
||||
is = intsetAdd(is,-4294967295,NULL);
|
||||
assert(is->encoding == INTSET_ENC_INT64);
|
||||
assert(intsetFind(is,65535));
|
||||
assert(intsetFind(is,-4294967295));
|
||||
checkConsistency(is);
|
||||
ok();
|
||||
}
|
||||
|
||||
printf("Stress lookups: "); {
|
||||
long num = 100000, size = 10000;
|
||||
int i, bits = 20;
|
||||
long long start;
|
||||
is = createSet(bits,size);
|
||||
checkConsistency(is);
|
||||
|
||||
start = usec();
|
||||
for (i = 0; i < num; i++) intsetSearch(is,rand() % ((1<<bits)-1),NULL);
|
||||
printf("%ld lookups, %ld element set, %lldusec\n",num,size,usec()-start);
|
||||
}
|
||||
|
||||
printf("Stress add+delete: "); {
|
||||
int i, v1, v2;
|
||||
is = intsetNew();
|
||||
for (i = 0; i < 0xffff; i++) {
|
||||
v1 = rand() % 0xfff;
|
||||
is = intsetAdd(is,v1,NULL);
|
||||
assert(intsetFind(is,v1));
|
||||
|
||||
v2 = rand() % 0xfff;
|
||||
is = intsetRemove(is,v2,NULL);
|
||||
assert(!intsetFind(is,v2));
|
||||
}
|
||||
checkConsistency(is);
|
||||
ok();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,19 @@
|
||||
#ifndef __INTSET_H
|
||||
#define __INTSET_H
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct intset {
|
||||
uint32_t encoding;
|
||||
uint32_t length;
|
||||
int8_t contents[];
|
||||
} intset;
|
||||
|
||||
intset *intsetNew(void);
|
||||
intset *intsetAdd(intset *is, int64_t value, uint8_t *success);
|
||||
intset *intsetRemove(intset *is, int64_t value, uint8_t *success);
|
||||
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);
|
||||
|
||||
#endif // __INTSET_H
|
||||
+14
-1
@@ -255,7 +255,8 @@ void freeClient(redisClient *c) {
|
||||
server.vm_blocked_clients--;
|
||||
}
|
||||
listRelease(c->io_keys);
|
||||
/* Master/slave cleanup */
|
||||
/* Master/slave cleanup.
|
||||
* Case 1: we lost the connection with a slave. */
|
||||
if (c->flags & REDIS_SLAVE) {
|
||||
if (c->replstate == REDIS_REPL_SEND_BULK && c->repldbfd != -1)
|
||||
close(c->repldbfd);
|
||||
@@ -264,9 +265,20 @@ void freeClient(redisClient *c) {
|
||||
redisAssert(ln != NULL);
|
||||
listDelNode(l,ln);
|
||||
}
|
||||
|
||||
/* Case 2: we lost the connection with the master. */
|
||||
if (c->flags & REDIS_MASTER) {
|
||||
server.master = NULL;
|
||||
server.replstate = REDIS_REPL_CONNECT;
|
||||
/* Since we lost the connection with the master, we should also
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
/* Release memory */
|
||||
zfree(c->argv);
|
||||
@@ -466,6 +478,7 @@ void closeTimedoutClients(void) {
|
||||
if (server.maxidletime &&
|
||||
!(c->flags & REDIS_SLAVE) && /* no timeout for slaves */
|
||||
!(c->flags & REDIS_MASTER) && /* no timeout for masters */
|
||||
!(c->flags & REDIS_BLOCKED) && /* no timeout for BLPOP */
|
||||
dictSize(c->pubsub_channels) == 0 && /* no timeout for pubsub */
|
||||
listLength(c->pubsub_patterns) == 0 &&
|
||||
(now - c->lastinteraction > server.maxidletime))
|
||||
|
||||
+25
-4
@@ -74,7 +74,16 @@ robj *createZiplistObject(void) {
|
||||
|
||||
robj *createSetObject(void) {
|
||||
dict *d = dictCreate(&setDictType,NULL);
|
||||
return createObject(REDIS_SET,d);
|
||||
robj *o = createObject(REDIS_SET,d);
|
||||
o->encoding = REDIS_ENCODING_HT;
|
||||
return o;
|
||||
}
|
||||
|
||||
robj *createIntsetObject(void) {
|
||||
intset *is = intsetNew();
|
||||
robj *o = createObject(REDIS_SET,is);
|
||||
o->encoding = REDIS_ENCODING_INTSET;
|
||||
return o;
|
||||
}
|
||||
|
||||
robj *createHashObject(void) {
|
||||
@@ -115,7 +124,16 @@ void freeListObject(robj *o) {
|
||||
}
|
||||
|
||||
void freeSetObject(robj *o) {
|
||||
dictRelease((dict*) o->ptr);
|
||||
switch (o->encoding) {
|
||||
case REDIS_ENCODING_HT:
|
||||
dictRelease((dict*) o->ptr);
|
||||
break;
|
||||
case REDIS_ENCODING_INTSET:
|
||||
zfree(o->ptr);
|
||||
break;
|
||||
default:
|
||||
redisPanic("Unknown set encoding type");
|
||||
}
|
||||
}
|
||||
|
||||
void freeZsetObject(robj *o) {
|
||||
@@ -358,6 +376,8 @@ int getLongLongFromObject(robj *o, long long *target) {
|
||||
if (o->encoding == REDIS_ENCODING_RAW) {
|
||||
value = strtoll(o->ptr, &eptr, 10);
|
||||
if (eptr[0] != '\0') return REDIS_ERR;
|
||||
if (errno == ERANGE && (value == LLONG_MIN || value == LLONG_MAX))
|
||||
return REDIS_ERR;
|
||||
} else if (o->encoding == REDIS_ENCODING_INT) {
|
||||
value = (long)o->ptr;
|
||||
} else {
|
||||
@@ -365,7 +385,7 @@ int getLongLongFromObject(robj *o, long long *target) {
|
||||
}
|
||||
}
|
||||
|
||||
*target = value;
|
||||
if (target) *target = value;
|
||||
return REDIS_OK;
|
||||
}
|
||||
|
||||
@@ -375,7 +395,7 @@ int getLongLongFromObjectOrReply(redisClient *c, robj *o, long long *target, con
|
||||
if (msg != NULL) {
|
||||
addReplySds(c, sdscatprintf(sdsempty(), "-ERR %s\r\n", msg));
|
||||
} else {
|
||||
addReplySds(c, sdsnew("-ERR value is not an integer\r\n"));
|
||||
addReplySds(c, sdsnew("-ERR value is not an integer or out of range\r\n"));
|
||||
}
|
||||
return REDIS_ERR;
|
||||
}
|
||||
@@ -409,6 +429,7 @@ char *strEncoding(int encoding) {
|
||||
case REDIS_ENCODING_ZIPMAP: return "zipmap";
|
||||
case REDIS_ENCODING_LINKEDLIST: return "linkedlist";
|
||||
case REDIS_ENCODING_ZIPLIST: return "ziplist";
|
||||
case REDIS_ENCODING_INTSET: return "intset";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,17 +260,29 @@ int rdbSaveObject(FILE *fp, robj *o) {
|
||||
}
|
||||
} else if (o->type == REDIS_SET) {
|
||||
/* Save a set value */
|
||||
dict *set = o->ptr;
|
||||
dictIterator *di = dictGetIterator(set);
|
||||
dictEntry *de;
|
||||
if (o->encoding == REDIS_ENCODING_HT) {
|
||||
dict *set = o->ptr;
|
||||
dictIterator *di = dictGetIterator(set);
|
||||
dictEntry *de;
|
||||
|
||||
if (rdbSaveLen(fp,dictSize(set)) == -1) return -1;
|
||||
while((de = dictNext(di)) != NULL) {
|
||||
robj *eleobj = dictGetEntryKey(de);
|
||||
if (rdbSaveLen(fp,dictSize(set)) == -1) return -1;
|
||||
while((de = dictNext(di)) != NULL) {
|
||||
robj *eleobj = dictGetEntryKey(de);
|
||||
if (rdbSaveStringObject(fp,eleobj) == -1) return -1;
|
||||
}
|
||||
dictReleaseIterator(di);
|
||||
} else if (o->encoding == REDIS_ENCODING_INTSET) {
|
||||
intset *is = o->ptr;
|
||||
int64_t llval;
|
||||
int i = 0;
|
||||
|
||||
if (rdbSaveStringObject(fp,eleobj) == -1) return -1;
|
||||
if (rdbSaveLen(fp,intsetLen(is)) == -1) return -1;
|
||||
while(intsetGet(is,i++,&llval)) {
|
||||
if (rdbSaveLongLongAsStringObject(fp,llval) == -1) return -1;
|
||||
}
|
||||
} else {
|
||||
redisPanic("Unknown set encoding");
|
||||
}
|
||||
dictReleaseIterator(di);
|
||||
} else if (o->type == REDIS_ZSET) {
|
||||
/* Save a set value */
|
||||
zset *zs = o->ptr;
|
||||
@@ -445,6 +457,7 @@ int rdbSaveBackground(char *filename) {
|
||||
|
||||
if (server.bgsavechildpid != -1) return REDIS_ERR;
|
||||
if (server.vm_enabled) waitEmptyIOJobsQueue();
|
||||
server.dirty_before_bgsave = server.dirty;
|
||||
if ((childpid = fork()) == 0) {
|
||||
/* Child */
|
||||
if (server.vm_enabled) vmReopenSwapFile();
|
||||
@@ -628,6 +641,7 @@ int rdbLoadDoubleValue(FILE *fp, double *val) {
|
||||
robj *rdbLoadObject(int type, FILE *fp) {
|
||||
robj *o, *ele, *dec;
|
||||
size_t len;
|
||||
unsigned int i;
|
||||
|
||||
redisLog(REDIS_DEBUG,"LOADING OBJECT %d (at %d)\n",type,ftell(fp));
|
||||
if (type == REDIS_STRING) {
|
||||
@@ -669,16 +683,41 @@ robj *rdbLoadObject(int type, FILE *fp) {
|
||||
} else if (type == REDIS_SET) {
|
||||
/* Read list/set value */
|
||||
if ((len = rdbLoadLen(fp,NULL)) == REDIS_RDB_LENERR) return NULL;
|
||||
o = createSetObject();
|
||||
/* It's faster to expand the dict to the right size asap in order
|
||||
* to avoid rehashing */
|
||||
if (len > DICT_HT_INITIAL_SIZE)
|
||||
dictExpand(o->ptr,len);
|
||||
|
||||
/* Use a regular set when there are too many entries. */
|
||||
if (len > server.set_max_intset_entries) {
|
||||
o = createSetObject();
|
||||
/* It's faster to expand the dict to the right size asap in order
|
||||
* to avoid rehashing */
|
||||
if (len > DICT_HT_INITIAL_SIZE)
|
||||
dictExpand(o->ptr,len);
|
||||
} else {
|
||||
o = createIntsetObject();
|
||||
}
|
||||
|
||||
/* Load every single element of the list/set */
|
||||
while(len--) {
|
||||
for (i = 0; i < len; i++) {
|
||||
long long llval;
|
||||
if ((ele = rdbLoadEncodedStringObject(fp)) == NULL) return NULL;
|
||||
ele = tryObjectEncoding(ele);
|
||||
dictAdd((dict*)o->ptr,ele,NULL);
|
||||
|
||||
if (o->encoding == REDIS_ENCODING_INTSET) {
|
||||
/* Fetch integer value from element */
|
||||
if (isObjectRepresentableAsLongLong(ele,&llval) == REDIS_OK) {
|
||||
o->ptr = intsetAdd(o->ptr,llval,NULL);
|
||||
} else {
|
||||
setTypeConvert(o,REDIS_ENCODING_HT);
|
||||
dictExpand(o->ptr,len);
|
||||
}
|
||||
}
|
||||
|
||||
/* This will also be called when the set was just converted
|
||||
* to regular hashtable encoded set */
|
||||
if (o->encoding == REDIS_ENCODING_HT) {
|
||||
dictAdd((dict*)o->ptr,ele,NULL);
|
||||
} else {
|
||||
decrRefCount(ele);
|
||||
}
|
||||
}
|
||||
} else if (type == REDIS_ZSET) {
|
||||
/* Read list/set value */
|
||||
@@ -875,7 +914,7 @@ void backgroundSaveDoneHandler(int statloc) {
|
||||
if (!bysignal && exitcode == 0) {
|
||||
redisLog(REDIS_NOTICE,
|
||||
"Background saving terminated with success");
|
||||
server.dirty = 0;
|
||||
server.dirty = server.dirty - server.dirty_before_bgsave;
|
||||
server.lastsave = time(NULL);
|
||||
} else if (!bysignal && exitcode != 0) {
|
||||
redisLog(REDIS_WARNING, "Background saving error");
|
||||
|
||||
+20
-14
@@ -65,8 +65,8 @@
|
||||
/* data type to hold offset in file and size */
|
||||
typedef struct {
|
||||
void *data;
|
||||
unsigned long size;
|
||||
unsigned long offset;
|
||||
size_t size;
|
||||
size_t offset;
|
||||
} pos;
|
||||
|
||||
static unsigned char level = 0;
|
||||
@@ -77,8 +77,8 @@ static pos positions[16];
|
||||
/* Hold a stack of errors */
|
||||
typedef struct {
|
||||
char error[16][1024];
|
||||
unsigned long offset[16];
|
||||
unsigned int level;
|
||||
size_t offset[16];
|
||||
size_t level;
|
||||
} errors_t;
|
||||
static errors_t errors;
|
||||
|
||||
@@ -112,7 +112,7 @@ int readBytes(void *target, long num) {
|
||||
if (p.offset + num > p.size) {
|
||||
return 0;
|
||||
} else {
|
||||
memcpy(target, (void*)((unsigned long)p.data + p.offset), num);
|
||||
memcpy(target, (void*)((size_t)p.data + p.offset), num);
|
||||
if (!peek) positions[level].offset += num;
|
||||
}
|
||||
return 1;
|
||||
@@ -494,15 +494,15 @@ void printCentered(int indent, int width, char* body) {
|
||||
printf("%s %s %s\n", head, body, tail);
|
||||
}
|
||||
|
||||
void printValid(int ops, int bytes) {
|
||||
void printValid(uint64_t ops, uint64_t bytes) {
|
||||
char body[80];
|
||||
sprintf(body, "Processed %d valid opcodes (in %d bytes)", ops, bytes);
|
||||
sprintf(body, "Processed %llu valid opcodes (in %llu bytes)", ops, bytes);
|
||||
printCentered(4, 80, body);
|
||||
}
|
||||
|
||||
void printSkipped(int bytes, int offset) {
|
||||
void printSkipped(uint64_t bytes, uint64_t offset) {
|
||||
char body[80];
|
||||
sprintf(body, "Skipped %d bytes (resuming at 0x%08x)", bytes, offset);
|
||||
sprintf(body, "Skipped %llu bytes (resuming at 0x%08llx)", bytes, offset);
|
||||
printCentered(4, 80, body);
|
||||
}
|
||||
|
||||
@@ -541,7 +541,7 @@ void printErrorStack(entry *e) {
|
||||
}
|
||||
|
||||
void process() {
|
||||
int i, num_errors = 0, num_valid_ops = 0, num_valid_bytes = 0;
|
||||
uint64_t num_errors = 0, num_valid_ops = 0, num_valid_bytes = 0;
|
||||
entry entry;
|
||||
processHeader();
|
||||
|
||||
@@ -558,7 +558,9 @@ void process() {
|
||||
num_valid_bytes = 0;
|
||||
|
||||
/* search for next valid entry */
|
||||
unsigned long offset = positions[0].offset + 1;
|
||||
uint64_t offset = positions[0].offset + 1;
|
||||
int i = 0;
|
||||
|
||||
while (!entry.success && offset < positions[0].size) {
|
||||
positions[1].offset = offset;
|
||||
|
||||
@@ -606,9 +608,9 @@ void process() {
|
||||
}
|
||||
|
||||
/* print summary on errors */
|
||||
if (num_errors > 0) {
|
||||
if (num_errors) {
|
||||
printf("\n");
|
||||
printf("Total unprocessable opcodes: %d\n", num_errors);
|
||||
printf("Total unprocessable opcodes: %llu\n", num_errors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -620,7 +622,7 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
|
||||
int fd;
|
||||
unsigned long size;
|
||||
off_t size;
|
||||
struct stat stat;
|
||||
void *data;
|
||||
|
||||
@@ -634,6 +636,10 @@ int main(int argc, char **argv) {
|
||||
size = stat.st_size;
|
||||
}
|
||||
|
||||
if (sizeof(size_t) == sizeof(int32_t) && size >= INT_MAX) {
|
||||
ERROR("Cannot check dump files >2GB on a 32-bit platform\n");
|
||||
}
|
||||
|
||||
data = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);
|
||||
if (data == MAP_FAILED) {
|
||||
ERROR("Cannot mmap: %s\n", argv[1]);
|
||||
|
||||
+119
-108
@@ -36,6 +36,8 @@
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include "anet.h"
|
||||
#include "sds.h"
|
||||
@@ -54,12 +56,13 @@ static struct config {
|
||||
int hostport;
|
||||
long repeat;
|
||||
int dbnum;
|
||||
int argn_from_stdin;
|
||||
int interactive;
|
||||
int shutdown;
|
||||
int monitor_mode;
|
||||
int pubsub_mode;
|
||||
int raw_output;
|
||||
int raw_output; /* output mode per command */
|
||||
int tty; /* flag for default output format */
|
||||
char mb_sep;
|
||||
char *auth;
|
||||
char *historyfile;
|
||||
} config;
|
||||
@@ -67,11 +70,14 @@ static struct config {
|
||||
static int cliReadReply(int fd);
|
||||
static void usage();
|
||||
|
||||
static int cliConnect(void) {
|
||||
/* Connect to the client. If force is not zero the connection is performed
|
||||
* even if there is already a connected socket. */
|
||||
static int cliConnect(int force) {
|
||||
char err[ANET_ERR_LEN];
|
||||
static int fd = ANET_ERR;
|
||||
|
||||
if (fd == ANET_ERR) {
|
||||
if (fd == ANET_ERR || force) {
|
||||
if (force) close(fd);
|
||||
fd = anetTcpConnect(err,config.hostip,config.hostport);
|
||||
if (fd == ANET_ERR) {
|
||||
fprintf(stderr, "Could not connect to Redis at %s:%d: %s", config.hostip, config.hostport, err);
|
||||
@@ -90,7 +96,7 @@ static sds cliReadLine(int fd) {
|
||||
ssize_t ret;
|
||||
|
||||
ret = read(fd,&c,1);
|
||||
if (ret == -1) {
|
||||
if (ret <= 0) {
|
||||
sdsfree(line);
|
||||
return NULL;
|
||||
} else if ((ret == 0) || (c == '\n')) {
|
||||
@@ -107,7 +113,7 @@ static int cliReadSingleLineReply(int fd, int quiet) {
|
||||
|
||||
if (reply == NULL) return 1;
|
||||
if (!quiet)
|
||||
printf("%s\n", reply);
|
||||
printf("%s", reply);
|
||||
sdsfree(reply);
|
||||
return 0;
|
||||
}
|
||||
@@ -134,7 +140,7 @@ static void printStringRepr(char *s, int len) {
|
||||
}
|
||||
s++;
|
||||
}
|
||||
printf("\"\n");
|
||||
printf("\"");
|
||||
}
|
||||
|
||||
static int cliReadBulkReply(int fd) {
|
||||
@@ -152,7 +158,7 @@ static int cliReadBulkReply(int fd) {
|
||||
reply = zmalloc(bulklen);
|
||||
anetRead(fd,reply,bulklen);
|
||||
anetRead(fd,crlf,2);
|
||||
if (config.raw_output || !isatty(fileno(stdout))) {
|
||||
if (config.raw_output || !config.tty) {
|
||||
if (bulklen && fwrite(reply,bulklen,1,stdout) == 0) {
|
||||
zfree(reply);
|
||||
return 1;
|
||||
@@ -169,6 +175,7 @@ static int cliReadBulkReply(int fd) {
|
||||
static int cliReadMultiBulkReply(int fd) {
|
||||
sds replylen = cliReadLine(fd);
|
||||
int elements, c = 1;
|
||||
int retval = 0;
|
||||
|
||||
if (replylen == NULL) return 1;
|
||||
elements = atoi(replylen);
|
||||
@@ -181,36 +188,45 @@ static int cliReadMultiBulkReply(int fd) {
|
||||
printf("(empty list or set)\n");
|
||||
}
|
||||
while(elements--) {
|
||||
printf("%d. ", c);
|
||||
if (cliReadReply(fd)) return 1;
|
||||
if (config.tty) printf("%d. ", c);
|
||||
if (cliReadReply(fd)) retval = 1;
|
||||
if (elements) printf("%c",config.mb_sep);
|
||||
c++;
|
||||
}
|
||||
return 0;
|
||||
return retval;
|
||||
}
|
||||
|
||||
static int cliReadReply(int fd) {
|
||||
char type;
|
||||
int nread;
|
||||
|
||||
if (anetRead(fd,&type,1) <= 0) {
|
||||
if ((nread = anetRead(fd,&type,1)) <= 0) {
|
||||
if (config.shutdown) return 0;
|
||||
exit(1);
|
||||
if (config.interactive &&
|
||||
(nread == 0 || (nread == -1 && errno == ECONNRESET)))
|
||||
{
|
||||
return ECONNRESET;
|
||||
} else {
|
||||
printf("I/O error while reading from socket: %s",strerror(errno));
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
switch(type) {
|
||||
case '-':
|
||||
printf("(error) ");
|
||||
if (config.tty) printf("(error) ");
|
||||
cliReadSingleLineReply(fd,0);
|
||||
return 1;
|
||||
case '+':
|
||||
return cliReadSingleLineReply(fd,0);
|
||||
case ':':
|
||||
printf("(integer) ");
|
||||
if (config.tty) printf("(integer) ");
|
||||
return cliReadSingleLineReply(fd,0);
|
||||
case '$':
|
||||
return cliReadBulkReply(fd);
|
||||
case '*':
|
||||
return cliReadMultiBulkReply(fd);
|
||||
default:
|
||||
printf("protocol error, got '%c' as reply type byte\n", type);
|
||||
printf("protocol error, got '%c' as reply type byte", type);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -235,17 +251,37 @@ static int selectDb(int fd) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void showInteractiveHelp(void) {
|
||||
printf(
|
||||
"\n"
|
||||
"Welcome to redis-cli " REDIS_VERSION "!\n"
|
||||
"Just type any valid Redis command to see a pretty printed output.\n"
|
||||
"\n"
|
||||
"It is possible to quote strings, like in:\n"
|
||||
" set \"my key\" \"some string \\xff\\n\"\n"
|
||||
"\n"
|
||||
"You can find a list of valid Redis commands at\n"
|
||||
" http://code.google.com/p/redis/wiki/CommandReference\n"
|
||||
"\n"
|
||||
"Note: redis-cli supports line editing, use up/down arrows for history."
|
||||
"\n\n");
|
||||
}
|
||||
|
||||
static int cliSendCommand(int argc, char **argv, int repeat) {
|
||||
char *command = argv[0];
|
||||
int fd, j, retval = 0;
|
||||
sds cmd;
|
||||
|
||||
config.raw_output = !strcasecmp(command,"info");
|
||||
if (!strcasecmp(command,"help")) {
|
||||
showInteractiveHelp();
|
||||
return 0;
|
||||
}
|
||||
if (!strcasecmp(command,"shutdown")) config.shutdown = 1;
|
||||
if (!strcasecmp(command,"monitor")) config.monitor_mode = 1;
|
||||
if (!strcasecmp(command,"subscribe") ||
|
||||
!strcasecmp(command,"psubscribe")) config.pubsub_mode = 1;
|
||||
if ((fd = cliConnect()) == -1) return 1;
|
||||
if ((fd = cliConnect(0)) == -1) return 1;
|
||||
|
||||
/* Select db number */
|
||||
retval = selectDb(fd);
|
||||
@@ -266,21 +302,21 @@ static int cliSendCommand(int argc, char **argv, int repeat) {
|
||||
while(repeat--) {
|
||||
anetWrite(fd,cmd,sdslen(cmd));
|
||||
while (config.monitor_mode) {
|
||||
cliReadSingleLineReply(fd,0);
|
||||
if (cliReadSingleLineReply(fd,0)) exit(1);
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
if (config.pubsub_mode) {
|
||||
printf("Reading messages... (press Ctrl-c to quit)\n");
|
||||
while (1) {
|
||||
cliReadReply(fd);
|
||||
printf("\n");
|
||||
printf("\n\n");
|
||||
}
|
||||
}
|
||||
|
||||
retval = cliReadReply(fd);
|
||||
if (retval) {
|
||||
return retval;
|
||||
}
|
||||
if (!config.raw_output && config.tty) printf("\n");
|
||||
if (retval) return retval;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -314,9 +350,16 @@ static int parseOptions(int argc, char **argv) {
|
||||
config.auth = argv[i+1];
|
||||
i++;
|
||||
} else if (!strcmp(argv[i],"-i")) {
|
||||
config.interactive = 1;
|
||||
fprintf(stderr,
|
||||
"Starting interactive mode using -i is deprecated. Interactive mode is started\n"
|
||||
"by default when redis-cli is executed without a command to execute.\n"
|
||||
);
|
||||
} else if (!strcmp(argv[i],"-c")) {
|
||||
config.argn_from_stdin = 1;
|
||||
fprintf(stderr,
|
||||
"Reading last argument from standard input using -c is deprecated.\n"
|
||||
"When standard input is connected to a pipe or regular file, it is\n"
|
||||
"automatically used as last argument.\n"
|
||||
);
|
||||
} else if (!strcmp(argv[i],"-v")) {
|
||||
printf("redis-cli shipped with Redis verison %s\n", REDIS_VERSION);
|
||||
exit(0);
|
||||
@@ -346,7 +389,7 @@ static sds readArgFromStdin(void) {
|
||||
|
||||
static void usage() {
|
||||
fprintf(stderr, "usage: redis-cli [-iv] [-h host] [-p port] [-a authpw] [-r repeat_times] [-n db_num] cmd arg1 arg2 arg3 ... argN\n");
|
||||
fprintf(stderr, "usage: echo \"argN\" | redis-cli -c [-h host] [-p port] [-a authpw] [-r repeat_times] [-n db_num] cmd arg1 arg2 ... arg(N-1)\n");
|
||||
fprintf(stderr, "usage: echo \"argN\" | redis-cli [-h host] [-p port] [-a authpw] [-r repeat_times] [-n db_num] cmd arg1 arg2 ... arg(N-1)\n");
|
||||
fprintf(stderr, "\nIf a pipe from standard input is detected this data is used as last argument.\n\n");
|
||||
fprintf(stderr, "example: cat /etc/passwd | redis-cli set my_passwd\n");
|
||||
fprintf(stderr, "example: redis-cli get my_passwd\n");
|
||||
@@ -366,87 +409,39 @@ static char **convertToSds(int count, char** args) {
|
||||
return sds;
|
||||
}
|
||||
|
||||
static char **splitArguments(char *line, int *argc) {
|
||||
char *p = line;
|
||||
char *current = NULL;
|
||||
char **vector = NULL;
|
||||
|
||||
*argc = 0;
|
||||
while(1) {
|
||||
/* skip blanks */
|
||||
while(*p && isspace(*p)) p++;
|
||||
if (*p) {
|
||||
/* get a token */
|
||||
int inq=0; /* set to 1 if we are in "quotes" */
|
||||
int done = 0;
|
||||
|
||||
if (current == NULL) current = sdsempty();
|
||||
while(!done) {
|
||||
if (inq) {
|
||||
if (*p == '\\' && *(p+1)) {
|
||||
char c;
|
||||
|
||||
p++;
|
||||
switch(*p) {
|
||||
case 'n': c = '\n'; break;
|
||||
case 'r': c = '\r'; break;
|
||||
case 't': c = '\t'; break;
|
||||
case 'b': c = '\b'; break;
|
||||
case 'a': c = '\a'; break;
|
||||
default: c = *p; break;
|
||||
}
|
||||
current = sdscatlen(current,&c,1);
|
||||
} else if (*p == '"') {
|
||||
done = 1;
|
||||
} else {
|
||||
current = sdscatlen(current,p,1);
|
||||
}
|
||||
} else {
|
||||
switch(*p) {
|
||||
case ' ':
|
||||
case '\n':
|
||||
case '\r':
|
||||
case '\t':
|
||||
case '\0':
|
||||
done=1;
|
||||
break;
|
||||
case '"':
|
||||
inq=1;
|
||||
break;
|
||||
default:
|
||||
current = sdscatlen(current,p,1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (*p) p++;
|
||||
}
|
||||
/* add the token to the vector */
|
||||
vector = zrealloc(vector,((*argc)+1)*sizeof(char*));
|
||||
vector[*argc] = current;
|
||||
(*argc)++;
|
||||
current = NULL;
|
||||
} else {
|
||||
return vector;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#define LINE_BUFLEN 4096
|
||||
static void repl() {
|
||||
int argc, j;
|
||||
char *line, **argv;
|
||||
char *line;
|
||||
sds *argv;
|
||||
|
||||
config.interactive = 1;
|
||||
while((line = linenoise("redis> ")) != NULL) {
|
||||
if (line[0] != '\0') {
|
||||
argv = splitArguments(line,&argc);
|
||||
argv = sdssplitargs(line,&argc);
|
||||
linenoiseHistoryAdd(line);
|
||||
if (config.historyfile) linenoiseHistorySave(config.historyfile);
|
||||
if (argc > 0) {
|
||||
if (argv == NULL) {
|
||||
printf("Invalid argument(s)\n");
|
||||
continue;
|
||||
} else if (argc > 0) {
|
||||
if (strcasecmp(argv[0],"quit") == 0 ||
|
||||
strcasecmp(argv[0],"exit") == 0)
|
||||
exit(0);
|
||||
else
|
||||
cliSendCommand(argc, argv, 1);
|
||||
{
|
||||
exit(0);
|
||||
} else {
|
||||
int err;
|
||||
|
||||
if ((err = cliSendCommand(argc, argv, 1)) != 0) {
|
||||
if (err == ECONNRESET) {
|
||||
printf("Reconnecting... ");
|
||||
fflush(stdout);
|
||||
if (cliConnect(1) == -1) exit(1);
|
||||
printf("OK\n");
|
||||
cliSendCommand(argc,argv,1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Free the argument vector */
|
||||
for (j = 0; j < argc; j++)
|
||||
@@ -459,22 +454,37 @@ static void repl() {
|
||||
exit(0);
|
||||
}
|
||||
|
||||
static int noninteractive(int argc, char **argv) {
|
||||
int retval = 0;
|
||||
struct stat s;
|
||||
fstat(fileno(stdin), &s);
|
||||
if (S_ISFIFO(s.st_mode) || S_ISREG(s.st_mode)) { /* pipe, regular file */
|
||||
argv = zrealloc(argv, (argc+1)*sizeof(char*));
|
||||
argv[argc] = readArgFromStdin();
|
||||
retval = cliSendCommand(argc+1, argv, config.repeat);
|
||||
} else {
|
||||
/* stdin is probably a tty, can be tested with S_ISCHR(s.st_mode) */
|
||||
retval = cliSendCommand(argc, argv, config.repeat);
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int firstarg;
|
||||
char **argvcopy;
|
||||
|
||||
config.hostip = "127.0.0.1";
|
||||
config.hostport = 6379;
|
||||
config.repeat = 1;
|
||||
config.dbnum = 0;
|
||||
config.argn_from_stdin = 0;
|
||||
config.shutdown = 0;
|
||||
config.interactive = 0;
|
||||
config.shutdown = 0;
|
||||
config.monitor_mode = 0;
|
||||
config.pubsub_mode = 0;
|
||||
config.raw_output = 0;
|
||||
config.auth = NULL;
|
||||
config.historyfile = NULL;
|
||||
config.tty = isatty(fileno(stdout)) || (getenv("FAKETTY") != NULL);
|
||||
config.mb_sep = '\n';
|
||||
|
||||
if (getenv("HOME") != NULL) {
|
||||
config.historyfile = malloc(256);
|
||||
@@ -488,19 +498,20 @@ int main(int argc, char **argv) {
|
||||
|
||||
if (config.auth != NULL) {
|
||||
char *authargv[2];
|
||||
int dbnum = config.dbnum;
|
||||
|
||||
/* We need to save the real configured database number and set it to
|
||||
* zero here, otherwise cliSendCommand() will try to perform the
|
||||
* SELECT command before the authentication, and it will fail. */
|
||||
config.dbnum = 0;
|
||||
authargv[0] = "AUTH";
|
||||
authargv[1] = config.auth;
|
||||
cliSendCommand(2, convertToSds(2, authargv), 1);
|
||||
config.dbnum = dbnum; /* restore the right DB number */
|
||||
}
|
||||
|
||||
if (argc == 0 || config.interactive == 1) repl();
|
||||
|
||||
argvcopy = convertToSds(argc+1, argv);
|
||||
if (config.argn_from_stdin) {
|
||||
sds lastarg = readArgFromStdin();
|
||||
argvcopy[argc] = lastarg;
|
||||
argc++;
|
||||
}
|
||||
return cliSendCommand(argc, argvcopy, config.repeat);
|
||||
/* Start interactive mode when no command is provided */
|
||||
if (argc == 0) repl();
|
||||
/* Otherwise, we have some arguments to execute */
|
||||
return noninteractive(argc,convertToSds(argc,argv));
|
||||
}
|
||||
|
||||
+79
-53
@@ -170,6 +170,7 @@ struct redisCommand readonlyCommandTable[] = {
|
||||
{"info",infoCommand,1,REDIS_CMD_INLINE,NULL,0,0,0},
|
||||
{"monitor",monitorCommand,1,REDIS_CMD_INLINE,NULL,0,0,0},
|
||||
{"ttl",ttlCommand,2,REDIS_CMD_INLINE,NULL,1,1,1},
|
||||
{"persist",persistCommand,2,REDIS_CMD_INLINE,NULL,1,1,1},
|
||||
{"slaveof",slaveofCommand,3,REDIS_CMD_INLINE,NULL,0,0,0},
|
||||
{"debug",debugCommand,-2,REDIS_CMD_INLINE,NULL,0,0,0},
|
||||
{"config",configCommand,-2,REDIS_CMD_BULK,NULL,0,0,0},
|
||||
@@ -435,6 +436,48 @@ void updateDictResizePolicy(void) {
|
||||
|
||||
/* ======================= Cron: called every 100 ms ======================== */
|
||||
|
||||
/* Try to expire a few timed out keys. The algorithm used is adaptive and
|
||||
* will use few CPU cycles if there are few expiring keys, otherwise
|
||||
* it will get more aggressive to avoid that too much memory is used by
|
||||
* keys that can be removed from the keyspace. */
|
||||
void activeExpireCycle(void) {
|
||||
int j;
|
||||
|
||||
for (j = 0; j < server.dbnum; j++) {
|
||||
int expired;
|
||||
redisDb *db = server.db+j;
|
||||
|
||||
/* Continue to expire if at the end of the cycle more than 25%
|
||||
* of the keys were expired. */
|
||||
do {
|
||||
long num = dictSize(db->expires);
|
||||
time_t now = time(NULL);
|
||||
|
||||
expired = 0;
|
||||
if (num > REDIS_EXPIRELOOKUPS_PER_CRON)
|
||||
num = REDIS_EXPIRELOOKUPS_PER_CRON;
|
||||
while (num--) {
|
||||
dictEntry *de;
|
||||
time_t t;
|
||||
|
||||
if ((de = dictGetRandomKey(db->expires)) == NULL) break;
|
||||
t = (time_t) dictGetEntryVal(de);
|
||||
if (now > t) {
|
||||
sds key = dictGetEntryKey(de);
|
||||
robj *keyobj = createStringObject(key,sdslen(key));
|
||||
|
||||
propagateExpire(db,keyobj);
|
||||
dbDelete(db,keyobj);
|
||||
decrRefCount(keyobj);
|
||||
expired++;
|
||||
server.stat_expiredkeys++;
|
||||
}
|
||||
}
|
||||
} while (expired > REDIS_EXPIRELOOKUPS_PER_CRON/4);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
|
||||
int j, loops = server.cronloops++;
|
||||
REDIS_NOTUSED(eventLoop);
|
||||
@@ -533,41 +576,10 @@ int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Try to expire a few timed out keys. The algorithm used is adaptive and
|
||||
* will use few CPU cycles if there are few expiring keys, otherwise
|
||||
* it will get more aggressive to avoid that too much memory is used by
|
||||
* keys that can be removed from the keyspace. */
|
||||
for (j = 0; j < server.dbnum; j++) {
|
||||
int expired;
|
||||
redisDb *db = server.db+j;
|
||||
|
||||
/* Continue to expire if at the end of the cycle more than 25%
|
||||
* of the keys were expired. */
|
||||
do {
|
||||
long num = dictSize(db->expires);
|
||||
time_t now = time(NULL);
|
||||
|
||||
expired = 0;
|
||||
if (num > REDIS_EXPIRELOOKUPS_PER_CRON)
|
||||
num = REDIS_EXPIRELOOKUPS_PER_CRON;
|
||||
while (num--) {
|
||||
dictEntry *de;
|
||||
time_t t;
|
||||
|
||||
if ((de = dictGetRandomKey(db->expires)) == NULL) break;
|
||||
t = (time_t) dictGetEntryVal(de);
|
||||
if (now > t) {
|
||||
sds key = dictGetEntryKey(de);
|
||||
robj *keyobj = createStringObject(key,sdslen(key));
|
||||
|
||||
dbDelete(db,keyobj);
|
||||
decrRefCount(keyobj);
|
||||
expired++;
|
||||
server.stat_expiredkeys++;
|
||||
}
|
||||
}
|
||||
} while (expired > REDIS_EXPIRELOOKUPS_PER_CRON/4);
|
||||
}
|
||||
/* Expire a few keys per cycle, only if this is a master.
|
||||
* On slaves we wait for DEL operations synthesized by the master
|
||||
* in order to guarantee a strict consistency. */
|
||||
if (server.masterhost == NULL) activeExpireCycle();
|
||||
|
||||
/* Swap a few keys on disk if we are over the memory limit and VM
|
||||
* is enbled. Try to free objects from the free list first. */
|
||||
@@ -731,6 +743,7 @@ void initServerConfig() {
|
||||
server.hash_max_zipmap_value = REDIS_HASH_MAX_ZIPMAP_VALUE;
|
||||
server.list_max_ziplist_entries = REDIS_LIST_MAX_ZIPLIST_ENTRIES;
|
||||
server.list_max_ziplist_value = REDIS_LIST_MAX_ZIPLIST_VALUE;
|
||||
server.set_max_intset_entries = REDIS_SET_MAX_INTSET_ENTRIES;
|
||||
server.shutdown_asap = 0;
|
||||
|
||||
resetServerSaveParams();
|
||||
@@ -900,9 +913,14 @@ int processCommand(redisClient *c) {
|
||||
resetClient(c);
|
||||
return 1;
|
||||
} else {
|
||||
int bulklen = atoi(((char*)c->argv[0]->ptr)+1);
|
||||
char *eptr;
|
||||
long bulklen = strtol(((char*)c->argv[0]->ptr)+1,&eptr,10);
|
||||
int perr = eptr[0] != '\0';
|
||||
|
||||
decrRefCount(c->argv[0]);
|
||||
if (bulklen < 0 || bulklen > 1024*1024*1024) {
|
||||
if (perr || bulklen == LONG_MIN || bulklen == LONG_MAX ||
|
||||
bulklen < 0 || bulklen > 1024*1024*1024)
|
||||
{
|
||||
c->argc--;
|
||||
addReplySds(c,sdsnew("-ERR invalid bulk write count\r\n"));
|
||||
resetClient(c);
|
||||
@@ -972,10 +990,14 @@ int processCommand(redisClient *c) {
|
||||
return 1;
|
||||
} else if (cmd->flags & REDIS_CMD_BULK && c->bulklen == -1) {
|
||||
/* This is a bulk command, we have to read the last argument yet. */
|
||||
int bulklen = atoi(c->argv[c->argc-1]->ptr);
|
||||
char *eptr;
|
||||
long bulklen = strtol(c->argv[c->argc-1]->ptr,&eptr,10);
|
||||
int perr = eptr[0] != '\0';
|
||||
|
||||
decrRefCount(c->argv[c->argc-1]);
|
||||
if (bulklen < 0 || bulklen > 1024*1024*1024) {
|
||||
if (perr || bulklen == LONG_MAX || bulklen == LONG_MIN ||
|
||||
bulklen < 0 || bulklen > 1024*1024*1024)
|
||||
{
|
||||
c->argc--;
|
||||
addReplySds(c,sdsnew("-ERR invalid bulk write count\r\n"));
|
||||
resetClient(c);
|
||||
@@ -1064,11 +1086,7 @@ int prepareForShutdown() {
|
||||
if (server.vm_enabled) unlink(server.vm_swap_file);
|
||||
} else {
|
||||
/* Snapshotting. Perform a SYNC SAVE and exit */
|
||||
if (rdbSave(server.dbfilename) == REDIS_OK) {
|
||||
if (server.daemonize)
|
||||
unlink(server.pidfile);
|
||||
redisLog(REDIS_WARNING,"%zu bytes used at exit",zmalloc_used_memory());
|
||||
} else {
|
||||
if (rdbSave(server.dbfilename) != REDIS_OK) {
|
||||
/* Ooops.. error saving! The best we can do is to continue
|
||||
* operating. Note that if there was a background saving process,
|
||||
* in the next cron() Redis will be notified that the background
|
||||
@@ -1078,6 +1096,7 @@ int prepareForShutdown() {
|
||||
return REDIS_ERR;
|
||||
}
|
||||
}
|
||||
if (server.daemonize) unlink(server.pidfile);
|
||||
redisLog(REDIS_WARNING,"Server exit now, bye bye...");
|
||||
return REDIS_OK;
|
||||
}
|
||||
@@ -1302,7 +1321,8 @@ void freeMemoryIfNeeded(void) {
|
||||
if (tryFreeOneObjectFromFreelist() == REDIS_OK) continue;
|
||||
for (j = 0; j < server.dbnum; j++) {
|
||||
int minttl = -1;
|
||||
robj *minkey = NULL;
|
||||
sds minkey = NULL;
|
||||
robj *keyobj = NULL;
|
||||
struct dictEntry *de;
|
||||
|
||||
if (dictSize(server.db[j].expires)) {
|
||||
@@ -1319,7 +1339,9 @@ void freeMemoryIfNeeded(void) {
|
||||
minttl = t;
|
||||
}
|
||||
}
|
||||
dbDelete(server.db+j,minkey);
|
||||
keyobj = createStringObject(minkey,sdslen(minkey));
|
||||
dbDelete(server.db+j,keyobj);
|
||||
decrRefCount(keyobj);
|
||||
}
|
||||
}
|
||||
if (!freed) return; /* nothing to free... */
|
||||
@@ -1350,9 +1372,17 @@ void linuxOvercommitMemoryWarning(void) {
|
||||
}
|
||||
#endif /* __linux__ */
|
||||
|
||||
void createPidFile(void) {
|
||||
/* Try to write the pid file in a best-effort way. */
|
||||
FILE *fp = fopen(server.pidfile,"w");
|
||||
if (fp) {
|
||||
fprintf(fp,"%d\n",getpid());
|
||||
fclose(fp);
|
||||
}
|
||||
}
|
||||
|
||||
void daemonize(void) {
|
||||
int fd;
|
||||
FILE *fp;
|
||||
|
||||
if (fork() != 0) exit(0); /* parent exits */
|
||||
setsid(); /* create a new session */
|
||||
@@ -1366,12 +1396,6 @@ void daemonize(void) {
|
||||
dup2(fd, STDERR_FILENO);
|
||||
if (fd > STDERR_FILENO) close(fd);
|
||||
}
|
||||
/* Try to write the pid file */
|
||||
fp = fopen(server.pidfile,"w");
|
||||
if (fp) {
|
||||
fprintf(fp,"%d\n",getpid());
|
||||
fclose(fp);
|
||||
}
|
||||
}
|
||||
|
||||
void version() {
|
||||
@@ -1404,6 +1428,7 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
if (server.daemonize) daemonize();
|
||||
initServer();
|
||||
if (server.daemonize) createPidFile();
|
||||
redisLog(REDIS_NOTICE,"Server started, Redis version " REDIS_VERSION);
|
||||
#ifdef __linux__
|
||||
linuxOvercommitMemoryWarning();
|
||||
@@ -1480,6 +1505,7 @@ void segvHandler(int sig, siginfo_t *info, void *secret) {
|
||||
redisLog(REDIS_WARNING,"%s", messages[i]);
|
||||
|
||||
/* free(messages); Don't call free() with possibly corrupted memory. */
|
||||
if (server.daemonize) unlink(server.pidfile);
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
|
||||
+32
-3
@@ -26,6 +26,7 @@
|
||||
#include "anet.h" /* Networking the easy way */
|
||||
#include "zipmap.h" /* Compact string -> string data structure */
|
||||
#include "ziplist.h" /* Compact list data structure */
|
||||
#include "intset.h" /* Compact integer set structure */
|
||||
#include "version.h"
|
||||
|
||||
/* Error codes */
|
||||
@@ -82,6 +83,7 @@
|
||||
#define REDIS_ENCODING_ZIPMAP 3 /* Encoded as zipmap */
|
||||
#define REDIS_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */
|
||||
#define REDIS_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
|
||||
#define REDIS_ENCODING_INTSET 6 /* Encoded as intset */
|
||||
|
||||
/* Object types only used for dumping to disk */
|
||||
#define REDIS_EXPIRETIME 253
|
||||
@@ -188,6 +190,7 @@
|
||||
#define REDIS_HASH_MAX_ZIPMAP_VALUE 512
|
||||
#define REDIS_LIST_MAX_ZIPLIST_ENTRIES 1024
|
||||
#define REDIS_LIST_MAX_ZIPLIST_VALUE 32
|
||||
#define REDIS_SET_MAX_INTSET_ENTRIES 4096
|
||||
|
||||
/* Sets operations codes */
|
||||
#define REDIS_OP_UNION 0
|
||||
@@ -283,7 +286,7 @@ typedef struct redisClient {
|
||||
sds querybuf;
|
||||
robj **argv, **mbargv;
|
||||
int argc, mbargc;
|
||||
int bulklen; /* bulk read len. -1 if not in bulk read mode */
|
||||
long bulklen; /* bulk read len. -1 if not in bulk read mode */
|
||||
int multibulk; /* multi bulk command format active */
|
||||
list *reply;
|
||||
int sentlen;
|
||||
@@ -332,6 +335,7 @@ struct redisServer {
|
||||
int fd;
|
||||
redisDb *db;
|
||||
long long dirty; /* changes to DB from the last save */
|
||||
long long dirty_before_bgsave; /* used to restore dirty on failed BGSAVE */
|
||||
list *clients;
|
||||
list *slaves, *monitors;
|
||||
char neterr[ANET_ERR_LEN];
|
||||
@@ -398,6 +402,7 @@ struct redisServer {
|
||||
size_t hash_max_zipmap_value;
|
||||
size_t list_max_ziplist_entries;
|
||||
size_t list_max_ziplist_value;
|
||||
size_t set_max_intset_entries;
|
||||
/* Virtual memory state */
|
||||
FILE *vm_fp;
|
||||
int vm_fd;
|
||||
@@ -535,6 +540,14 @@ typedef struct {
|
||||
listNode *ln; /* Entry in linked list */
|
||||
} listTypeEntry;
|
||||
|
||||
/* Structure to hold set iteration abstraction. */
|
||||
typedef struct {
|
||||
robj *subject;
|
||||
int encoding;
|
||||
int ii; /* intset iterator */
|
||||
dictIterator *di;
|
||||
} setTypeIterator;
|
||||
|
||||
/* Structure to hold hash iteration abstration. Note that iteration over
|
||||
* hashes involves both fields and values. Because it is possible that
|
||||
* not both are required, store pointers in the iterator to avoid
|
||||
@@ -633,6 +646,7 @@ robj *createStringObjectFromLongLong(long long value);
|
||||
robj *createListObject(void);
|
||||
robj *createZiplistObject(void);
|
||||
robj *createSetObject(void);
|
||||
robj *createIntsetObject(void);
|
||||
robj *createHashObject(void);
|
||||
robj *createZsetObject(void);
|
||||
int getLongFromObjectOrReply(redisClient *c, robj *o, long *target, const char *msg);
|
||||
@@ -716,6 +730,18 @@ int dontWaitForSwappedKey(redisClient *c, robj *key);
|
||||
void handleClientsBlockedOnSwappedKey(redisDb *db, robj *key);
|
||||
vmpointer *vmSwapObjectBlocking(robj *val);
|
||||
|
||||
/* Set data type */
|
||||
robj *setTypeCreate(robj *value);
|
||||
int setTypeAdd(robj *subject, robj *value);
|
||||
int setTypeRemove(robj *subject, robj *value);
|
||||
int setTypeIsMember(robj *subject, robj *value);
|
||||
setTypeIterator *setTypeInitIterator(robj *subject);
|
||||
void setTypeReleaseIterator(setTypeIterator *si);
|
||||
robj *setTypeNext(setTypeIterator *si);
|
||||
robj *setTypeRandomElement(robj *subject);
|
||||
unsigned long setTypeSize(robj *subject);
|
||||
void setTypeConvert(robj *subject, int enc);
|
||||
|
||||
/* Hash data type */
|
||||
void convertToRealHash(robj *o);
|
||||
void hashTypeTryConversion(robj *subject, robj **argv, int start, int end);
|
||||
@@ -744,6 +770,8 @@ int stringmatch(const char *pattern, const char *string, int nocase);
|
||||
long long memtoll(const char *p, int *err);
|
||||
int ll2string(char *s, size_t len, long long value);
|
||||
int isStringRepresentableAsLong(sds s, long *longval);
|
||||
int isStringRepresentableAsLongLong(sds s, long long *longval);
|
||||
int isObjectRepresentableAsLongLong(robj *o, long long *llongval);
|
||||
|
||||
/* Configuration */
|
||||
void loadServerConfig(char *filename);
|
||||
@@ -752,10 +780,10 @@ void resetServerSaveParams();
|
||||
|
||||
/* db.c -- Keyspace access API */
|
||||
int removeExpire(redisDb *db, robj *key);
|
||||
void propagateExpire(redisDb *db, robj *key);
|
||||
int expireIfNeeded(redisDb *db, robj *key);
|
||||
int deleteIfVolatile(redisDb *db, robj *key);
|
||||
time_t getExpire(redisDb *db, robj *key);
|
||||
int setExpire(redisDb *db, robj *key, time_t when);
|
||||
void setExpire(redisDb *db, robj *key, time_t when);
|
||||
robj *lookupKey(redisDb *db, robj *key);
|
||||
robj *lookupKeyRead(redisDb *db, robj *key);
|
||||
robj *lookupKeyWrite(redisDb *db, robj *key);
|
||||
@@ -838,6 +866,7 @@ void expireCommand(redisClient *c);
|
||||
void expireatCommand(redisClient *c);
|
||||
void getsetCommand(redisClient *c);
|
||||
void ttlCommand(redisClient *c);
|
||||
void persistCommand(redisClient *c);
|
||||
void slaveofCommand(redisClient *c);
|
||||
void debugCommand(redisClient *c);
|
||||
void msetCommand(redisClient *c);
|
||||
|
||||
+16
-4
@@ -138,7 +138,7 @@ int syncRead(int fd, char *ptr, ssize_t size, int timeout) {
|
||||
while(size) {
|
||||
if (aeWait(fd,AE_READABLE,1000) & AE_READABLE) {
|
||||
nread = read(fd,ptr,size);
|
||||
if (nread == -1) return -1;
|
||||
if (nread <= 0) return -1;
|
||||
ptr += nread;
|
||||
size -= nread;
|
||||
totread += nread;
|
||||
@@ -176,6 +176,13 @@ void syncCommand(redisClient *c) {
|
||||
/* ignore SYNC if aleady slave or in monitor mode */
|
||||
if (c->flags & REDIS_SLAVE) return;
|
||||
|
||||
/* Refuse SYNC requests if we are a slave but the link with our master
|
||||
* is not ok... */
|
||||
if (server.masterhost && server.replstate != REDIS_REPL_CONNECTED) {
|
||||
addReplySds(c,sdsnew("-ERR Can't SYNC while not connected with my master\r\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
/* SYNC can't be issued when the server has pending data to send to
|
||||
* the client about already issued commands. We need a fresh reply
|
||||
* buffer registering the differences between the BGSAVE and the current
|
||||
@@ -392,7 +399,12 @@ int syncWithMaster(void) {
|
||||
strerror(errno));
|
||||
return REDIS_ERR;
|
||||
}
|
||||
if (buf[0] != '$') {
|
||||
if (buf[0] == '-') {
|
||||
close(fd);
|
||||
redisLog(REDIS_WARNING,"MASTER aborted replication with an error: %s",
|
||||
buf+1);
|
||||
return REDIS_ERR;
|
||||
} else if (buf[0] != '$') {
|
||||
close(fd);
|
||||
redisLog(REDIS_WARNING,"Bad protocol from MASTER, the first byte is not '$', are you sure the host and port are right?");
|
||||
return REDIS_ERR;
|
||||
@@ -416,9 +428,9 @@ int syncWithMaster(void) {
|
||||
int nread, nwritten;
|
||||
|
||||
nread = read(fd,buf,(dumpsize < 1024)?dumpsize:1024);
|
||||
if (nread == -1) {
|
||||
if (nread <= 0) {
|
||||
redisLog(REDIS_WARNING,"I/O error trying to sync with MASTER: %s",
|
||||
strerror(errno));
|
||||
(nread == -1) ? strerror(errno) : "connection lost");
|
||||
close(fd);
|
||||
close(dfd);
|
||||
return REDIS_ERR;
|
||||
|
||||
@@ -382,3 +382,92 @@ sds sdscatrepr(sds s, char *p, size_t len) {
|
||||
}
|
||||
return sdscatlen(s,"\"",1);
|
||||
}
|
||||
|
||||
/* Split a line into arguments, where every argument can be in the
|
||||
* following programming-language REPL-alike form:
|
||||
*
|
||||
* foo bar "newline are supported\n" and "\xff\x00otherstuff"
|
||||
*
|
||||
* The number of arguments is stored into *argc, and an array
|
||||
* of sds is returned. The caller should sdsfree() all the returned
|
||||
* strings and finally zfree() the array itself.
|
||||
*
|
||||
* Note that sdscatrepr() is able to convert back a string into
|
||||
* a quoted string in the same format sdssplitargs() is able to parse.
|
||||
*/
|
||||
sds *sdssplitargs(char *line, int *argc) {
|
||||
char *p = line;
|
||||
char *current = NULL;
|
||||
char **vector = NULL;
|
||||
|
||||
*argc = 0;
|
||||
while(1) {
|
||||
/* skip blanks */
|
||||
while(*p && isspace(*p)) p++;
|
||||
if (*p) {
|
||||
/* get a token */
|
||||
int inq=0; /* set to 1 if we are in "quotes" */
|
||||
int done=0;
|
||||
|
||||
if (current == NULL) current = sdsempty();
|
||||
while(!done) {
|
||||
if (inq) {
|
||||
if (*p == '\\' && *(p+1)) {
|
||||
char c;
|
||||
|
||||
p++;
|
||||
switch(*p) {
|
||||
case 'n': c = '\n'; break;
|
||||
case 'r': c = '\r'; break;
|
||||
case 't': c = '\t'; break;
|
||||
case 'b': c = '\b'; break;
|
||||
case 'a': c = '\a'; break;
|
||||
default: c = *p; break;
|
||||
}
|
||||
current = sdscatlen(current,&c,1);
|
||||
} else if (*p == '"') {
|
||||
/* closing quote must be followed by a space */
|
||||
if (*(p+1) && !isspace(*(p+1))) goto err;
|
||||
done=1;
|
||||
} else if (!*p) {
|
||||
/* unterminated quotes */
|
||||
goto err;
|
||||
} else {
|
||||
current = sdscatlen(current,p,1);
|
||||
}
|
||||
} else {
|
||||
switch(*p) {
|
||||
case ' ':
|
||||
case '\n':
|
||||
case '\r':
|
||||
case '\t':
|
||||
case '\0':
|
||||
done=1;
|
||||
break;
|
||||
case '"':
|
||||
inq=1;
|
||||
break;
|
||||
default:
|
||||
current = sdscatlen(current,p,1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (*p) p++;
|
||||
}
|
||||
/* add the token to the vector */
|
||||
vector = zrealloc(vector,((*argc)+1)*sizeof(char*));
|
||||
vector[*argc] = current;
|
||||
(*argc)++;
|
||||
current = NULL;
|
||||
} else {
|
||||
return vector;
|
||||
}
|
||||
}
|
||||
|
||||
err:
|
||||
while((*argc)--)
|
||||
sdsfree(vector[*argc]);
|
||||
zfree(vector);
|
||||
if (current) sdsfree(current);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -70,5 +70,6 @@ void sdstolower(sds s);
|
||||
void sdstoupper(sds s);
|
||||
sds sdsfromlonglong(long long value);
|
||||
sds sdscatrepr(sds s, char *p, size_t len);
|
||||
sds *sdssplitargs(char *line, int *argc);
|
||||
|
||||
#endif
|
||||
|
||||
+16
-12
@@ -202,7 +202,7 @@ void sortCommand(redisClient *c) {
|
||||
/* Load the sorting vector with all the objects to sort */
|
||||
switch(sortval->type) {
|
||||
case REDIS_LIST: vectorlen = listTypeLength(sortval); break;
|
||||
case REDIS_SET: vectorlen = dictSize((dict*)sortval->ptr); break;
|
||||
case REDIS_SET: vectorlen = setTypeSize(sortval); break;
|
||||
case REDIS_ZSET: vectorlen = dictSize(((zset*)sortval->ptr)->dict); break;
|
||||
default: vectorlen = 0; redisPanic("Bad SORT type"); /* Avoid GCC warning */
|
||||
}
|
||||
@@ -219,18 +219,20 @@ void sortCommand(redisClient *c) {
|
||||
j++;
|
||||
}
|
||||
listTypeReleaseIterator(li);
|
||||
} else {
|
||||
dict *set;
|
||||
} else if (sortval->type == REDIS_SET) {
|
||||
setTypeIterator *si = setTypeInitIterator(sortval);
|
||||
robj *ele;
|
||||
while((ele = setTypeNext(si)) != NULL) {
|
||||
vector[j].obj = ele;
|
||||
vector[j].u.score = 0;
|
||||
vector[j].u.cmpobj = NULL;
|
||||
j++;
|
||||
}
|
||||
setTypeReleaseIterator(si);
|
||||
} else if (sortval->type == REDIS_ZSET) {
|
||||
dict *set = ((zset*)sortval->ptr)->dict;
|
||||
dictIterator *di;
|
||||
dictEntry *setele;
|
||||
|
||||
if (sortval->type == REDIS_SET) {
|
||||
set = sortval->ptr;
|
||||
} else {
|
||||
zset *zs = sortval->ptr;
|
||||
set = zs->dict;
|
||||
}
|
||||
|
||||
di = dictGetIterator(set);
|
||||
while((setele = dictNext(di)) != NULL) {
|
||||
vector[j].obj = dictGetEntryKey(setele);
|
||||
@@ -239,6 +241,8 @@ void sortCommand(redisClient *c) {
|
||||
j++;
|
||||
}
|
||||
dictReleaseIterator(di);
|
||||
} else {
|
||||
redisPanic("Unknown type");
|
||||
}
|
||||
redisAssert(j == vectorlen);
|
||||
|
||||
@@ -369,7 +373,7 @@ void sortCommand(redisClient *c) {
|
||||
}
|
||||
|
||||
/* Cleanup */
|
||||
if (sortval->type == REDIS_LIST)
|
||||
if (sortval->type == REDIS_LIST || sortval->type == REDIS_SET)
|
||||
for (j = 0; j < vectorlen; j++)
|
||||
decrRefCount(vector[j].obj);
|
||||
decrRefCount(sortval);
|
||||
|
||||
+20
-1
@@ -782,9 +782,20 @@ int handleClientsWaitingListPush(redisClient *c, robj *key, robj *ele) {
|
||||
/* Blocking RPOP/LPOP */
|
||||
void blockingPopGenericCommand(redisClient *c, int where) {
|
||||
robj *o;
|
||||
long long lltimeout;
|
||||
time_t timeout;
|
||||
int j;
|
||||
|
||||
/* Make sure timeout is an integer value */
|
||||
if (getLongLongFromObjectOrReply(c,c->argv[c->argc-1],&lltimeout,
|
||||
"timeout is not an integer") != REDIS_OK) return;
|
||||
|
||||
/* Make sure the timeout is not negative */
|
||||
if (lltimeout < 0) {
|
||||
addReplySds(c,sdsnew("-ERR timeout is negative\r\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
for (j = 1; j < c->argc-1; j++) {
|
||||
o = lookupKeyWrite(c->db,c->argv[j]);
|
||||
if (o != NULL) {
|
||||
@@ -823,8 +834,16 @@ void blockingPopGenericCommand(redisClient *c, int where) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* If we are inside a MULTI/EXEC and the list is empty the only thing
|
||||
* we can do is treating it as a timeout (even with timeout 0). */
|
||||
if (c->flags & REDIS_MULTI) {
|
||||
addReply(c,shared.nullmultibulk);
|
||||
return;
|
||||
}
|
||||
|
||||
/* If the list is empty or the key does not exists we must block */
|
||||
timeout = strtol(c->argv[c->argc-1]->ptr,NULL,10);
|
||||
timeout = lltimeout;
|
||||
if (timeout > 0) timeout += time(NULL);
|
||||
blockForKeys(c,c->argv+1,c->argc-2,timeout);
|
||||
}
|
||||
|
||||
+283
-129
@@ -4,12 +4,182 @@
|
||||
* Set Commands
|
||||
*----------------------------------------------------------------------------*/
|
||||
|
||||
/* Factory method to return a set that *can* hold "value". When the object has
|
||||
* an integer-encodable value, an intset will be returned. Otherwise a regular
|
||||
* hash table. */
|
||||
robj *setTypeCreate(robj *value) {
|
||||
if (isObjectRepresentableAsLongLong(value,NULL) == REDIS_OK)
|
||||
return createIntsetObject();
|
||||
return createSetObject();
|
||||
}
|
||||
|
||||
int setTypeAdd(robj *subject, robj *value) {
|
||||
long long llval;
|
||||
if (subject->encoding == REDIS_ENCODING_HT) {
|
||||
if (dictAdd(subject->ptr,value,NULL) == DICT_OK) {
|
||||
incrRefCount(value);
|
||||
return 1;
|
||||
}
|
||||
} else if (subject->encoding == REDIS_ENCODING_INTSET) {
|
||||
if (isObjectRepresentableAsLongLong(value,&llval) == REDIS_OK) {
|
||||
uint8_t success = 0;
|
||||
subject->ptr = intsetAdd(subject->ptr,llval,&success);
|
||||
if (success) {
|
||||
/* Convert to regular set when the intset contains
|
||||
* too many entries. */
|
||||
if (intsetLen(subject->ptr) > server.set_max_intset_entries)
|
||||
setTypeConvert(subject,REDIS_ENCODING_HT);
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
/* Failed to get integer from object, convert to regular set. */
|
||||
setTypeConvert(subject,REDIS_ENCODING_HT);
|
||||
|
||||
/* The set *was* an intset and this value is not integer
|
||||
* encodable, so dictAdd should always work. */
|
||||
redisAssert(dictAdd(subject->ptr,value,NULL) == DICT_OK);
|
||||
incrRefCount(value);
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
redisPanic("Unknown set encoding");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int setTypeRemove(robj *subject, robj *value) {
|
||||
long long llval;
|
||||
if (subject->encoding == REDIS_ENCODING_HT) {
|
||||
if (dictDelete(subject->ptr,value) == DICT_OK) {
|
||||
if (htNeedsResize(subject->ptr)) dictResize(subject->ptr);
|
||||
return 1;
|
||||
}
|
||||
} else if (subject->encoding == REDIS_ENCODING_INTSET) {
|
||||
if (isObjectRepresentableAsLongLong(value,&llval) == REDIS_OK) {
|
||||
uint8_t success;
|
||||
subject->ptr = intsetRemove(subject->ptr,llval,&success);
|
||||
if (success) return 1;
|
||||
}
|
||||
} else {
|
||||
redisPanic("Unknown set encoding");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int setTypeIsMember(robj *subject, robj *value) {
|
||||
long long llval;
|
||||
if (subject->encoding == REDIS_ENCODING_HT) {
|
||||
return dictFind((dict*)subject->ptr,value) != NULL;
|
||||
} else if (subject->encoding == REDIS_ENCODING_INTSET) {
|
||||
if (isObjectRepresentableAsLongLong(value,&llval) == REDIS_OK) {
|
||||
return intsetFind((intset*)subject->ptr,llval);
|
||||
}
|
||||
} else {
|
||||
redisPanic("Unknown set encoding");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
setTypeIterator *setTypeInitIterator(robj *subject) {
|
||||
setTypeIterator *si = zmalloc(sizeof(setTypeIterator));
|
||||
si->subject = subject;
|
||||
si->encoding = subject->encoding;
|
||||
if (si->encoding == REDIS_ENCODING_HT) {
|
||||
si->di = dictGetIterator(subject->ptr);
|
||||
} else if (si->encoding == REDIS_ENCODING_INTSET) {
|
||||
si->ii = 0;
|
||||
} else {
|
||||
redisPanic("Unknown set encoding");
|
||||
}
|
||||
return si;
|
||||
}
|
||||
|
||||
void setTypeReleaseIterator(setTypeIterator *si) {
|
||||
if (si->encoding == REDIS_ENCODING_HT)
|
||||
dictReleaseIterator(si->di);
|
||||
zfree(si);
|
||||
}
|
||||
|
||||
/* Move to the next entry in the set. Returns the object at the current
|
||||
* position, or NULL when the end is reached. This object will have its
|
||||
* refcount incremented, so the caller needs to take care of this. */
|
||||
robj *setTypeNext(setTypeIterator *si) {
|
||||
robj *ret = NULL;
|
||||
if (si->encoding == REDIS_ENCODING_HT) {
|
||||
dictEntry *de = dictNext(si->di);
|
||||
if (de != NULL) {
|
||||
ret = dictGetEntryKey(de);
|
||||
incrRefCount(ret);
|
||||
}
|
||||
} else if (si->encoding == REDIS_ENCODING_INTSET) {
|
||||
int64_t llval;
|
||||
if (intsetGet(si->subject->ptr,si->ii++,&llval))
|
||||
ret = createStringObjectFromLongLong(llval);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
/* Return random element from set. The returned object will always have
|
||||
* an incremented refcount. */
|
||||
robj *setTypeRandomElement(robj *subject) {
|
||||
robj *ret = NULL;
|
||||
if (subject->encoding == REDIS_ENCODING_HT) {
|
||||
dictEntry *de = dictGetRandomKey(subject->ptr);
|
||||
ret = dictGetEntryKey(de);
|
||||
incrRefCount(ret);
|
||||
} else if (subject->encoding == REDIS_ENCODING_INTSET) {
|
||||
long long llval = intsetRandom(subject->ptr);
|
||||
ret = createStringObjectFromLongLong(llval);
|
||||
} else {
|
||||
redisPanic("Unknown set encoding");
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
unsigned long setTypeSize(robj *subject) {
|
||||
if (subject->encoding == REDIS_ENCODING_HT) {
|
||||
return dictSize((dict*)subject->ptr);
|
||||
} else if (subject->encoding == REDIS_ENCODING_INTSET) {
|
||||
return intsetLen((intset*)subject->ptr);
|
||||
} else {
|
||||
redisPanic("Unknown set encoding");
|
||||
}
|
||||
}
|
||||
|
||||
/* Convert the set to specified encoding. The resulting dict (when converting
|
||||
* to a hashtable) is presized to hold the number of elements in the original
|
||||
* set. */
|
||||
void setTypeConvert(robj *subject, int enc) {
|
||||
setTypeIterator *si;
|
||||
robj *element;
|
||||
redisAssert(subject->type == REDIS_SET);
|
||||
|
||||
if (enc == REDIS_ENCODING_HT) {
|
||||
dict *d = dictCreate(&setDictType,NULL);
|
||||
/* Presize the dict to avoid rehashing */
|
||||
dictExpand(d,intsetLen(subject->ptr));
|
||||
|
||||
/* setTypeGet returns a robj with incremented refcount */
|
||||
si = setTypeInitIterator(subject);
|
||||
while ((element = setTypeNext(si)) != NULL)
|
||||
redisAssert(dictAdd(d,element,NULL) == DICT_OK);
|
||||
setTypeReleaseIterator(si);
|
||||
|
||||
subject->encoding = REDIS_ENCODING_HT;
|
||||
zfree(subject->ptr);
|
||||
subject->ptr = d;
|
||||
} else {
|
||||
redisPanic("Unsupported set conversion");
|
||||
}
|
||||
}
|
||||
|
||||
void saddCommand(redisClient *c) {
|
||||
robj *set;
|
||||
|
||||
set = lookupKeyWrite(c->db,c->argv[1]);
|
||||
if (set == NULL) {
|
||||
set = createSetObject();
|
||||
set = setTypeCreate(c->argv[2]);
|
||||
dbAdd(c->db,c->argv[1],set);
|
||||
} else {
|
||||
if (set->type != REDIS_SET) {
|
||||
@@ -17,8 +187,7 @@ void saddCommand(redisClient *c) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (dictAdd(set->ptr,c->argv[2],NULL) == DICT_OK) {
|
||||
incrRefCount(c->argv[2]);
|
||||
if (setTypeAdd(set,c->argv[2])) {
|
||||
touchWatchedKey(c->db,c->argv[1]);
|
||||
server.dirty++;
|
||||
addReply(c,shared.cone);
|
||||
@@ -33,11 +202,10 @@ void sremCommand(redisClient *c) {
|
||||
if ((set = lookupKeyWriteOrReply(c,c->argv[1],shared.czero)) == NULL ||
|
||||
checkType(c,set,REDIS_SET)) return;
|
||||
|
||||
if (dictDelete(set->ptr,c->argv[2]) == DICT_OK) {
|
||||
server.dirty++;
|
||||
if (setTypeRemove(set,c->argv[2])) {
|
||||
if (setTypeSize(set) == 0) dbDelete(c->db,c->argv[1]);
|
||||
touchWatchedKey(c->db,c->argv[1]);
|
||||
if (htNeedsResize(set->ptr)) dictResize(set->ptr);
|
||||
if (dictSize((dict*)set->ptr) == 0) dbDelete(c->db,c->argv[1]);
|
||||
server.dirty++;
|
||||
addReply(c,shared.cone);
|
||||
} else {
|
||||
addReply(c,shared.czero);
|
||||
@@ -45,40 +213,48 @@ void sremCommand(redisClient *c) {
|
||||
}
|
||||
|
||||
void smoveCommand(redisClient *c) {
|
||||
robj *srcset, *dstset;
|
||||
|
||||
robj *srcset, *dstset, *ele;
|
||||
srcset = lookupKeyWrite(c->db,c->argv[1]);
|
||||
dstset = lookupKeyWrite(c->db,c->argv[2]);
|
||||
ele = c->argv[3];
|
||||
|
||||
/* If the source key does not exist return 0, if it's of the wrong type
|
||||
* raise an error */
|
||||
if (srcset == NULL || srcset->type != REDIS_SET) {
|
||||
addReply(c, srcset ? shared.wrongtypeerr : shared.czero);
|
||||
return;
|
||||
}
|
||||
/* Error if the destination key is not a set as well */
|
||||
if (dstset && dstset->type != REDIS_SET) {
|
||||
addReply(c,shared.wrongtypeerr);
|
||||
return;
|
||||
}
|
||||
/* Remove the element from the source set */
|
||||
if (dictDelete(srcset->ptr,c->argv[3]) == DICT_ERR) {
|
||||
/* Key not found in the src set! return zero */
|
||||
/* If the source key does not exist return 0 */
|
||||
if (srcset == NULL) {
|
||||
addReply(c,shared.czero);
|
||||
return;
|
||||
}
|
||||
if (dictSize((dict*)srcset->ptr) == 0 && srcset != dstset)
|
||||
dbDelete(c->db,c->argv[1]);
|
||||
|
||||
/* If the source key has the wrong type, or the destination key
|
||||
* is set and has the wrong type, return with an error. */
|
||||
if (checkType(c,srcset,REDIS_SET) ||
|
||||
(dstset && checkType(c,dstset,REDIS_SET))) return;
|
||||
|
||||
/* If srcset and dstset are equal, SMOVE is a no-op */
|
||||
if (srcset == dstset) {
|
||||
addReply(c,shared.cone);
|
||||
return;
|
||||
}
|
||||
|
||||
/* If the element cannot be removed from the src set, return 0. */
|
||||
if (!setTypeRemove(srcset,ele)) {
|
||||
addReply(c,shared.czero);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Remove the src set from the database when empty */
|
||||
if (setTypeSize(srcset) == 0) dbDelete(c->db,c->argv[1]);
|
||||
touchWatchedKey(c->db,c->argv[1]);
|
||||
touchWatchedKey(c->db,c->argv[2]);
|
||||
server.dirty++;
|
||||
/* Add the element to the destination set */
|
||||
|
||||
/* Create the destination set when it doesn't exist */
|
||||
if (!dstset) {
|
||||
dstset = createSetObject();
|
||||
dstset = setTypeCreate(ele);
|
||||
dbAdd(c->db,c->argv[2],dstset);
|
||||
}
|
||||
if (dictAdd(dstset->ptr,c->argv[3],NULL) == DICT_OK)
|
||||
incrRefCount(c->argv[3]);
|
||||
|
||||
/* An extra key has changed when ele was successfully added to dstset */
|
||||
if (setTypeAdd(dstset,ele)) server.dirty++;
|
||||
addReply(c,shared.cone);
|
||||
}
|
||||
|
||||
@@ -88,7 +264,7 @@ void sismemberCommand(redisClient *c) {
|
||||
if ((set = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL ||
|
||||
checkType(c,set,REDIS_SET)) return;
|
||||
|
||||
if (dictFind(set->ptr,c->argv[2]))
|
||||
if (setTypeIsMember(set,c->argv[2]))
|
||||
addReply(c,shared.cone);
|
||||
else
|
||||
addReply(c,shared.czero);
|
||||
@@ -96,75 +272,63 @@ void sismemberCommand(redisClient *c) {
|
||||
|
||||
void scardCommand(redisClient *c) {
|
||||
robj *o;
|
||||
dict *s;
|
||||
|
||||
if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL ||
|
||||
checkType(c,o,REDIS_SET)) return;
|
||||
|
||||
s = o->ptr;
|
||||
addReplyUlong(c,dictSize(s));
|
||||
addReplyUlong(c,setTypeSize(o));
|
||||
}
|
||||
|
||||
void spopCommand(redisClient *c) {
|
||||
robj *set;
|
||||
dictEntry *de;
|
||||
robj *set, *ele;
|
||||
|
||||
if ((set = lookupKeyWriteOrReply(c,c->argv[1],shared.nullbulk)) == NULL ||
|
||||
checkType(c,set,REDIS_SET)) return;
|
||||
|
||||
de = dictGetRandomKey(set->ptr);
|
||||
if (de == NULL) {
|
||||
ele = setTypeRandomElement(set);
|
||||
if (ele == NULL) {
|
||||
addReply(c,shared.nullbulk);
|
||||
} else {
|
||||
robj *ele = dictGetEntryKey(de);
|
||||
|
||||
setTypeRemove(set,ele);
|
||||
addReplyBulk(c,ele);
|
||||
dictDelete(set->ptr,ele);
|
||||
if (htNeedsResize(set->ptr)) dictResize(set->ptr);
|
||||
if (dictSize((dict*)set->ptr) == 0) dbDelete(c->db,c->argv[1]);
|
||||
decrRefCount(ele);
|
||||
if (setTypeSize(set) == 0) dbDelete(c->db,c->argv[1]);
|
||||
touchWatchedKey(c->db,c->argv[1]);
|
||||
server.dirty++;
|
||||
}
|
||||
}
|
||||
|
||||
void srandmemberCommand(redisClient *c) {
|
||||
robj *set;
|
||||
dictEntry *de;
|
||||
robj *set, *ele;
|
||||
|
||||
if ((set = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL ||
|
||||
checkType(c,set,REDIS_SET)) return;
|
||||
|
||||
de = dictGetRandomKey(set->ptr);
|
||||
if (de == NULL) {
|
||||
ele = setTypeRandomElement(set);
|
||||
if (ele == NULL) {
|
||||
addReply(c,shared.nullbulk);
|
||||
} else {
|
||||
robj *ele = dictGetEntryKey(de);
|
||||
|
||||
addReplyBulk(c,ele);
|
||||
decrRefCount(ele);
|
||||
}
|
||||
}
|
||||
|
||||
int qsortCompareSetsByCardinality(const void *s1, const void *s2) {
|
||||
dict **d1 = (void*) s1, **d2 = (void*) s2;
|
||||
|
||||
return dictSize(*d1)-dictSize(*d2);
|
||||
return setTypeSize(*(robj**)s1)-setTypeSize(*(robj**)s2);
|
||||
}
|
||||
|
||||
void sinterGenericCommand(redisClient *c, robj **setskeys, unsigned long setsnum, robj *dstkey) {
|
||||
dict **dv = zmalloc(sizeof(dict*)*setsnum);
|
||||
dictIterator *di;
|
||||
dictEntry *de;
|
||||
robj *lenobj = NULL, *dstset = NULL;
|
||||
void sinterGenericCommand(redisClient *c, robj **setkeys, unsigned long setnum, robj *dstkey) {
|
||||
robj **sets = zmalloc(sizeof(robj*)*setnum);
|
||||
setTypeIterator *si;
|
||||
robj *ele, *lenobj = NULL, *dstset = NULL;
|
||||
unsigned long j, cardinality = 0;
|
||||
|
||||
for (j = 0; j < setsnum; j++) {
|
||||
robj *setobj;
|
||||
|
||||
setobj = dstkey ?
|
||||
lookupKeyWrite(c->db,setskeys[j]) :
|
||||
lookupKeyRead(c->db,setskeys[j]);
|
||||
for (j = 0; j < setnum; j++) {
|
||||
robj *setobj = dstkey ?
|
||||
lookupKeyWrite(c->db,setkeys[j]) :
|
||||
lookupKeyRead(c->db,setkeys[j]);
|
||||
if (!setobj) {
|
||||
zfree(dv);
|
||||
zfree(sets);
|
||||
if (dstkey) {
|
||||
if (dbDelete(c->db,dstkey)) {
|
||||
touchWatchedKey(c->db,dstkey);
|
||||
@@ -176,16 +340,15 @@ void sinterGenericCommand(redisClient *c, robj **setskeys, unsigned long setsnum
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (setobj->type != REDIS_SET) {
|
||||
zfree(dv);
|
||||
addReply(c,shared.wrongtypeerr);
|
||||
if (checkType(c,setobj,REDIS_SET)) {
|
||||
zfree(sets);
|
||||
return;
|
||||
}
|
||||
dv[j] = setobj->ptr;
|
||||
sets[j] = setobj;
|
||||
}
|
||||
/* Sort sets from the smallest to largest, this will improve our
|
||||
* algorithm's performace */
|
||||
qsort(dv,setsnum,sizeof(dict*),qsortCompareSetsByCardinality);
|
||||
qsort(sets,setnum,sizeof(robj*),qsortCompareSetsByCardinality);
|
||||
|
||||
/* The first thing we should output is the total number of elements...
|
||||
* since this is a multi-bulk write, but at this stage we don't know
|
||||
@@ -199,39 +362,37 @@ void sinterGenericCommand(redisClient *c, robj **setskeys, unsigned long setsnum
|
||||
} else {
|
||||
/* If we have a target key where to store the resulting set
|
||||
* create this key with an empty set inside */
|
||||
dstset = createSetObject();
|
||||
dstset = createIntsetObject();
|
||||
}
|
||||
|
||||
/* Iterate all the elements of the first (smallest) set, and test
|
||||
* the element against all the other sets, if at least one set does
|
||||
* not include the element it is discarded */
|
||||
di = dictGetIterator(dv[0]);
|
||||
si = setTypeInitIterator(sets[0]);
|
||||
while((ele = setTypeNext(si)) != NULL) {
|
||||
for (j = 1; j < setnum; j++)
|
||||
if (!setTypeIsMember(sets[j],ele)) break;
|
||||
|
||||
while((de = dictNext(di)) != NULL) {
|
||||
robj *ele;
|
||||
|
||||
for (j = 1; j < setsnum; j++)
|
||||
if (dictFind(dv[j],dictGetEntryKey(de)) == NULL) break;
|
||||
if (j != setsnum)
|
||||
continue; /* at least one set does not contain the member */
|
||||
ele = dictGetEntryKey(de);
|
||||
if (!dstkey) {
|
||||
addReplyBulk(c,ele);
|
||||
cardinality++;
|
||||
} else {
|
||||
dictAdd(dstset->ptr,ele,NULL);
|
||||
incrRefCount(ele);
|
||||
/* Only take action when all sets contain the member */
|
||||
if (j == setnum) {
|
||||
if (!dstkey) {
|
||||
addReplyBulk(c,ele);
|
||||
cardinality++;
|
||||
} else {
|
||||
setTypeAdd(dstset,ele);
|
||||
}
|
||||
}
|
||||
decrRefCount(ele);
|
||||
}
|
||||
dictReleaseIterator(di);
|
||||
setTypeReleaseIterator(si);
|
||||
|
||||
if (dstkey) {
|
||||
/* Store the resulting set into the target, if the intersection
|
||||
* is not an empty set. */
|
||||
dbDelete(c->db,dstkey);
|
||||
if (dictSize((dict*)dstset->ptr) > 0) {
|
||||
if (setTypeSize(dstset) > 0) {
|
||||
dbAdd(c->db,dstkey,dstset);
|
||||
addReplyLongLong(c,dictSize((dict*)dstset->ptr));
|
||||
addReplyLongLong(c,setTypeSize(dstset));
|
||||
} else {
|
||||
decrRefCount(dstset);
|
||||
addReply(c,shared.czero);
|
||||
@@ -241,7 +402,7 @@ void sinterGenericCommand(redisClient *c, robj **setskeys, unsigned long setsnum
|
||||
} else {
|
||||
lenobj->ptr = sdscatprintf(sdsempty(),"*%lu\r\n",cardinality);
|
||||
}
|
||||
zfree(dv);
|
||||
zfree(sets);
|
||||
}
|
||||
|
||||
void sinterCommand(redisClient *c) {
|
||||
@@ -252,85 +413,78 @@ void sinterstoreCommand(redisClient *c) {
|
||||
sinterGenericCommand(c,c->argv+2,c->argc-2,c->argv[1]);
|
||||
}
|
||||
|
||||
void sunionDiffGenericCommand(redisClient *c, robj **setskeys, int setsnum, robj *dstkey, int op) {
|
||||
dict **dv = zmalloc(sizeof(dict*)*setsnum);
|
||||
dictIterator *di;
|
||||
dictEntry *de;
|
||||
robj *dstset = NULL;
|
||||
#define REDIS_OP_UNION 0
|
||||
#define REDIS_OP_DIFF 1
|
||||
#define REDIS_OP_INTER 2
|
||||
|
||||
void sunionDiffGenericCommand(redisClient *c, robj **setkeys, int setnum, robj *dstkey, int op) {
|
||||
robj **sets = zmalloc(sizeof(robj*)*setnum);
|
||||
setTypeIterator *si;
|
||||
robj *ele, *dstset = NULL;
|
||||
int j, cardinality = 0;
|
||||
|
||||
for (j = 0; j < setsnum; j++) {
|
||||
robj *setobj;
|
||||
|
||||
setobj = dstkey ?
|
||||
lookupKeyWrite(c->db,setskeys[j]) :
|
||||
lookupKeyRead(c->db,setskeys[j]);
|
||||
for (j = 0; j < setnum; j++) {
|
||||
robj *setobj = dstkey ?
|
||||
lookupKeyWrite(c->db,setkeys[j]) :
|
||||
lookupKeyRead(c->db,setkeys[j]);
|
||||
if (!setobj) {
|
||||
dv[j] = NULL;
|
||||
sets[j] = NULL;
|
||||
continue;
|
||||
}
|
||||
if (setobj->type != REDIS_SET) {
|
||||
zfree(dv);
|
||||
addReply(c,shared.wrongtypeerr);
|
||||
if (checkType(c,setobj,REDIS_SET)) {
|
||||
zfree(sets);
|
||||
return;
|
||||
}
|
||||
dv[j] = setobj->ptr;
|
||||
sets[j] = setobj;
|
||||
}
|
||||
|
||||
/* We need a temp set object to store our union. If the dstkey
|
||||
* is not NULL (that is, we are inside an SUNIONSTORE operation) then
|
||||
* this set object will be the resulting object to set into the target key*/
|
||||
dstset = createSetObject();
|
||||
dstset = createIntsetObject();
|
||||
|
||||
/* Iterate all the elements of all the sets, add every element a single
|
||||
* time to the result set */
|
||||
for (j = 0; j < setsnum; j++) {
|
||||
if (op == REDIS_OP_DIFF && j == 0 && !dv[j]) break; /* result set is empty */
|
||||
if (!dv[j]) continue; /* non existing keys are like empty sets */
|
||||
for (j = 0; j < setnum; j++) {
|
||||
if (op == REDIS_OP_DIFF && j == 0 && !sets[j]) break; /* result set is empty */
|
||||
if (!sets[j]) continue; /* non existing keys are like empty sets */
|
||||
|
||||
di = dictGetIterator(dv[j]);
|
||||
|
||||
while((de = dictNext(di)) != NULL) {
|
||||
robj *ele;
|
||||
|
||||
/* dictAdd will not add the same element multiple times */
|
||||
ele = dictGetEntryKey(de);
|
||||
si = setTypeInitIterator(sets[j]);
|
||||
while((ele = setTypeNext(si)) != NULL) {
|
||||
if (op == REDIS_OP_UNION || j == 0) {
|
||||
if (dictAdd(dstset->ptr,ele,NULL) == DICT_OK) {
|
||||
incrRefCount(ele);
|
||||
if (setTypeAdd(dstset,ele)) {
|
||||
cardinality++;
|
||||
}
|
||||
} else if (op == REDIS_OP_DIFF) {
|
||||
if (dictDelete(dstset->ptr,ele) == DICT_OK) {
|
||||
if (setTypeRemove(dstset,ele)) {
|
||||
cardinality--;
|
||||
}
|
||||
}
|
||||
decrRefCount(ele);
|
||||
}
|
||||
dictReleaseIterator(di);
|
||||
setTypeReleaseIterator(si);
|
||||
|
||||
/* result set is empty? Exit asap. */
|
||||
/* Exit when result set is empty. */
|
||||
if (op == REDIS_OP_DIFF && cardinality == 0) break;
|
||||
}
|
||||
|
||||
/* Output the content of the resulting set, if not in STORE mode */
|
||||
if (!dstkey) {
|
||||
addReplySds(c,sdscatprintf(sdsempty(),"*%d\r\n",cardinality));
|
||||
di = dictGetIterator(dstset->ptr);
|
||||
while((de = dictNext(di)) != NULL) {
|
||||
robj *ele;
|
||||
|
||||
ele = dictGetEntryKey(de);
|
||||
si = setTypeInitIterator(dstset);
|
||||
while((ele = setTypeNext(si)) != NULL) {
|
||||
addReplyBulk(c,ele);
|
||||
decrRefCount(ele);
|
||||
}
|
||||
dictReleaseIterator(di);
|
||||
setTypeReleaseIterator(si);
|
||||
decrRefCount(dstset);
|
||||
} else {
|
||||
/* If we have a target key where to store the resulting set
|
||||
* create this key with the result set inside */
|
||||
dbDelete(c->db,dstkey);
|
||||
if (dictSize((dict*)dstset->ptr) > 0) {
|
||||
if (setTypeSize(dstset) > 0) {
|
||||
dbAdd(c->db,dstkey,dstset);
|
||||
addReplyLongLong(c,dictSize((dict*)dstset->ptr));
|
||||
addReplyLongLong(c,setTypeSize(dstset));
|
||||
} else {
|
||||
decrRefCount(dstset);
|
||||
addReply(c,shared.czero);
|
||||
@@ -338,7 +492,7 @@ void sunionDiffGenericCommand(redisClient *c, robj **setskeys, int setsnum, robj
|
||||
touchWatchedKey(c->db,dstkey);
|
||||
server.dirty++;
|
||||
}
|
||||
zfree(dv);
|
||||
zfree(sets);
|
||||
}
|
||||
|
||||
void sunionCommand(redisClient *c) {
|
||||
|
||||
@@ -17,7 +17,6 @@ void setGenericCommand(redisClient *c, int nx, robj *key, robj *val, robj *expir
|
||||
}
|
||||
}
|
||||
|
||||
if (nx) deleteIfVolatile(c->db,key);
|
||||
retval = dbAdd(c->db,key,val);
|
||||
if (retval == REDIS_ERR) {
|
||||
if (!nx) {
|
||||
|
||||
+27
-7
@@ -200,24 +200,44 @@ int ll2string(char *s, size_t len, long long value) {
|
||||
return l;
|
||||
}
|
||||
|
||||
/* Check if the nul-terminated string 's' can be represented by a long
|
||||
/* Check if the sds string 's' can be represented by a long long
|
||||
* (that is, is a number that fits into long without any other space or
|
||||
* character before or after the digits).
|
||||
* character before or after the digits, so that converting this number
|
||||
* back to a string will result in the same bytes as the original string).
|
||||
*
|
||||
* If so, the function returns REDIS_OK and *longval is set to the value
|
||||
* If so, the function returns REDIS_OK and *llongval is set to the value
|
||||
* of the number. Otherwise REDIS_ERR is returned */
|
||||
int isStringRepresentableAsLong(sds s, long *longval) {
|
||||
int isStringRepresentableAsLongLong(sds s, long long *llongval) {
|
||||
char buf[32], *endptr;
|
||||
long value;
|
||||
long long value;
|
||||
int slen;
|
||||
|
||||
value = strtol(s, &endptr, 10);
|
||||
value = strtoll(s, &endptr, 10);
|
||||
if (endptr[0] != '\0') return REDIS_ERR;
|
||||
slen = ll2string(buf,32,value);
|
||||
|
||||
/* If the number converted back into a string is not identical
|
||||
* then it's not possible to encode the string as integer */
|
||||
if (sdslen(s) != (unsigned)slen || memcmp(buf,s,slen)) return REDIS_ERR;
|
||||
if (longval) *longval = value;
|
||||
if (llongval) *llongval = value;
|
||||
return REDIS_OK;
|
||||
}
|
||||
|
||||
int isStringRepresentableAsLong(sds s, long *longval) {
|
||||
long long ll;
|
||||
|
||||
if (isStringRepresentableAsLongLong(s,&ll) == REDIS_ERR) return REDIS_ERR;
|
||||
if (ll < LONG_MIN || ll > LONG_MAX) return REDIS_ERR;
|
||||
*longval = (long)ll;
|
||||
return REDIS_OK;
|
||||
}
|
||||
|
||||
int isObjectRepresentableAsLongLong(robj *o, long long *llongval) {
|
||||
redisAssert(o->type == REDIS_STRING);
|
||||
if (o->encoding == REDIS_ENCODING_INT) {
|
||||
if (llongval) *llongval = (long) o->ptr;
|
||||
return REDIS_OK;
|
||||
} else {
|
||||
return isStringRepresentableAsLongLong(o->ptr,llongval);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
#define REDIS_VERSION "2.1.2"
|
||||
#define REDIS_VERSION "2.1.4"
|
||||
|
||||
@@ -395,15 +395,20 @@ double computeObjectSwappability(robj *o) {
|
||||
z = (o->type == REDIS_ZSET);
|
||||
d = z ? ((zset*)o->ptr)->dict : o->ptr;
|
||||
|
||||
asize = sizeof(dict)+(sizeof(struct dictEntry*)*dictSlots(d));
|
||||
if (z) asize += sizeof(zset)-sizeof(dict);
|
||||
if (dictSize(d)) {
|
||||
de = dictGetRandomKey(d);
|
||||
ele = dictGetEntryKey(de);
|
||||
elesize = (ele->encoding == REDIS_ENCODING_RAW) ?
|
||||
(sizeof(*o)+sdslen(ele->ptr)) : sizeof(*o);
|
||||
asize += (sizeof(struct dictEntry)+elesize)*dictSize(d);
|
||||
if (z) asize += sizeof(zskiplistNode)*dictSize(d);
|
||||
if (!z && o->encoding == REDIS_ENCODING_INTSET) {
|
||||
intset *is = o->ptr;
|
||||
asize = sizeof(*is)+is->encoding*is->length;
|
||||
} else {
|
||||
asize = sizeof(dict)+(sizeof(struct dictEntry*)*dictSlots(d));
|
||||
if (z) asize += sizeof(zset)-sizeof(dict);
|
||||
if (dictSize(d)) {
|
||||
de = dictGetRandomKey(d);
|
||||
ele = dictGetEntryKey(de);
|
||||
elesize = (ele->encoding == REDIS_ENCODING_RAW) ?
|
||||
(sizeof(*o)+sdslen(ele->ptr)) : sizeof(*o);
|
||||
asize += (sizeof(struct dictEntry)+elesize)*dictSize(d);
|
||||
if (z) asize += sizeof(zskiplistNode)*dictSize(d);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case REDIS_HASH:
|
||||
@@ -543,7 +548,15 @@ void freeIOJob(iojob *j) {
|
||||
|
||||
/* Every time a thread finished a Job, it writes a byte into the write side
|
||||
* of an unix pipe in order to "awake" the main thread, and this function
|
||||
* is called. */
|
||||
* is called.
|
||||
*
|
||||
* Note that this is called both by the event loop, when a I/O thread
|
||||
* sends a byte in the notification pipe, and is also directly called from
|
||||
* waitEmptyIOJobsQueue().
|
||||
*
|
||||
* In the latter case we don't want to swap more, so we use the
|
||||
* "privdata" argument setting it to a not NULL value to signal this
|
||||
* condition. */
|
||||
void vmThreadedIOCompletedJob(aeEventLoop *el, int fd, void *privdata,
|
||||
int mask)
|
||||
{
|
||||
@@ -553,6 +566,8 @@ void vmThreadedIOCompletedJob(aeEventLoop *el, int fd, void *privdata,
|
||||
REDIS_NOTUSED(mask);
|
||||
REDIS_NOTUSED(privdata);
|
||||
|
||||
if (privdata != NULL) trytoswap = 0; /* check the comments above... */
|
||||
|
||||
/* For every byte we read in the read side of the pipe, there is one
|
||||
* I/O job completed to process. */
|
||||
while((retval = read(fd,buf,1)) == 1) {
|
||||
@@ -864,7 +879,8 @@ void waitEmptyIOJobsQueue(void) {
|
||||
io_processed_len = listLength(server.io_processed);
|
||||
unlockThreadedIO();
|
||||
if (io_processed_len) {
|
||||
vmThreadedIOCompletedJob(NULL,server.io_ready_pipe_read,NULL,0);
|
||||
vmThreadedIOCompletedJob(NULL,server.io_ready_pipe_read,
|
||||
(void*)0xdeadbeef,0);
|
||||
usleep(1000); /* 1 millisecond */
|
||||
} else {
|
||||
usleep(10000); /* 10 milliseconds */
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
start_server {tags {"cli"}} {
|
||||
proc open_cli {} {
|
||||
set ::env(TERM) dumb
|
||||
set fd [open [format "|src/redis-cli -p %d -n 9" [srv port]] "r+"]
|
||||
fconfigure $fd -buffering none
|
||||
fconfigure $fd -blocking false
|
||||
fconfigure $fd -translation binary
|
||||
assert_equal "redis> " [read_cli $fd]
|
||||
set _ $fd
|
||||
}
|
||||
|
||||
proc close_cli {fd} {
|
||||
close $fd
|
||||
}
|
||||
|
||||
proc read_cli {fd} {
|
||||
set buf [read $fd]
|
||||
while {[string length $buf] == 0} {
|
||||
# wait some time and try again
|
||||
after 10
|
||||
set buf [read $fd]
|
||||
}
|
||||
set _ $buf
|
||||
}
|
||||
|
||||
proc write_cli {fd buf} {
|
||||
puts $fd $buf
|
||||
flush $fd
|
||||
}
|
||||
|
||||
# Helpers to run tests in interactive mode
|
||||
proc run_command {fd cmd} {
|
||||
write_cli $fd $cmd
|
||||
set lines [split [read_cli $fd] "\n"]
|
||||
assert_equal "redis> " [lindex $lines end]
|
||||
join [lrange $lines 0 end-1] "\n"
|
||||
}
|
||||
|
||||
proc test_interactive_cli {name code} {
|
||||
set ::env(FAKETTY) 1
|
||||
set fd [open_cli]
|
||||
test "Interactive CLI: $name" $code
|
||||
close_cli $fd
|
||||
unset ::env(FAKETTY)
|
||||
}
|
||||
|
||||
# Helpers to run tests where stdout is not a tty
|
||||
proc write_tmpfile {contents} {
|
||||
set tmp [tmpfile "cli"]
|
||||
set tmpfd [open $tmp "w"]
|
||||
puts -nonewline $tmpfd $contents
|
||||
close $tmpfd
|
||||
set _ $tmp
|
||||
}
|
||||
|
||||
proc _run_cli {opts args} {
|
||||
set cmd [format "src/redis-cli -p %d -n 9 $args" [srv port]]
|
||||
foreach {key value} $opts {
|
||||
if {$key eq "pipe"} {
|
||||
set cmd "sh -c \"$value | $cmd\""
|
||||
}
|
||||
if {$key eq "path"} {
|
||||
set cmd "$cmd < $value"
|
||||
}
|
||||
}
|
||||
|
||||
set fd [open "|$cmd" "r"]
|
||||
fconfigure $fd -buffering none
|
||||
fconfigure $fd -translation binary
|
||||
set resp [read $fd 1048576]
|
||||
close $fd
|
||||
set _ $resp
|
||||
}
|
||||
|
||||
proc run_cli {args} {
|
||||
_run_cli {} {*}$args
|
||||
}
|
||||
|
||||
proc run_cli_with_input_pipe {cmd args} {
|
||||
_run_cli [list pipe $cmd] {*}$args
|
||||
}
|
||||
|
||||
proc run_cli_with_input_file {path args} {
|
||||
_run_cli [list path $path] {*}$args
|
||||
}
|
||||
|
||||
proc test_nontty_cli {name code} {
|
||||
test "Non-interactive non-TTY CLI: $name" $code
|
||||
}
|
||||
|
||||
# Helpers to run tests where stdout is a tty (fake it)
|
||||
proc test_tty_cli {name code} {
|
||||
set ::env(FAKETTY) 1
|
||||
test "Non-interactive TTY CLI: $name" $code
|
||||
unset ::env(FAKETTY)
|
||||
}
|
||||
|
||||
test_interactive_cli "INFO response should be printed raw" {
|
||||
set lines [split [run_command $fd info] "\n"]
|
||||
foreach line $lines {
|
||||
assert [regexp {^[a-z0-9_]+:[a-z0-9_]+} $line]
|
||||
}
|
||||
}
|
||||
|
||||
test_interactive_cli "Status reply" {
|
||||
assert_equal "OK" [run_command $fd "set key foo"]
|
||||
}
|
||||
|
||||
test_interactive_cli "Integer reply" {
|
||||
assert_equal "(integer) 1" [run_command $fd "incr counter"]
|
||||
}
|
||||
|
||||
test_interactive_cli "Bulk reply" {
|
||||
r set key foo
|
||||
assert_equal "\"foo\"" [run_command $fd "get key"]
|
||||
}
|
||||
|
||||
test_interactive_cli "Multi-bulk reply" {
|
||||
r rpush list foo
|
||||
r rpush list bar
|
||||
assert_equal "1. \"foo\"\n2. \"bar\"" [run_command $fd "lrange list 0 -1"]
|
||||
}
|
||||
|
||||
test_interactive_cli "Parsing quotes" {
|
||||
assert_equal "OK" [run_command $fd "set key \"bar\""]
|
||||
assert_equal "bar" [r get key]
|
||||
assert_equal "OK" [run_command $fd "set key \" bar \""]
|
||||
assert_equal " bar " [r get key]
|
||||
assert_equal "OK" [run_command $fd "set key \"\\\"bar\\\"\""]
|
||||
assert_equal "\"bar\"" [r get key]
|
||||
assert_equal "OK" [run_command $fd "set key \"\tbar\t\""]
|
||||
assert_equal "\tbar\t" [r get key]
|
||||
|
||||
# invalid quotation
|
||||
assert_equal "Invalid argument(s)" [run_command $fd "get \"\"key"]
|
||||
assert_equal "Invalid argument(s)" [run_command $fd "get \"key\"x"]
|
||||
|
||||
# quotes after the argument are weird, but should be allowed
|
||||
assert_equal "OK" [run_command $fd "set key\"\" bar"]
|
||||
assert_equal "bar" [r get key]
|
||||
}
|
||||
|
||||
test_tty_cli "Status reply" {
|
||||
assert_equal "OK\n" [run_cli set key bar]
|
||||
assert_equal "bar" [r get key]
|
||||
}
|
||||
|
||||
test_tty_cli "Integer reply" {
|
||||
r del counter
|
||||
assert_equal "(integer) 1\n" [run_cli incr counter]
|
||||
}
|
||||
|
||||
test_tty_cli "Bulk reply" {
|
||||
r set key "tab\tnewline\n"
|
||||
assert_equal "\"tab\\tnewline\\n\"\n" [run_cli get key]
|
||||
}
|
||||
|
||||
test_tty_cli "Multi-bulk reply" {
|
||||
r del list
|
||||
r rpush list foo
|
||||
r rpush list bar
|
||||
assert_equal "1. \"foo\"\n2. \"bar\"\n" [run_cli lrange list 0 -1]
|
||||
}
|
||||
|
||||
test_tty_cli "Read last argument from pipe" {
|
||||
assert_equal "OK\n" [run_cli_with_input_pipe "echo foo" set key]
|
||||
assert_equal "foo\n" [r get key]
|
||||
}
|
||||
|
||||
test_tty_cli "Read last argument from file" {
|
||||
set tmpfile [write_tmpfile "from file"]
|
||||
assert_equal "OK\n" [run_cli_with_input_file $tmpfile set key]
|
||||
assert_equal "from file" [r get key]
|
||||
}
|
||||
|
||||
test_nontty_cli "Status reply" {
|
||||
assert_equal "OK" [run_cli set key bar]
|
||||
assert_equal "bar" [r get key]
|
||||
}
|
||||
|
||||
test_nontty_cli "Integer reply" {
|
||||
r del counter
|
||||
assert_equal "1" [run_cli incr counter]
|
||||
}
|
||||
|
||||
test_nontty_cli "Bulk reply" {
|
||||
r set key "tab\tnewline\n"
|
||||
assert_equal "tab\tnewline\n" [run_cli get key]
|
||||
}
|
||||
|
||||
test_nontty_cli "Multi-bulk reply" {
|
||||
r del list
|
||||
r rpush list foo
|
||||
r rpush list bar
|
||||
assert_equal "foo\nbar" [run_cli lrange list 0 -1]
|
||||
}
|
||||
|
||||
test_nontty_cli "Read last argument from pipe" {
|
||||
assert_equal "OK" [run_cli_with_input_pipe "echo foo" set key]
|
||||
assert_equal "foo\n" [r get key]
|
||||
}
|
||||
|
||||
test_nontty_cli "Read last argument from file" {
|
||||
set tmpfile [write_tmpfile "from file"]
|
||||
assert_equal "OK" [run_cli_with_input_file $tmpfile set key]
|
||||
assert_equal "from file" [r get key]
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,24 @@ start_server {tags {"repl"}} {
|
||||
}
|
||||
assert_equal [r debug digest] [r -1 debug digest]
|
||||
}
|
||||
|
||||
test {MASTER and SLAVE consistency with expire} {
|
||||
createComplexDataset r 50000 useexpire
|
||||
after 4000 ;# Make sure everything expired before taking the digest
|
||||
if {[r debug digest] ne [r -1 debug digest]} {
|
||||
set csv1 [csvdump r]
|
||||
set csv2 [csvdump {r -1}]
|
||||
set fd [open /tmp/repldump1.txt w]
|
||||
puts -nonewline $fd $csv1
|
||||
close $fd
|
||||
set fd [open /tmp/repldump2.txt w]
|
||||
puts -nonewline $fd $csv2
|
||||
close $fd
|
||||
puts "Master - Slave inconsistency"
|
||||
puts "Run diff -u against /tmp/repldump*.txt for more info"
|
||||
}
|
||||
assert_equal [r debug digest] [r -1 debug digest]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+47
-20
@@ -83,7 +83,9 @@ proc ping_server {host port} {
|
||||
}
|
||||
close $fd
|
||||
} e]} {
|
||||
puts "Can't PING server at $host:$port... $e"
|
||||
puts -nonewline "."
|
||||
} else {
|
||||
puts -nonewline "ok"
|
||||
}
|
||||
return $retval
|
||||
}
|
||||
@@ -170,14 +172,33 @@ proc start_server {options {code undefined}} {
|
||||
|
||||
if {$::valgrind} {
|
||||
exec valgrind src/redis-server $config_file > $stdout 2> $stderr &
|
||||
after 2000
|
||||
} else {
|
||||
exec src/redis-server $config_file > $stdout 2> $stderr &
|
||||
after 500
|
||||
}
|
||||
|
||||
# check that the server actually started
|
||||
if {$code ne "undefined" && ![ping_server $::host $::port]} {
|
||||
# ugly but tries to be as fast as possible...
|
||||
set retrynum 20
|
||||
set serverisup 0
|
||||
|
||||
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
|
||||
}
|
||||
} else {
|
||||
set serverisup 1
|
||||
}
|
||||
puts {}
|
||||
|
||||
if {!$serverisup} {
|
||||
error_and_quit $config_file [exec cat $stderr]
|
||||
}
|
||||
|
||||
@@ -230,7 +251,11 @@ proc start_server {options {code undefined}} {
|
||||
|
||||
# execute provided block
|
||||
set curnum $::testnum
|
||||
catch { uplevel 1 $code } err
|
||||
if {![catch { uplevel 1 $code } err]} {
|
||||
# zero exit status is good
|
||||
unset err
|
||||
}
|
||||
|
||||
if {$curnum == $::testnum} {
|
||||
# don't check for leaks when no tests were executed
|
||||
dict set srv "skipleaks" 1
|
||||
@@ -241,22 +266,24 @@ proc start_server {options {code undefined}} {
|
||||
|
||||
# allow an exception to bubble up the call chain but still kill this
|
||||
# server, because we want to reuse the ports when the tests are re-run
|
||||
if {$err eq "exception"} {
|
||||
puts [format "Logged warnings (pid %d):" [dict get $srv "pid"]]
|
||||
set warnings [warnings_from_file [dict get $srv "stdout"]]
|
||||
if {[string length $warnings] > 0} {
|
||||
puts "$warnings"
|
||||
} else {
|
||||
puts "(none)"
|
||||
if {[info exists err]} {
|
||||
if {$err eq "exception"} {
|
||||
puts [format "Logged warnings (pid %d):" [dict get $srv "pid"]]
|
||||
set warnings [warnings_from_file [dict get $srv "stdout"]]
|
||||
if {[string length $warnings] > 0} {
|
||||
puts "$warnings"
|
||||
} else {
|
||||
puts "(none)"
|
||||
}
|
||||
# kill this server without checking for leaks
|
||||
dict set srv "skipleaks" 1
|
||||
kill_server $srv
|
||||
error "exception"
|
||||
} elseif {[string length $err] > 0} {
|
||||
puts "Error executing the suite, aborting..."
|
||||
puts $err
|
||||
exit 1
|
||||
}
|
||||
# kill this server without checking for leaks
|
||||
dict set srv "skipleaks" 1
|
||||
kill_server $srv
|
||||
error "exception"
|
||||
} elseif {[string length $err] > 0} {
|
||||
puts "Error executing the suite, aborting..."
|
||||
puts $err
|
||||
exit 1
|
||||
}
|
||||
|
||||
set ::tags [lrange $::tags 0 end-[llength $tags]]
|
||||
|
||||
@@ -36,8 +36,8 @@ proc assert_encoding {enc key} {
|
||||
# Swapped out values don't have an encoding, so make sure that
|
||||
# the value is swapped in before checking the encoding.
|
||||
set dbg [r debug object $key]
|
||||
while {[string match "* swapped:*" $dbg]} {
|
||||
[r debug swapin $key]
|
||||
while {[string match "* swapped at:*" $dbg]} {
|
||||
r debug swapin $key
|
||||
set dbg [r debug object $key]
|
||||
}
|
||||
assert_match "* encoding:$enc *" $dbg
|
||||
|
||||
@@ -140,12 +140,19 @@ proc findKeyWithType {r type} {
|
||||
return {}
|
||||
}
|
||||
|
||||
proc createComplexDataset {r ops} {
|
||||
proc createComplexDataset {r ops {opt {}}} {
|
||||
for {set j 0} {$j < $ops} {incr j} {
|
||||
set k [randomKey]
|
||||
set k2 [randomKey]
|
||||
set f [randomValue]
|
||||
set v [randomValue]
|
||||
|
||||
if {[lsearch -exact $opt useexpire] != -1} {
|
||||
if {rand() < 0.1} {
|
||||
{*}$r expire [randomKey] [randomInt 2]
|
||||
}
|
||||
}
|
||||
|
||||
randpath {
|
||||
set d [expr {rand()}]
|
||||
} {
|
||||
|
||||
@@ -25,7 +25,14 @@ proc execute_tests name {
|
||||
# are nested, use "srv 0 pid" to get the pid of the inner server. To access
|
||||
# outer servers, use "srv -1 pid" etcetera.
|
||||
set ::servers {}
|
||||
proc srv {level property} {
|
||||
proc srv {args} {
|
||||
set level 0
|
||||
if {[string is integer [lindex $args 0]]} {
|
||||
set level [lindex $args 0]
|
||||
set property [lindex $args 1]
|
||||
} else {
|
||||
set property [lindex $args 0]
|
||||
}
|
||||
set srv [lindex $::servers end+$level]
|
||||
dict get $srv $property
|
||||
}
|
||||
@@ -88,6 +95,7 @@ proc main {} {
|
||||
execute_tests "unit/cas"
|
||||
execute_tests "integration/replication"
|
||||
execute_tests "integration/aof"
|
||||
# execute_tests "integration/redis-cli"
|
||||
execute_tests "unit/pubsub"
|
||||
|
||||
# run tests with VM enabled
|
||||
|
||||
+3
-11
@@ -148,12 +148,11 @@ start_server {tags {"basic"}} {
|
||||
r get novar2
|
||||
} {foobared}
|
||||
|
||||
test {SETNX will overwrite EXPIREing key} {
|
||||
test {SETNX against volatile key} {
|
||||
r set x 10
|
||||
r expire x 10000
|
||||
r setnx x 20
|
||||
r get x
|
||||
} {20}
|
||||
list [r setnx x 20] [r get x]
|
||||
} {0 10}
|
||||
|
||||
test {EXISTS} {
|
||||
set res {}
|
||||
@@ -362,13 +361,6 @@ start_server {tags {"basic"}} {
|
||||
list [r msetnx x1 xxx y2 yyy] [r get x1] [r get y2]
|
||||
} {1 xxx yyy}
|
||||
|
||||
test {MSETNX should remove all the volatile keys even on failure} {
|
||||
r mset x 1 y 2 z 3
|
||||
r expire y 10000
|
||||
r expire z 10000
|
||||
list [r msetnx x A y B z C] [r mget x y z]
|
||||
} {0 {1 {} {}}}
|
||||
|
||||
test {STRLEN against non existing key} {
|
||||
r strlen notakey
|
||||
} {0}
|
||||
|
||||
+16
-4
@@ -1,12 +1,13 @@
|
||||
start_server {tags {"expire"}} {
|
||||
test {EXPIRE - don't set timeouts multiple times} {
|
||||
test {EXPIRE - set timeouts multiple times} {
|
||||
r set x foobar
|
||||
set v1 [r expire x 5]
|
||||
set v2 [r ttl x]
|
||||
set v3 [r expire x 10]
|
||||
set v4 [r ttl x]
|
||||
r expire x 4
|
||||
list $v1 $v2 $v3 $v4
|
||||
} {1 5 0 5}
|
||||
} {1 5 1 10}
|
||||
|
||||
test {EXPIRE - It should be still possible to read 'x'} {
|
||||
r get x
|
||||
@@ -19,13 +20,13 @@ start_server {tags {"expire"}} {
|
||||
} {{} 0}
|
||||
}
|
||||
|
||||
test {EXPIRE - Delete on write policy} {
|
||||
test {EXPIRE - write on expire should work} {
|
||||
r del x
|
||||
r lpush x foo
|
||||
r expire x 1000
|
||||
r lpush x bar
|
||||
r lrange x 0 -1
|
||||
} {bar}
|
||||
} {bar foo}
|
||||
|
||||
test {EXPIREAT - Check for EXPIRE alike behavior} {
|
||||
r del x
|
||||
@@ -59,4 +60,15 @@ start_server {tags {"expire"}} {
|
||||
catch {r setex z -10 foo} e
|
||||
set _ $e
|
||||
} {*invalid expire*}
|
||||
|
||||
test {PERSIST can undo an EXPIRE} {
|
||||
r set x foo
|
||||
r expire x 50
|
||||
list [r ttl x] [r persist x] [r ttl x] [r get x]
|
||||
} {50 1 -1 foo}
|
||||
|
||||
test {PERSIST returns 0 against non existing or non volatile keys} {
|
||||
r set x foo
|
||||
list [r persist foo] [r persist nokeyatall]
|
||||
} {0 0}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
start_server {} {
|
||||
start_server {tags {"other"}} {
|
||||
test {SAVE - make sure there are all the types as values} {
|
||||
# Wait for a background saving in progress to terminate
|
||||
waitForBgsave r
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
start_server {} {
|
||||
start_server {tags {"protocol"}} {
|
||||
test {Handle an empty query well} {
|
||||
set fd [r channel]
|
||||
puts -nonewline $fd "\r\n"
|
||||
@@ -27,6 +27,13 @@ start_server {} {
|
||||
gets $fd
|
||||
} {*invalid bulk*count*}
|
||||
|
||||
test {bulk payload is not a number} {
|
||||
set fd [r channel]
|
||||
puts -nonewline $fd "SET x blabla\r\n"
|
||||
flush $fd
|
||||
gets $fd
|
||||
} {*invalid bulk*count*}
|
||||
|
||||
test {Multi bulk request not followed by bulk args} {
|
||||
set fd [r channel]
|
||||
puts -nonewline $fd "*1\r\nfoo\r\n"
|
||||
|
||||
+154
-152
@@ -1,5 +1,101 @@
|
||||
start_server {tags {"sort"}} {
|
||||
test {SORT ALPHA against integer encoded strings} {
|
||||
start_server {
|
||||
tags {"sort"}
|
||||
overrides {
|
||||
"list-max-ziplist-value" 16
|
||||
"list-max-ziplist-entries" 32
|
||||
"set-max-intset-entries" 32
|
||||
}
|
||||
} {
|
||||
proc create_random_dataset {num cmd} {
|
||||
set tosort {}
|
||||
set result {}
|
||||
array set seenrand {}
|
||||
r del tosort
|
||||
for {set i 0} {$i < $num} {incr i} {
|
||||
# Make sure all the weights are different because
|
||||
# Redis does not use a stable sort but Tcl does.
|
||||
while 1 {
|
||||
randpath {
|
||||
set rint [expr int(rand()*1000000)]
|
||||
} {
|
||||
set rint [expr rand()]
|
||||
}
|
||||
if {![info exists seenrand($rint)]} break
|
||||
}
|
||||
set seenrand($rint) x
|
||||
r $cmd tosort $i
|
||||
r set weight_$i $rint
|
||||
r hset wobj_$i weight $rint
|
||||
lappend tosort [list $i $rint]
|
||||
}
|
||||
set sorted [lsort -index 1 -real $tosort]
|
||||
for {set i 0} {$i < $num} {incr i} {
|
||||
lappend result [lindex $sorted $i 0]
|
||||
}
|
||||
set _ $result
|
||||
}
|
||||
|
||||
foreach {num cmd enc title} {
|
||||
16 lpush ziplist "Ziplist"
|
||||
1000 lpush linkedlist "Linked list"
|
||||
10000 lpush linkedlist "Big Linked list"
|
||||
16 sadd intset "Intset"
|
||||
1000 sadd hashtable "Hash table"
|
||||
10000 sadd hashtable "Big Hash table"
|
||||
} {
|
||||
set result [create_random_dataset $num $cmd]
|
||||
assert_encoding $enc tosort
|
||||
|
||||
test "$title: SORT BY key" {
|
||||
assert_equal $result [r sort tosort {BY weight_*}]
|
||||
}
|
||||
|
||||
test "$title: SORT BY hash field" {
|
||||
assert_equal $result [r sort tosort {BY wobj_*->weight}]
|
||||
}
|
||||
}
|
||||
|
||||
set result [create_random_dataset 16 lpush]
|
||||
test "SORT GET #" {
|
||||
assert_equal [lsort -integer $result] [r sort tosort GET #]
|
||||
}
|
||||
|
||||
test "SORT GET <const>" {
|
||||
r del foo
|
||||
set res [r sort tosort GET foo]
|
||||
assert_equal 16 [llength $res]
|
||||
foreach item $res { assert_equal {} $item }
|
||||
}
|
||||
|
||||
test "SORT GET (key and hash) with sanity check" {
|
||||
set l1 [r sort tosort GET # GET weight_*]
|
||||
set l2 [r sort tosort GET # GET wobj_*->weight]
|
||||
foreach {id1 w1} $l1 {id2 w2} $l2 {
|
||||
assert_equal $id1 $id2
|
||||
assert_equal $w1 [r get weight_$id1]
|
||||
assert_equal $w2 [r get weight_$id1]
|
||||
}
|
||||
}
|
||||
|
||||
test "SORT BY key STORE" {
|
||||
r sort tosort {BY weight_*} store sort-res
|
||||
assert_equal $result [r lrange sort-res 0 -1]
|
||||
assert_equal 16 [r llen sort-res]
|
||||
assert_encoding ziplist sort-res
|
||||
}
|
||||
|
||||
test "SORT BY hash field STORE" {
|
||||
r sort tosort {BY wobj_*->weight} store sort-res
|
||||
assert_equal $result [r lrange sort-res 0 -1]
|
||||
assert_equal 16 [r llen sort-res]
|
||||
assert_encoding ziplist sort-res
|
||||
}
|
||||
|
||||
test "SORT DESC" {
|
||||
assert_equal [lsort -decreasing -integer $result] [r sort tosort {DESC}]
|
||||
}
|
||||
|
||||
test "SORT ALPHA against integer encoded strings" {
|
||||
r del mylist
|
||||
r lpush mylist 2
|
||||
r lpush mylist 1
|
||||
@@ -8,155 +104,7 @@ start_server {tags {"sort"}} {
|
||||
r sort mylist alpha
|
||||
} {1 10 2 3}
|
||||
|
||||
tags {"slow"} {
|
||||
set res {}
|
||||
test {Create a random list and a random set} {
|
||||
set tosort {}
|
||||
array set seenrand {}
|
||||
for {set i 0} {$i < 10000} {incr i} {
|
||||
while 1 {
|
||||
# Make sure all the weights are different because
|
||||
# Redis does not use a stable sort but Tcl does.
|
||||
randpath {
|
||||
set rint [expr int(rand()*1000000)]
|
||||
} {
|
||||
set rint [expr rand()]
|
||||
}
|
||||
if {![info exists seenrand($rint)]} break
|
||||
}
|
||||
set seenrand($rint) x
|
||||
r lpush tosort $i
|
||||
r sadd tosort-set $i
|
||||
r set weight_$i $rint
|
||||
r hset wobj_$i weight $rint
|
||||
lappend tosort [list $i $rint]
|
||||
}
|
||||
set sorted [lsort -index 1 -real $tosort]
|
||||
for {set i 0} {$i < 10000} {incr i} {
|
||||
lappend res [lindex $sorted $i 0]
|
||||
}
|
||||
format {}
|
||||
} {}
|
||||
|
||||
test {SORT with BY against the newly created list} {
|
||||
r sort tosort {BY weight_*}
|
||||
} $res
|
||||
|
||||
test {SORT with BY (hash field) against the newly created list} {
|
||||
r sort tosort {BY wobj_*->weight}
|
||||
} $res
|
||||
|
||||
test {SORT with GET (key+hash) with sanity check of each element (list)} {
|
||||
set err {}
|
||||
set l1 [r sort tosort GET # GET weight_*]
|
||||
set l2 [r sort tosort GET # GET wobj_*->weight]
|
||||
foreach {id1 w1} $l1 {id2 w2} $l2 {
|
||||
set realweight [r get weight_$id1]
|
||||
if {$id1 != $id2} {
|
||||
set err "ID mismatch $id1 != $id2"
|
||||
break
|
||||
}
|
||||
if {$realweight != $w1 || $realweight != $w2} {
|
||||
set err "Weights mismatch! w1: $w1 w2: $w2 real: $realweight"
|
||||
break
|
||||
}
|
||||
}
|
||||
set _ $err
|
||||
} {}
|
||||
|
||||
test {SORT with BY, but against the newly created set} {
|
||||
r sort tosort-set {BY weight_*}
|
||||
} $res
|
||||
|
||||
test {SORT with BY (hash field), but against the newly created set} {
|
||||
r sort tosort-set {BY wobj_*->weight}
|
||||
} $res
|
||||
|
||||
test {SORT with BY and STORE against the newly created list} {
|
||||
r sort tosort {BY weight_*} store sort-res
|
||||
r lrange sort-res 0 -1
|
||||
} $res
|
||||
|
||||
test {SORT with BY (hash field) and STORE against the newly created list} {
|
||||
r sort tosort {BY wobj_*->weight} store sort-res
|
||||
r lrange sort-res 0 -1
|
||||
} $res
|
||||
|
||||
test {SORT direct, numeric, against the newly created list} {
|
||||
r sort tosort
|
||||
} [lsort -integer $res]
|
||||
|
||||
test {SORT decreasing sort} {
|
||||
r sort tosort {DESC}
|
||||
} [lsort -decreasing -integer $res]
|
||||
|
||||
test {SORT speed, sorting 10000 elements list using BY, 100 times} {
|
||||
set start [clock clicks -milliseconds]
|
||||
for {set i 0} {$i < 100} {incr i} {
|
||||
set sorted [r sort tosort {BY weight_* LIMIT 0 10}]
|
||||
}
|
||||
set elapsed [expr [clock clicks -milliseconds]-$start]
|
||||
puts -nonewline "\n Average time to sort: [expr double($elapsed)/100] milliseconds "
|
||||
flush stdout
|
||||
format {}
|
||||
} {}
|
||||
|
||||
test {SORT speed, as above but against hash field} {
|
||||
set start [clock clicks -milliseconds]
|
||||
for {set i 0} {$i < 100} {incr i} {
|
||||
set sorted [r sort tosort {BY wobj_*->weight LIMIT 0 10}]
|
||||
}
|
||||
set elapsed [expr [clock clicks -milliseconds]-$start]
|
||||
puts -nonewline "\n Average time to sort: [expr double($elapsed)/100] milliseconds "
|
||||
flush stdout
|
||||
format {}
|
||||
} {}
|
||||
|
||||
test {SORT speed, sorting 10000 elements list directly, 100 times} {
|
||||
set start [clock clicks -milliseconds]
|
||||
for {set i 0} {$i < 100} {incr i} {
|
||||
set sorted [r sort tosort {LIMIT 0 10}]
|
||||
}
|
||||
set elapsed [expr [clock clicks -milliseconds]-$start]
|
||||
puts -nonewline "\n Average time to sort: [expr double($elapsed)/100] milliseconds "
|
||||
flush stdout
|
||||
format {}
|
||||
} {}
|
||||
|
||||
test {SORT speed, pseudo-sorting 10000 elements list, BY <const>, 100 times} {
|
||||
set start [clock clicks -milliseconds]
|
||||
for {set i 0} {$i < 100} {incr i} {
|
||||
set sorted [r sort tosort {BY nokey LIMIT 0 10}]
|
||||
}
|
||||
set elapsed [expr [clock clicks -milliseconds]-$start]
|
||||
puts -nonewline "\n Average time to sort: [expr double($elapsed)/100] milliseconds "
|
||||
flush stdout
|
||||
format {}
|
||||
} {}
|
||||
}
|
||||
|
||||
test {SORT regression for issue #19, sorting floats} {
|
||||
r flushdb
|
||||
foreach x {1.1 5.10 3.10 7.44 2.1 5.75 6.12 0.25 1.15} {
|
||||
r lpush mylist $x
|
||||
}
|
||||
r sort mylist
|
||||
} [lsort -real {1.1 5.10 3.10 7.44 2.1 5.75 6.12 0.25 1.15}]
|
||||
|
||||
test {SORT with GET #} {
|
||||
r del mylist
|
||||
r lpush mylist 1
|
||||
r lpush mylist 2
|
||||
r lpush mylist 3
|
||||
r mset weight_1 10 weight_2 5 weight_3 30
|
||||
r sort mylist BY weight_* GET #
|
||||
} {2 1 3}
|
||||
|
||||
test {SORT with constant GET} {
|
||||
r sort mylist GET foo
|
||||
} {{} {} {}}
|
||||
|
||||
test {SORT against sorted sets} {
|
||||
test "SORT sorted set" {
|
||||
r del zset
|
||||
r zadd zset 1 a
|
||||
r zadd zset 5 b
|
||||
@@ -166,7 +114,7 @@ start_server {tags {"sort"}} {
|
||||
r sort zset alpha desc
|
||||
} {e d c b a}
|
||||
|
||||
test {Sorted sets +inf and -inf handling} {
|
||||
test "SORT sorted set: +inf and -inf handling" {
|
||||
r del zset
|
||||
r zadd zset -100 a
|
||||
r zadd zset 200 b
|
||||
@@ -176,4 +124,58 @@ start_server {tags {"sort"}} {
|
||||
r zadd zset -inf min
|
||||
r zrange zset 0 -1
|
||||
} {min c a b d max}
|
||||
|
||||
test "SORT regression for issue #19, sorting floats" {
|
||||
r flushdb
|
||||
set floats {1.1 5.10 3.10 7.44 2.1 5.75 6.12 0.25 1.15}
|
||||
foreach x $floats {
|
||||
r lpush mylist $x
|
||||
}
|
||||
assert_equal [lsort -real $floats] [r sort mylist]
|
||||
}
|
||||
|
||||
tags {"slow"} {
|
||||
set num 100
|
||||
set res [create_random_dataset $num lpush]
|
||||
|
||||
test "SORT speed, $num element list BY key, 100 times" {
|
||||
set start [clock clicks -milliseconds]
|
||||
for {set i 0} {$i < 100} {incr i} {
|
||||
set sorted [r sort tosort {BY weight_* LIMIT 0 10}]
|
||||
}
|
||||
set elapsed [expr [clock clicks -milliseconds]-$start]
|
||||
puts -nonewline "\n Average time to sort: [expr double($elapsed)/100] milliseconds "
|
||||
flush stdout
|
||||
}
|
||||
|
||||
test "SORT speed, $num element list BY hash field, 100 times" {
|
||||
set start [clock clicks -milliseconds]
|
||||
for {set i 0} {$i < 100} {incr i} {
|
||||
set sorted [r sort tosort {BY wobj_*->weight LIMIT 0 10}]
|
||||
}
|
||||
set elapsed [expr [clock clicks -milliseconds]-$start]
|
||||
puts -nonewline "\n Average time to sort: [expr double($elapsed)/100] milliseconds "
|
||||
flush stdout
|
||||
}
|
||||
|
||||
test "SORT speed, $num element list directly, 100 times" {
|
||||
set start [clock clicks -milliseconds]
|
||||
for {set i 0} {$i < 100} {incr i} {
|
||||
set sorted [r sort tosort {LIMIT 0 10}]
|
||||
}
|
||||
set elapsed [expr [clock clicks -milliseconds]-$start]
|
||||
puts -nonewline "\n Average time to sort: [expr double($elapsed)/100] milliseconds "
|
||||
flush stdout
|
||||
}
|
||||
|
||||
test "SORT speed, $num element list BY <const>, 100 times" {
|
||||
set start [clock clicks -milliseconds]
|
||||
for {set i 0} {$i < 100} {incr i} {
|
||||
set sorted [r sort tosort {BY nokey LIMIT 0 10}]
|
||||
}
|
||||
set elapsed [expr [clock clicks -milliseconds]-$start]
|
||||
puts -nonewline "\n Average time to sort: [expr double($elapsed)/100] milliseconds "
|
||||
flush stdout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,6 +139,28 @@ start_server {
|
||||
assert_equal 0 [r exists blist1]
|
||||
}
|
||||
|
||||
test "$pop: with negative timeout" {
|
||||
set rd [redis_deferring_client]
|
||||
$rd $pop blist1 -1
|
||||
assert_error "ERR*is negative*" {$rd read}
|
||||
}
|
||||
|
||||
test "$pop: with non-integer timeout" {
|
||||
set rd [redis_deferring_client]
|
||||
$rd $pop blist1 1.1
|
||||
assert_error "ERR*not an integer*" {$rd read}
|
||||
}
|
||||
|
||||
test "$pop: with zero timeout should block indefinitely" {
|
||||
# To test this, use a timeout of 0 and wait a second.
|
||||
# The blocking pop should still be waiting for a push.
|
||||
set rd [redis_deferring_client]
|
||||
$rd $pop blist1 0
|
||||
after 1000
|
||||
r rpush blist1 foo
|
||||
assert_equal {blist1 foo} [$rd read]
|
||||
}
|
||||
|
||||
test "$pop: second argument is not a list" {
|
||||
set rd [redis_deferring_client]
|
||||
r del blist1 blist2
|
||||
@@ -172,6 +194,17 @@ start_server {
|
||||
}
|
||||
}
|
||||
|
||||
test {BLPOP inside a transaction} {
|
||||
r del xlist
|
||||
r lpush xlist foo
|
||||
r lpush xlist bar
|
||||
r multi
|
||||
r blpop xlist 0
|
||||
r blpop xlist 0
|
||||
r blpop xlist 0
|
||||
r exec
|
||||
} {{xlist bar} {xlist foo} {}}
|
||||
|
||||
test {LPUSHX, RPUSHX - generic} {
|
||||
r del xlist
|
||||
assert_equal 0 [r lpushx xlist a]
|
||||
|
||||
+272
-125
@@ -1,151 +1,298 @@
|
||||
start_server {tags {"set"}} {
|
||||
test {SADD, SCARD, SISMEMBER, SMEMBERS basics} {
|
||||
r sadd myset foo
|
||||
r sadd myset bar
|
||||
list [r scard myset] [r sismember myset foo] \
|
||||
[r sismember myset bar] [r sismember myset bla] \
|
||||
[lsort [r smembers myset]]
|
||||
} {2 1 1 0 {bar foo}}
|
||||
start_server {
|
||||
tags {"set"}
|
||||
overrides {
|
||||
"set-max-intset-entries" 512
|
||||
}
|
||||
} {
|
||||
proc create_set {key entries} {
|
||||
r del $key
|
||||
foreach entry $entries { r sadd $key $entry }
|
||||
}
|
||||
|
||||
test {SADD adding the same element multiple times} {
|
||||
r sadd myset foo
|
||||
r sadd myset foo
|
||||
r sadd myset foo
|
||||
r scard myset
|
||||
} {2}
|
||||
test {SADD, SCARD, SISMEMBER, SMEMBERS basics - regular set} {
|
||||
create_set myset {foo}
|
||||
assert_encoding hashtable myset
|
||||
assert_equal 1 [r sadd myset bar]
|
||||
assert_equal 0 [r sadd myset bar]
|
||||
assert_equal 2 [r scard myset]
|
||||
assert_equal 1 [r sismember myset foo]
|
||||
assert_equal 1 [r sismember myset bar]
|
||||
assert_equal 0 [r sismember myset bla]
|
||||
assert_equal {bar foo} [lsort [r smembers myset]]
|
||||
}
|
||||
|
||||
test {SADD, SCARD, SISMEMBER, SMEMBERS basics - intset} {
|
||||
create_set myset {17}
|
||||
assert_encoding intset myset
|
||||
assert_equal 1 [r sadd myset 16]
|
||||
assert_equal 0 [r sadd myset 16]
|
||||
assert_equal 2 [r scard myset]
|
||||
assert_equal 1 [r sismember myset 16]
|
||||
assert_equal 1 [r sismember myset 17]
|
||||
assert_equal 0 [r sismember myset 18]
|
||||
assert_equal {16 17} [lsort [r smembers myset]]
|
||||
}
|
||||
|
||||
test {SADD against non set} {
|
||||
r lpush mylist foo
|
||||
catch {r sadd mylist bar} err
|
||||
format $err
|
||||
} {ERR*kind*}
|
||||
assert_error ERR*kind* {r sadd mylist bar}
|
||||
}
|
||||
|
||||
test {SREM basics} {
|
||||
r sadd myset ciao
|
||||
r srem myset foo
|
||||
lsort [r smembers myset]
|
||||
} {bar ciao}
|
||||
test "SADD a non-integer against an intset" {
|
||||
create_set myset {1 2 3}
|
||||
assert_encoding intset myset
|
||||
assert_equal 1 [r sadd myset a]
|
||||
assert_encoding hashtable myset
|
||||
}
|
||||
|
||||
test {Mass SADD and SINTER with two sets} {
|
||||
for {set i 0} {$i < 1000} {incr i} {
|
||||
r sadd set1 $i
|
||||
r sadd set2 [expr $i+995]
|
||||
}
|
||||
lsort [r sinter set1 set2]
|
||||
} {995 996 997 998 999}
|
||||
test "SADD an integer larger than 64 bits" {
|
||||
create_set myset {213244124402402314402033402}
|
||||
assert_encoding hashtable myset
|
||||
assert_equal 1 [r sismember myset 213244124402402314402033402]
|
||||
}
|
||||
|
||||
test {SUNION with two sets} {
|
||||
lsort [r sunion set1 set2]
|
||||
} [lsort -uniq "[r smembers set1] [r smembers set2]"]
|
||||
test "SADD overflows the maximum allowed integers in an intset" {
|
||||
r del myset
|
||||
for {set i 0} {$i < 512} {incr i} { r sadd myset $i }
|
||||
assert_encoding intset myset
|
||||
assert_equal 1 [r sadd myset 512]
|
||||
assert_encoding hashtable myset
|
||||
}
|
||||
|
||||
test {SINTERSTORE with two sets} {
|
||||
r sinterstore setres set1 set2
|
||||
lsort [r smembers setres]
|
||||
} {995 996 997 998 999}
|
||||
test "Set encoding after DEBUG RELOAD" {
|
||||
r del myintset myhashset mylargeintset
|
||||
for {set i 0} {$i < 100} {incr i} { r sadd myintset $i }
|
||||
for {set i 0} {$i < 1280} {incr i} { r sadd mylargeintset $i }
|
||||
for {set i 0} {$i < 256} {incr i} { r sadd myhashset [format "i%03d" $i] }
|
||||
assert_encoding intset myintset
|
||||
assert_encoding hashtable mylargeintset
|
||||
assert_encoding hashtable myhashset
|
||||
|
||||
test {SINTERSTORE with two sets, after a DEBUG RELOAD} {
|
||||
r debug reload
|
||||
r sinterstore setres set1 set2
|
||||
lsort [r smembers setres]
|
||||
} {995 996 997 998 999}
|
||||
assert_encoding intset myintset
|
||||
assert_encoding hashtable mylargeintset
|
||||
assert_encoding hashtable myhashset
|
||||
}
|
||||
|
||||
test {SUNIONSTORE with two sets} {
|
||||
r sunionstore setres set1 set2
|
||||
lsort [r smembers setres]
|
||||
} [lsort -uniq "[r smembers set1] [r smembers set2]"]
|
||||
test {SREM basics - regular set} {
|
||||
create_set myset {foo bar ciao}
|
||||
assert_encoding hashtable myset
|
||||
assert_equal 0 [r srem myset qux]
|
||||
assert_equal 1 [r srem myset foo]
|
||||
assert_equal {bar ciao} [lsort [r smembers myset]]
|
||||
}
|
||||
|
||||
test {SUNIONSTORE against non existing keys} {
|
||||
r set setres xxx
|
||||
list [r sunionstore setres foo111 bar222] [r exists xxx]
|
||||
} {0 0}
|
||||
test {SREM basics - intset} {
|
||||
create_set myset {3 4 5}
|
||||
assert_encoding intset myset
|
||||
assert_equal 0 [r srem myset 6]
|
||||
assert_equal 1 [r srem myset 4]
|
||||
assert_equal {3 5} [lsort [r smembers myset]]
|
||||
}
|
||||
|
||||
test {SINTER against three sets} {
|
||||
r sadd set3 999
|
||||
r sadd set3 995
|
||||
r sadd set3 1000
|
||||
r sadd set3 2000
|
||||
lsort [r sinter set1 set2 set3]
|
||||
} {995 999}
|
||||
|
||||
test {SINTERSTORE with three sets} {
|
||||
r sinterstore setres set1 set2 set3
|
||||
lsort [r smembers setres]
|
||||
} {995 999}
|
||||
|
||||
test {SUNION with non existing keys} {
|
||||
lsort [r sunion nokey1 set1 set2 nokey2]
|
||||
} [lsort -uniq "[r smembers set1] [r smembers set2]"]
|
||||
|
||||
test {SDIFF with two sets} {
|
||||
for {set i 5} {$i < 1000} {incr i} {
|
||||
foreach {type} {hashtable intset} {
|
||||
for {set i 1} {$i <= 5} {incr i} {
|
||||
r del [format "set%d" $i]
|
||||
}
|
||||
for {set i 0} {$i < 200} {incr i} {
|
||||
r sadd set1 $i
|
||||
r sadd set2 [expr $i+195]
|
||||
}
|
||||
foreach i {199 195 1000 2000} {
|
||||
r sadd set3 $i
|
||||
}
|
||||
for {set i 5} {$i < 200} {incr i} {
|
||||
r sadd set4 $i
|
||||
}
|
||||
lsort [r sdiff set1 set4]
|
||||
} {0 1 2 3 4}
|
||||
|
||||
test {SDIFF with three sets} {
|
||||
r sadd set5 0
|
||||
lsort [r sdiff set1 set4 set5]
|
||||
} {1 2 3 4}
|
||||
|
||||
test {SDIFFSTORE with three sets} {
|
||||
r sdiffstore sres set1 set4 set5
|
||||
lsort [r smembers sres]
|
||||
} {1 2 3 4}
|
||||
|
||||
test {SPOP basics} {
|
||||
r del myset
|
||||
r sadd myset 1
|
||||
r sadd myset 2
|
||||
r sadd myset 3
|
||||
list [lsort [list [r spop myset] [r spop myset] [r spop myset]]] [r scard myset]
|
||||
} {{1 2 3} 0}
|
||||
|
||||
test {SRANDMEMBER} {
|
||||
r del myset
|
||||
r sadd myset a
|
||||
r sadd myset b
|
||||
r sadd myset c
|
||||
unset -nocomplain myset
|
||||
array set myset {}
|
||||
for {set i 0} {$i < 100} {incr i} {
|
||||
set myset([r srandmember myset]) 1
|
||||
# To make sure the sets are encoded as the type we are testing -- also
|
||||
# when the VM is enabled and the values may be swapped in and out
|
||||
# while the tests are running -- an extra element is added to every
|
||||
# set that determines its encoding.
|
||||
set large 200
|
||||
if {$type eq "hashtable"} {
|
||||
set large foo
|
||||
}
|
||||
lsort [array names myset]
|
||||
} {a b c}
|
||||
|
||||
test {SMOVE basics} {
|
||||
r sadd myset1 a
|
||||
r sadd myset1 b
|
||||
r sadd myset1 c
|
||||
r sadd myset2 x
|
||||
r sadd myset2 y
|
||||
r sadd myset2 z
|
||||
r smove myset1 myset2 a
|
||||
list [lsort [r smembers myset2]] [lsort [r smembers myset1]]
|
||||
} {{a x y z} {b c}}
|
||||
|
||||
test {SMOVE non existing key} {
|
||||
list [r smove myset1 myset2 foo] [lsort [r smembers myset2]] [lsort [r smembers myset1]]
|
||||
} {0 {a x y z} {b c}}
|
||||
for {set i 1} {$i <= 5} {incr i} {
|
||||
r sadd [format "set%d" $i] $large
|
||||
}
|
||||
|
||||
test {SMOVE non existing src set} {
|
||||
list [r smove noset myset2 foo] [lsort [r smembers myset2]]
|
||||
} {0 {a x y z}}
|
||||
test "Generated sets must be encoded as $type" {
|
||||
for {set i 1} {$i <= 5} {incr i} {
|
||||
assert_encoding $type [format "set%d" $i]
|
||||
}
|
||||
}
|
||||
|
||||
test {SMOVE non existing dst set} {
|
||||
list [r smove myset2 myset3 y] [lsort [r smembers myset2]] [lsort [r smembers myset3]]
|
||||
} {1 {a x z} y}
|
||||
test "SINTER with two sets - $type" {
|
||||
assert_equal [list 195 196 197 198 199 $large] [lsort [r sinter set1 set2]]
|
||||
}
|
||||
|
||||
test {SMOVE wrong src key type} {
|
||||
test "SINTERSTORE with two sets - $type" {
|
||||
r sinterstore setres set1 set2
|
||||
assert_encoding $type setres
|
||||
assert_equal [list 195 196 197 198 199 $large] [lsort [r smembers setres]]
|
||||
}
|
||||
|
||||
test "SINTERSTORE with two sets, after a DEBUG RELOAD - $type" {
|
||||
r debug reload
|
||||
r sinterstore setres set1 set2
|
||||
assert_encoding $type setres
|
||||
assert_equal [list 195 196 197 198 199 $large] [lsort [r smembers setres]]
|
||||
}
|
||||
|
||||
test "SUNION with two sets - $type" {
|
||||
set expected [lsort -uniq "[r smembers set1] [r smembers set2]"]
|
||||
assert_equal $expected [lsort [r sunion set1 set2]]
|
||||
}
|
||||
|
||||
test "SUNIONSTORE with two sets - $type" {
|
||||
r sunionstore setres set1 set2
|
||||
assert_encoding $type setres
|
||||
set expected [lsort -uniq "[r smembers set1] [r smembers set2]"]
|
||||
assert_equal $expected [lsort [r smembers setres]]
|
||||
}
|
||||
|
||||
test "SINTER against three sets - $type" {
|
||||
assert_equal [list 195 199 $large] [lsort [r sinter set1 set2 set3]]
|
||||
}
|
||||
|
||||
test "SINTERSTORE with three sets - $type" {
|
||||
r sinterstore setres set1 set2 set3
|
||||
assert_equal [list 195 199 $large] [lsort [r smembers setres]]
|
||||
}
|
||||
|
||||
test "SUNION with non existing keys - $type" {
|
||||
set expected [lsort -uniq "[r smembers set1] [r smembers set2]"]
|
||||
assert_equal $expected [lsort [r sunion nokey1 set1 set2 nokey2]]
|
||||
}
|
||||
|
||||
test "SDIFF with two sets - $type" {
|
||||
assert_equal {0 1 2 3 4} [lsort [r sdiff set1 set4]]
|
||||
}
|
||||
|
||||
test "SDIFF with three sets - $type" {
|
||||
assert_equal {1 2 3 4} [lsort [r sdiff set1 set4 set5]]
|
||||
}
|
||||
|
||||
test "SDIFFSTORE with three sets - $type" {
|
||||
r sdiffstore setres set1 set4 set5
|
||||
# The type is determined by type of the first key to diff against.
|
||||
# See the implementation for more information.
|
||||
assert_encoding $type setres
|
||||
assert_equal {1 2 3 4} [lsort [r smembers setres]]
|
||||
}
|
||||
}
|
||||
|
||||
test "SINTER against non-set should throw error" {
|
||||
r set key1 x
|
||||
assert_error "ERR*wrong kind*" {r sinter key1 noset}
|
||||
}
|
||||
|
||||
test "SUNION against non-set should throw error" {
|
||||
r set key1 x
|
||||
assert_error "ERR*wrong kind*" {r sunion key1 noset}
|
||||
}
|
||||
|
||||
test "SINTERSTORE against non existing keys should delete dstkey" {
|
||||
r set setres xxx
|
||||
assert_equal 0 [r sinterstore setres foo111 bar222]
|
||||
assert_equal 0 [r exists setres]
|
||||
}
|
||||
|
||||
test "SUNIONSTORE against non existing keys should delete dstkey" {
|
||||
r set setres xxx
|
||||
assert_equal 0 [r sunionstore setres foo111 bar222]
|
||||
assert_equal 0 [r exists setres]
|
||||
}
|
||||
|
||||
foreach {type contents} {hashtable {a b c} intset {1 2 3}} {
|
||||
test "SPOP basics - $type" {
|
||||
create_set myset $contents
|
||||
assert_encoding $type myset
|
||||
assert_equal $contents [lsort [list [r spop myset] [r spop myset] [r spop myset]]]
|
||||
assert_equal 0 [r scard myset]
|
||||
}
|
||||
|
||||
test "SRANDMEMBER - $type" {
|
||||
create_set myset $contents
|
||||
unset -nocomplain myset
|
||||
array set myset {}
|
||||
for {set i 0} {$i < 100} {incr i} {
|
||||
set myset([r srandmember myset]) 1
|
||||
}
|
||||
assert_equal $contents [lsort [array names myset]]
|
||||
}
|
||||
}
|
||||
|
||||
proc setup_move {} {
|
||||
r del myset3 myset4
|
||||
create_set myset1 {1 a b}
|
||||
create_set myset2 {2 3 4}
|
||||
assert_encoding hashtable myset1
|
||||
assert_encoding intset myset2
|
||||
}
|
||||
|
||||
test "SMOVE basics - from regular set to intset" {
|
||||
# move a non-integer element to an intset should convert encoding
|
||||
setup_move
|
||||
assert_equal 1 [r smove myset1 myset2 a]
|
||||
assert_equal {1 b} [lsort [r smembers myset1]]
|
||||
assert_equal {2 3 4 a} [lsort [r smembers myset2]]
|
||||
assert_encoding hashtable myset2
|
||||
|
||||
# move an integer element should not convert the encoding
|
||||
setup_move
|
||||
assert_equal 1 [r smove myset1 myset2 1]
|
||||
assert_equal {a b} [lsort [r smembers myset1]]
|
||||
assert_equal {1 2 3 4} [lsort [r smembers myset2]]
|
||||
assert_encoding intset myset2
|
||||
}
|
||||
|
||||
test "SMOVE basics - from intset to regular set" {
|
||||
setup_move
|
||||
assert_equal 1 [r smove myset2 myset1 2]
|
||||
assert_equal {1 2 a b} [lsort [r smembers myset1]]
|
||||
assert_equal {3 4} [lsort [r smembers myset2]]
|
||||
}
|
||||
|
||||
test "SMOVE non existing key" {
|
||||
setup_move
|
||||
assert_equal 0 [r smove myset1 myset2 foo]
|
||||
assert_equal {1 a b} [lsort [r smembers myset1]]
|
||||
assert_equal {2 3 4} [lsort [r smembers myset2]]
|
||||
}
|
||||
|
||||
test "SMOVE non existing src set" {
|
||||
setup_move
|
||||
assert_equal 0 [r smove noset myset2 foo]
|
||||
assert_equal {2 3 4} [lsort [r smembers myset2]]
|
||||
}
|
||||
|
||||
test "SMOVE from regular set to non existing destination set" {
|
||||
setup_move
|
||||
assert_equal 1 [r smove myset1 myset3 a]
|
||||
assert_equal {1 b} [lsort [r smembers myset1]]
|
||||
assert_equal {a} [lsort [r smembers myset3]]
|
||||
assert_encoding hashtable myset3
|
||||
}
|
||||
|
||||
test "SMOVE from intset to non existing destination set" {
|
||||
setup_move
|
||||
assert_equal 1 [r smove myset2 myset3 2]
|
||||
assert_equal {3 4} [lsort [r smembers myset2]]
|
||||
assert_equal {2} [lsort [r smembers myset3]]
|
||||
assert_encoding intset myset3
|
||||
}
|
||||
|
||||
test "SMOVE wrong src key type" {
|
||||
r set x 10
|
||||
catch {r smove x myset2 foo} err
|
||||
format $err
|
||||
} {ERR*}
|
||||
assert_error "ERR*wrong kind*" {r smove x myset2 foo}
|
||||
}
|
||||
|
||||
test {SMOVE wrong dst key type} {
|
||||
test "SMOVE wrong dst key type" {
|
||||
r set x 10
|
||||
catch {r smove myset2 x foo} err
|
||||
format $err
|
||||
} {ERR*}
|
||||
assert_error "ERR*wrong kind*" {r smove myset2 x foo}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-6
@@ -1,12 +1,10 @@
|
||||
# redis-sha1.rb - Copyright (C) 2009 Salvatore Sanfilippo
|
||||
# redis-copy.rb - Copyright (C) 2009-2010 Salvatore Sanfilippo
|
||||
# BSD license, See the COPYING file for more information.
|
||||
#
|
||||
# Performs the SHA1 sum of the whole datset.
|
||||
# This is useful to spot bugs in persistence related code and to make sure
|
||||
# Slaves and Masters are in SYNC.
|
||||
# Copy the whole dataset from one Redis instance to another one
|
||||
#
|
||||
# If you hack this code make sure to sort keys and set elements as this are
|
||||
# unsorted elements. Otherwise the sum may differ with equal dataset.
|
||||
# WARNING: currently hashes and sorted sets are not supported! This
|
||||
# program should be updated.
|
||||
|
||||
require 'rubygems'
|
||||
require 'redis'
|
||||
|
||||
Reference in New Issue
Block a user