Compare commits
262
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4834969032 | ||
|
|
71c37605d0 | ||
|
|
3e78344d87 | ||
|
|
e5f1de1448 | ||
|
|
8507c3f26e | ||
|
|
534c3bc21a | ||
|
|
3d07ed983e | ||
|
|
ce8b6ede34 | ||
|
|
68c0e6e331 | ||
|
|
32e0d2376f | ||
|
|
9d4f51e1ff | ||
|
|
73a78adf13 | ||
|
|
c7c3b23787 | ||
|
|
9fe7cd8f14 | ||
|
|
2b09884faf | ||
|
|
e19a06d2dd | ||
|
|
fe43406929 | ||
|
|
9ce638695c | ||
|
|
cd2ee8b113 | ||
|
|
1da93f85cc | ||
|
|
86fb7b20bf | ||
|
|
9e0e5ccbf4 | ||
|
|
8fd1031b10 | ||
|
|
f03aed3ca9 | ||
|
|
ecc48369ce | ||
|
|
08c26591e3 | ||
|
|
fc9b4e7958 | ||
|
|
5aa347b377 | ||
|
|
0aecb135c9 | ||
|
|
80bde9844b | ||
|
|
7af83a0c11 | ||
|
|
20f047965c | ||
|
|
880ca07719 | ||
|
|
bbe18eacda | ||
|
|
6c3bfb00fc | ||
|
|
bec4bfaa0b | ||
|
|
1e585d01de | ||
|
|
1d6711a764 | ||
|
|
cf7475b9b8 | ||
|
|
3ff82790e1 | ||
|
|
fb1d5717de | ||
|
|
58ac1f8bbe | ||
|
|
b6c794acf6 | ||
|
|
448d696549 | ||
|
|
c7e0c410d6 | ||
|
|
3968550135 | ||
|
|
3a27b3d0d8 | ||
|
|
14b2f7b033 | ||
|
|
bb64c7d8b2 | ||
|
|
c5e0bc1070 | ||
|
|
8b87876094 | ||
|
|
0d6f11f4d1 | ||
|
|
3213e8de92 | ||
|
|
777cc5b987 | ||
|
|
c202ba2b35 | ||
|
|
744fe7f348 | ||
|
|
7cdf272d46 | ||
|
|
9df1f73e4c | ||
|
|
bf18044082 | ||
|
|
81c4be42c1 | ||
|
|
12fd92bf67 | ||
|
|
9f43264f86 | ||
|
|
54871412c2 | ||
|
|
f39bb4107c | ||
|
|
394b974920 | ||
|
|
378218d746 | ||
|
|
bd33014f90 | ||
|
|
3176f8e955 | ||
|
|
3b0f008615 | ||
|
|
44f9e0d7c7 | ||
|
|
b2fc998ad6 | ||
|
|
c560ade831 | ||
|
|
411f4b4c12 | ||
|
|
9849cf2df5 | ||
|
|
62015d4fb0 | ||
|
|
d23cae5f06 | ||
|
|
ffbf8f4b8e | ||
|
|
5c9a7b891f | ||
|
|
02817d8b3d | ||
|
|
4dc37c240e | ||
|
|
b278d352c5 | ||
|
|
30377f154f | ||
|
|
690560e8db | ||
|
|
cf0abff567 | ||
|
|
77bdfb5339 | ||
|
|
0dd8fd062d | ||
|
|
43385c4375 | ||
|
|
eac2a79cf5 | ||
|
|
2ead41e05b | ||
|
|
a974531d1a | ||
|
|
7777379814 | ||
|
|
096c74c886 | ||
|
|
2dbf2047ac | ||
|
|
65ce839d09 | ||
|
|
cb51bb4320 | ||
|
|
4cf8fdbbd3 | ||
|
|
52e7192e4a | ||
|
|
0c1d28e2d0 | ||
|
|
3841236074 | ||
|
|
db146de086 | ||
|
|
f74c7394d7 | ||
|
|
ca6aed02f8 | ||
|
|
1051d93837 | ||
|
|
61b7a176ef | ||
|
|
cff5f36d94 | ||
|
|
05e8db24ed | ||
|
|
c9bab6f302 | ||
|
|
8296e5c846 | ||
|
|
a67181f88a | ||
|
|
6f3d357d8f | ||
|
|
6f58486139 | ||
|
|
a9419e2386 | ||
|
|
f579b77449 | ||
|
|
ef2c7a5bbb | ||
|
|
14629278e3 | ||
|
|
83b8eaa638 | ||
|
|
a62b8410ef | ||
|
|
aba57a1bb7 | ||
|
|
596749e75f | ||
|
|
db74d71eb3 | ||
|
|
6bd0d342dd | ||
|
|
7895835df6 | ||
|
|
7e11825ef4 | ||
|
|
092e4de613 | ||
|
|
51b627d916 | ||
|
|
c328834832 | ||
|
|
a8322f44b3 | ||
|
|
4e5e0d3719 | ||
|
|
ff57b8d550 | ||
|
|
2ef829d65c | ||
|
|
c1e9186f06 | ||
|
|
e4298d11e2 | ||
|
|
d60c17cbb3 | ||
|
|
227965221a | ||
|
|
6c001bfc0d | ||
|
|
2b689ad641 | ||
|
|
3e7349fdaf | ||
|
|
247d2a734b | ||
|
|
2290c4bee1 | ||
|
|
e3dfd8c811 | ||
|
|
a036c64c01 | ||
|
|
febe102bf6 | ||
|
|
7fa493912e | ||
|
|
9ab91b8c6c | ||
|
|
83af8ef1fd | ||
|
|
f5b29c6444 | ||
|
|
dce7cefb7c | ||
|
|
d05f5c8f64 | ||
|
|
476eea95da | ||
|
|
bd9259d12d | ||
|
|
ed5cc77ce0 | ||
|
|
7d39c149c4 | ||
|
|
0e21efdb1c | ||
|
|
32844178ac | ||
|
|
5b4bec9d33 | ||
|
|
5941022b52 | ||
|
|
e245a2046a | ||
|
|
067647a783 | ||
|
|
abf52c7cf4 | ||
|
|
66b5afdaa4 | ||
|
|
c241f51607 | ||
|
|
e711e6f614 | ||
|
|
19880ab851 | ||
|
|
80e1695652 | ||
|
|
132be8aed5 | ||
|
|
46d89a9abb | ||
|
|
590717782f | ||
|
|
8d519a85f4 | ||
|
|
f63e81c202 | ||
|
|
f2ad89a314 | ||
|
|
1ab64d405e | ||
|
|
c8452ab005 | ||
|
|
9a65f9cd3e | ||
|
|
8a1219d93b | ||
|
|
ef2a95c461 | ||
|
|
e623bd22ba | ||
|
|
b89302c462 | ||
|
|
14c4ddb5a6 | ||
|
|
f4b27ae222 | ||
|
|
bd01334da1 | ||
|
|
a3a1460525 | ||
|
|
39c70e728b | ||
|
|
7b5e7f3ed0 | ||
|
|
fe56c67405 | ||
|
|
0ce8323c0d | ||
|
|
3d56311f0c | ||
|
|
26897c03e2 | ||
|
|
d506334b67 | ||
|
|
2f282aee0b | ||
|
|
29226a3919 | ||
|
|
12ff0c0d79 | ||
|
|
e3e94ec35c | ||
|
|
0b800c4332 | ||
|
|
6dd0d6f7dd | ||
|
|
60acac4cd0 | ||
|
|
9042d1c249 | ||
|
|
14d6318b32 | ||
|
|
da6b7516f1 | ||
|
|
20c6a7fe2c | ||
|
|
2f2987ffc5 | ||
|
|
187fa78637 | ||
|
|
7a30be1237 | ||
|
|
7b41701eb8 | ||
|
|
e245ed9a44 | ||
|
|
093adb7ef9 | ||
|
|
b0392e75ec | ||
|
|
92b39a0abf | ||
|
|
db693be00d | ||
|
|
11dd3f4b03 | ||
|
|
0e49938b68 | ||
|
|
5401fe7fb9 | ||
|
|
35ca670060 | ||
|
|
fddeeae724 | ||
|
|
7829287691 | ||
|
|
ac3c012a7f | ||
|
|
4ebb4d54c6 | ||
|
|
1e394b73b7 | ||
|
|
1465755e38 | ||
|
|
3c19ae941d | ||
|
|
6491717c88 | ||
|
|
d6f5ec6f09 | ||
|
|
acaa18f1d1 | ||
|
|
15be174cf1 | ||
|
|
be28050ac0 | ||
|
|
c426d85c4c | ||
|
|
4e9c30a6ca | ||
|
|
4a30adde31 | ||
|
|
fd174cca23 | ||
|
|
1c2352f0dd | ||
|
|
fe408ad23f | ||
|
|
dd071ff6c9 | ||
|
|
73cbc33a0a | ||
|
|
eb0e5fe4e3 | ||
|
|
0bdeb861f4 | ||
|
|
53d46fa712 | ||
|
|
e52a036ac6 | ||
|
|
8513f31be0 | ||
|
|
41a886605a | ||
|
|
dfc82fca54 | ||
|
|
f4ac796c34 | ||
|
|
d4ae76d1a6 | ||
|
|
b9a8e56439 | ||
|
|
b28cbe90e1 | ||
|
|
1a8d7cd9c2 | ||
|
|
e8f5d21940 | ||
|
|
1daee8c748 | ||
|
|
ab1d5fe11a | ||
|
|
9a375e5408 | ||
|
|
445a2a2b1b | ||
|
|
b65ddfb16a | ||
|
|
e6ea603ad3 | ||
|
|
993716c351 | ||
|
|
9674ede85a | ||
|
|
780815dd6e | ||
|
|
bb5b8b3a6f | ||
|
|
4017a11144 | ||
|
|
dd760bd5e6 | ||
|
|
de5ca516ae | ||
|
|
49890c8ee9 | ||
|
|
82ae4f30ed | ||
|
|
eaeba1b2c8 | ||
|
|
3447062d5e |
+11
-2642
File diff suppressed because it is too large
Load Diff
@@ -216,7 +216,7 @@ Inside the root are the following important directories:
|
||||
|
||||
* `src`: contains the Redis implementation, written in C.
|
||||
* `tests`: contains the unit tests, implemented in Tcl.
|
||||
* `deps`: contains libraries Redis uses. Everything needed to compile Redis is inside this directory; your system just needs to provide `libc`, a POSIX compatible interface and a C compiler. Notably `deps` contains a copy of `jemalloc`, which is the default allocator of Redis under Linux. Note that under `deps` there are also things which started with the Redis project, but for which the main repository is not `antirez/redis`. An exception to this rule is `deps/geohash-int` which is the low level geocoding library used by Redis: it originated from a different project, but at this point it diverged so much that it is developed as a separated entity directly inside the Redis repository.
|
||||
* `deps`: contains libraries Redis uses. Everything needed to compile Redis is inside this directory; your system just needs to provide `libc`, a POSIX compatible interface and a C compiler. Notably `deps` contains a copy of `jemalloc`, which is the default allocator of Redis under Linux. Note that under `deps` there are also things which started with the Redis project, but for which the main repository is not `anitrez/redis`. An exception to this rule is `deps/geohash-int` which is the low level geocoding library used by Redis: it originated from a different project, but at this point it diverged so much that it is developed as a separated entity directly inside the Redis repository.
|
||||
|
||||
There are a few more directories but they are not very important for our goals
|
||||
here. We'll focus mostly on `src`, where the Redis implementation is contained,
|
||||
@@ -227,7 +227,7 @@ of complexity incrementally.
|
||||
Note: lately Redis was refactored quite a bit. Function names and file
|
||||
names have been changed, so you may find that this documentation reflects the
|
||||
`unstable` branch more closely. For instance in Redis 3.0 the `server.c`
|
||||
and `server.h` files were named `redis.c` and `redis.h`. However the overall
|
||||
and `server.h` files were named to `redis.c` and `redis.h`. However the overall
|
||||
structure is the same. Keep in mind that all the new developments and pull
|
||||
requests should be performed against the `unstable` branch.
|
||||
|
||||
|
||||
+1
-1
@@ -625,7 +625,7 @@ replica-priority 100
|
||||
# memory to never hit a real out-of-memory condition before the master hits
|
||||
# the configured maxmemory setting.
|
||||
#
|
||||
# replica-ignore-maxmemory yes
|
||||
# replica-ingore-maxmemory yes
|
||||
|
||||
############################# LAZY FREEING ####################################
|
||||
|
||||
|
||||
+1
-21
@@ -21,11 +21,6 @@ NODEPS:=clean distclean
|
||||
|
||||
# Default settings
|
||||
STD=-std=c99 -pedantic -DREDIS_STATIC=''
|
||||
ifneq (,$(findstring clang,$(CC)))
|
||||
ifneq (,$(findstring FreeBSD,$(uname_S)))
|
||||
STD+=-Wno-c11-extensions
|
||||
endif
|
||||
endif
|
||||
WARN=-Wall -W -Wno-missing-field-initializers
|
||||
OPT=$(OPTIMIZATION)
|
||||
|
||||
@@ -46,10 +41,6 @@ endif
|
||||
# To get ARM stack traces if Redis crashes we need a special C flag.
|
||||
ifneq (,$(filter aarch64 armv,$(uname_M)))
|
||||
CFLAGS+=-funwind-tables
|
||||
else
|
||||
ifneq (,$(findstring armv,$(uname_M)))
|
||||
CFLAGS+=-funwind-tables
|
||||
endif
|
||||
endif
|
||||
|
||||
# Backwards compatibility for selecting an allocator
|
||||
@@ -102,20 +93,10 @@ else
|
||||
ifeq ($(uname_S),OpenBSD)
|
||||
# OpenBSD
|
||||
FINAL_LIBS+= -lpthread
|
||||
ifeq ($(USE_BACKTRACE),yes)
|
||||
FINAL_CFLAGS+= -DUSE_BACKTRACE -I/usr/local/include
|
||||
FINAL_LDFLAGS+= -L/usr/local/lib
|
||||
FINAL_LIBS+= -lexecinfo
|
||||
endif
|
||||
|
||||
else
|
||||
ifeq ($(uname_S),FreeBSD)
|
||||
# FreeBSD
|
||||
FINAL_LIBS+= -lpthread -lexecinfo
|
||||
else
|
||||
ifeq ($(uname_S),DragonFly)
|
||||
# FreeBSD
|
||||
FINAL_LIBS+= -lpthread -lexecinfo
|
||||
FINAL_LIBS+= -lpthread
|
||||
else
|
||||
# All the other OSes (notably Linux)
|
||||
FINAL_LDFLAGS+= -rdynamic
|
||||
@@ -125,7 +106,6 @@ endif
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
# Include paths to dependencies
|
||||
FINAL_CFLAGS+= -I../deps/hiredis -I../deps/linenoise -I../deps/lua/src
|
||||
|
||||
|
||||
@@ -677,7 +677,6 @@ int loadAppendOnlyFile(char *filename) {
|
||||
int old_aof_state = server.aof_state;
|
||||
long loops = 0;
|
||||
off_t valid_up_to = 0; /* Offset of latest well-formed command loaded. */
|
||||
off_t valid_before_multi = 0; /* Offset before MULTI command loaded. */
|
||||
|
||||
if (fp == NULL) {
|
||||
serverLog(LL_WARNING,"Fatal error: can't open the append log file for reading: %s",strerror(errno));
|
||||
@@ -778,28 +777,16 @@ int loadAppendOnlyFile(char *filename) {
|
||||
/* Command lookup */
|
||||
cmd = lookupCommand(argv[0]->ptr);
|
||||
if (!cmd) {
|
||||
serverLog(LL_WARNING,
|
||||
"Unknown command '%s' reading the append only file",
|
||||
(char*)argv[0]->ptr);
|
||||
serverLog(LL_WARNING,"Unknown command '%s' reading the append only file", (char*)argv[0]->ptr);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (cmd == server.multiCommand) valid_before_multi = valid_up_to;
|
||||
|
||||
/* Run the command in the context of a fake client */
|
||||
fakeClient->cmd = cmd;
|
||||
if (fakeClient->flags & CLIENT_MULTI &&
|
||||
fakeClient->cmd->proc != execCommand)
|
||||
{
|
||||
queueMultiCommand(fakeClient);
|
||||
} else {
|
||||
cmd->proc(fakeClient);
|
||||
}
|
||||
cmd->proc(fakeClient);
|
||||
|
||||
/* The fake client should not have a reply */
|
||||
serverAssert(fakeClient->bufpos == 0 &&
|
||||
listLength(fakeClient->reply) == 0);
|
||||
|
||||
serverAssert(fakeClient->bufpos == 0 && listLength(fakeClient->reply) == 0);
|
||||
/* The fake client should never get blocked */
|
||||
serverAssert((fakeClient->flags & CLIENT_BLOCKED) == 0);
|
||||
|
||||
@@ -814,12 +801,7 @@ int loadAppendOnlyFile(char *filename) {
|
||||
* If the client is in the middle of a MULTI/EXEC, handle it as it was
|
||||
* a short read, even if technically the protocol is correct: we want
|
||||
* to remove the unprocessed tail and continue. */
|
||||
if (fakeClient->flags & CLIENT_MULTI) {
|
||||
serverLog(LL_WARNING,
|
||||
"Revert incomplete MULTI/EXEC transaction in AOF file");
|
||||
valid_up_to = valid_before_multi;
|
||||
goto uxeof;
|
||||
}
|
||||
if (fakeClient->flags & CLIENT_MULTI) goto uxeof;
|
||||
|
||||
loaded_ok: /* DB loaded, cleanup and return C_OK to the caller. */
|
||||
fclose(fp);
|
||||
@@ -1139,47 +1121,25 @@ int rewriteStreamObject(rio *r, robj *key, robj *o) {
|
||||
streamID id;
|
||||
int64_t numfields;
|
||||
|
||||
if (s->length) {
|
||||
/* Reconstruct the stream data using XADD commands. */
|
||||
while(streamIteratorGetID(&si,&id,&numfields)) {
|
||||
/* Emit a two elements array for each item. The first is
|
||||
* the ID, the second is an array of field-value pairs. */
|
||||
/* Reconstruct the stream data using XADD commands. */
|
||||
while(streamIteratorGetID(&si,&id,&numfields)) {
|
||||
/* Emit a two elements array for each item. The first is
|
||||
* the ID, the second is an array of field-value pairs. */
|
||||
|
||||
/* Emit the XADD <key> <id> ...fields... command. */
|
||||
if (rioWriteBulkCount(r,'*',3+numfields*2) == 0) return 0;
|
||||
if (rioWriteBulkString(r,"XADD",4) == 0) return 0;
|
||||
if (rioWriteBulkObject(r,key) == 0) return 0;
|
||||
if (rioWriteBulkStreamID(r,&id) == 0) return 0;
|
||||
while(numfields--) {
|
||||
unsigned char *field, *value;
|
||||
int64_t field_len, value_len;
|
||||
streamIteratorGetField(&si,&field,&value,&field_len,&value_len);
|
||||
if (rioWriteBulkString(r,(char*)field,field_len) == 0) return 0;
|
||||
if (rioWriteBulkString(r,(char*)value,value_len) == 0) return 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* Use the XADD MAXLEN 0 trick to generate an empty stream if
|
||||
* the key we are serializing is an empty string, which is possible
|
||||
* for the Stream type. */
|
||||
if (rioWriteBulkCount(r,'*',7) == 0) return 0;
|
||||
/* Emit the XADD <key> <id> ...fields... command. */
|
||||
if (rioWriteBulkCount(r,'*',3+numfields*2) == 0) return 0;
|
||||
if (rioWriteBulkString(r,"XADD",4) == 0) return 0;
|
||||
if (rioWriteBulkObject(r,key) == 0) return 0;
|
||||
if (rioWriteBulkString(r,"MAXLEN",6) == 0) return 0;
|
||||
if (rioWriteBulkString(r,"0",1) == 0) return 0;
|
||||
if (rioWriteBulkStreamID(r,&s->last_id) == 0) return 0;
|
||||
if (rioWriteBulkString(r,"x",1) == 0) return 0;
|
||||
if (rioWriteBulkString(r,"y",1) == 0) return 0;
|
||||
if (rioWriteBulkStreamID(r,&id) == 0) return 0;
|
||||
while(numfields--) {
|
||||
unsigned char *field, *value;
|
||||
int64_t field_len, value_len;
|
||||
streamIteratorGetField(&si,&field,&value,&field_len,&value_len);
|
||||
if (rioWriteBulkString(r,(char*)field,field_len) == 0) return 0;
|
||||
if (rioWriteBulkString(r,(char*)value,value_len) == 0) return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Append XSETID after XADD, make sure lastid is correct,
|
||||
* in case of XDEL lastid. */
|
||||
if (rioWriteBulkCount(r,'*',3) == 0) return 0;
|
||||
if (rioWriteBulkString(r,"XSETID",6) == 0) return 0;
|
||||
if (rioWriteBulkObject(r,key) == 0) return 0;
|
||||
if (rioWriteBulkStreamID(r,&s->last_id) == 0) return 0;
|
||||
|
||||
|
||||
/* Create all the stream consumer groups. */
|
||||
if (s->cgroups) {
|
||||
raxIterator ri;
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/* This file implements atomic counters using __atomic or __sync macros if
|
||||
* available, otherwise synchronizing different threads using a mutex.
|
||||
*
|
||||
* The exported interface is composed of three macros:
|
||||
* The exported interaface is composed of three macros:
|
||||
*
|
||||
* atomicIncr(var,count) -- Increment the atomic counter
|
||||
* atomicGetIncr(var,oldvalue_var,count) -- Get and increment the atomic counter
|
||||
|
||||
+6
-14
@@ -5164,10 +5164,10 @@ try_again:
|
||||
serverAssertWithInfo(c,NULL,rioWriteBulkLongLong(&cmd,dbid));
|
||||
}
|
||||
|
||||
int non_expired = 0; /* Number of keys that we'll find non expired.
|
||||
Note that serializing large keys may take some time
|
||||
so certain keys that were found non expired by the
|
||||
lookupKey() function, may be expired later. */
|
||||
int expired = 0; /* Number of keys that we'll find already expired.
|
||||
Note that serializing large keys may take some time
|
||||
so certain keys that were found non expired by the
|
||||
lookupKey() function, may be expired later. */
|
||||
|
||||
/* Create RESTORE payload and generate the protocol to call the command. */
|
||||
for (j = 0; j < num_keys; j++) {
|
||||
@@ -5177,16 +5177,11 @@ try_again:
|
||||
if (expireat != -1) {
|
||||
ttl = expireat-mstime();
|
||||
if (ttl < 0) {
|
||||
expired++;
|
||||
continue;
|
||||
}
|
||||
if (ttl < 1) ttl = 1;
|
||||
}
|
||||
|
||||
/* Relocate valid (non expired) keys into the array in successive
|
||||
* positions to remove holes created by the keys that were present
|
||||
* in the first lookup but are now expired after the second lookup. */
|
||||
kv[non_expired++] = kv[j];
|
||||
|
||||
serverAssertWithInfo(c,NULL,
|
||||
rioWriteBulkCount(&cmd,'*',replace ? 5 : 4));
|
||||
|
||||
@@ -5214,9 +5209,6 @@ try_again:
|
||||
serverAssertWithInfo(c,NULL,rioWriteBulkString(&cmd,"REPLACE",7));
|
||||
}
|
||||
|
||||
/* Fix the actual number of keys we are migrating. */
|
||||
num_keys = non_expired;
|
||||
|
||||
/* Transfer the query to the other node in 64K chunks. */
|
||||
errno = 0;
|
||||
{
|
||||
@@ -5258,7 +5250,7 @@ try_again:
|
||||
* command name itself. */
|
||||
if (!copy) newargv = zmalloc(sizeof(robj*)*(num_keys+1));
|
||||
|
||||
for (j = 0; j < num_keys; j++) {
|
||||
for (j = 0; j < num_keys-expired; j++) {
|
||||
if (syncReadLine(cs->fd, buf2, sizeof(buf2), timeout) <= 0) {
|
||||
socket_error = 1;
|
||||
break;
|
||||
|
||||
+3
-3
@@ -120,7 +120,7 @@ const char *configEnumGetName(configEnum *ce, int val) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Wrapper for configEnumGetName() returning "unknown" instead of NULL if
|
||||
/* Wrapper for configEnumGetName() returning "unknown" insetad of NULL if
|
||||
* there is no match. */
|
||||
const char *configEnumGetNameOrUnknown(configEnum *ce, int val) {
|
||||
const char *name = configEnumGetName(ce,val);
|
||||
@@ -684,7 +684,7 @@ void loadServerConfigFromString(char *config) {
|
||||
goto loaderr;
|
||||
}
|
||||
} else if ((!strcasecmp(argv[0],"cluster-slave-no-failover") ||
|
||||
!strcasecmp(argv[0],"cluster-replica-no-failover")) &&
|
||||
!strcasecmp(argv[0],"cluster-replica-no-failiver")) &&
|
||||
argc == 2)
|
||||
{
|
||||
server.cluster_slave_no_failover = yesnotoi(argv[1]);
|
||||
@@ -1248,7 +1248,7 @@ void configSetCommand(client *c) {
|
||||
if (server.maxmemory < zmalloc_used_memory()) {
|
||||
serverLog(LL_WARNING,"WARNING: the new maxmemory value set via CONFIG SET is smaller than the current memory usage. This will result in key eviction and/or the inability to accept new write commands depending on the maxmemory-policy.");
|
||||
}
|
||||
freeMemoryIfNeededAndSafe();
|
||||
freeMemoryIfNeeded();
|
||||
}
|
||||
} config_set_memory_field(
|
||||
"proto-max-bulk-len",server.proto_max_bulk_len) {
|
||||
|
||||
+1
-3
@@ -62,9 +62,7 @@
|
||||
#endif
|
||||
|
||||
/* Test for backtrace() */
|
||||
#if defined(__APPLE__) || (defined(__linux__) && defined(__GLIBC__)) || \
|
||||
defined(__FreeBSD__) || (defined(__OpenBSD__) && defined(USE_BACKTRACE))\
|
||||
|| defined(__DragonFly__)
|
||||
#if defined(__APPLE__) || (defined(__linux__) && defined(__GLIBC__))
|
||||
#define HAVE_BACKTRACE 1
|
||||
#endif
|
||||
|
||||
|
||||
@@ -38,8 +38,6 @@
|
||||
* C-level DB API
|
||||
*----------------------------------------------------------------------------*/
|
||||
|
||||
int keyIsExpired(redisDb *db, robj *key);
|
||||
|
||||
/* Update LFU when an object is accessed.
|
||||
* Firstly, decrement the counter if the decrement time is reached.
|
||||
* Then logarithmically increment the counter, and update the access time. */
|
||||
@@ -104,10 +102,7 @@ robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) {
|
||||
/* Key expired. If we are in the context of a master, expireIfNeeded()
|
||||
* returns 0 only when the key does not exist at all, so it's safe
|
||||
* to return NULL ASAP. */
|
||||
if (server.masterhost == NULL) {
|
||||
server.stat_keyspace_misses++;
|
||||
return NULL;
|
||||
}
|
||||
if (server.masterhost == NULL) return NULL;
|
||||
|
||||
/* However if we are in the context of a slave, expireIfNeeded() will
|
||||
* not really try to expire the key, it only returns information
|
||||
@@ -126,7 +121,6 @@ robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) {
|
||||
server.current_client->cmd &&
|
||||
server.current_client->cmd->flags & CMD_READONLY)
|
||||
{
|
||||
server.stat_keyspace_misses++;
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
@@ -212,7 +206,7 @@ void dbOverwrite(redisDb *db, robj *key, robj *val) {
|
||||
* 2) clients WATCHing for the destination key notified.
|
||||
* 3) The expire time of the key is reset (the key is made persistent).
|
||||
*
|
||||
* All the new keys in the database should be created via this interface. */
|
||||
* All the new keys in the database should be craeted via this interface. */
|
||||
void setKey(redisDb *db, robj *key, robj *val) {
|
||||
if (lookupKeyWrite(db,key) == NULL) {
|
||||
dbAdd(db,key,val);
|
||||
@@ -549,7 +543,7 @@ void keysCommand(client *c) {
|
||||
|
||||
if (allkeys || stringmatchlen(pattern,plen,key,sdslen(key),0)) {
|
||||
keyobj = createStringObject(key,sdslen(key));
|
||||
if (!keyIsExpired(c->db,keyobj)) {
|
||||
if (expireIfNeeded(c->db,keyobj) == 0) {
|
||||
addReplyBulk(c,keyobj);
|
||||
numkeys++;
|
||||
}
|
||||
@@ -1126,25 +1120,6 @@ void propagateExpire(redisDb *db, robj *key, int lazy) {
|
||||
decrRefCount(argv[1]);
|
||||
}
|
||||
|
||||
/* Check if the key is expired. */
|
||||
int keyIsExpired(redisDb *db, robj *key) {
|
||||
mstime_t when = getExpire(db,key);
|
||||
|
||||
if (when < 0) return 0; /* No expire for this key */
|
||||
|
||||
/* Don't expire anything while loading. It will be done later. */
|
||||
if (server.loading) return 0;
|
||||
|
||||
/* If we are in the context of a Lua script, we pretend that time is
|
||||
* blocked to when the Lua script started. This way a key can expire
|
||||
* only the first time it is accessed and not in the middle of the
|
||||
* script execution, making propagation to slaves / AOF consistent.
|
||||
* See issue #1525 on Github for more information. */
|
||||
mstime_t now = server.lua_caller ? server.lua_time_start : mstime();
|
||||
|
||||
return now > when;
|
||||
}
|
||||
|
||||
/* This function is called when we are going to perform some operation
|
||||
* in a given key, but such key may be already logically expired even if
|
||||
* it still exists in the database. The main way this function is called
|
||||
@@ -1165,17 +1140,32 @@ int keyIsExpired(redisDb *db, robj *key) {
|
||||
* The return value of the function is 0 if the key is still valid,
|
||||
* otherwise the function returns 1 if the key is expired. */
|
||||
int expireIfNeeded(redisDb *db, robj *key) {
|
||||
if (!keyIsExpired(db,key)) return 0;
|
||||
mstime_t when = getExpire(db,key);
|
||||
mstime_t now;
|
||||
|
||||
/* If we are running in the context of a slave, instead of
|
||||
* evicting the expired key from the database, we return ASAP:
|
||||
if (when < 0) return 0; /* No expire for this key */
|
||||
|
||||
/* Don't expire anything while loading. It will be done later. */
|
||||
if (server.loading) return 0;
|
||||
|
||||
/* If we are in the context of a Lua script, we pretend that time is
|
||||
* blocked to when the Lua script started. This way a key can expire
|
||||
* only the first time it is accessed and not in the middle of the
|
||||
* script execution, making propagation to slaves / AOF consistent.
|
||||
* See issue #1525 on Github for more information. */
|
||||
now = server.lua_caller ? server.lua_time_start : mstime();
|
||||
|
||||
/* 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 1;
|
||||
if (server.masterhost != NULL) return now > when;
|
||||
|
||||
/* Return when this key has not expired */
|
||||
if (now <= when) return 0;
|
||||
|
||||
/* Delete the key */
|
||||
server.stat_expiredkeys++;
|
||||
|
||||
+137
-333
@@ -37,11 +37,7 @@
|
||||
|
||||
#ifdef HAVE_BACKTRACE
|
||||
#include <execinfo.h>
|
||||
#ifndef __OpenBSD__
|
||||
#include <ucontext.h>
|
||||
#else
|
||||
typedef ucontext_t sigcontext_t;
|
||||
#endif
|
||||
#include <fcntl.h>
|
||||
#include "bio.h"
|
||||
#include <unistd.h>
|
||||
@@ -74,7 +70,7 @@ void xorDigest(unsigned char *digest, void *ptr, size_t len) {
|
||||
digest[j] ^= hash[j];
|
||||
}
|
||||
|
||||
void xorStringObjectDigest(unsigned char *digest, robj *o) {
|
||||
void xorObjectDigest(unsigned char *digest, robj *o) {
|
||||
o = getDecodedObject(o);
|
||||
xorDigest(digest,o->ptr,sdslen(o->ptr));
|
||||
decrRefCount(o);
|
||||
@@ -104,151 +100,12 @@ void mixDigest(unsigned char *digest, void *ptr, size_t len) {
|
||||
SHA1Final(digest,&ctx);
|
||||
}
|
||||
|
||||
void mixStringObjectDigest(unsigned char *digest, robj *o) {
|
||||
void mixObjectDigest(unsigned char *digest, robj *o) {
|
||||
o = getDecodedObject(o);
|
||||
mixDigest(digest,o->ptr,sdslen(o->ptr));
|
||||
decrRefCount(o);
|
||||
}
|
||||
|
||||
/* This function computes the digest of a data structure stored in the
|
||||
* object 'o'. It is the core of the DEBUG DIGEST command: when taking the
|
||||
* digest of a whole dataset, we take the digest of the key and the value
|
||||
* pair, and xor all those together.
|
||||
*
|
||||
* Note that this function does not reset the initial 'digest' passed, it
|
||||
* will continue mixing this object digest to anything that was already
|
||||
* present. */
|
||||
void xorObjectDigest(redisDb *db, robj *keyobj, unsigned char *digest, robj *o) {
|
||||
uint32_t aux = htonl(o->type);
|
||||
mixDigest(digest,&aux,sizeof(aux));
|
||||
long long expiretime = getExpire(db,keyobj);
|
||||
char buf[128];
|
||||
|
||||
/* Save the key and associated value */
|
||||
if (o->type == OBJ_STRING) {
|
||||
mixStringObjectDigest(digest,o);
|
||||
} else if (o->type == OBJ_LIST) {
|
||||
listTypeIterator *li = listTypeInitIterator(o,0,LIST_TAIL);
|
||||
listTypeEntry entry;
|
||||
while(listTypeNext(li,&entry)) {
|
||||
robj *eleobj = listTypeGet(&entry);
|
||||
mixStringObjectDigest(digest,eleobj);
|
||||
decrRefCount(eleobj);
|
||||
}
|
||||
listTypeReleaseIterator(li);
|
||||
} else if (o->type == OBJ_SET) {
|
||||
setTypeIterator *si = setTypeInitIterator(o);
|
||||
sds sdsele;
|
||||
while((sdsele = setTypeNextObject(si)) != NULL) {
|
||||
xorDigest(digest,sdsele,sdslen(sdsele));
|
||||
sdsfree(sdsele);
|
||||
}
|
||||
setTypeReleaseIterator(si);
|
||||
} else if (o->type == OBJ_ZSET) {
|
||||
unsigned char eledigest[20];
|
||||
|
||||
if (o->encoding == OBJ_ENCODING_ZIPLIST) {
|
||||
unsigned char *zl = o->ptr;
|
||||
unsigned char *eptr, *sptr;
|
||||
unsigned char *vstr;
|
||||
unsigned int vlen;
|
||||
long long vll;
|
||||
double score;
|
||||
|
||||
eptr = ziplistIndex(zl,0);
|
||||
serverAssert(eptr != NULL);
|
||||
sptr = ziplistNext(zl,eptr);
|
||||
serverAssert(sptr != NULL);
|
||||
|
||||
while (eptr != NULL) {
|
||||
serverAssert(ziplistGet(eptr,&vstr,&vlen,&vll));
|
||||
score = zzlGetScore(sptr);
|
||||
|
||||
memset(eledigest,0,20);
|
||||
if (vstr != NULL) {
|
||||
mixDigest(eledigest,vstr,vlen);
|
||||
} else {
|
||||
ll2string(buf,sizeof(buf),vll);
|
||||
mixDigest(eledigest,buf,strlen(buf));
|
||||
}
|
||||
|
||||
snprintf(buf,sizeof(buf),"%.17g",score);
|
||||
mixDigest(eledigest,buf,strlen(buf));
|
||||
xorDigest(digest,eledigest,20);
|
||||
zzlNext(zl,&eptr,&sptr);
|
||||
}
|
||||
} else if (o->encoding == OBJ_ENCODING_SKIPLIST) {
|
||||
zset *zs = o->ptr;
|
||||
dictIterator *di = dictGetIterator(zs->dict);
|
||||
dictEntry *de;
|
||||
|
||||
while((de = dictNext(di)) != NULL) {
|
||||
sds sdsele = dictGetKey(de);
|
||||
double *score = dictGetVal(de);
|
||||
|
||||
snprintf(buf,sizeof(buf),"%.17g",*score);
|
||||
memset(eledigest,0,20);
|
||||
mixDigest(eledigest,sdsele,sdslen(sdsele));
|
||||
mixDigest(eledigest,buf,strlen(buf));
|
||||
xorDigest(digest,eledigest,20);
|
||||
}
|
||||
dictReleaseIterator(di);
|
||||
} else {
|
||||
serverPanic("Unknown sorted set encoding");
|
||||
}
|
||||
} else if (o->type == OBJ_HASH) {
|
||||
hashTypeIterator *hi = hashTypeInitIterator(o);
|
||||
while (hashTypeNext(hi) != C_ERR) {
|
||||
unsigned char eledigest[20];
|
||||
sds sdsele;
|
||||
|
||||
memset(eledigest,0,20);
|
||||
sdsele = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_KEY);
|
||||
mixDigest(eledigest,sdsele,sdslen(sdsele));
|
||||
sdsfree(sdsele);
|
||||
sdsele = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_VALUE);
|
||||
mixDigest(eledigest,sdsele,sdslen(sdsele));
|
||||
sdsfree(sdsele);
|
||||
xorDigest(digest,eledigest,20);
|
||||
}
|
||||
hashTypeReleaseIterator(hi);
|
||||
} else if (o->type == OBJ_STREAM) {
|
||||
streamIterator si;
|
||||
streamIteratorStart(&si,o->ptr,NULL,NULL,0);
|
||||
streamID id;
|
||||
int64_t numfields;
|
||||
|
||||
while(streamIteratorGetID(&si,&id,&numfields)) {
|
||||
sds itemid = sdscatfmt(sdsempty(),"%U.%U",id.ms,id.seq);
|
||||
mixDigest(digest,itemid,sdslen(itemid));
|
||||
sdsfree(itemid);
|
||||
|
||||
while(numfields--) {
|
||||
unsigned char *field, *value;
|
||||
int64_t field_len, value_len;
|
||||
streamIteratorGetField(&si,&field,&value,
|
||||
&field_len,&value_len);
|
||||
mixDigest(digest,field,field_len);
|
||||
mixDigest(digest,value,value_len);
|
||||
}
|
||||
}
|
||||
streamIteratorStop(&si);
|
||||
} else if (o->type == OBJ_MODULE) {
|
||||
RedisModuleDigest md;
|
||||
moduleValue *mv = o->ptr;
|
||||
moduleType *mt = mv->type;
|
||||
moduleInitDigestContext(md);
|
||||
if (mt->digest) {
|
||||
mt->digest(&md,mv->value);
|
||||
xorDigest(digest,md.x,sizeof(md.x));
|
||||
}
|
||||
} else {
|
||||
serverPanic("Unknown object type");
|
||||
}
|
||||
/* If the key has an expire, add it to the mix */
|
||||
if (expiretime != -1) xorDigest(digest,"!!expire!!",10);
|
||||
}
|
||||
|
||||
/* Compute the dataset digest. Since keys, sets elements, hashes elements
|
||||
* are not ordered, we use a trick: every aggregate digest is the xor
|
||||
* of the digests of their elements. This way the order will not change
|
||||
@@ -257,6 +114,7 @@ void xorObjectDigest(redisDb *db, robj *keyobj, unsigned char *digest, robj *o)
|
||||
* a different digest. */
|
||||
void computeDatasetDigest(unsigned char *final) {
|
||||
unsigned char digest[20];
|
||||
char buf[128];
|
||||
dictIterator *di = NULL;
|
||||
dictEntry *de;
|
||||
int j;
|
||||
@@ -279,6 +137,7 @@ void computeDatasetDigest(unsigned char *final) {
|
||||
while((de = dictNext(di)) != NULL) {
|
||||
sds key;
|
||||
robj *keyobj, *o;
|
||||
long long expiretime;
|
||||
|
||||
memset(digest,0,20); /* This key-val digest */
|
||||
key = dictGetKey(de);
|
||||
@@ -287,8 +146,134 @@ void computeDatasetDigest(unsigned char *final) {
|
||||
mixDigest(digest,key,sdslen(key));
|
||||
|
||||
o = dictGetVal(de);
|
||||
xorObjectDigest(db,keyobj,digest,o);
|
||||
|
||||
aux = htonl(o->type);
|
||||
mixDigest(digest,&aux,sizeof(aux));
|
||||
expiretime = getExpire(db,keyobj);
|
||||
|
||||
/* Save the key and associated value */
|
||||
if (o->type == OBJ_STRING) {
|
||||
mixObjectDigest(digest,o);
|
||||
} else if (o->type == OBJ_LIST) {
|
||||
listTypeIterator *li = listTypeInitIterator(o,0,LIST_TAIL);
|
||||
listTypeEntry entry;
|
||||
while(listTypeNext(li,&entry)) {
|
||||
robj *eleobj = listTypeGet(&entry);
|
||||
mixObjectDigest(digest,eleobj);
|
||||
decrRefCount(eleobj);
|
||||
}
|
||||
listTypeReleaseIterator(li);
|
||||
} else if (o->type == OBJ_SET) {
|
||||
setTypeIterator *si = setTypeInitIterator(o);
|
||||
sds sdsele;
|
||||
while((sdsele = setTypeNextObject(si)) != NULL) {
|
||||
xorDigest(digest,sdsele,sdslen(sdsele));
|
||||
sdsfree(sdsele);
|
||||
}
|
||||
setTypeReleaseIterator(si);
|
||||
} else if (o->type == OBJ_ZSET) {
|
||||
unsigned char eledigest[20];
|
||||
|
||||
if (o->encoding == OBJ_ENCODING_ZIPLIST) {
|
||||
unsigned char *zl = o->ptr;
|
||||
unsigned char *eptr, *sptr;
|
||||
unsigned char *vstr;
|
||||
unsigned int vlen;
|
||||
long long vll;
|
||||
double score;
|
||||
|
||||
eptr = ziplistIndex(zl,0);
|
||||
serverAssert(eptr != NULL);
|
||||
sptr = ziplistNext(zl,eptr);
|
||||
serverAssert(sptr != NULL);
|
||||
|
||||
while (eptr != NULL) {
|
||||
serverAssert(ziplistGet(eptr,&vstr,&vlen,&vll));
|
||||
score = zzlGetScore(sptr);
|
||||
|
||||
memset(eledigest,0,20);
|
||||
if (vstr != NULL) {
|
||||
mixDigest(eledigest,vstr,vlen);
|
||||
} else {
|
||||
ll2string(buf,sizeof(buf),vll);
|
||||
mixDigest(eledigest,buf,strlen(buf));
|
||||
}
|
||||
|
||||
snprintf(buf,sizeof(buf),"%.17g",score);
|
||||
mixDigest(eledigest,buf,strlen(buf));
|
||||
xorDigest(digest,eledigest,20);
|
||||
zzlNext(zl,&eptr,&sptr);
|
||||
}
|
||||
} else if (o->encoding == OBJ_ENCODING_SKIPLIST) {
|
||||
zset *zs = o->ptr;
|
||||
dictIterator *di = dictGetIterator(zs->dict);
|
||||
dictEntry *de;
|
||||
|
||||
while((de = dictNext(di)) != NULL) {
|
||||
sds sdsele = dictGetKey(de);
|
||||
double *score = dictGetVal(de);
|
||||
|
||||
snprintf(buf,sizeof(buf),"%.17g",*score);
|
||||
memset(eledigest,0,20);
|
||||
mixDigest(eledigest,sdsele,sdslen(sdsele));
|
||||
mixDigest(eledigest,buf,strlen(buf));
|
||||
xorDigest(digest,eledigest,20);
|
||||
}
|
||||
dictReleaseIterator(di);
|
||||
} else {
|
||||
serverPanic("Unknown sorted set encoding");
|
||||
}
|
||||
} else if (o->type == OBJ_HASH) {
|
||||
hashTypeIterator *hi = hashTypeInitIterator(o);
|
||||
while (hashTypeNext(hi) != C_ERR) {
|
||||
unsigned char eledigest[20];
|
||||
sds sdsele;
|
||||
|
||||
memset(eledigest,0,20);
|
||||
sdsele = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_KEY);
|
||||
mixDigest(eledigest,sdsele,sdslen(sdsele));
|
||||
sdsfree(sdsele);
|
||||
sdsele = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_VALUE);
|
||||
mixDigest(eledigest,sdsele,sdslen(sdsele));
|
||||
sdsfree(sdsele);
|
||||
xorDigest(digest,eledigest,20);
|
||||
}
|
||||
hashTypeReleaseIterator(hi);
|
||||
} else if (o->type == OBJ_STREAM) {
|
||||
streamIterator si;
|
||||
streamIteratorStart(&si,o->ptr,NULL,NULL,0);
|
||||
streamID id;
|
||||
int64_t numfields;
|
||||
|
||||
while(streamIteratorGetID(&si,&id,&numfields)) {
|
||||
sds itemid = sdscatfmt(sdsempty(),"%U.%U",id.ms,id.seq);
|
||||
mixDigest(digest,itemid,sdslen(itemid));
|
||||
sdsfree(itemid);
|
||||
|
||||
while(numfields--) {
|
||||
unsigned char *field, *value;
|
||||
int64_t field_len, value_len;
|
||||
streamIteratorGetField(&si,&field,&value,
|
||||
&field_len,&value_len);
|
||||
mixDigest(digest,field,field_len);
|
||||
mixDigest(digest,value,value_len);
|
||||
}
|
||||
}
|
||||
streamIteratorStop(&si);
|
||||
} else if (o->type == OBJ_MODULE) {
|
||||
RedisModuleDigest md;
|
||||
moduleValue *mv = o->ptr;
|
||||
moduleType *mt = mv->type;
|
||||
moduleInitDigestContext(md);
|
||||
if (mt->digest) {
|
||||
mt->digest(&md,mv->value);
|
||||
xorDigest(digest,md.x,sizeof(md.x));
|
||||
}
|
||||
} else {
|
||||
serverPanic("Unknown object type");
|
||||
}
|
||||
/* If the key has an expire, add it to the mix */
|
||||
if (expiretime != -1) xorDigest(digest,"!!expire!!",10);
|
||||
/* We can finally xor the key-val digest to the final digest */
|
||||
xorDigest(final,digest,20);
|
||||
decrRefCount(keyobj);
|
||||
@@ -304,7 +289,6 @@ void debugCommand(client *c) {
|
||||
"CHANGE-REPL-ID -- Change the replication IDs of the instance. Dangerous, should be used only for testing the replication subsystem.",
|
||||
"CRASH-AND-RECOVER <milliseconds> -- Hard crash and restart after <milliseconds> delay.",
|
||||
"DIGEST -- Output a hex signature representing the current DB content.",
|
||||
"DIGEST-VALUE <key-1> ... <key-N>-- Output a hex signature of the values of all the specified keys.",
|
||||
"ERROR <string> -- Return a Redis protocol error with <string> as message. Useful for clients unit tests to simulate Redis errors.",
|
||||
"LOG <message> -- write message to the server log.",
|
||||
"HTSTATS <dbid> -- Return hash table statistics of the specified Redis database.",
|
||||
@@ -322,7 +306,6 @@ void debugCommand(client *c) {
|
||||
"SLEEP <seconds> -- Stop the server for <seconds>. Decimals allowed.",
|
||||
"STRUCTSIZE -- Return the size of different Redis core C structures.",
|
||||
"ZIPLIST <key> -- Show low level info about the ziplist encoding.",
|
||||
"STRINGMATCH-TEST -- Run a fuzz tester against the stringmatchlen() function.",
|
||||
NULL
|
||||
};
|
||||
addReplyHelp(c, help);
|
||||
@@ -349,6 +332,7 @@ NULL
|
||||
zfree(ptr);
|
||||
addReply(c,shared.ok);
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"assert")) {
|
||||
if (c->argc >= 3) c->argv[2] = tryObjectEncoding(c->argv[2]);
|
||||
serverAssertWithInfo(c,c->argv[0],1 == 2);
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"log") && c->argc == 3) {
|
||||
serverLog(LL_WARNING, "DEBUG LOG: %s", (char*)c->argv[2]->ptr);
|
||||
@@ -361,10 +345,7 @@ NULL
|
||||
return;
|
||||
}
|
||||
emptyDb(-1,EMPTYDB_NO_FLAGS,NULL);
|
||||
protectClient(c);
|
||||
int ret = rdbLoad(server.rdb_filename,NULL);
|
||||
unprotectClient(c);
|
||||
if (ret != C_OK) {
|
||||
if (rdbLoad(server.rdb_filename,NULL) != C_OK) {
|
||||
addReplyError(c,"Error trying to load the RDB dump");
|
||||
return;
|
||||
}
|
||||
@@ -373,10 +354,7 @@ NULL
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"loadaof")) {
|
||||
if (server.aof_state != AOF_OFF) flushAppendOnlyFile(1);
|
||||
emptyDb(-1,EMPTYDB_NO_FLAGS,NULL);
|
||||
protectClient(c);
|
||||
int ret = loadAppendOnlyFile(server.aof_filename);
|
||||
unprotectClient(c);
|
||||
if (ret != C_OK) {
|
||||
if (loadAppendOnlyFile(server.aof_filename) != C_OK) {
|
||||
addReply(c,shared.err);
|
||||
return;
|
||||
}
|
||||
@@ -507,28 +485,15 @@ NULL
|
||||
}
|
||||
addReply(c,shared.ok);
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"digest") && c->argc == 2) {
|
||||
/* DEBUG DIGEST (form without keys specified) */
|
||||
unsigned char digest[20];
|
||||
sds d = sdsempty();
|
||||
int j;
|
||||
|
||||
computeDatasetDigest(digest);
|
||||
for (int i = 0; i < 20; i++) d = sdscatprintf(d, "%02x",digest[i]);
|
||||
for (j = 0; j < 20; j++)
|
||||
d = sdscatprintf(d, "%02x",digest[j]);
|
||||
addReplyStatus(c,d);
|
||||
sdsfree(d);
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"digest-value") && c->argc >= 2) {
|
||||
/* DEBUG DIGEST-VALUE key key key ... key. */
|
||||
addReplyMultiBulkLen(c,c->argc-2);
|
||||
for (int j = 2; j < c->argc; j++) {
|
||||
unsigned char digest[20];
|
||||
memset(digest,0,20); /* Start with a clean result */
|
||||
robj *o = lookupKeyReadWithFlags(c->db,c->argv[j],LOOKUP_NOTOUCH);
|
||||
if (o) xorObjectDigest(c->db,c->argv[j],digest,o);
|
||||
|
||||
sds d = sdsempty();
|
||||
for (int i = 0; i < 20; i++) d = sdscatprintf(d, "%02x",digest[i]);
|
||||
addReplyStatus(c,d);
|
||||
sdsfree(d);
|
||||
}
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"sleep") && c->argc == 3) {
|
||||
double dtime = strtod(c->argv[2]->ptr,NULL);
|
||||
long long utime = dtime*1000000;
|
||||
@@ -620,10 +585,6 @@ NULL
|
||||
changeReplicationId();
|
||||
clearReplicationId2();
|
||||
addReply(c,shared.ok);
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"stringmatch-test") && c->argc == 2)
|
||||
{
|
||||
stringmatchlen_fuzz_test();
|
||||
addReplyStatus(c,"Apparently Redis did not crash: test passed");
|
||||
} else {
|
||||
addReplySubcommandSyntaxError(c);
|
||||
return;
|
||||
@@ -762,22 +723,6 @@ static void *getMcontextEip(ucontext_t *uc) {
|
||||
#elif defined(__aarch64__) /* Linux AArch64 */
|
||||
return (void*) uc->uc_mcontext.pc;
|
||||
#endif
|
||||
#elif defined(__FreeBSD__)
|
||||
/* FreeBSD */
|
||||
#if defined(__i386__)
|
||||
return (void*) uc->uc_mcontext.mc_eip;
|
||||
#elif defined(__x86_64__)
|
||||
return (void*) uc->uc_mcontext.mc_rip;
|
||||
#endif
|
||||
#elif defined(__OpenBSD__)
|
||||
/* OpenBSD */
|
||||
#if defined(__i386__)
|
||||
return (void*) uc->sc_eip;
|
||||
#elif defined(__x86_64__)
|
||||
return (void*) uc->sc_rip;
|
||||
#endif
|
||||
#elif defined(__DragonFly__)
|
||||
return (void*) uc->uc_mcontext.mc_rip;
|
||||
#else
|
||||
return NULL;
|
||||
#endif
|
||||
@@ -919,145 +864,6 @@ void logRegisters(ucontext_t *uc) {
|
||||
);
|
||||
logStackContent((void**)uc->uc_mcontext.gregs[15]);
|
||||
#endif
|
||||
#elif defined(__FreeBSD__)
|
||||
#if defined(__x86_64__)
|
||||
serverLog(LL_WARNING,
|
||||
"\n"
|
||||
"RAX:%016lx RBX:%016lx\nRCX:%016lx RDX:%016lx\n"
|
||||
"RDI:%016lx RSI:%016lx\nRBP:%016lx RSP:%016lx\n"
|
||||
"R8 :%016lx R9 :%016lx\nR10:%016lx R11:%016lx\n"
|
||||
"R12:%016lx R13:%016lx\nR14:%016lx R15:%016lx\n"
|
||||
"RIP:%016lx EFL:%016lx\nCSGSFS:%016lx",
|
||||
(unsigned long) uc->uc_mcontext.mc_rax,
|
||||
(unsigned long) uc->uc_mcontext.mc_rbx,
|
||||
(unsigned long) uc->uc_mcontext.mc_rcx,
|
||||
(unsigned long) uc->uc_mcontext.mc_rdx,
|
||||
(unsigned long) uc->uc_mcontext.mc_rdi,
|
||||
(unsigned long) uc->uc_mcontext.mc_rsi,
|
||||
(unsigned long) uc->uc_mcontext.mc_rbp,
|
||||
(unsigned long) uc->uc_mcontext.mc_rsp,
|
||||
(unsigned long) uc->uc_mcontext.mc_r8,
|
||||
(unsigned long) uc->uc_mcontext.mc_r9,
|
||||
(unsigned long) uc->uc_mcontext.mc_r10,
|
||||
(unsigned long) uc->uc_mcontext.mc_r11,
|
||||
(unsigned long) uc->uc_mcontext.mc_r12,
|
||||
(unsigned long) uc->uc_mcontext.mc_r13,
|
||||
(unsigned long) uc->uc_mcontext.mc_r14,
|
||||
(unsigned long) uc->uc_mcontext.mc_r15,
|
||||
(unsigned long) uc->uc_mcontext.mc_rip,
|
||||
(unsigned long) uc->uc_mcontext.mc_rflags,
|
||||
(unsigned long) uc->uc_mcontext.mc_cs
|
||||
);
|
||||
logStackContent((void**)uc->uc_mcontext.mc_rsp);
|
||||
#elif defined(__i386__)
|
||||
serverLog(LL_WARNING,
|
||||
"\n"
|
||||
"EAX:%08lx EBX:%08lx ECX:%08lx EDX:%08lx\n"
|
||||
"EDI:%08lx ESI:%08lx EBP:%08lx ESP:%08lx\n"
|
||||
"SS :%08lx EFL:%08lx EIP:%08lx CS:%08lx\n"
|
||||
"DS :%08lx ES :%08lx FS :%08lx GS:%08lx",
|
||||
(unsigned long) uc->uc_mcontext.mc_eax,
|
||||
(unsigned long) uc->uc_mcontext.mc_ebx,
|
||||
(unsigned long) uc->uc_mcontext.mc_ebx,
|
||||
(unsigned long) uc->uc_mcontext.mc_edx,
|
||||
(unsigned long) uc->uc_mcontext.mc_edi,
|
||||
(unsigned long) uc->uc_mcontext.mc_esi,
|
||||
(unsigned long) uc->uc_mcontext.mc_ebp,
|
||||
(unsigned long) uc->uc_mcontext.mc_esp,
|
||||
(unsigned long) uc->uc_mcontext.mc_ss,
|
||||
(unsigned long) uc->uc_mcontext.mc_eflags,
|
||||
(unsigned long) uc->uc_mcontext.mc_eip,
|
||||
(unsigned long) uc->uc_mcontext.mc_cs,
|
||||
(unsigned long) uc->uc_mcontext.mc_es,
|
||||
(unsigned long) uc->uc_mcontext.mc_fs,
|
||||
(unsigned long) uc->uc_mcontext.mc_gs
|
||||
);
|
||||
logStackContent((void**)uc->uc_mcontext.mc_esp);
|
||||
#endif
|
||||
#elif defined(__OpenBSD__)
|
||||
#if defined(__x86_64__)
|
||||
serverLog(LL_WARNING,
|
||||
"\n"
|
||||
"RAX:%016lx RBX:%016lx\nRCX:%016lx RDX:%016lx\n"
|
||||
"RDI:%016lx RSI:%016lx\nRBP:%016lx RSP:%016lx\n"
|
||||
"R8 :%016lx R9 :%016lx\nR10:%016lx R11:%016lx\n"
|
||||
"R12:%016lx R13:%016lx\nR14:%016lx R15:%016lx\n"
|
||||
"RIP:%016lx EFL:%016lx\nCSGSFS:%016lx",
|
||||
(unsigned long) uc->sc_rax,
|
||||
(unsigned long) uc->sc_rbx,
|
||||
(unsigned long) uc->sc_rcx,
|
||||
(unsigned long) uc->sc_rdx,
|
||||
(unsigned long) uc->sc_rdi,
|
||||
(unsigned long) uc->sc_rsi,
|
||||
(unsigned long) uc->sc_rbp,
|
||||
(unsigned long) uc->sc_rsp,
|
||||
(unsigned long) uc->sc_r8,
|
||||
(unsigned long) uc->sc_r9,
|
||||
(unsigned long) uc->sc_r10,
|
||||
(unsigned long) uc->sc_r11,
|
||||
(unsigned long) uc->sc_r12,
|
||||
(unsigned long) uc->sc_r13,
|
||||
(unsigned long) uc->sc_r14,
|
||||
(unsigned long) uc->sc_r15,
|
||||
(unsigned long) uc->sc_rip,
|
||||
(unsigned long) uc->sc_rflags,
|
||||
(unsigned long) uc->sc_cs
|
||||
);
|
||||
logStackContent((void**)uc->sc_rsp);
|
||||
#elif defined(__i386__)
|
||||
serverLog(LL_WARNING,
|
||||
"\n"
|
||||
"EAX:%08lx EBX:%08lx ECX:%08lx EDX:%08lx\n"
|
||||
"EDI:%08lx ESI:%08lx EBP:%08lx ESP:%08lx\n"
|
||||
"SS :%08lx EFL:%08lx EIP:%08lx CS:%08lx\n"
|
||||
"DS :%08lx ES :%08lx FS :%08lx GS:%08lx",
|
||||
(unsigned long) uc->sc_eax,
|
||||
(unsigned long) uc->sc_ebx,
|
||||
(unsigned long) uc->sc_ebx,
|
||||
(unsigned long) uc->sc_edx,
|
||||
(unsigned long) uc->sc_edi,
|
||||
(unsigned long) uc->sc_esi,
|
||||
(unsigned long) uc->sc_ebp,
|
||||
(unsigned long) uc->sc_esp,
|
||||
(unsigned long) uc->sc_ss,
|
||||
(unsigned long) uc->sc_eflags,
|
||||
(unsigned long) uc->sc_eip,
|
||||
(unsigned long) uc->sc_cs,
|
||||
(unsigned long) uc->sc_es,
|
||||
(unsigned long) uc->sc_fs,
|
||||
(unsigned long) uc->sc_gs
|
||||
);
|
||||
logStackContent((void**)uc->sc_esp);
|
||||
#endif
|
||||
#elif defined(__DragonFly__)
|
||||
serverLog(LL_WARNING,
|
||||
"\n"
|
||||
"RAX:%016lx RBX:%016lx\nRCX:%016lx RDX:%016lx\n"
|
||||
"RDI:%016lx RSI:%016lx\nRBP:%016lx RSP:%016lx\n"
|
||||
"R8 :%016lx R9 :%016lx\nR10:%016lx R11:%016lx\n"
|
||||
"R12:%016lx R13:%016lx\nR14:%016lx R15:%016lx\n"
|
||||
"RIP:%016lx EFL:%016lx\nCSGSFS:%016lx",
|
||||
(unsigned long) uc->uc_mcontext.mc_rax,
|
||||
(unsigned long) uc->uc_mcontext.mc_rbx,
|
||||
(unsigned long) uc->uc_mcontext.mc_rcx,
|
||||
(unsigned long) uc->uc_mcontext.mc_rdx,
|
||||
(unsigned long) uc->uc_mcontext.mc_rdi,
|
||||
(unsigned long) uc->uc_mcontext.mc_rsi,
|
||||
(unsigned long) uc->uc_mcontext.mc_rbp,
|
||||
(unsigned long) uc->uc_mcontext.mc_rsp,
|
||||
(unsigned long) uc->uc_mcontext.mc_r8,
|
||||
(unsigned long) uc->uc_mcontext.mc_r9,
|
||||
(unsigned long) uc->uc_mcontext.mc_r10,
|
||||
(unsigned long) uc->uc_mcontext.mc_r11,
|
||||
(unsigned long) uc->uc_mcontext.mc_r12,
|
||||
(unsigned long) uc->uc_mcontext.mc_r13,
|
||||
(unsigned long) uc->uc_mcontext.mc_r14,
|
||||
(unsigned long) uc->uc_mcontext.mc_r15,
|
||||
(unsigned long) uc->uc_mcontext.mc_rip,
|
||||
(unsigned long) uc->uc_mcontext.mc_rflags,
|
||||
(unsigned long) uc->uc_mcontext.mc_cs
|
||||
);
|
||||
logStackContent((void**)uc->uc_mcontext.mc_rsp);
|
||||
#else
|
||||
serverLog(LL_WARNING,
|
||||
" Dumping of registers not supported for this OS/arch");
|
||||
@@ -1377,8 +1183,6 @@ void serverLogHexDump(int level, char *descr, void *value, size_t len) {
|
||||
void watchdogSignalHandler(int sig, siginfo_t *info, void *secret) {
|
||||
#ifdef HAVE_BACKTRACE
|
||||
ucontext_t *uc = (ucontext_t*) secret;
|
||||
#else
|
||||
(void)secret;
|
||||
#endif
|
||||
UNUSED(info);
|
||||
UNUSED(sig);
|
||||
|
||||
+3
-14
@@ -364,7 +364,7 @@ size_t freeMemoryGetNotCountedMemory(void) {
|
||||
}
|
||||
}
|
||||
if (server.aof_state != AOF_OFF) {
|
||||
overhead += sdsalloc(server.aof_buf)+aofRewriteBufferSize();
|
||||
overhead += sdslen(server.aof_buf)+aofRewriteBufferSize();
|
||||
}
|
||||
return overhead;
|
||||
}
|
||||
@@ -444,8 +444,8 @@ int getMaxmemoryState(size_t *total, size_t *logical, size_t *tofree, float *lev
|
||||
* Otehrwise if we are over the memory limit, but not enough memory
|
||||
* was freed to return back under the limit, the function returns C_ERR. */
|
||||
int freeMemoryIfNeeded(void) {
|
||||
/* By default replicas should ignore maxmemory
|
||||
* and just be masters exact copies. */
|
||||
/* By default slaves should ignore maxmemory and just be masters excat
|
||||
* copies. */
|
||||
if (server.masterhost && server.repl_slave_ignore_maxmemory) return C_OK;
|
||||
|
||||
size_t mem_reported, mem_tofree, mem_freed;
|
||||
@@ -622,14 +622,3 @@ cant_free:
|
||||
return C_ERR;
|
||||
}
|
||||
|
||||
/* This is a wrapper for freeMemoryIfNeeded() that only really calls the
|
||||
* function if right now there are the conditions to do so safely:
|
||||
*
|
||||
* - There must be no script in timeout condition.
|
||||
* - Nor we are loading data right now.
|
||||
*
|
||||
*/
|
||||
int freeMemoryIfNeededAndSafe(void) {
|
||||
if (server.lua_timedout || server.loading) return C_OK;
|
||||
return freeMemoryIfNeeded();
|
||||
}
|
||||
|
||||
+7
-57
@@ -98,11 +98,6 @@ struct commandHelp {
|
||||
"Get the current connection name",
|
||||
9,
|
||||
"2.6.9" },
|
||||
{ "CLIENT ID",
|
||||
"-",
|
||||
"Returns the client ID for the current connection",
|
||||
9,
|
||||
"5.0.0" },
|
||||
{ "CLIENT KILL",
|
||||
"[ip:port] [ID client-id] [TYPE normal|master|slave|pubsub] [ADDR ip:port] [SKIPME yes/no]",
|
||||
"Kill the connection of a client",
|
||||
@@ -128,11 +123,6 @@ struct commandHelp {
|
||||
"Set the current connection name",
|
||||
9,
|
||||
"2.6.9" },
|
||||
{ "CLIENT UNBLOCK",
|
||||
"client-id [TIMEOUT|ERROR]",
|
||||
"Unblock a client blocked in a blocking command from a different connection",
|
||||
9,
|
||||
"5.0.0" },
|
||||
{ "CLUSTER ADDSLOTS",
|
||||
"slot [slot ...]",
|
||||
"Assign new hash slots to receiving node",
|
||||
@@ -155,7 +145,7 @@ struct commandHelp {
|
||||
"3.0.0" },
|
||||
{ "CLUSTER FAILOVER",
|
||||
"[FORCE|TAKEOVER]",
|
||||
"Forces a replica to perform a manual failover of its master.",
|
||||
"Forces a slave to perform a manual failover of its master.",
|
||||
12,
|
||||
"3.0.0" },
|
||||
{ "CLUSTER FORGET",
|
||||
@@ -188,14 +178,9 @@ struct commandHelp {
|
||||
"Get Cluster config for the node",
|
||||
12,
|
||||
"3.0.0" },
|
||||
{ "CLUSTER REPLICAS",
|
||||
"node-id",
|
||||
"List replica nodes of the specified master node",
|
||||
12,
|
||||
"5.0.0" },
|
||||
{ "CLUSTER REPLICATE",
|
||||
"node-id",
|
||||
"Reconfigure a node as a replica of the specified master node",
|
||||
"Reconfigure a node as a slave of the specified master node",
|
||||
12,
|
||||
"3.0.0" },
|
||||
{ "CLUSTER RESET",
|
||||
@@ -220,7 +205,7 @@ struct commandHelp {
|
||||
"3.0.0" },
|
||||
{ "CLUSTER SLAVES",
|
||||
"node-id",
|
||||
"List replica nodes of the specified master node",
|
||||
"List slave nodes of the specified master node",
|
||||
12,
|
||||
"3.0.0" },
|
||||
{ "CLUSTER SLOTS",
|
||||
@@ -705,12 +690,12 @@ struct commandHelp {
|
||||
"1.0.0" },
|
||||
{ "READONLY",
|
||||
"-",
|
||||
"Enables read queries for a connection to a cluster replica node",
|
||||
"Enables read queries for a connection to a cluster slave node",
|
||||
12,
|
||||
"3.0.0" },
|
||||
{ "READWRITE",
|
||||
"-",
|
||||
"Disables read queries for a connection to a cluster replica node",
|
||||
"Disables read queries for a connection to a cluster slave node",
|
||||
12,
|
||||
"3.0.0" },
|
||||
{ "RENAME",
|
||||
@@ -723,11 +708,6 @@ struct commandHelp {
|
||||
"Rename a key, only if the new key does not exist",
|
||||
0,
|
||||
"1.0.0" },
|
||||
{ "REPLICAOF",
|
||||
"host port",
|
||||
"Make the server a replica of another instance, or promote it as master.",
|
||||
9,
|
||||
"5.0.0" },
|
||||
{ "RESTORE",
|
||||
"key ttl serialized-value [REPLACE]",
|
||||
"Create a key using the provided serialized value, previously obtained using DUMP.",
|
||||
@@ -865,7 +845,7 @@ struct commandHelp {
|
||||
"1.0.0" },
|
||||
{ "SLAVEOF",
|
||||
"host port",
|
||||
"Make the server a replica of another instance, or promote it as master. Deprecated starting with Redis 5. Use REPLICAOF instead.",
|
||||
"Make the server a slave of another instance, or promote it as master",
|
||||
9,
|
||||
"1.0.0" },
|
||||
{ "SLOWLOG",
|
||||
@@ -974,7 +954,7 @@ struct commandHelp {
|
||||
7,
|
||||
"2.2.0" },
|
||||
{ "WAIT",
|
||||
"numreplicas timeout",
|
||||
"numslaves timeout",
|
||||
"Wait for the synchronous replication of all the write commands sent in the context of the current connection",
|
||||
0,
|
||||
"3.0.0" },
|
||||
@@ -983,36 +963,11 @@ struct commandHelp {
|
||||
"Watch the given keys to determine execution of the MULTI/EXEC block",
|
||||
7,
|
||||
"2.2.0" },
|
||||
{ "XACK",
|
||||
"key group ID [ID ...]",
|
||||
"Marks a pending message as correctly processed, effectively removing it from the pending entries list of the consumer group. Return value of the command is the number of messages successfully acknowledged, that is, the IDs we were actually able to resolve in the PEL.",
|
||||
14,
|
||||
"5.0.0" },
|
||||
{ "XADD",
|
||||
"key ID field string [field string ...]",
|
||||
"Appends a new entry to a stream",
|
||||
14,
|
||||
"5.0.0" },
|
||||
{ "XCLAIM",
|
||||
"key group consumer min-idle-time ID [ID ...] [IDLE ms] [TIME ms-unix-time] [RETRYCOUNT count] [force] [justid]",
|
||||
"Changes (or acquires) ownership of a message in a consumer group, as if the message was delivered to the specified consumer.",
|
||||
14,
|
||||
"5.0.0" },
|
||||
{ "XDEL",
|
||||
"key ID [ID ...]",
|
||||
"Removes the specified entries from the stream. Returns the number of items actually deleted, that may be different from the number of IDs passed in case certain IDs do not exist.",
|
||||
14,
|
||||
"5.0.0" },
|
||||
{ "XGROUP",
|
||||
"[CREATE key groupname id-or-$] [SETID key id-or-$] [DESTROY key groupname] [DELCONSUMER key groupname consumername]",
|
||||
"Create, destroy, and manage consumer groups.",
|
||||
14,
|
||||
"5.0.0" },
|
||||
{ "XINFO",
|
||||
"[CONSUMERS key groupname] [GROUPS key] [STREAM key] [HELP]",
|
||||
"Get information on streams and consumer groups",
|
||||
14,
|
||||
"5.0.0" },
|
||||
{ "XLEN",
|
||||
"key",
|
||||
"Return the number of entires in a stream",
|
||||
@@ -1043,11 +998,6 @@ struct commandHelp {
|
||||
"Return a range of elements in a stream, with IDs matching the specified IDs interval, in reverse order (from greater to smaller IDs) compared to XRANGE",
|
||||
14,
|
||||
"5.0.0" },
|
||||
{ "XTRIM",
|
||||
"key MAXLEN [~] count",
|
||||
"Trims the stream to (approximately if '~' is passed) a certain size",
|
||||
14,
|
||||
"5.0.0" },
|
||||
{ "ZADD",
|
||||
"key [NX|XX] [CH] [INCR] score member [score member ...]",
|
||||
"Add one or more members to a sorted set, or update its score if it already exists",
|
||||
|
||||
+1
-1
@@ -123,7 +123,7 @@ static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {
|
||||
} else {
|
||||
/* Check for the case where we know we cannot find the value,
|
||||
* but do know the insert position. */
|
||||
if (value > _intsetGet(is,max)) {
|
||||
if (value > _intsetGet(is,intrev32ifbe(is->length)-1)) {
|
||||
if (pos) *pos = intrev32ifbe(is->length);
|
||||
return 0;
|
||||
} else if (value < _intsetGet(is,0)) {
|
||||
|
||||
+2
-14
@@ -562,21 +562,11 @@ sds latencyCommandGenSparkeline(char *event, struct latencyTimeSeries *ts) {
|
||||
*
|
||||
* LATENCY HISTORY: return time-latency samples for the specified event.
|
||||
* LATENCY LATEST: return the latest latency for all the events classes.
|
||||
* LATENCY DOCTOR: returns a human readable analysis of instance latency.
|
||||
* LATENCY DOCTOR: returns an human readable analysis of instance latency.
|
||||
* LATENCY GRAPH: provide an ASCII graph of the latency of the specified event.
|
||||
* LATENCY RESET: reset data of a specified event or all the data if no event provided.
|
||||
*/
|
||||
void latencyCommand(client *c) {
|
||||
const char *help[] = {
|
||||
"DOCTOR -- Returns a human readable latency analysis report.",
|
||||
"GRAPH <event> -- Returns an ASCII latency graph for the event class.",
|
||||
"HISTORY <event> -- Returns time-latency samples for the event class.",
|
||||
"LATEST -- Returns the latest latency samples for all events.",
|
||||
"RESET [event ...] -- Resets latency data of one or more event classes.",
|
||||
" (default: reset all data for all event classes)",
|
||||
"HELP -- Prints this help.",
|
||||
NULL
|
||||
};
|
||||
struct latencyTimeSeries *ts;
|
||||
|
||||
if (!strcasecmp(c->argv[1]->ptr,"history") && c->argc == 3) {
|
||||
@@ -621,10 +611,8 @@ NULL
|
||||
resets += latencyResetEvent(c->argv[j]->ptr);
|
||||
addReplyLongLong(c,resets);
|
||||
}
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"help") && c->argc >= 2) {
|
||||
addReplyHelp(c, help);
|
||||
} else {
|
||||
addReplySubcommandSyntaxError(c);
|
||||
addReply(c,shared.syntaxerr);
|
||||
}
|
||||
return;
|
||||
|
||||
|
||||
+1
-1
@@ -274,7 +274,7 @@ void lolwut5Command(client *c) {
|
||||
lwCanvas *canvas = lwDrawSchotter(cols,squares_per_row,squares_per_col);
|
||||
sds rendered = lwRenderCanvas(canvas);
|
||||
rendered = sdscat(rendered,
|
||||
"\nGeorg Nees - schotter, plotter on paper, 1968. Redis ver. ");
|
||||
"\nGeorg nees - schotter, plotter on paper, 1968. Redis ver. ");
|
||||
rendered = sdscat(rendered,REDIS_VERSION);
|
||||
rendered = sdscatlen(rendered,"\n",1);
|
||||
addReplyBulkSds(c,rendered);
|
||||
|
||||
+1
-3
@@ -4221,7 +4221,6 @@ typedef struct RedisModuleTimer {
|
||||
RedisModule *module; /* Module reference. */
|
||||
RedisModuleTimerProc callback; /* The callback to invoke on expire. */
|
||||
void *data; /* Private data for the callback. */
|
||||
int dbid; /* Database number selected by the original client. */
|
||||
} RedisModuleTimer;
|
||||
|
||||
/* This is the timer handler that is called by the main event loop. We schedule
|
||||
@@ -4248,7 +4247,7 @@ int moduleTimerHandler(struct aeEventLoop *eventLoop, long long id, void *client
|
||||
|
||||
ctx.module = timer->module;
|
||||
ctx.client = moduleFreeContextReusedClient;
|
||||
selectDb(ctx.client, timer->dbid);
|
||||
selectDb(ctx.client, 0);
|
||||
timer->callback(&ctx,timer->data);
|
||||
moduleFreeContext(&ctx);
|
||||
raxRemove(Timers,(unsigned char*)ri.key,ri.key_len,NULL);
|
||||
@@ -4273,7 +4272,6 @@ RedisModuleTimerID RM_CreateTimer(RedisModuleCtx *ctx, mstime_t period, RedisMod
|
||||
timer->module = ctx->module;
|
||||
timer->callback = callback;
|
||||
timer->data = data;
|
||||
timer->dbid = ctx->client->db->id;
|
||||
uint64_t expiretime = ustime()+period*1000;
|
||||
uint64_t key;
|
||||
|
||||
|
||||
+1
-18
@@ -35,7 +35,6 @@
|
||||
void initClientMultiState(client *c) {
|
||||
c->mstate.commands = NULL;
|
||||
c->mstate.count = 0;
|
||||
c->mstate.cmd_flags = 0;
|
||||
}
|
||||
|
||||
/* Release all the resources associated with MULTI/EXEC state */
|
||||
@@ -68,7 +67,6 @@ void queueMultiCommand(client *c) {
|
||||
for (j = 0; j < c->argc; j++)
|
||||
incrRefCount(mc->argv[j]);
|
||||
c->mstate.count++;
|
||||
c->mstate.cmd_flags |= c->cmd->flags;
|
||||
}
|
||||
|
||||
void discardTransaction(client *c) {
|
||||
@@ -139,21 +137,6 @@ void execCommand(client *c) {
|
||||
goto handle_monitor;
|
||||
}
|
||||
|
||||
/* If there are write commands inside the transaction, and this is a read
|
||||
* only slave, we want to send an error. This happens when the transaction
|
||||
* was initiated when the instance was a master or a writable replica and
|
||||
* then the configuration changed (for example instance was turned into
|
||||
* a replica). */
|
||||
if (!server.loading && server.masterhost && server.repl_slave_ro &&
|
||||
!(c->flags & CLIENT_MASTER) && c->mstate.cmd_flags & CMD_WRITE)
|
||||
{
|
||||
addReplyError(c,
|
||||
"Transaction contains write commands but instance "
|
||||
"is now a read-only slave. EXEC aborted.");
|
||||
discardTransaction(c);
|
||||
goto handle_monitor;
|
||||
}
|
||||
|
||||
/* Exec all the queued commands */
|
||||
unwatchAllKeys(c); /* Unwatch ASAP otherwise we'll waste CPU cycles */
|
||||
orig_argv = c->argv;
|
||||
@@ -175,7 +158,7 @@ void execCommand(client *c) {
|
||||
must_propagate = 1;
|
||||
}
|
||||
|
||||
call(c,server.loading ? CMD_CALL_NONE : CMD_CALL_FULL);
|
||||
call(c,CMD_CALL_FULL|CMD_CALL_NOQUEUE);
|
||||
|
||||
/* Commands may alter argc/argv, restore mstate. */
|
||||
c->mstate.commands[j].argc = c->argc;
|
||||
|
||||
+26
-71
@@ -160,32 +160,6 @@ client *createClient(int fd) {
|
||||
return c;
|
||||
}
|
||||
|
||||
/* This funciton puts the client in the queue of clients that should write
|
||||
* their output buffers to the socket. Note that it does not *yet* install
|
||||
* the write handler, to start clients are put in a queue of clients that need
|
||||
* to write, so we try to do that before returning in the event loop (see the
|
||||
* handleClientsWithPendingWrites() function).
|
||||
* If we fail and there is more data to write, compared to what the socket
|
||||
* buffers can hold, then we'll really install the handler. */
|
||||
void clientInstallWriteHandler(client *c) {
|
||||
/* Schedule the client to write the output buffers to the socket only
|
||||
* if not already done and, for slaves, if the slave can actually receive
|
||||
* writes at this stage. */
|
||||
if (!(c->flags & CLIENT_PENDING_WRITE) &&
|
||||
(c->replstate == REPL_STATE_NONE ||
|
||||
(c->replstate == SLAVE_STATE_ONLINE && !c->repl_put_online_on_ack)))
|
||||
{
|
||||
/* Here instead of installing the write handler, we just flag the
|
||||
* client and put it into a list of clients that have something
|
||||
* to write to the socket. This way before re-entering the event
|
||||
* loop, we can try to directly write to the client sockets avoiding
|
||||
* a system call. We'll only really install the write handler if
|
||||
* we'll not be able to write the whole reply at once. */
|
||||
c->flags |= CLIENT_PENDING_WRITE;
|
||||
listAddNodeHead(server.clients_pending_write,c);
|
||||
}
|
||||
}
|
||||
|
||||
/* This function is called every time we are going to transmit new data
|
||||
* to the client. The behavior is the following:
|
||||
*
|
||||
@@ -223,9 +197,24 @@ int prepareClientToWrite(client *c) {
|
||||
|
||||
if (c->fd <= 0) return C_ERR; /* Fake client for AOF loading. */
|
||||
|
||||
/* Schedule the client to write the output buffers to the socket, unless
|
||||
* it should already be setup to do so (it has already pending data). */
|
||||
if (!clientHasPendingReplies(c)) clientInstallWriteHandler(c);
|
||||
/* Schedule the client to write the output buffers to the socket only
|
||||
* if not already done (there were no pending writes already and the client
|
||||
* was yet not flagged), and, for slaves, if the slave can actually
|
||||
* receive writes at this stage. */
|
||||
if (!clientHasPendingReplies(c) &&
|
||||
!(c->flags & CLIENT_PENDING_WRITE) &&
|
||||
(c->replstate == REPL_STATE_NONE ||
|
||||
(c->replstate == SLAVE_STATE_ONLINE && !c->repl_put_online_on_ack)))
|
||||
{
|
||||
/* Here instead of installing the write handler, we just flag the
|
||||
* client and put it into a list of clients that have something
|
||||
* to write to the socket. This way before re-entering the event
|
||||
* loop, we can try to directly write to the client sockets avoiding
|
||||
* a system call. We'll only really install the write handler if
|
||||
* we'll not be able to write the whole reply at once. */
|
||||
c->flags |= CLIENT_PENDING_WRITE;
|
||||
listAddNodeHead(server.clients_pending_write,c);
|
||||
}
|
||||
|
||||
/* Authorize the caller to queue in the output buffer of this client. */
|
||||
return C_OK;
|
||||
@@ -365,13 +354,19 @@ void addReplyErrorLength(client *c, const char *s, size_t len) {
|
||||
* Where the master must propagate the first change even if the second
|
||||
* will produce an error. However it is useful to log such events since
|
||||
* they are rare and may hint at errors in a script or a bug in Redis. */
|
||||
if (c->flags & (CLIENT_MASTER|CLIENT_SLAVE) && !(c->flags & CLIENT_MONITOR)) {
|
||||
if (c->flags & (CLIENT_MASTER|CLIENT_SLAVE)) {
|
||||
char* to = c->flags & CLIENT_MASTER? "master": "replica";
|
||||
char* from = c->flags & CLIENT_MASTER? "replica": "master";
|
||||
char *cmdname = c->lastcmd ? c->lastcmd->name : "<unknown>";
|
||||
serverLog(LL_WARNING,"== CRITICAL == This %s is sending an error "
|
||||
"to its %s: '%s' after processing the command "
|
||||
"'%s'", from, to, s, cmdname);
|
||||
/* Here we want to panic because when a master is sending an
|
||||
* error to some slave in the context of replication, this can
|
||||
* only create some kind of offset or data desynchronization. Better
|
||||
* to catch it ASAP and crash instead of continuing. */
|
||||
if (c->flags & CLIENT_SLAVE)
|
||||
serverPanic("Continuing is unsafe: replication protocol violation.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -823,13 +818,6 @@ void unlinkClient(client *c) {
|
||||
void freeClient(client *c) {
|
||||
listNode *ln;
|
||||
|
||||
/* If a client is protected, yet we need to free it right now, make sure
|
||||
* to at least use asynchronous freeing. */
|
||||
if (c->flags & CLIENT_PROTECTED) {
|
||||
freeClientAsync(c);
|
||||
return;
|
||||
}
|
||||
|
||||
/* If it is our master that's beging disconnected we should make sure
|
||||
* to cache the state to try a partial resynchronization later.
|
||||
*
|
||||
@@ -1066,10 +1054,6 @@ int handleClientsWithPendingWrites(void) {
|
||||
c->flags &= ~CLIENT_PENDING_WRITE;
|
||||
listDelNode(server.clients_pending_write,ln);
|
||||
|
||||
/* If a client is protected, don't do anything,
|
||||
* that may trigger write error or recreate handler. */
|
||||
if (c->flags & CLIENT_PROTECTED) continue;
|
||||
|
||||
/* Try to write buffers to the client socket. */
|
||||
if (writeToClient(c->fd,c,0) == C_ERR) continue;
|
||||
|
||||
@@ -1121,34 +1105,6 @@ void resetClient(client *c) {
|
||||
}
|
||||
}
|
||||
|
||||
/* This funciton is used when we want to re-enter the event loop but there
|
||||
* is the risk that the client we are dealing with will be freed in some
|
||||
* way. This happens for instance in:
|
||||
*
|
||||
* * DEBUG RELOAD and similar.
|
||||
* * When a Lua script is in -BUSY state.
|
||||
*
|
||||
* So the function will protect the client by doing two things:
|
||||
*
|
||||
* 1) It removes the file events. This way it is not possible that an
|
||||
* error is signaled on the socket, freeing the client.
|
||||
* 2) Moreover it makes sure that if the client is freed in a different code
|
||||
* path, it is not really released, but only marked for later release. */
|
||||
void protectClient(client *c) {
|
||||
c->flags |= CLIENT_PROTECTED;
|
||||
aeDeleteFileEvent(server.el,c->fd,AE_READABLE);
|
||||
aeDeleteFileEvent(server.el,c->fd,AE_WRITABLE);
|
||||
}
|
||||
|
||||
/* This will undo the client protection done by protectClient() */
|
||||
void unprotectClient(client *c) {
|
||||
if (c->flags & CLIENT_PROTECTED) {
|
||||
c->flags &= ~CLIENT_PROTECTED;
|
||||
aeCreateFileEvent(server.el,c->fd,AE_READABLE,readQueryFromClient,c);
|
||||
if (clientHasPendingReplies(c)) clientInstallWriteHandler(c);
|
||||
}
|
||||
}
|
||||
|
||||
/* Like processMultibulkBuffer(), but for the inline protocol instead of RESP,
|
||||
* this function consumes the client query buffer and creates a command ready
|
||||
* to be executed inside the client structure. Returns C_OK if the command
|
||||
@@ -1464,7 +1420,7 @@ void processInputBuffer(client *c) {
|
||||
}
|
||||
|
||||
/* Trim to pos */
|
||||
if (server.current_client != NULL && c->qb_pos) {
|
||||
if (c->qb_pos) {
|
||||
sdsrange(c->querybuf,c->qb_pos,-1);
|
||||
c->qb_pos = 0;
|
||||
}
|
||||
@@ -2103,7 +2059,6 @@ int checkClientOutputBufferLimits(client *c) {
|
||||
* called from contexts where the client can't be freed safely, i.e. from the
|
||||
* lower level functions pushing data inside the client output buffers. */
|
||||
void asyncCloseClientOnOutputBufferLimitReached(client *c) {
|
||||
if (c->fd == -1) return; /* It is unsafe to free fake clients. */
|
||||
serverAssert(c->reply_bytes < SIZE_MAX-(1024*64));
|
||||
if (c->reply_bytes == 0 || c->flags & CLIENT_CLOSE_ASAP) return;
|
||||
if (checkClientOutputBufferLimits(c)) {
|
||||
|
||||
+22
-21
@@ -185,7 +185,7 @@ robj *createStringObjectFromLongDouble(long double value, int humanfriendly) {
|
||||
/* Duplicate a string object, with the guarantee that the returned object
|
||||
* has the same encoding as the original one.
|
||||
*
|
||||
* This function also guarantees that duplicating a small integer object
|
||||
* This function also guarantees that duplicating a small integere object
|
||||
* (or a string object that contains a representation of a small integer)
|
||||
* will always result in a fresh object that is unshared (refcount == 1).
|
||||
*
|
||||
@@ -1011,7 +1011,7 @@ struct redisMemOverhead *getMemoryOverheadData(void) {
|
||||
|
||||
mem = 0;
|
||||
if (server.aof_state != AOF_OFF) {
|
||||
mem += sdsalloc(server.aof_buf);
|
||||
mem += sdslen(server.aof_buf);
|
||||
mem += aofRewriteBufferSize();
|
||||
}
|
||||
mh->aof_buffer = mem;
|
||||
@@ -1285,18 +1285,9 @@ NULL
|
||||
*
|
||||
* Usage: MEMORY usage <key> */
|
||||
void memoryCommand(client *c) {
|
||||
if (!strcasecmp(c->argv[1]->ptr,"help") && c->argc == 2) {
|
||||
const char *help[] = {
|
||||
"DOCTOR - Return memory problems reports.",
|
||||
"MALLOC-STATS -- Return internal statistics report from the memory allocator.",
|
||||
"PURGE -- Attempt to purge dirty pages for reclamation by the allocator.",
|
||||
"STATS -- Return information about the memory usage of the server.",
|
||||
"USAGE <key> [SAMPLES <count>] -- Return memory in bytes used by <key> and its value. Nested values are sampled up to <count> times (default: 5).",
|
||||
NULL
|
||||
};
|
||||
addReplyHelp(c, help);
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"usage") && c->argc >= 3) {
|
||||
dictEntry *de;
|
||||
robj *o;
|
||||
|
||||
if (!strcasecmp(c->argv[1]->ptr,"usage") && c->argc >= 3) {
|
||||
long long samples = OBJ_COMPUTE_SIZE_DEF_SAMPLES;
|
||||
for (int j = 3; j < c->argc; j++) {
|
||||
if (!strcasecmp(c->argv[j]->ptr,"samples") &&
|
||||
@@ -1315,12 +1306,10 @@ NULL
|
||||
return;
|
||||
}
|
||||
}
|
||||
if ((de = dictFind(c->db->dict,c->argv[2]->ptr)) == NULL) {
|
||||
addReply(c, shared.nullbulk);
|
||||
return;
|
||||
}
|
||||
size_t usage = objectComputeSize(dictGetVal(de),samples);
|
||||
usage += sdsAllocSize(dictGetKey(de));
|
||||
if ((o = objectCommandLookupOrReply(c,c->argv[2],shared.nullbulk))
|
||||
== NULL) return;
|
||||
size_t usage = objectComputeSize(o,samples);
|
||||
usage += sdsAllocSize(c->argv[2]->ptr);
|
||||
usage += sizeof(dictEntry);
|
||||
addReplyLongLong(c,usage);
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"stats") && c->argc == 2) {
|
||||
@@ -1445,7 +1434,19 @@ NULL
|
||||
addReply(c, shared.ok);
|
||||
/* Nothing to do for other allocators. */
|
||||
#endif
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"help") && c->argc == 2) {
|
||||
addReplyMultiBulkLen(c,5);
|
||||
addReplyBulkCString(c,
|
||||
"MEMORY DOCTOR - Outputs memory problems report");
|
||||
addReplyBulkCString(c,
|
||||
"MEMORY USAGE <key> [SAMPLES <count>] - Estimate memory usage of key");
|
||||
addReplyBulkCString(c,
|
||||
"MEMORY STATS - Show memory usage details");
|
||||
addReplyBulkCString(c,
|
||||
"MEMORY PURGE - Ask the allocator to release memory");
|
||||
addReplyBulkCString(c,
|
||||
"MEMORY MALLOC-STATS - Show allocator internal stats");
|
||||
} else {
|
||||
addReplyErrorFormat(c, "Unknown subcommand or wrong number of arguments for '%s'. Try MEMORY HELP", (char*)c->argv[1]->ptr);
|
||||
addReplyError(c,"Syntax error. Try MEMORY HELP");
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@
|
||||
* container: 2 bits, NONE=1, ZIPLIST=2.
|
||||
* recompress: 1 bit, bool, true if node is temporarry decompressed for usage.
|
||||
* attempted_compress: 1 bit, boolean, used for verifying during testing.
|
||||
* extra: 10 bits, free for future use; pads out the remainder of 32 bits */
|
||||
* extra: 12 bits, free for future use; pads out the remainder of 32 bits */
|
||||
typedef struct quicklistNode {
|
||||
struct quicklistNode *prev;
|
||||
struct quicklistNode *next;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* Rax -- A radix tree implementation.
|
||||
*
|
||||
* Copyright (c) 2017-2018, Salvatore Sanfilippo <antirez at gmail dot com>
|
||||
* Copyright (c) 2017, Salvatore Sanfilippo <antirez at gmail dot com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
@@ -51,18 +51,14 @@ void *raxNotFound = (void*)"rax-not-found-pointer";
|
||||
|
||||
void raxDebugShowNode(const char *msg, raxNode *n);
|
||||
|
||||
/* Turn debugging messages on/off by compiling with RAX_DEBUG_MSG macro on.
|
||||
* When RAX_DEBUG_MSG is defined by default Rax operations will emit a lot
|
||||
* of debugging info to the standard output, however you can still turn
|
||||
* debugging on/off in order to enable it only when you suspect there is an
|
||||
* operation causing a bug using the function raxSetDebugMsg(). */
|
||||
#ifdef RAX_DEBUG_MSG
|
||||
/* Turn debugging messages on/off. */
|
||||
#if 0
|
||||
#define debugf(...) \
|
||||
if (raxDebugMsg) { \
|
||||
do { \
|
||||
printf("%s:%s:%d:\t", __FILE__, __FUNCTION__, __LINE__); \
|
||||
printf(__VA_ARGS__); \
|
||||
fflush(stdout); \
|
||||
}
|
||||
} while (0);
|
||||
|
||||
#define debugnode(msg,n) raxDebugShowNode(msg,n)
|
||||
#else
|
||||
@@ -70,16 +66,6 @@ void raxDebugShowNode(const char *msg, raxNode *n);
|
||||
#define debugnode(msg,n)
|
||||
#endif
|
||||
|
||||
/* By default log debug info if RAX_DEBUG_MSG is defined. */
|
||||
static int raxDebugMsg = 1;
|
||||
|
||||
/* When debug messages are enabled, turn them on/off dynamically. By
|
||||
* default they are enabled. Set the state to 0 to disable, and 1 to
|
||||
* re-enable. */
|
||||
void raxSetDebugMsg(int onoff) {
|
||||
raxDebugMsg = onoff;
|
||||
}
|
||||
|
||||
/* ------------------------- raxStack functions --------------------------
|
||||
* The raxStack is a simple stack of pointers that is capable of switching
|
||||
* from using a stack-allocated array to dynamic heap once a given number of
|
||||
@@ -148,43 +134,12 @@ static inline void raxStackFree(raxStack *ts) {
|
||||
* Radix tree implementation
|
||||
* --------------------------------------------------------------------------*/
|
||||
|
||||
/* Return the padding needed in the characters section of a node having size
|
||||
* 'nodesize'. The padding is needed to store the child pointers to aligned
|
||||
* addresses. Note that we add 4 to the node size because the node has a four
|
||||
* bytes header. */
|
||||
#define raxPadding(nodesize) ((sizeof(void*)-((nodesize+4) % sizeof(void*))) & (sizeof(void*)-1))
|
||||
|
||||
/* Return the pointer to the last child pointer in a node. For the compressed
|
||||
* nodes this is the only child pointer. */
|
||||
#define raxNodeLastChildPtr(n) ((raxNode**) ( \
|
||||
((char*)(n)) + \
|
||||
raxNodeCurrentLength(n) - \
|
||||
sizeof(raxNode*) - \
|
||||
(((n)->iskey && !(n)->isnull) ? sizeof(void*) : 0) \
|
||||
))
|
||||
|
||||
/* Return the pointer to the first child pointer. */
|
||||
#define raxNodeFirstChildPtr(n) ((raxNode**) ( \
|
||||
(n)->data + \
|
||||
(n)->size + \
|
||||
raxPadding((n)->size)))
|
||||
|
||||
/* Return the current total size of the node. Note that the second line
|
||||
* computes the padding after the string of characters, needed in order to
|
||||
* save pointers to aligned addresses. */
|
||||
#define raxNodeCurrentLength(n) ( \
|
||||
sizeof(raxNode)+(n)->size+ \
|
||||
raxPadding((n)->size)+ \
|
||||
((n)->iscompr ? sizeof(raxNode*) : sizeof(raxNode*)*(n)->size)+ \
|
||||
(((n)->iskey && !(n)->isnull)*sizeof(void*)) \
|
||||
)
|
||||
|
||||
/* Allocate a new non compressed node with the specified number of children.
|
||||
* If datafiled is true, the allocation is made large enough to hold the
|
||||
* associated data pointer.
|
||||
* Returns the new node pointer. On out of memory NULL is returned. */
|
||||
raxNode *raxNewNode(size_t children, int datafield) {
|
||||
size_t nodesize = sizeof(raxNode)+children+raxPadding(children)+
|
||||
size_t nodesize = sizeof(raxNode)+children+
|
||||
sizeof(raxNode*)*children;
|
||||
if (datafield) nodesize += sizeof(void*);
|
||||
raxNode *node = rax_malloc(nodesize);
|
||||
@@ -212,6 +167,13 @@ rax *raxNew(void) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Return the current total size of the node. */
|
||||
#define raxNodeCurrentLength(n) ( \
|
||||
sizeof(raxNode)+(n)->size+ \
|
||||
((n)->iscompr ? sizeof(raxNode*) : sizeof(raxNode*)*(n)->size)+ \
|
||||
(((n)->iskey && !(n)->isnull)*sizeof(void*)) \
|
||||
)
|
||||
|
||||
/* realloc the node to make room for auxiliary data in order
|
||||
* to store an item in that node. On out of memory NULL is returned. */
|
||||
raxNode *raxReallocForData(raxNode *n, void *data) {
|
||||
@@ -254,17 +216,18 @@ void *raxGetData(raxNode *n) {
|
||||
raxNode *raxAddChild(raxNode *n, unsigned char c, raxNode **childptr, raxNode ***parentlink) {
|
||||
assert(n->iscompr == 0);
|
||||
|
||||
size_t curlen = raxNodeCurrentLength(n);
|
||||
n->size++;
|
||||
size_t newlen = raxNodeCurrentLength(n);
|
||||
n->size--; /* For now restore the orignal size. We'll update it only on
|
||||
success at the end. */
|
||||
size_t curlen = sizeof(raxNode)+
|
||||
n->size+
|
||||
sizeof(raxNode*)*n->size;
|
||||
size_t newlen;
|
||||
|
||||
/* Alloc the new child we will link to 'n'. */
|
||||
raxNode *child = raxNewNode(0,0);
|
||||
if (child == NULL) return NULL;
|
||||
|
||||
/* Make space in the original node. */
|
||||
if (n->iskey) curlen += sizeof(void*);
|
||||
newlen = curlen+sizeof(raxNode*)+1; /* Add 1 char and 1 pointer. */
|
||||
raxNode *newn = rax_realloc(n,newlen);
|
||||
if (newn == NULL) {
|
||||
rax_free(child);
|
||||
@@ -272,34 +235,14 @@ raxNode *raxAddChild(raxNode *n, unsigned char c, raxNode **childptr, raxNode **
|
||||
}
|
||||
n = newn;
|
||||
|
||||
/* After the reallocation, we have up to 8/16 (depending on the system
|
||||
* pointer size, and the required node padding) bytes at the end, that is,
|
||||
* the additional char in the 'data' section, plus one pointer to the new
|
||||
* child, plus the padding needed in order to store addresses into aligned
|
||||
* locations.
|
||||
/* After the reallocation, we have 5/9 (depending on the system
|
||||
* pointer size) bytes at the end, that is, the additional char
|
||||
* in the 'data' section, plus one pointer to the new child:
|
||||
*
|
||||
* So if we start with the following node, having "abde" edges.
|
||||
*
|
||||
* Note:
|
||||
* - We assume 4 bytes pointer for simplicity.
|
||||
* - Each space below corresponds to one byte
|
||||
*
|
||||
* [HDR*][abde][Aptr][Bptr][Dptr][Eptr]|AUXP|
|
||||
*
|
||||
* After the reallocation we need: 1 byte for the new edge character
|
||||
* plus 4 bytes for a new child pointer (assuming 32 bit machine).
|
||||
* However after adding 1 byte to the edge char, the header + the edge
|
||||
* characters are no longer aligned, so we also need 3 bytes of padding.
|
||||
* In total the reallocation will add 1+4+3 bytes = 8 bytes:
|
||||
*
|
||||
* (Blank bytes are represented by ".")
|
||||
*
|
||||
* [HDR*][abde][Aptr][Bptr][Dptr][Eptr]|AUXP|[....][....]
|
||||
* [numc][abx][ap][bp][xp]|auxp|.....
|
||||
*
|
||||
* Let's find where to insert the new child in order to make sure
|
||||
* it is inserted in-place lexicographically. Assuming we are adding
|
||||
* a child "c" in our case pos will be = 2 after the end of the following
|
||||
* loop. */
|
||||
* it is inserted in-place lexicographically. */
|
||||
int pos;
|
||||
for (pos = 0; pos < n->size; pos++) {
|
||||
if (n->data[pos] > c) break;
|
||||
@@ -309,81 +252,55 @@ raxNode *raxAddChild(raxNode *n, unsigned char c, raxNode **childptr, raxNode **
|
||||
* so that we can mess with the other data without overwriting it.
|
||||
* We will obtain something like that:
|
||||
*
|
||||
* [HDR*][abde][Aptr][Bptr][Dptr][Eptr][....][....]|AUXP|
|
||||
*/
|
||||
unsigned char *src, *dst;
|
||||
* [numc][abx][ap][bp][xp].....|auxp| */
|
||||
unsigned char *src;
|
||||
if (n->iskey && !n->isnull) {
|
||||
src = ((unsigned char*)n+curlen-sizeof(void*));
|
||||
dst = ((unsigned char*)n+newlen-sizeof(void*));
|
||||
memmove(dst,src,sizeof(void*));
|
||||
src = n->data+n->size+sizeof(raxNode*)*n->size;
|
||||
memmove(src+1+sizeof(raxNode*),src,sizeof(void*));
|
||||
}
|
||||
|
||||
/* Compute the "shift", that is, how many bytes we need to move the
|
||||
* pointers section forward because of the addition of the new child
|
||||
* byte in the string section. Note that if we had no padding, that
|
||||
* would be always "1", since we are adding a single byte in the string
|
||||
* section of the node (where now there is "abde" basically).
|
||||
*
|
||||
* However we have padding, so it could be zero, or up to 8.
|
||||
*
|
||||
* Another way to think at the shift is, how many bytes we need to
|
||||
* move child pointers forward *other than* the obvious sizeof(void*)
|
||||
* needed for the additional pointer itself. */
|
||||
size_t shift = newlen - curlen - sizeof(void*);
|
||||
|
||||
/* We said we are adding a node with edge 'c'. The insertion
|
||||
* point is between 'b' and 'd', so the 'pos' variable value is
|
||||
* the index of the first child pointer that we need to move forward
|
||||
* to make space for our new pointer.
|
||||
*
|
||||
/* Now imagine we are adding a node with edge 'c'. The insertion
|
||||
* point is between 'b' and 'x', so the 'pos' variable value is
|
||||
* To start, move all the child pointers after the insertion point
|
||||
* of shift+sizeof(pointer) bytes on the right, to obtain:
|
||||
* of 1+sizeof(pointer) bytes on the right, to obtain:
|
||||
*
|
||||
* [HDR*][abde][Aptr][Bptr][....][....][Dptr][Eptr]|AUXP|
|
||||
*/
|
||||
src = n->data+n->size+
|
||||
raxPadding(n->size)+
|
||||
sizeof(raxNode*)*pos;
|
||||
memmove(src+shift+sizeof(raxNode*),src,sizeof(raxNode*)*(n->size-pos));
|
||||
|
||||
/* Move the pointers to the left of the insertion position as well. Often
|
||||
* we don't need to do anything if there was already some padding to use. In
|
||||
* that case the final destination of the pointers will be the same, however
|
||||
* in our example there was no pre-existing padding, so we added one byte
|
||||
* plus thre bytes of padding. After the next memmove() things will look
|
||||
* like thata:
|
||||
*
|
||||
* [HDR*][abde][....][Aptr][Bptr][....][Dptr][Eptr]|AUXP|
|
||||
*/
|
||||
if (shift) {
|
||||
src = (unsigned char*) raxNodeFirstChildPtr(n);
|
||||
memmove(src+shift,src,sizeof(raxNode*)*pos);
|
||||
}
|
||||
* [numc][abx][ap][bp].....[xp]|auxp| */
|
||||
src = n->data+n->size+sizeof(raxNode*)*pos;
|
||||
memmove(src+1+sizeof(raxNode*),src,sizeof(raxNode*)*(n->size-pos));
|
||||
|
||||
/* Now make the space for the additional char in the data section,
|
||||
* but also move the pointers before the insertion point to the right
|
||||
* by shift bytes, in order to obtain the following:
|
||||
* but also move the pointers before the insertion point in the right
|
||||
* by 1 byte, in order to obtain the following:
|
||||
*
|
||||
* [HDR*][ab.d][e...][Aptr][Bptr][....][Dptr][Eptr]|AUXP|
|
||||
*/
|
||||
* [numc][ab.x][ap][bp]....[xp]|auxp| */
|
||||
src = n->data+pos;
|
||||
memmove(src+1,src,n->size-pos);
|
||||
memmove(src+1,src,n->size-pos+sizeof(raxNode*)*pos);
|
||||
|
||||
/* We can now set the character and its child node pointer to get:
|
||||
*
|
||||
* [HDR*][abcd][e...][Aptr][Bptr][....][Dptr][Eptr]|AUXP|
|
||||
* [HDR*][abcd][e...][Aptr][Bptr][Cptr][Dptr][Eptr]|AUXP|
|
||||
*/
|
||||
* [numc][abcx][ap][bp][cp]....|auxp|
|
||||
* [numc][abcx][ap][bp][cp][xp]|auxp| */
|
||||
n->data[pos] = c;
|
||||
n->size++;
|
||||
src = (unsigned char*) raxNodeFirstChildPtr(n);
|
||||
raxNode **childfield = (raxNode**)(src+sizeof(raxNode*)*pos);
|
||||
raxNode **childfield = (raxNode**)(n->data+n->size+sizeof(raxNode*)*pos);
|
||||
memcpy(childfield,&child,sizeof(child));
|
||||
*childptr = child;
|
||||
*parentlink = childfield;
|
||||
return n;
|
||||
}
|
||||
|
||||
/* Return the pointer to the last child pointer in a node. For the compressed
|
||||
* nodes this is the only child pointer. */
|
||||
#define raxNodeLastChildPtr(n) ((raxNode**) ( \
|
||||
((char*)(n)) + \
|
||||
raxNodeCurrentLength(n) - \
|
||||
sizeof(raxNode*) - \
|
||||
(((n)->iskey && !(n)->isnull) ? sizeof(void*) : 0) \
|
||||
))
|
||||
|
||||
/* Return the pointer to the first child pointer. */
|
||||
#define raxNodeFirstChildPtr(n) ((raxNode**)((n)->data+(n)->size))
|
||||
|
||||
/* Turn the node 'n', that must be a node without any children, into a
|
||||
* compressed node representing a set of nodes linked one after the other
|
||||
* and having exactly one child each. The node can be a key or not: this
|
||||
@@ -404,7 +321,7 @@ raxNode *raxCompressNode(raxNode *n, unsigned char *s, size_t len, raxNode **chi
|
||||
if (*child == NULL) return NULL;
|
||||
|
||||
/* Make space in the parent node. */
|
||||
newsize = sizeof(raxNode)+len+raxPadding(len)+sizeof(raxNode*);
|
||||
newsize = sizeof(raxNode)+len+sizeof(raxNode*);
|
||||
if (n->iskey) {
|
||||
data = raxGetData(n); /* To restore it later. */
|
||||
if (!n->isnull) newsize += sizeof(void*);
|
||||
@@ -702,14 +619,13 @@ int raxGenericInsert(rax *rax, unsigned char *s, size_t len, void *data, void **
|
||||
raxNode *postfix = NULL;
|
||||
|
||||
if (trimmedlen) {
|
||||
nodesize = sizeof(raxNode)+trimmedlen+raxPadding(trimmedlen)+
|
||||
sizeof(raxNode*);
|
||||
nodesize = sizeof(raxNode)+trimmedlen+sizeof(raxNode*);
|
||||
if (h->iskey && !h->isnull) nodesize += sizeof(void*);
|
||||
trimmed = rax_malloc(nodesize);
|
||||
}
|
||||
|
||||
if (postfixlen) {
|
||||
nodesize = sizeof(raxNode)+postfixlen+raxPadding(postfixlen)+
|
||||
nodesize = sizeof(raxNode)+postfixlen+
|
||||
sizeof(raxNode*);
|
||||
postfix = rax_malloc(nodesize);
|
||||
}
|
||||
@@ -785,12 +701,11 @@ int raxGenericInsert(rax *rax, unsigned char *s, size_t len, void *data, void **
|
||||
|
||||
/* Allocate postfix & trimmed nodes ASAP to fail for OOM gracefully. */
|
||||
size_t postfixlen = h->size - j;
|
||||
size_t nodesize = sizeof(raxNode)+postfixlen+raxPadding(postfixlen)+
|
||||
sizeof(raxNode*);
|
||||
size_t nodesize = sizeof(raxNode)+postfixlen+sizeof(raxNode*);
|
||||
if (data != NULL) nodesize += sizeof(void*);
|
||||
raxNode *postfix = rax_malloc(nodesize);
|
||||
|
||||
nodesize = sizeof(raxNode)+j+raxPadding(j)+sizeof(raxNode*);
|
||||
nodesize = sizeof(raxNode)+j+sizeof(raxNode*);
|
||||
if (h->iskey && !h->isnull) nodesize += sizeof(void*);
|
||||
raxNode *trimmed = rax_malloc(nodesize);
|
||||
|
||||
@@ -960,7 +875,7 @@ raxNode *raxRemoveChild(raxNode *parent, raxNode *child) {
|
||||
return parent;
|
||||
}
|
||||
|
||||
/* Otherwise we need to scan for the child pointer and memmove()
|
||||
/* Otherwise we need to scan for the children pointer and memmove()
|
||||
* accordingly.
|
||||
*
|
||||
* 1. To start we seek the first element in both the children
|
||||
@@ -985,21 +900,13 @@ raxNode *raxRemoveChild(raxNode *parent, raxNode *child) {
|
||||
debugf("raxRemoveChild tail len: %d\n", taillen);
|
||||
memmove(e,e+1,taillen);
|
||||
|
||||
/* Compute the shift, that is the amount of bytes we should move our
|
||||
* child pointers to the left, since the removal of one edge character
|
||||
* and the corresponding padding change, may change the layout.
|
||||
* We just check if in the old version of the node there was at the
|
||||
* end just a single byte and all padding: in that case removing one char
|
||||
* will remove a whole sizeof(void*) word. */
|
||||
size_t shift = ((parent->size+4) % sizeof(void*)) == 1 ? sizeof(void*) : 0;
|
||||
/* Since we have one data byte less, also child pointers start one byte
|
||||
* before now. */
|
||||
memmove(((char*)cp)-1,cp,(parent->size-taillen-1)*sizeof(raxNode**));
|
||||
|
||||
/* Move the children pointers before the deletion point. */
|
||||
if (shift)
|
||||
memmove(((char*)cp)-shift,cp,(parent->size-taillen-1)*sizeof(raxNode**));
|
||||
|
||||
/* Move the remaining "tail" pointers at the right position as well. */
|
||||
/* Move the remaining "tail" pointer at the right position as well. */
|
||||
size_t valuelen = (parent->iskey && !parent->isnull) ? sizeof(void*) : 0;
|
||||
memmove(((char*)c)-shift,c+1,taillen*sizeof(raxNode**)+valuelen);
|
||||
memmove(((char*)c)-1,c+1,taillen*sizeof(raxNode**)+valuelen);
|
||||
|
||||
/* 4. Update size. */
|
||||
parent->size--;
|
||||
@@ -1165,7 +1072,7 @@ int raxRemove(rax *rax, unsigned char *s, size_t len, void **old) {
|
||||
if (nodes > 1) {
|
||||
/* If we can compress, create the new node and populate it. */
|
||||
size_t nodesize =
|
||||
sizeof(raxNode)+comprsize+raxPadding(comprsize)+sizeof(raxNode*);
|
||||
sizeof(raxNode)+comprsize+sizeof(raxNode*);
|
||||
raxNode *new = rax_malloc(nodesize);
|
||||
/* An out of memory here just means we cannot optimize this
|
||||
* node, but the tree is left in a consistent state. */
|
||||
@@ -1406,7 +1313,7 @@ int raxIteratorNextStep(raxIterator *it, int noup) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Seek the grestest key in the subtree at the current node. Return 0 on
|
||||
/* Seek the greatest key in the subtree at the current node. Return 0 on
|
||||
* out of memory, otherwise 1. This is an helper function for different
|
||||
* iteration functions below. */
|
||||
int raxSeekGreatest(raxIterator *it) {
|
||||
@@ -1886,7 +1793,6 @@ void raxShow(rax *rax) {
|
||||
|
||||
/* Used by debugnode() macro to show info about a given node. */
|
||||
void raxDebugShowNode(const char *msg, raxNode *n) {
|
||||
if (raxDebugMsg == 0) return;
|
||||
printf("%s: %p [%.*s] key:%d size:%d children:",
|
||||
msg, (void*)n, (int)n->size, (char*)n->data, n->iskey, n->size);
|
||||
int numcld = n->iscompr ? 1 : n->size;
|
||||
@@ -1901,43 +1807,4 @@ void raxDebugShowNode(const char *msg, raxNode *n) {
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
/* Touch all the nodes of a tree returning a check sum. This is useful
|
||||
* in order to make Valgrind detect if there is something wrong while
|
||||
* reading the data structure.
|
||||
*
|
||||
* This function was used in order to identify Rax bugs after a big refactoring
|
||||
* using this technique:
|
||||
*
|
||||
* 1. The rax-test is executed using Valgrind, adding a printf() so that for
|
||||
* the fuzz tester we see what iteration in the loop we are in.
|
||||
* 2. After every modification of the radix tree made by the fuzz tester
|
||||
* in rax-test.c, we add a call to raxTouch().
|
||||
* 3. Now as soon as an operation will corrupt the tree, raxTouch() will
|
||||
* detect it (via Valgrind) immediately. We can add more calls to narrow
|
||||
* the state.
|
||||
* 4. At this point a good idea is to enable Rax debugging messages immediately
|
||||
* before the moment the tree is corrupted, to see what happens.
|
||||
*/
|
||||
unsigned long raxTouch(raxNode *n) {
|
||||
debugf("Touching %p\n", (void*)n);
|
||||
unsigned long sum = 0;
|
||||
if (n->iskey) {
|
||||
sum += (unsigned long)raxGetData(n);
|
||||
}
|
||||
|
||||
int numchildren = n->iscompr ? 1 : n->size;
|
||||
raxNode **cp = raxNodeFirstChildPtr(n);
|
||||
int count = 0;
|
||||
for (int i = 0; i < numchildren; i++) {
|
||||
if (numchildren > 1) {
|
||||
sum += (long)n->data[i];
|
||||
}
|
||||
raxNode *child;
|
||||
memcpy(&child,cp,sizeof(child));
|
||||
if (child == (void*)0x65d1760) count++;
|
||||
if (count > 1) exit(1);
|
||||
sum += raxTouch(child);
|
||||
cp++;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
@@ -1,33 +1,3 @@
|
||||
/* Rax -- A radix tree implementation.
|
||||
*
|
||||
* Copyright (c) 2017-2018, Salvatore Sanfilippo <antirez at gmail dot com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* * Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* * Neither the name of Redis nor the names of its contributors may be used
|
||||
* to endorse or promote products derived from this software without
|
||||
* specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#ifndef RAX_H
|
||||
#define RAX_H
|
||||
|
||||
@@ -107,16 +77,16 @@ typedef struct raxNode {
|
||||
* Note how the character is not stored in the children but in the
|
||||
* edge of the parents:
|
||||
*
|
||||
* [header iscompr=0][abc][a-ptr][b-ptr][c-ptr](value-ptr?)
|
||||
* [header strlen=0][abc][a-ptr][b-ptr][c-ptr](value-ptr?)
|
||||
*
|
||||
* if node is compressed (iscompr bit is 1) the node has 1 children.
|
||||
* if node is compressed (strlen != 0) the node has 1 children.
|
||||
* In that case the 'size' bytes of the string stored immediately at
|
||||
* the start of the data section, represent a sequence of successive
|
||||
* nodes linked one after the other, for which only the last one in
|
||||
* the sequence is actually represented as a node, and pointed to by
|
||||
* the current compressed node.
|
||||
*
|
||||
* [header iscompr=1][xyz][z-ptr](value-ptr?)
|
||||
* [header strlen=3][xyz][z-ptr](value-ptr?)
|
||||
*
|
||||
* Both compressed and not compressed nodes can represent a key
|
||||
* with associated data in the radix tree at any level (not just terminal
|
||||
@@ -206,8 +176,6 @@ void raxStop(raxIterator *it);
|
||||
int raxEOF(raxIterator *it);
|
||||
void raxShow(rax *rax);
|
||||
uint64_t raxSize(rax *rax);
|
||||
unsigned long raxTouch(raxNode *n);
|
||||
void raxSetDebugMsg(int onoff);
|
||||
|
||||
/* Internal API. May be used by the node callback in order to access rax nodes
|
||||
* in a low level way, so this function is exported as well. */
|
||||
|
||||
@@ -1645,9 +1645,6 @@ robj *rdbLoadObject(int rdbtype, rio *rdb) {
|
||||
* node: the entries inside the listpack itself are delta-encoded
|
||||
* relatively to this ID. */
|
||||
sds nodekey = rdbGenericLoadStringObject(rdb,RDB_LOAD_SDS,NULL);
|
||||
if (nodekey == NULL) {
|
||||
rdbExitReportCorruptRDB("Stream master ID loading failed: invalid encoding or I/O error.");
|
||||
}
|
||||
if (sdslen(nodekey) != sizeof(streamID)) {
|
||||
rdbExitReportCorruptRDB("Stream node key entry is not the "
|
||||
"size of a stream ID");
|
||||
|
||||
+106
-452
@@ -67,7 +67,6 @@
|
||||
#define REDIS_CLI_HISTFILE_DEFAULT ".rediscli_history"
|
||||
#define REDIS_CLI_RCFILE_ENV "REDISCLI_RCFILE"
|
||||
#define REDIS_CLI_RCFILE_DEFAULT ".redisclirc"
|
||||
#define REDIS_CLI_AUTH_ENV "REDISCLI_AUTH"
|
||||
|
||||
#define CLUSTER_MANAGER_SLOTS 16384
|
||||
#define CLUSTER_MANAGER_MIGRATE_TIMEOUT 60000
|
||||
@@ -117,7 +116,6 @@
|
||||
#define CLUSTER_MANAGER_CMD_FLAG_REPLACE 1 << 6
|
||||
#define CLUSTER_MANAGER_CMD_FLAG_COPY 1 << 7
|
||||
#define CLUSTER_MANAGER_CMD_FLAG_COLOR 1 << 8
|
||||
#define CLUSTER_MANAGER_CMD_FLAG_CHECK_OWNERS 1 << 9
|
||||
|
||||
#define CLUSTER_MANAGER_OPT_GETFRIENDS 1 << 0
|
||||
#define CLUSTER_MANAGER_OPT_COLD 1 << 1
|
||||
@@ -1379,9 +1377,6 @@ static int parseOptions(int argc, char **argv) {
|
||||
} else if (!strcmp(argv[i],"--cluster-use-empty-masters")) {
|
||||
config.cluster_manager_command.flags |=
|
||||
CLUSTER_MANAGER_CMD_FLAG_EMPTYMASTER;
|
||||
} else if (!strcmp(argv[i],"--cluster-search-multiple-owners")) {
|
||||
config.cluster_manager_command.flags |=
|
||||
CLUSTER_MANAGER_CMD_FLAG_CHECK_OWNERS;
|
||||
} else if (!strcmp(argv[i],"-v") || !strcmp(argv[i], "--version")) {
|
||||
sds version = cliVersion();
|
||||
printf("redis-cli %s\n", version);
|
||||
@@ -1424,14 +1419,6 @@ static int parseOptions(int argc, char **argv) {
|
||||
return i;
|
||||
}
|
||||
|
||||
static void parseEnv() {
|
||||
/* Set auth from env, but do not overwrite CLI arguments if passed */
|
||||
char *auth = getenv(REDIS_CLI_AUTH_ENV);
|
||||
if (auth != NULL && config.auth == NULL) {
|
||||
config.auth = auth;
|
||||
}
|
||||
}
|
||||
|
||||
static sds readArgFromStdin(void) {
|
||||
char buf[1024];
|
||||
sds arg = sdsempty();
|
||||
@@ -1459,9 +1446,6 @@ static void usage(void) {
|
||||
" -p <port> Server port (default: 6379).\n"
|
||||
" -s <socket> Server socket (overrides hostname and port).\n"
|
||||
" -a <password> Password to use when connecting to the server.\n"
|
||||
" You can also use the " REDIS_CLI_AUTH_ENV " environment\n"
|
||||
" variable to pass this password more safely\n"
|
||||
" (if both are used, this argument takes predecence).\n"
|
||||
" -u <uri> Server URI.\n"
|
||||
" -r <repeat> Execute specified command N times.\n"
|
||||
" -i <interval> When -r is used, waits <interval> seconds per command.\n"
|
||||
@@ -1850,7 +1834,7 @@ static int evalMode(int argc, char **argv) {
|
||||
if (eval_ldb) {
|
||||
if (!config.eval_ldb) {
|
||||
/* If the debugging session ended immediately, there was an
|
||||
* error compiling the script. Show it and they don't enter
|
||||
* error compiling the script. Show it and don't enter
|
||||
* the REPL at all. */
|
||||
printf("Eval debugging session can't start:\n");
|
||||
cliReadReply(0);
|
||||
@@ -1933,7 +1917,6 @@ static dictType clusterManagerDictType = {
|
||||
};
|
||||
|
||||
typedef int clusterManagerCommandProc(int argc, char **argv);
|
||||
typedef int (*clusterManagerOnReplyError)(redisReply *reply, int bulk_idx);
|
||||
|
||||
/* Cluster Manager helper functions */
|
||||
|
||||
@@ -1995,17 +1978,14 @@ typedef struct clusterManagerCommandDef {
|
||||
clusterManagerCommandDef clusterManagerCommands[] = {
|
||||
{"create", clusterManagerCommandCreate, -2, "host1:port1 ... hostN:portN",
|
||||
"replicas <arg>"},
|
||||
{"check", clusterManagerCommandCheck, -1, "host:port",
|
||||
"search-multiple-owners"},
|
||||
{"check", clusterManagerCommandCheck, -1, "host:port", NULL},
|
||||
{"info", clusterManagerCommandInfo, -1, "host:port", NULL},
|
||||
{"fix", clusterManagerCommandFix, -1, "host:port",
|
||||
"search-multiple-owners"},
|
||||
{"fix", clusterManagerCommandFix, -1, "host:port", NULL},
|
||||
{"reshard", clusterManagerCommandReshard, -1, "host:port",
|
||||
"from <arg>,to <arg>,slots <arg>,yes,timeout <arg>,pipeline <arg>,"
|
||||
"replace"},
|
||||
"from <arg>,to <arg>,slots <arg>,yes,timeout <arg>,pipeline <arg>"},
|
||||
{"rebalance", clusterManagerCommandRebalance, -1, "host:port",
|
||||
"weight <node1=w1...nodeN=wN>,use-empty-masters,"
|
||||
"timeout <arg>,simulate,pipeline <arg>,threshold <arg>,replace"},
|
||||
"timeout <arg>,simulate,pipeline <arg>,threshold <arg>"},
|
||||
{"add-node", clusterManagerCommandAddNode, 2,
|
||||
"new_host:new_port existing_host:existing_port", "slave,master-id <arg>"},
|
||||
{"del-node", clusterManagerCommandDeleteNode, 2, "host:port node_id",NULL},
|
||||
@@ -2196,44 +2176,6 @@ static int clusterManagerCheckRedisReply(clusterManagerNode *n,
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Execute MULTI command on a cluster node. */
|
||||
static int clusterManagerStartTransaction(clusterManagerNode *node) {
|
||||
redisReply *reply = CLUSTER_MANAGER_COMMAND(node, "MULTI");
|
||||
int success = clusterManagerCheckRedisReply(node, reply, NULL);
|
||||
if (reply) freeReplyObject(reply);
|
||||
return success;
|
||||
}
|
||||
|
||||
/* Execute EXEC command on a cluster node. */
|
||||
static int clusterManagerExecTransaction(clusterManagerNode *node,
|
||||
clusterManagerOnReplyError onerror)
|
||||
{
|
||||
redisReply *reply = CLUSTER_MANAGER_COMMAND(node, "EXEC");
|
||||
int success = clusterManagerCheckRedisReply(node, reply, NULL);
|
||||
if (success) {
|
||||
if (reply->type != REDIS_REPLY_ARRAY) {
|
||||
success = 0;
|
||||
goto cleanup;
|
||||
}
|
||||
size_t i;
|
||||
for (i = 0; i < reply->elements; i++) {
|
||||
redisReply *r = reply->element[i];
|
||||
char *err = NULL;
|
||||
success = clusterManagerCheckRedisReply(node, r, &err);
|
||||
if (!success && onerror) success = onerror(r, i);
|
||||
if (err) {
|
||||
if (!success)
|
||||
CLUSTER_MANAGER_PRINT_REPLY_ERROR(node, err);
|
||||
zfree(err);
|
||||
}
|
||||
if (!success) break;
|
||||
}
|
||||
}
|
||||
cleanup:
|
||||
if (reply) freeReplyObject(reply);
|
||||
return success;
|
||||
}
|
||||
|
||||
static int clusterManagerNodeConnect(clusterManagerNode *node) {
|
||||
if (node->context) redisFree(node->context);
|
||||
node->context = redisConnect(node->ip, node->port);
|
||||
@@ -2784,7 +2726,8 @@ static int clusterManagerSetSlot(clusterManagerNode *node1,
|
||||
if (err != NULL) {
|
||||
*err = zmalloc((reply->len + 1) * sizeof(char));
|
||||
strcpy(*err, reply->str);
|
||||
} else CLUSTER_MANAGER_PRINT_REPLY_ERROR(node1, reply->str);
|
||||
CLUSTER_MANAGER_PRINT_REPLY_ERROR(node1, err);
|
||||
}
|
||||
goto cleanup;
|
||||
}
|
||||
cleanup:
|
||||
@@ -2792,84 +2735,6 @@ cleanup:
|
||||
return success;
|
||||
}
|
||||
|
||||
static int clusterManagerClearSlotStatus(clusterManagerNode *node, int slot) {
|
||||
redisReply *reply = CLUSTER_MANAGER_COMMAND(node,
|
||||
"CLUSTER SETSLOT %d %s", slot, "STABLE");
|
||||
int success = clusterManagerCheckRedisReply(node, reply, NULL);
|
||||
if (reply) freeReplyObject(reply);
|
||||
return success;
|
||||
}
|
||||
|
||||
static int clusterManagerDelSlot(clusterManagerNode *node, int slot,
|
||||
int ignore_unassigned_err)
|
||||
{
|
||||
redisReply *reply = CLUSTER_MANAGER_COMMAND(node,
|
||||
"CLUSTER DELSLOTS %d", slot);
|
||||
char *err = NULL;
|
||||
int success = clusterManagerCheckRedisReply(node, reply, &err);
|
||||
if (!success && reply && reply->type == REDIS_REPLY_ERROR &&
|
||||
ignore_unassigned_err &&
|
||||
strstr(reply->str, "already unassigned") != NULL) success = 1;
|
||||
if (!success && err != NULL) {
|
||||
CLUSTER_MANAGER_PRINT_REPLY_ERROR(node, err);
|
||||
zfree(err);
|
||||
}
|
||||
if (reply) freeReplyObject(reply);
|
||||
return success;
|
||||
}
|
||||
|
||||
static int clusterManagerAddSlot(clusterManagerNode *node, int slot) {
|
||||
redisReply *reply = CLUSTER_MANAGER_COMMAND(node,
|
||||
"CLUSTER ADDSLOTS %d", slot);
|
||||
int success = clusterManagerCheckRedisReply(node, reply, NULL);
|
||||
if (reply) freeReplyObject(reply);
|
||||
return success;
|
||||
}
|
||||
|
||||
static signed int clusterManagerCountKeysInSlot(clusterManagerNode *node,
|
||||
int slot)
|
||||
{
|
||||
redisReply *reply = CLUSTER_MANAGER_COMMAND(node,
|
||||
"CLUSTER COUNTKEYSINSLOT %d", slot);
|
||||
int count = -1;
|
||||
int success = clusterManagerCheckRedisReply(node, reply, NULL);
|
||||
if (success && reply->type == REDIS_REPLY_INTEGER) count = reply->integer;
|
||||
if (reply) freeReplyObject(reply);
|
||||
return count;
|
||||
}
|
||||
|
||||
static int clusterManagerBumpEpoch(clusterManagerNode *node) {
|
||||
redisReply *reply = CLUSTER_MANAGER_COMMAND(node, "CLUSTER BUMPEPOCH");
|
||||
int success = clusterManagerCheckRedisReply(node, reply, NULL);
|
||||
if (reply) freeReplyObject(reply);
|
||||
return success;
|
||||
}
|
||||
|
||||
static int clusterManagerIgnoreUnassignedErr(redisReply *reply, int bulk_idx) {
|
||||
if (bulk_idx == 0 && reply) {
|
||||
if (reply->type == REDIS_REPLY_ERROR)
|
||||
return strstr(reply->str, "already unassigned") != NULL;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int clusterManagerSetSlotOwner(clusterManagerNode *owner,
|
||||
int slot,
|
||||
int do_clear)
|
||||
{
|
||||
int success = clusterManagerStartTransaction(owner);
|
||||
if (!success) return 0;
|
||||
/* Ensure the slot is not already assigned. */
|
||||
clusterManagerDelSlot(owner, slot, 1);
|
||||
/* Add the slot and bump epoch. */
|
||||
clusterManagerAddSlot(owner, slot);
|
||||
if (do_clear) clusterManagerClearSlotStatus(owner, slot);
|
||||
clusterManagerBumpEpoch(owner);
|
||||
success = clusterManagerExecTransaction(owner,
|
||||
clusterManagerIgnoreUnassignedErr);
|
||||
return success;
|
||||
}
|
||||
|
||||
/* Migrate keys taken from reply->elements. It returns the reply from the
|
||||
* MIGRATE command, or NULL if something goes wrong. If the argument 'dots'
|
||||
* is not NULL, a dot will be printed for every migrated key. */
|
||||
@@ -2950,8 +2815,8 @@ static int clusterManagerMigrateKeysInSlot(clusterManagerNode *source,
|
||||
char **err)
|
||||
{
|
||||
int success = 1;
|
||||
int replace_existing_keys = (config.cluster_manager_command.flags &
|
||||
(CLUSTER_MANAGER_CMD_FLAG_FIX | CLUSTER_MANAGER_CMD_FLAG_REPLACE));
|
||||
int do_fix = (config.cluster_manager_command.flags &
|
||||
CLUSTER_MANAGER_CMD_FLAG_FIX);
|
||||
while (1) {
|
||||
char *dots = NULL;
|
||||
redisReply *reply = NULL, *migrate_reply = NULL;
|
||||
@@ -2965,7 +2830,7 @@ static int clusterManagerMigrateKeysInSlot(clusterManagerNode *source,
|
||||
if (err != NULL) {
|
||||
*err = zmalloc((reply->len + 1) * sizeof(char));
|
||||
strcpy(*err, reply->str);
|
||||
CLUSTER_MANAGER_PRINT_REPLY_ERROR(source, *err);
|
||||
CLUSTER_MANAGER_PRINT_REPLY_ERROR(source, err);
|
||||
}
|
||||
goto next;
|
||||
}
|
||||
@@ -2982,23 +2847,15 @@ static int clusterManagerMigrateKeysInSlot(clusterManagerNode *source,
|
||||
dots);
|
||||
if (migrate_reply == NULL) goto next;
|
||||
if (migrate_reply->type == REDIS_REPLY_ERROR) {
|
||||
int is_busy = strstr(migrate_reply->str, "BUSYKEY") != NULL;
|
||||
int not_served = strstr(migrate_reply->str, "slot not served") != NULL;
|
||||
if (replace_existing_keys && (is_busy || not_served)) {
|
||||
/* If the key already exists, try to migrate keys
|
||||
* adding REPLACE option.
|
||||
* If the key's slot is not served, try to assign slot
|
||||
* to the target node. */
|
||||
if (not_served)
|
||||
clusterManagerSetSlot(source, target, slot, "node", NULL);
|
||||
if (do_fix && strstr(migrate_reply->str, "BUSYKEY")) {
|
||||
clusterManagerLogWarn("*** Target key exists. "
|
||||
"Replacing it for FIX.\n");
|
||||
freeReplyObject(migrate_reply);
|
||||
/* Try to migrate keys adding REPLACE option. */
|
||||
migrate_reply = clusterManagerMigrateKeysInReply(source,
|
||||
target,
|
||||
reply,
|
||||
is_busy,
|
||||
timeout,
|
||||
1, timeout,
|
||||
NULL);
|
||||
success = (migrate_reply != NULL &&
|
||||
migrate_reply->type != REDIS_REPLY_ERROR);
|
||||
@@ -3084,7 +2941,7 @@ static int clusterManagerMoveSlot(clusterManagerNode *source,
|
||||
if (err != NULL) {
|
||||
*err = zmalloc((r->len + 1) * sizeof(char));
|
||||
strcpy(*err, r->str);
|
||||
CLUSTER_MANAGER_PRINT_REPLY_ERROR(n, *err);
|
||||
CLUSTER_MANAGER_PRINT_REPLY_ERROR(n, err);
|
||||
}
|
||||
}
|
||||
freeReplyObject(r);
|
||||
@@ -3444,8 +3301,8 @@ static sds clusterManagerGetConfigSignature(clusterManagerNode *node) {
|
||||
nodename = token;
|
||||
tot_size = (p - token);
|
||||
name_len = tot_size++; // Make room for ':' in tot_size
|
||||
}
|
||||
if (++i == 8) break;
|
||||
} else if (i == 8) break;
|
||||
i++;
|
||||
}
|
||||
if (i != 8) continue;
|
||||
if (nodename == NULL) continue;
|
||||
@@ -3484,7 +3341,7 @@ static sds clusterManagerGetConfigSignature(clusterManagerNode *node) {
|
||||
char *sp = cfg + name_len;
|
||||
*(sp++) = ':';
|
||||
for (i = 0; i < c; i++) {
|
||||
if (i > 0) *(sp++) = ',';
|
||||
if (i > 0) *(sp++) = '|';
|
||||
int slen = strlen(slots[i]);
|
||||
memcpy(sp, slots[i], slen);
|
||||
sp += slen;
|
||||
@@ -3642,34 +3499,6 @@ static clusterManagerNode *clusterManagerNodeWithLeastReplicas() {
|
||||
return node;
|
||||
}
|
||||
|
||||
/* This fucntion returns a random master node, return NULL if none */
|
||||
|
||||
static clusterManagerNode *clusterManagerNodeMasterRandom() {
|
||||
int master_count = 0;
|
||||
int idx;
|
||||
listIter li;
|
||||
listNode *ln;
|
||||
listRewind(cluster_manager.nodes, &li);
|
||||
while ((ln = listNext(&li)) != NULL) {
|
||||
clusterManagerNode *n = ln->value;
|
||||
if (n->flags & CLUSTER_MANAGER_FLAG_SLAVE) continue;
|
||||
master_count++;
|
||||
}
|
||||
|
||||
srand(time(NULL));
|
||||
idx = rand() % master_count;
|
||||
listRewind(cluster_manager.nodes, &li);
|
||||
while ((ln = listNext(&li)) != NULL) {
|
||||
clusterManagerNode *n = ln->value;
|
||||
if (n->flags & CLUSTER_MANAGER_FLAG_SLAVE) continue;
|
||||
if (!idx--) {
|
||||
return n;
|
||||
}
|
||||
}
|
||||
/* Can not be reached */
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int clusterManagerFixSlotsCoverage(char *all_slots) {
|
||||
int i, fixed = 0;
|
||||
list *none = NULL, *single = NULL, *multi = NULL;
|
||||
@@ -3742,22 +3571,26 @@ static int clusterManagerFixSlotsCoverage(char *all_slots) {
|
||||
"across the cluster:\n");
|
||||
clusterManagerPrintSlotsList(none);
|
||||
if (confirmWithYes("Fix these slots by covering with a random node?")){
|
||||
srand(time(NULL));
|
||||
listIter li;
|
||||
listNode *ln;
|
||||
listRewind(none, &li);
|
||||
while ((ln = listNext(&li)) != NULL) {
|
||||
sds slot = ln->value;
|
||||
int s = atoi(slot);
|
||||
clusterManagerNode *n = clusterManagerNodeMasterRandom();
|
||||
long idx = (long) (rand() % listLength(cluster_manager.nodes));
|
||||
listNode *node_n = listIndex(cluster_manager.nodes, idx);
|
||||
assert(node_n != NULL);
|
||||
clusterManagerNode *n = node_n->value;
|
||||
clusterManagerLogInfo(">>> Covering slot %s with %s:%d\n",
|
||||
slot, n->ip, n->port);
|
||||
if (!clusterManagerSetSlotOwner(n, s, 0)) {
|
||||
fixed = -1;
|
||||
goto cleanup;
|
||||
}
|
||||
redisReply *r = CLUSTER_MANAGER_COMMAND(n,
|
||||
"CLUSTER ADDSLOTS %s", slot);
|
||||
if (!clusterManagerCheckRedisReply(n, r, NULL)) fixed = -1;
|
||||
if (r) freeReplyObject(r);
|
||||
if (fixed < 0) goto cleanup;
|
||||
/* Since CLUSTER ADDSLOTS succeeded, we also update the slot
|
||||
* info into the node struct, in order to keep it synced */
|
||||
n->slots[s] = 1;
|
||||
n->slots[atoi(slot)] = 1;
|
||||
fixed++;
|
||||
}
|
||||
}
|
||||
@@ -3765,7 +3598,7 @@ static int clusterManagerFixSlotsCoverage(char *all_slots) {
|
||||
|
||||
/* Handle case "2": keys only in one node. */
|
||||
if (listLength(single) > 0) {
|
||||
printf("The following uncovered slots have keys in just one node:\n");
|
||||
printf("The following uncovered slots have keys in just one node:\n");
|
||||
clusterManagerPrintSlotsList(single);
|
||||
if (confirmWithYes("Fix these slots by covering with those nodes?")){
|
||||
listIter li;
|
||||
@@ -3773,7 +3606,6 @@ static int clusterManagerFixSlotsCoverage(char *all_slots) {
|
||||
listRewind(single, &li);
|
||||
while ((ln = listNext(&li)) != NULL) {
|
||||
sds slot = ln->value;
|
||||
int s = atoi(slot);
|
||||
dictEntry *entry = dictFind(clusterManagerUncoveredSlots, slot);
|
||||
assert(entry != NULL);
|
||||
list *nodes = (list *) dictGetVal(entry);
|
||||
@@ -3782,10 +3614,11 @@ static int clusterManagerFixSlotsCoverage(char *all_slots) {
|
||||
clusterManagerNode *n = fn->value;
|
||||
clusterManagerLogInfo(">>> Covering slot %s with %s:%d\n",
|
||||
slot, n->ip, n->port);
|
||||
if (!clusterManagerSetSlotOwner(n, s, 0)) {
|
||||
fixed = -1;
|
||||
goto cleanup;
|
||||
}
|
||||
redisReply *r = CLUSTER_MANAGER_COMMAND(n,
|
||||
"CLUSTER ADDSLOTS %s", slot);
|
||||
if (!clusterManagerCheckRedisReply(n, r, NULL)) fixed = -1;
|
||||
if (r) freeReplyObject(r);
|
||||
if (fixed < 0) goto cleanup;
|
||||
/* Since CLUSTER ADDSLOTS succeeded, we also update the slot
|
||||
* info into the node struct, in order to keep it synced */
|
||||
n->slots[atoi(slot)] = 1;
|
||||
@@ -3818,10 +3651,16 @@ static int clusterManagerFixSlotsCoverage(char *all_slots) {
|
||||
clusterManagerLogInfo(">>> Covering slot %s moving keys "
|
||||
"to %s:%d\n", slot,
|
||||
target->ip, target->port);
|
||||
if (!clusterManagerSetSlotOwner(target, s, 1)) {
|
||||
fixed = -1;
|
||||
goto cleanup;
|
||||
}
|
||||
redisReply *r = CLUSTER_MANAGER_COMMAND(target,
|
||||
"CLUSTER ADDSLOTS %s", slot);
|
||||
if (!clusterManagerCheckRedisReply(target, r, NULL)) fixed = -1;
|
||||
if (r) freeReplyObject(r);
|
||||
if (fixed < 0) goto cleanup;
|
||||
r = CLUSTER_MANAGER_COMMAND(target,
|
||||
"CLUSTER SETSLOT %s %s", slot, "STABLE");
|
||||
if (!clusterManagerCheckRedisReply(target, r, NULL)) fixed = -1;
|
||||
if (r) freeReplyObject(r);
|
||||
if (fixed < 0) goto cleanup;
|
||||
/* Since CLUSTER ADDSLOTS succeeded, we also update the slot
|
||||
* info into the node struct, in order to keep it synced */
|
||||
target->slots[atoi(slot)] = 1;
|
||||
@@ -3831,16 +3670,16 @@ static int clusterManagerFixSlotsCoverage(char *all_slots) {
|
||||
while ((nln = listNext(&nli)) != NULL) {
|
||||
clusterManagerNode *src = nln->value;
|
||||
if (src == target) continue;
|
||||
/* Assign the slot to target node in the source node. */
|
||||
if (!clusterManagerSetSlot(src, target, s, "NODE", NULL))
|
||||
fixed = -1;
|
||||
if (fixed < 0) goto cleanup;
|
||||
/* Set the source node in 'importing' state
|
||||
* (even if we will actually migrate keys away)
|
||||
* in order to avoid receiving redirections
|
||||
* for MIGRATE. */
|
||||
if (!clusterManagerSetSlot(src, target, s,
|
||||
"IMPORTING", NULL)) fixed = -1;
|
||||
redisReply *r = CLUSTER_MANAGER_COMMAND(src,
|
||||
"CLUSTER SETSLOT %s %s %s", slot,
|
||||
"IMPORTING", target->name);
|
||||
if (!clusterManagerCheckRedisReply(target, r, NULL))
|
||||
fixed = -1;
|
||||
if (r) freeReplyObject(r);
|
||||
if (fixed < 0) goto cleanup;
|
||||
int opts = CLUSTER_MANAGER_OPT_VERBOSE |
|
||||
CLUSTER_MANAGER_OPT_COLD;
|
||||
@@ -3848,9 +3687,6 @@ static int clusterManagerFixSlotsCoverage(char *all_slots) {
|
||||
fixed = -1;
|
||||
goto cleanup;
|
||||
}
|
||||
if (!clusterManagerClearSlotStatus(src, s))
|
||||
fixed = -1;
|
||||
if (fixed < 0) goto cleanup;
|
||||
}
|
||||
fixed++;
|
||||
}
|
||||
@@ -3884,22 +3720,11 @@ static int clusterManagerFixOpenSlot(int slot) {
|
||||
while ((ln = listNext(&li)) != NULL) {
|
||||
clusterManagerNode *n = ln->value;
|
||||
if (n->flags & CLUSTER_MANAGER_FLAG_SLAVE) continue;
|
||||
if (n->slots[slot]) listAddNodeTail(owners, n);
|
||||
else {
|
||||
redisReply *r = CLUSTER_MANAGER_COMMAND(n,
|
||||
"CLUSTER COUNTKEYSINSLOT %d", slot);
|
||||
success = clusterManagerCheckRedisReply(n, r, NULL);
|
||||
if (success && r->integer > 0) {
|
||||
clusterManagerLogWarn("*** Found keys about slot %d "
|
||||
"in non-owner node %s:%d!\n", slot,
|
||||
n->ip, n->port);
|
||||
listAddNodeTail(owners, n);
|
||||
}
|
||||
if (r) freeReplyObject(r);
|
||||
if (!success) goto cleanup;
|
||||
if (n->slots[slot]) {
|
||||
if (owner == NULL) owner = n;
|
||||
listAddNodeTail(owners, n);
|
||||
}
|
||||
}
|
||||
if (listLength(owners) == 1) owner = listFirst(owners)->value;
|
||||
listRewind(cluster_manager.nodes, &li);
|
||||
while ((ln = listNext(&li)) != NULL) {
|
||||
clusterManagerNode *n = ln->value;
|
||||
@@ -3910,7 +3735,7 @@ static int clusterManagerFixOpenSlot(int slot) {
|
||||
sds migrating_slot = n->migrating[i];
|
||||
if (atoi(migrating_slot) == slot) {
|
||||
char *sep = (listLength(migrating) == 0 ? "" : ",");
|
||||
migrating_str = sdscatfmt(migrating_str, "%s%s:%u",
|
||||
migrating_str = sdscatfmt(migrating_str, "%s%S:%u",
|
||||
sep, n->ip, n->port);
|
||||
listAddNodeTail(migrating, n);
|
||||
is_migrating = 1;
|
||||
@@ -3923,7 +3748,7 @@ static int clusterManagerFixOpenSlot(int slot) {
|
||||
sds importing_slot = n->importing[i];
|
||||
if (atoi(importing_slot) == slot) {
|
||||
char *sep = (listLength(importing) == 0 ? "" : ",");
|
||||
importing_str = sdscatfmt(importing_str, "%s%s:%u",
|
||||
importing_str = sdscatfmt(importing_str, "%s%S:%u",
|
||||
sep, n->ip, n->port);
|
||||
listAddNodeTail(importing, n);
|
||||
is_importing = 1;
|
||||
@@ -3942,20 +3767,15 @@ static int clusterManagerFixOpenSlot(int slot) {
|
||||
clusterManagerLogWarn("*** Found keys about slot %d "
|
||||
"in node %s:%d!\n", slot, n->ip,
|
||||
n->port);
|
||||
char *sep = (listLength(importing) == 0 ? "" : ",");
|
||||
importing_str = sdscatfmt(importing_str, "%s%S:%u",
|
||||
sep, n->ip, n->port);
|
||||
listAddNodeTail(importing, n);
|
||||
}
|
||||
if (r) freeReplyObject(r);
|
||||
if (!success) goto cleanup;
|
||||
}
|
||||
}
|
||||
if (sdslen(migrating_str) > 0)
|
||||
printf("Set as migrating in: %s\n", migrating_str);
|
||||
if (sdslen(importing_str) > 0)
|
||||
printf("Set as importing in: %s\n", importing_str);
|
||||
/* If there is no slot owner, set as owner the node with the biggest
|
||||
printf("Set as migrating in: %s\n", migrating_str);
|
||||
printf("Set as importing in: %s\n", importing_str);
|
||||
/* If there is no slot owner, set as owner the slot with the biggest
|
||||
* number of keys, among the set of migrating / importing nodes. */
|
||||
if (owner == NULL) {
|
||||
clusterManagerLogInfo(">>> Nobody claims ownership, "
|
||||
@@ -3973,9 +3793,15 @@ static int clusterManagerFixOpenSlot(int slot) {
|
||||
// Use ADDSLOTS to assign the slot.
|
||||
clusterManagerLogWarn("*** Configuring %s:%d as the slot owner\n",
|
||||
owner->ip, owner->port);
|
||||
success = clusterManagerClearSlotStatus(owner, slot);
|
||||
redisReply *reply = CLUSTER_MANAGER_COMMAND(owner, "CLUSTER "
|
||||
"SETSLOT %d %s",
|
||||
slot, "STABLE");
|
||||
success = clusterManagerCheckRedisReply(owner, reply, NULL);
|
||||
if (reply) freeReplyObject(reply);
|
||||
if (!success) goto cleanup;
|
||||
success = clusterManagerSetSlotOwner(owner, slot, 0);
|
||||
reply = CLUSTER_MANAGER_COMMAND(owner, "CLUSTER ADDSLOTS %d", slot);
|
||||
success = clusterManagerCheckRedisReply(owner, reply, NULL);
|
||||
if (reply) freeReplyObject(reply);
|
||||
if (!success) goto cleanup;
|
||||
/* Since CLUSTER ADDSLOTS succeeded, we also update the slot
|
||||
* info into the node struct, in order to keep it synced */
|
||||
@@ -3983,7 +3809,9 @@ static int clusterManagerFixOpenSlot(int slot) {
|
||||
/* Make sure this information will propagate. Not strictly needed
|
||||
* since there is no past owner, so all the other nodes will accept
|
||||
* whatever epoch this node will claim the slot with. */
|
||||
success = clusterManagerBumpEpoch(owner);
|
||||
reply = CLUSTER_MANAGER_COMMAND(owner, "CLUSTER BUMPEPOCH");
|
||||
success = clusterManagerCheckRedisReply(owner, reply, NULL);
|
||||
if (reply) freeReplyObject(reply);
|
||||
if (!success) goto cleanup;
|
||||
/* Remove the owner from the list of migrating/importing
|
||||
* nodes. */
|
||||
@@ -3999,38 +3827,32 @@ static int clusterManagerFixOpenSlot(int slot) {
|
||||
* in migrating state, since migrating is a valid state only for
|
||||
* slot owners. */
|
||||
if (listLength(owners) > 1) {
|
||||
/* Owner cannot be NULL at this point, since if there are more owners,
|
||||
* the owner has been set in the previous condition (owner == NULL). */
|
||||
assert(owner != NULL);
|
||||
owner = clusterManagerGetNodeWithMostKeysInSlot(owners, slot, NULL);
|
||||
listRewind(owners, &li);
|
||||
redisReply *reply = NULL;
|
||||
while ((ln = listNext(&li)) != NULL) {
|
||||
clusterManagerNode *n = ln->value;
|
||||
if (n == owner) continue;
|
||||
success = clusterManagerDelSlot(n, slot, 1);
|
||||
if (!success) goto cleanup;
|
||||
n->slots[slot] = 0;
|
||||
/* Assign the slot to the owner in the node 'n' configuration.' */
|
||||
success = clusterManagerSetSlot(n, owner, slot, "node", NULL);
|
||||
reply = CLUSTER_MANAGER_COMMAND(n, "CLUSTER DELSLOT %d", slot);
|
||||
success = clusterManagerCheckRedisReply(n, reply, NULL);
|
||||
if (reply) freeReplyObject(reply);
|
||||
if (!success) goto cleanup;
|
||||
success = clusterManagerSetSlot(n, owner, slot, "importing", NULL);
|
||||
if (!success) goto cleanup;
|
||||
/* Avoid duplicates. */
|
||||
clusterManagerRemoveNodeFromList(importing, n);
|
||||
clusterManagerRemoveNodeFromList(importing, n); //Avoid duplicates
|
||||
listAddNodeTail(importing, n);
|
||||
/* Ensure that the node is not in the migrating list. */
|
||||
clusterManagerRemoveNodeFromList(migrating, n);
|
||||
}
|
||||
reply = CLUSTER_MANAGER_COMMAND(owner, "CLUSTER BUMPEPOCH");
|
||||
success = clusterManagerCheckRedisReply(owner, reply, NULL);
|
||||
if (reply) freeReplyObject(reply);
|
||||
if (!success) goto cleanup;
|
||||
}
|
||||
int move_opts = CLUSTER_MANAGER_OPT_VERBOSE;
|
||||
/* Case 1: The slot is in migrating state in one node, and in
|
||||
* importing state in 1 node. That's trivial to address. */
|
||||
/* Case 1: The slot is in migrating state in one slot, and in
|
||||
* importing state in 1 slot. That's trivial to address. */
|
||||
if (listLength(migrating) == 1 && listLength(importing) == 1) {
|
||||
clusterManagerNode *src = listFirst(migrating)->value;
|
||||
clusterManagerNode *dst = listFirst(importing)->value;
|
||||
clusterManagerLogInfo(">>> Case 1: Moving slot %d from "
|
||||
"%s:%d to %s:%d\n", slot,
|
||||
src->ip, src->port, dst->ip, dst->port);
|
||||
move_opts |= CLUSTER_MANAGER_OPT_UPDATE;
|
||||
success = clusterManagerMoveSlot(src, dst, slot, move_opts, NULL);
|
||||
}
|
||||
/* Case 2: There are multiple nodes that claim the slot as importing,
|
||||
@@ -4038,7 +3860,7 @@ static int clusterManagerFixOpenSlot(int slot) {
|
||||
* the slot. In this case we just move all the keys to the owner
|
||||
* according to the configuration. */
|
||||
else if (listLength(migrating) == 0 && listLength(importing) > 0) {
|
||||
clusterManagerLogInfo(">>> Case 2: Moving all the %d slot keys to its "
|
||||
clusterManagerLogInfo(">>> Moving all the %d slot keys to its "
|
||||
"owner %s:%d\n", slot, owner->ip, owner->port);
|
||||
move_opts |= CLUSTER_MANAGER_OPT_COLD;
|
||||
listRewind(importing, &li);
|
||||
@@ -4049,117 +3871,38 @@ static int clusterManagerFixOpenSlot(int slot) {
|
||||
if (!success) goto cleanup;
|
||||
clusterManagerLogInfo(">>> Setting %d as STABLE in "
|
||||
"%s:%d\n", slot, n->ip, n->port);
|
||||
success = clusterManagerClearSlotStatus(n, slot);
|
||||
|
||||
redisReply *r = CLUSTER_MANAGER_COMMAND(n, "CLUSTER SETSLOT %d %s",
|
||||
slot, "STABLE");
|
||||
success = clusterManagerCheckRedisReply(n, r, NULL);
|
||||
if (r) freeReplyObject(r);
|
||||
if (!success) goto cleanup;
|
||||
}
|
||||
/* Since the slot has been moved in "cold" mode, ensure that all the
|
||||
* other nodes update their own configuration about the slot itself. */
|
||||
listRewind(cluster_manager.nodes, &li);
|
||||
while ((ln = listNext(&li)) != NULL) {
|
||||
clusterManagerNode *n = ln->value;
|
||||
if (n == owner) continue;
|
||||
if (n->flags & CLUSTER_MANAGER_FLAG_SLAVE) continue;
|
||||
success = clusterManagerSetSlot(n, owner, slot, "NODE", NULL);
|
||||
if (!success) goto cleanup;
|
||||
}
|
||||
}
|
||||
/* Case 3: The slot is in migrating state in one node but multiple
|
||||
* other nodes claim to be in importing state and don't have any key in
|
||||
* the slot. We search for the importing node having the same ID as
|
||||
* the destination node of the migrating node.
|
||||
* In that case we move the slot from the migrating node to this node and
|
||||
* we close the importing states on all the other importing nodes.
|
||||
* If no importing node has the same ID as the destination node of the
|
||||
* migrating node, the slot's state is closed on both the migrating node
|
||||
* and the importing nodes. */
|
||||
else if (listLength(migrating) == 1 && listLength(importing) > 1) {
|
||||
int try_to_fix = 1;
|
||||
clusterManagerNode *src = listFirst(migrating)->value;
|
||||
clusterManagerNode *dst = NULL;
|
||||
sds target_id = NULL;
|
||||
for (int i = 0; i < src->migrating_count; i += 2) {
|
||||
sds migrating_slot = src->migrating[i];
|
||||
if (atoi(migrating_slot) == slot) {
|
||||
target_id = src->migrating[i + 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert(target_id != NULL);
|
||||
listIter li;
|
||||
listNode *ln;
|
||||
listRewind(importing, &li);
|
||||
while ((ln = listNext(&li)) != NULL) {
|
||||
clusterManagerNode *n = ln->value;
|
||||
int count = clusterManagerCountKeysInSlot(n, slot);
|
||||
if (count > 0) {
|
||||
try_to_fix = 0;
|
||||
break;
|
||||
}
|
||||
if (strcmp(n->name, target_id) == 0) dst = n;
|
||||
}
|
||||
if (!try_to_fix) goto unhandled_case;
|
||||
if (dst != NULL) {
|
||||
clusterManagerLogInfo(">>> Case 3: Moving slot %d from %s:%d to "
|
||||
"%s:%d and closing it on all the other "
|
||||
"importing nodes.\n",
|
||||
slot, src->ip, src->port,
|
||||
dst->ip, dst->port);
|
||||
/* Move the slot to the destination node. */
|
||||
success = clusterManagerMoveSlot(src, dst, slot, move_opts, NULL);
|
||||
if (!success) goto cleanup;
|
||||
/* Close slot on all the other importing nodes. */
|
||||
listRewind(importing, &li);
|
||||
while ((ln = listNext(&li)) != NULL) {
|
||||
clusterManagerNode *n = ln->value;
|
||||
if (dst == n) continue;
|
||||
success = clusterManagerClearSlotStatus(n, slot);
|
||||
if (!success) goto cleanup;
|
||||
}
|
||||
} else {
|
||||
clusterManagerLogInfo(">>> Case 3: Closing slot %d on both "
|
||||
"migrating and importing nodes.\n", slot);
|
||||
/* Close the slot on both the migrating node and the importing
|
||||
* nodes. */
|
||||
success = clusterManagerClearSlotStatus(src, slot);
|
||||
if (!success) goto cleanup;
|
||||
listRewind(importing, &li);
|
||||
while ((ln = listNext(&li)) != NULL) {
|
||||
clusterManagerNode *n = ln->value;
|
||||
success = clusterManagerClearSlotStatus(n, slot);
|
||||
if (!success) goto cleanup;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
int try_to_close_slot = (listLength(importing) == 0 &&
|
||||
listLength(migrating) == 1);
|
||||
if (try_to_close_slot) {
|
||||
clusterManagerNode *n = listFirst(migrating)->value;
|
||||
if (!owner || owner != n) {
|
||||
redisReply *r = CLUSTER_MANAGER_COMMAND(n,
|
||||
"CLUSTER GETKEYSINSLOT %d %d", slot, 10);
|
||||
success = clusterManagerCheckRedisReply(n, r, NULL);
|
||||
if (r) {
|
||||
if (success) try_to_close_slot = (r->elements == 0);
|
||||
freeReplyObject(r);
|
||||
}
|
||||
if (!success) goto cleanup;
|
||||
redisReply *r = CLUSTER_MANAGER_COMMAND(n,
|
||||
"CLUSTER GETKEYSINSLOT %d %d", slot, 10);
|
||||
success = clusterManagerCheckRedisReply(n, r, NULL);
|
||||
if (r) {
|
||||
if (success) try_to_close_slot = (r->elements == 0);
|
||||
freeReplyObject(r);
|
||||
}
|
||||
if (!success) goto cleanup;
|
||||
}
|
||||
/* Case 4: There are no slots claiming to be in importing state, but
|
||||
* there is a migrating node that actually don't have any key or is the
|
||||
* slot owner. We can just close the slot, probably a reshard
|
||||
* interrupted in the middle. */
|
||||
/* Case 3: There are no slots claiming to be in importing state, but
|
||||
* there is a migrating node that actually don't have any key. We
|
||||
* can just close the slot, probably a reshard interrupted in the middle. */
|
||||
if (try_to_close_slot) {
|
||||
clusterManagerNode *n = listFirst(migrating)->value;
|
||||
clusterManagerLogInfo(">>> Case 4: Closing slot %d on %s:%d\n",
|
||||
slot, n->ip, n->port);
|
||||
redisReply *r = CLUSTER_MANAGER_COMMAND(n, "CLUSTER SETSLOT %d %s",
|
||||
slot, "STABLE");
|
||||
success = clusterManagerCheckRedisReply(n, r, NULL);
|
||||
if (r) freeReplyObject(r);
|
||||
if (!success) goto cleanup;
|
||||
} else {
|
||||
unhandled_case:
|
||||
success = 0;
|
||||
clusterManagerLogErr("[ERR] Sorry, redis-cli can't fix this slot "
|
||||
"yet (work in progress). Slot is set as "
|
||||
@@ -4177,55 +3920,17 @@ cleanup:
|
||||
return success;
|
||||
}
|
||||
|
||||
static int clusterManagerFixMultipleSlotOwners(int slot, list *owners) {
|
||||
clusterManagerLogInfo(">>> Fixing multiple owners for slot %d...\n", slot);
|
||||
int success = 0;
|
||||
assert(listLength(owners) > 1);
|
||||
clusterManagerNode *owner = clusterManagerGetNodeWithMostKeysInSlot(owners,
|
||||
slot,
|
||||
NULL);
|
||||
if (!owner) owner = listFirst(owners)->value;
|
||||
clusterManagerLogInfo(">>> Setting slot %d owner: %s:%d\n",
|
||||
slot, owner->ip, owner->port);
|
||||
/* Set the slot owner. */
|
||||
if (!clusterManagerSetSlotOwner(owner, slot, 0)) return 0;
|
||||
listIter li;
|
||||
listNode *ln;
|
||||
listRewind(cluster_manager.nodes, &li);
|
||||
/* Update configuration in all the other master nodes by assigning the slot
|
||||
* itself to the new owner, and by eventually migrating keys if the node
|
||||
* has keys for the slot. */
|
||||
while ((ln = listNext(&li)) != NULL) {
|
||||
clusterManagerNode *n = ln->value;
|
||||
if (n == owner) continue;
|
||||
if (n->flags & CLUSTER_MANAGER_FLAG_SLAVE) continue;
|
||||
int count = clusterManagerCountKeysInSlot(n, slot);
|
||||
success = (count >= 0);
|
||||
if (!success) break;
|
||||
clusterManagerDelSlot(n, slot, 1);
|
||||
if (!clusterManagerSetSlot(n, owner, slot, "node", NULL)) return 0;
|
||||
if (count > 0) {
|
||||
int opts = CLUSTER_MANAGER_OPT_VERBOSE |
|
||||
CLUSTER_MANAGER_OPT_COLD;
|
||||
success = clusterManagerMoveSlot(n, owner, slot, opts, NULL);
|
||||
if (!success) break;
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
static int clusterManagerCheckCluster(int quiet) {
|
||||
listNode *ln = listFirst(cluster_manager.nodes);
|
||||
if (!ln) return 0;
|
||||
int result = 1;
|
||||
int do_fix = config.cluster_manager_command.flags &
|
||||
CLUSTER_MANAGER_CMD_FLAG_FIX;
|
||||
clusterManagerNode *node = ln->value;
|
||||
clusterManagerLogInfo(">>> Performing Cluster Check (using node %s:%d)\n",
|
||||
node->ip, node->port);
|
||||
int result = 1, consistent = 0;
|
||||
int do_fix = config.cluster_manager_command.flags &
|
||||
CLUSTER_MANAGER_CMD_FLAG_FIX;
|
||||
if (!quiet) clusterManagerShowNodes();
|
||||
consistent = clusterManagerIsConfigConsistent();
|
||||
if (!consistent) {
|
||||
if (!clusterManagerIsConfigConsistent()) {
|
||||
sds err = sdsnew("[ERR] Nodes don't agree about configuration!");
|
||||
clusterManagerOnError(err);
|
||||
result = 0;
|
||||
@@ -4233,7 +3938,7 @@ static int clusterManagerCheckCluster(int quiet) {
|
||||
clusterManagerLogOk("[OK] All nodes agree about slots "
|
||||
"configuration.\n");
|
||||
}
|
||||
/* Check open slots */
|
||||
// Check open slots
|
||||
clusterManagerLogInfo(">>> Check for open slots...\n");
|
||||
listIter li;
|
||||
listRewind(cluster_manager.nodes, &li);
|
||||
@@ -4292,7 +3997,7 @@ static int clusterManagerCheckCluster(int quiet) {
|
||||
clusterManagerLogErr("%s.\n", (char *) errstr);
|
||||
sdsfree(errstr);
|
||||
if (do_fix) {
|
||||
/* Fix open slots. */
|
||||
// Fix open slots.
|
||||
dictReleaseIterator(iter);
|
||||
iter = dictGetIterator(open_slots);
|
||||
while ((entry = dictNext(iter)) != NULL) {
|
||||
@@ -4320,58 +4025,12 @@ static int clusterManagerCheckCluster(int quiet) {
|
||||
result = 0;
|
||||
if (do_fix/* && result*/) {
|
||||
dictType dtype = clusterManagerDictType;
|
||||
dtype.keyDestructor = dictSdsDestructor;
|
||||
dtype.valDestructor = dictListDestructor;
|
||||
clusterManagerUncoveredSlots = dictCreate(&dtype, NULL);
|
||||
int fixed = clusterManagerFixSlotsCoverage(slots);
|
||||
if (fixed > 0) result = 1;
|
||||
}
|
||||
}
|
||||
int search_multiple_owners = config.cluster_manager_command.flags &
|
||||
CLUSTER_MANAGER_CMD_FLAG_CHECK_OWNERS;
|
||||
if (search_multiple_owners) {
|
||||
/* Check whether there are multiple owners, even when slots are
|
||||
* fully covered and there are no open slots. */
|
||||
clusterManagerLogInfo(">>> Check for multiple slot owners...\n");
|
||||
int slot = 0;
|
||||
for (; slot < CLUSTER_MANAGER_SLOTS; slot++) {
|
||||
listIter li;
|
||||
listNode *ln;
|
||||
listRewind(cluster_manager.nodes, &li);
|
||||
list *owners = listCreate();
|
||||
while ((ln = listNext(&li)) != NULL) {
|
||||
clusterManagerNode *n = ln->value;
|
||||
if (n->flags & CLUSTER_MANAGER_FLAG_SLAVE) continue;
|
||||
if (n->slots[slot]) listAddNodeTail(owners, n);
|
||||
else {
|
||||
/* Nodes having keys for the slot will be considered
|
||||
* owners too. */
|
||||
int count = clusterManagerCountKeysInSlot(n, slot);
|
||||
if (count > 0) listAddNodeTail(owners, n);
|
||||
}
|
||||
}
|
||||
if (listLength(owners) > 1) {
|
||||
result = 0;
|
||||
clusterManagerLogErr("[WARNING] Slot %d has %d owners:\n",
|
||||
slot, listLength(owners));
|
||||
listRewind(owners, &li);
|
||||
while ((ln = listNext(&li)) != NULL) {
|
||||
clusterManagerNode *n = ln->value;
|
||||
clusterManagerLogErr(" %s:%d\n", n->ip, n->port);
|
||||
}
|
||||
if (do_fix) {
|
||||
result = clusterManagerFixMultipleSlotOwners(slot, owners);
|
||||
if (!result) {
|
||||
clusterManagerLogErr("Failed to fix multiple owners "
|
||||
"for slot %d\n", slot);
|
||||
listRelease(owners);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
listRelease(owners);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -4402,7 +4061,7 @@ static clusterManagerNode *clusterNodeForResharding(char *id,
|
||||
static list *clusterManagerComputeReshardTable(list *sources, int numslots) {
|
||||
list *moved = listCreate();
|
||||
int src_count = listLength(sources), i = 0, tot_slots = 0, j;
|
||||
clusterManagerNode **sorted = zmalloc(src_count * sizeof(*sorted));
|
||||
clusterManagerNode **sorted = zmalloc(src_count * sizeof(**sorted));
|
||||
listIter li;
|
||||
listNode *ln;
|
||||
listRewind(sources, &li);
|
||||
@@ -5441,13 +5100,10 @@ static int clusterManagerCommandSetTimeout(int argc, char **argv) {
|
||||
n->port);
|
||||
ok_count++;
|
||||
continue;
|
||||
reply_err:;
|
||||
int need_free = 0;
|
||||
reply_err:
|
||||
if (err == NULL) err = "";
|
||||
else need_free = 1;
|
||||
clusterManagerLogErr("ERR setting node-timeot for %s:%d: %s\n", n->ip,
|
||||
n->port, err);
|
||||
if (need_free) zfree(err);
|
||||
err_count++;
|
||||
}
|
||||
clusterManagerLogInfo(">>> New node timeout set. %d OK, %d ERR.\n",
|
||||
@@ -5624,7 +5280,7 @@ static int clusterManagerCommandCall(int argc, char **argv) {
|
||||
if (status != REDIS_OK || reply == NULL )
|
||||
printf("%s:%d: Failed!\n", n->ip, n->port);
|
||||
else {
|
||||
sds formatted_reply = cliFormatReplyRaw(reply);
|
||||
sds formatted_reply = cliFormatReplyTTY(reply, "");
|
||||
printf("%s:%d: %s\n", n->ip, n->port, (char *) formatted_reply);
|
||||
sdsfree(formatted_reply);
|
||||
}
|
||||
@@ -7001,8 +6657,6 @@ int main(int argc, char **argv) {
|
||||
argc -= firstarg;
|
||||
argv += firstarg;
|
||||
|
||||
parseEnv();
|
||||
|
||||
/* Cluster Manager mode */
|
||||
if (CLUSTER_MANAGER_MODE()) {
|
||||
clusterManagerCommandProc *proc = validateClusterManagerCommand();
|
||||
|
||||
@@ -1245,18 +1245,6 @@ void readSyncBulkPayload(aeEventLoop *el, int fd, void *privdata, int mask) {
|
||||
if (eof_reached) {
|
||||
int aof_is_enabled = server.aof_state != AOF_OFF;
|
||||
|
||||
/* Ensure background save doesn't overwrite synced data */
|
||||
if (server.rdb_child_pid != -1) {
|
||||
serverLog(LL_NOTICE,
|
||||
"Replica is about to load the RDB file received from the "
|
||||
"master, but there is a pending RDB child running. "
|
||||
"Killing process %ld and removing its temp file to avoid "
|
||||
"any race",
|
||||
(long) server.rdb_child_pid);
|
||||
kill(server.rdb_child_pid,SIGUSR1);
|
||||
rdbRemoveTempFile(server.rdb_child_pid);
|
||||
}
|
||||
|
||||
if (rename(server.repl_transfer_tmpfile,server.rdb_filename) == -1) {
|
||||
serverLog(LL_WARNING,"Failed trying to rename the temp DB into dump.rdb in MASTER <-> REPLICA synchronization: %s", strerror(errno));
|
||||
cancelReplicationHandshake();
|
||||
|
||||
+6
-5
@@ -560,7 +560,7 @@ int luaRedisGenericCommand(lua_State *lua, int raise_error) {
|
||||
}
|
||||
|
||||
/* Run the command */
|
||||
int call_flags = CMD_CALL_SLOWLOG | CMD_CALL_STATS;
|
||||
int call_flags = CMD_CALL_SLOWLOG | CMD_CALL_STATS | CMD_CALL_NOQUEUE;
|
||||
if (server.lua_replicate_commands) {
|
||||
/* Set flags according to redis.set_repl() settings. */
|
||||
if (server.lua_repl & PROPAGATE_AOF)
|
||||
@@ -1242,7 +1242,7 @@ void luaMaskCountHook(lua_State *lua, lua_Debug *ar) {
|
||||
* we need to mask the client executing the script from the event loop.
|
||||
* If we don't do that the client may disconnect and could no longer be
|
||||
* here when the EVAL command will return. */
|
||||
protectClient(server.lua_caller);
|
||||
aeDeleteFileEvent(server.el, server.lua_caller->fd, AE_READABLE);
|
||||
}
|
||||
if (server.lua_timedout) processEventsWhileBlocked();
|
||||
if (server.lua_kill) {
|
||||
@@ -1370,9 +1370,10 @@ void evalGenericCommand(client *c, int evalsha) {
|
||||
if (delhook) lua_sethook(lua,NULL,0,0); /* Disable hook */
|
||||
if (server.lua_timedout) {
|
||||
server.lua_timedout = 0;
|
||||
/* Restore the client that was protected when the script timeout
|
||||
* was detected. */
|
||||
unprotectClient(c);
|
||||
/* Restore the readable handler that was unregistered when the
|
||||
* script timeout was detected. */
|
||||
aeCreateFileEvent(server.el,c->fd,AE_READABLE,
|
||||
readQueryFromClient,c);
|
||||
if (server.masterhost && server.master)
|
||||
queueClientForReprocessing(server.master);
|
||||
}
|
||||
|
||||
@@ -695,7 +695,7 @@ sds sdscatfmt(sds s, char const *fmt, ...) {
|
||||
* s = sdstrim(s,"Aa. :");
|
||||
* printf("%s\n", s);
|
||||
*
|
||||
* Output will be just "HelloWorld".
|
||||
* Output will be just "Hello World".
|
||||
*/
|
||||
sds sdstrim(sds s, const char *cset) {
|
||||
char *start, *end, *sp, *ep;
|
||||
|
||||
+3
-18
@@ -452,15 +452,13 @@ struct redisCommand sentinelcmds[] = {
|
||||
{"info",sentinelInfoCommand,-1,"",0,NULL,0,0,0,0,0},
|
||||
{"role",sentinelRoleCommand,1,"l",0,NULL,0,0,0,0,0},
|
||||
{"client",clientCommand,-2,"rs",0,NULL,0,0,0,0,0},
|
||||
{"shutdown",shutdownCommand,-1,"",0,NULL,0,0,0,0,0},
|
||||
{"auth",authCommand,2,"sltF",0,NULL,0,0,0,0,0}
|
||||
{"shutdown",shutdownCommand,-1,"",0,NULL,0,0,0,0,0}
|
||||
};
|
||||
|
||||
/* This function overwrites a few normal Redis config default with Sentinel
|
||||
* specific defaults. */
|
||||
void initSentinelConfig(void) {
|
||||
server.port = REDIS_SENTINEL_PORT;
|
||||
server.protected_mode = 0; /* Sentinel must be exposed. */
|
||||
}
|
||||
|
||||
/* Perform the Sentinel mode initialization. */
|
||||
@@ -1943,25 +1941,12 @@ werr:
|
||||
/* Send the AUTH command with the specified master password if needed.
|
||||
* Note that for slaves the password set for the master is used.
|
||||
*
|
||||
* In case this Sentinel requires a password as well, via the "requirepass"
|
||||
* configuration directive, we assume we should use the local password in
|
||||
* order to authenticate when connecting with the other Sentinels as well.
|
||||
* So basically all the Sentinels share the same password and use it to
|
||||
* authenticate reciprocally.
|
||||
*
|
||||
* We don't check at all if the command was successfully transmitted
|
||||
* to the instance as if it fails Sentinel will detect the instance down,
|
||||
* will disconnect and reconnect the link and so forth. */
|
||||
void sentinelSendAuthIfNeeded(sentinelRedisInstance *ri, redisAsyncContext *c) {
|
||||
char *auth_pass = NULL;
|
||||
|
||||
if (ri->flags & SRI_MASTER) {
|
||||
auth_pass = ri->auth_pass;
|
||||
} else if (ri->flags & SRI_SLAVE) {
|
||||
auth_pass = ri->master->auth_pass;
|
||||
} else if (ri->flags & SRI_SENTINEL) {
|
||||
if (server.requirepass) auth_pass = server.requirepass;
|
||||
}
|
||||
char *auth_pass = (ri->flags & SRI_MASTER) ? ri->auth_pass :
|
||||
ri->master->auth_pass;
|
||||
|
||||
if (auth_pass) {
|
||||
if (redisAsyncCommand(c, sentinelDiscardReplyCallback, ri, "%s %s",
|
||||
|
||||
+43
-38
@@ -307,20 +307,19 @@ struct redisCommand redisCommandTable[] = {
|
||||
{"pfcount",pfcountCommand,-2,"r",0,NULL,1,-1,1,0,0},
|
||||
{"pfmerge",pfmergeCommand,-2,"wm",0,NULL,1,-1,1,0,0},
|
||||
{"pfdebug",pfdebugCommand,-3,"w",0,NULL,0,0,0,0,0},
|
||||
{"xadd",xaddCommand,-5,"wmFR",0,NULL,1,1,1,0,0},
|
||||
{"xadd",xaddCommand,-5,"wmF",0,NULL,1,1,1,0,0},
|
||||
{"xrange",xrangeCommand,-4,"r",0,NULL,1,1,1,0,0},
|
||||
{"xrevrange",xrevrangeCommand,-4,"r",0,NULL,1,1,1,0,0},
|
||||
{"xlen",xlenCommand,2,"rF",0,NULL,1,1,1,0,0},
|
||||
{"xread",xreadCommand,-4,"rs",0,xreadGetKeys,1,1,1,0,0},
|
||||
{"xreadgroup",xreadCommand,-7,"ws",0,xreadGetKeys,1,1,1,0,0},
|
||||
{"xgroup",xgroupCommand,-2,"wm",0,NULL,2,2,1,0,0},
|
||||
{"xsetid",xsetidCommand,3,"wmF",0,NULL,1,1,1,0,0},
|
||||
{"xack",xackCommand,-4,"wF",0,NULL,1,1,1,0,0},
|
||||
{"xpending",xpendingCommand,-3,"rR",0,NULL,1,1,1,0,0},
|
||||
{"xclaim",xclaimCommand,-6,"wRF",0,NULL,1,1,1,0,0},
|
||||
{"xinfo",xinfoCommand,-2,"rR",0,NULL,2,2,1,0,0},
|
||||
{"xpending",xpendingCommand,-3,"r",0,NULL,1,1,1,0,0},
|
||||
{"xclaim",xclaimCommand,-6,"wF",0,NULL,1,1,1,0,0},
|
||||
{"xinfo",xinfoCommand,-2,"r",0,NULL,2,2,1,0,0},
|
||||
{"xdel",xdelCommand,-3,"wF",0,NULL,1,1,1,0,0},
|
||||
{"xtrim",xtrimCommand,-2,"wFR",0,NULL,1,1,1,0,0},
|
||||
{"xtrim",xtrimCommand,-2,"wF",0,NULL,1,1,1,0,0},
|
||||
{"post",securityWarningCommand,-1,"lt",0,NULL,0,0,0,0,0},
|
||||
{"host:",securityWarningCommand,-1,"lt",0,NULL,0,0,0,0,0},
|
||||
{"latency",latencyCommand,-2,"aslt",0,NULL,0,0,0,0,0},
|
||||
@@ -1526,10 +1525,10 @@ void initServerConfig(void) {
|
||||
server.runid[CONFIG_RUN_ID_SIZE] = '\0';
|
||||
changeReplicationId();
|
||||
clearReplicationId2();
|
||||
server.timezone = getTimeZone(); /* Initialized by tzset(). */
|
||||
server.timezone = timezone; /* Initialized by tzset(). */
|
||||
server.configfile = NULL;
|
||||
server.executable = NULL;
|
||||
server.hz = server.config_hz = CONFIG_DEFAULT_HZ;
|
||||
server.config_hz = CONFIG_DEFAULT_HZ;
|
||||
server.dynamic_hz = CONFIG_DEFAULT_DYNAMIC_HZ;
|
||||
server.arch_bits = (sizeof(long) == 8) ? 64 : 32;
|
||||
server.port = CONFIG_DEFAULT_SERVER_PORT;
|
||||
@@ -1703,7 +1702,6 @@ void initServerConfig(void) {
|
||||
server.expireCommand = lookupCommandByCString("expire");
|
||||
server.pexpireCommand = lookupCommandByCString("pexpire");
|
||||
server.xclaimCommand = lookupCommandByCString("xclaim");
|
||||
server.xgroupCommand = lookupCommandByCString("xgroup");
|
||||
|
||||
/* Slow log */
|
||||
server.slowlog_log_slower_than = CONFIG_DEFAULT_SLOWLOG_LOG_SLOWER_THAN;
|
||||
@@ -1958,13 +1956,9 @@ int listenToPort(int port, int *fds, int *count) {
|
||||
}
|
||||
if (fds[*count] == ANET_ERR) {
|
||||
serverLog(LL_WARNING,
|
||||
"Could not create server TCP listening socket %s:%d: %s",
|
||||
"Creating Server TCP listening socket %s:%d: %s",
|
||||
server.bindaddr[j] ? server.bindaddr[j] : "*",
|
||||
port, server.neterr);
|
||||
if (errno == ENOPROTOOPT || errno == EPROTONOSUPPORT ||
|
||||
errno == ESOCKTNOSUPPORT || errno == EPFNOSUPPORT ||
|
||||
errno == EAFNOSUPPORT || errno == EADDRNOTAVAIL)
|
||||
continue;
|
||||
return C_ERR;
|
||||
}
|
||||
anetNonBlock(NULL,fds[*count]);
|
||||
@@ -2416,7 +2410,7 @@ void call(client *c, int flags) {
|
||||
int client_old_flags = c->flags;
|
||||
struct redisCommand *real_cmd = c->cmd;
|
||||
|
||||
/* Sent the command to clients in MONITOR mode, only if the commands are
|
||||
/* Send the command to clients in MONITOR mode, only if the commands are
|
||||
* not generated from reading an AOF. */
|
||||
if (listLength(server.monitors) &&
|
||||
!server.loading &&
|
||||
@@ -2425,6 +2419,25 @@ void call(client *c, int flags) {
|
||||
replicationFeedMonitors(c,server.monitors,c->db->id,c->argv,c->argc);
|
||||
}
|
||||
|
||||
/* If the client is in the context of a transaction, reply with
|
||||
* +QUEUED and just accumulate the command in the client transaction
|
||||
* commands vector.
|
||||
*
|
||||
* Note that CALL_NOQUEUE can override this check and execute the command
|
||||
* straight away even if the client is in "MULTI" state. This is useful
|
||||
* every time we want to actually execute commands even if the client is
|
||||
* in MULTI state, like in scripting.c or in the implementation of EXEC
|
||||
* itself. */
|
||||
if (!(flags & CMD_CALL_NOQUEUE) &&
|
||||
c->flags & CLIENT_MULTI &&
|
||||
c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&
|
||||
c->cmd->proc != multiCommand && c->cmd->proc != watchCommand)
|
||||
{
|
||||
queueMultiCommand(c);
|
||||
addReply(c,shared.queued);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Initialization: clear the flags that must be set by the command on
|
||||
* demand, and initialize the array for additional commands propagation. */
|
||||
c->flags &= ~(CLIENT_FORCE_AOF|CLIENT_FORCE_REPL|CLIENT_PREVENT_PROP);
|
||||
@@ -2607,23 +2620,24 @@ int processCommand(client *c) {
|
||||
}
|
||||
|
||||
/* Handle the maxmemory directive.
|
||||
*
|
||||
* First we try to free some memory if possible (if there are volatile
|
||||
* keys in the dataset). If there are not the only thing we can do
|
||||
* is returning an error.
|
||||
*
|
||||
* Note that we do not want to reclaim memory if we are here re-entering
|
||||
* the event loop since there is a busy Lua script running in timeout
|
||||
* condition, to avoid mixing the propagation of scripts with the
|
||||
* propagation of DELs due to eviction. */
|
||||
* condition, to avoid mixing the propagation of scripts with the propagation
|
||||
* of DELs due to eviction. */
|
||||
if (server.maxmemory && !server.lua_timedout) {
|
||||
int out_of_memory = freeMemoryIfNeededAndSafe() == C_ERR;
|
||||
int out_of_memory = freeMemoryIfNeeded() == C_ERR;
|
||||
/* freeMemoryIfNeeded may flush slave output buffers. This may result
|
||||
* into a slave, that may be the active client, to be freed. */
|
||||
if (server.current_client == NULL) return C_ERR;
|
||||
|
||||
/* It was impossible to free enough memory, and the command the client
|
||||
* is trying to execute is denied during OOM conditions or the client
|
||||
* is in MULTI/EXEC context? Error. */
|
||||
if (out_of_memory &&
|
||||
(c->cmd->flags & CMD_DENYOOM ||
|
||||
(c->flags & CLIENT_MULTI && c->cmd->proc != execCommand))) {
|
||||
* is trying to execute is denied during OOM conditions? Error. */
|
||||
if ((c->cmd->flags & CMD_DENYOOM) && out_of_memory) {
|
||||
flagTransaction(c);
|
||||
addReply(c, shared.oomerr);
|
||||
return C_OK;
|
||||
@@ -2719,18 +2733,9 @@ int processCommand(client *c) {
|
||||
}
|
||||
|
||||
/* Exec the command */
|
||||
if (c->flags & CLIENT_MULTI &&
|
||||
c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&
|
||||
c->cmd->proc != multiCommand && c->cmd->proc != watchCommand)
|
||||
{
|
||||
queueMultiCommand(c);
|
||||
addReply(c,shared.queued);
|
||||
} else {
|
||||
call(c,CMD_CALL_FULL);
|
||||
c->woff = server.master_repl_offset;
|
||||
if (listLength(server.ready_keys))
|
||||
handleClientsBlockedOnKeys();
|
||||
}
|
||||
call(c,CMD_CALL_FULL);
|
||||
c->woff = server.master_repl_offset;
|
||||
if (listLength(server.ready_keys)) handleClientsBlockedOnKeys();
|
||||
return C_OK;
|
||||
}
|
||||
|
||||
@@ -3243,11 +3248,11 @@ sds genRedisInfoString(char *section) {
|
||||
"allocator_frag_ratio:%.2f\r\n"
|
||||
"allocator_frag_bytes:%zu\r\n"
|
||||
"allocator_rss_ratio:%.2f\r\n"
|
||||
"allocator_rss_bytes:%zd\r\n"
|
||||
"allocator_rss_bytes:%zu\r\n"
|
||||
"rss_overhead_ratio:%.2f\r\n"
|
||||
"rss_overhead_bytes:%zd\r\n"
|
||||
"rss_overhead_bytes:%zu\r\n"
|
||||
"mem_fragmentation_ratio:%.2f\r\n"
|
||||
"mem_fragmentation_bytes:%zd\r\n"
|
||||
"mem_fragmentation_bytes:%zu\r\n"
|
||||
"mem_not_counted_for_evict:%zu\r\n"
|
||||
"mem_replication_backlog:%zu\r\n"
|
||||
"mem_clients_slaves:%zu\r\n"
|
||||
|
||||
+7
-13
@@ -256,7 +256,6 @@ typedef long long mstime_t; /* millisecond time type. */
|
||||
#define CLIENT_LUA_DEBUG (1<<25) /* Run EVAL in debug mode. */
|
||||
#define CLIENT_LUA_DEBUG_SYNC (1<<26) /* EVAL debugging without fork() */
|
||||
#define CLIENT_MODULE (1<<27) /* Non connected client used by some module. */
|
||||
#define CLIENT_PROTECTED (1<<28) /* Client should not be freed for now. */
|
||||
|
||||
/* Client block type (btype field in client structure)
|
||||
* if CLIENT_BLOCKED flag is set. */
|
||||
@@ -417,6 +416,9 @@ typedef long long mstime_t; /* millisecond time type. */
|
||||
#define CMD_CALL_PROPAGATE_REPL (1<<3)
|
||||
#define CMD_CALL_PROPAGATE (CMD_CALL_PROPAGATE_AOF|CMD_CALL_PROPAGATE_REPL)
|
||||
#define CMD_CALL_FULL (CMD_CALL_SLOWLOG | CMD_CALL_STATS | CMD_CALL_PROPAGATE)
|
||||
/* --- call() special semantics that can be requested by the caller
|
||||
* --- but are not included in CALL_FULL. */
|
||||
#define CMD_CALL_NOQUEUE (1<<4)
|
||||
|
||||
/* Command propagation flags, see propagate() function */
|
||||
#define PROPAGATE_NONE 0
|
||||
@@ -654,9 +656,6 @@ typedef struct multiCmd {
|
||||
typedef struct multiState {
|
||||
multiCmd *commands; /* Array of MULTI commands */
|
||||
int count; /* Total number of MULTI commands */
|
||||
int cmd_flags; /* The accumulated command flags OR-ed together.
|
||||
So if at least a command has a given flag, it
|
||||
will be set in this field. */
|
||||
int minreplicas; /* MINREPLICAS for synchronous replication */
|
||||
time_t minreplicas_timeout; /* MINREPLICAS timeout as unixtime. */
|
||||
} multiState;
|
||||
@@ -867,11 +866,11 @@ struct redisMemOverhead {
|
||||
float dataset_perc;
|
||||
float peak_perc;
|
||||
float total_frag;
|
||||
ssize_t total_frag_bytes;
|
||||
size_t total_frag_bytes;
|
||||
float allocator_frag;
|
||||
ssize_t allocator_frag_bytes;
|
||||
size_t allocator_frag_bytes;
|
||||
float allocator_rss;
|
||||
ssize_t allocator_rss_bytes;
|
||||
size_t allocator_rss_bytes;
|
||||
float rss_extra;
|
||||
size_t rss_extra_bytes;
|
||||
size_t num_dbs;
|
||||
@@ -993,8 +992,7 @@ struct redisServer {
|
||||
struct redisCommand *delCommand, *multiCommand, *lpushCommand,
|
||||
*lpopCommand, *rpopCommand, *zpopminCommand,
|
||||
*zpopmaxCommand, *sremCommand, *execCommand,
|
||||
*expireCommand, *pexpireCommand, *xclaimCommand,
|
||||
*xgroupCommand;
|
||||
*expireCommand, *pexpireCommand, *xclaimCommand;
|
||||
/* Fields used only for stats */
|
||||
time_t stat_starttime; /* Server start time */
|
||||
long long stat_numcommands; /* Number of processed commands */
|
||||
@@ -1478,8 +1476,6 @@ int clientHasPendingReplies(client *c);
|
||||
void unlinkClient(client *c);
|
||||
int writeToClient(int fd, client *c, int handler_installed);
|
||||
void linkClient(client *c);
|
||||
void protectClient(client *c);
|
||||
void unprotectClient(client *c);
|
||||
|
||||
#ifdef __GNUC__
|
||||
void addReplyErrorFormat(client *c, const char *fmt, ...)
|
||||
@@ -1702,7 +1698,6 @@ int zslLexValueLteMax(sds value, zlexrangespec *spec);
|
||||
int getMaxmemoryState(size_t *total, size_t *logical, size_t *tofree, float *level);
|
||||
size_t freeMemoryGetNotCountedMemory();
|
||||
int freeMemoryIfNeeded(void);
|
||||
int freeMemoryIfNeededAndSafe(void);
|
||||
int processCommand(client *c);
|
||||
void setupSignalHandlers(void);
|
||||
struct redisCommand *lookupCommand(sds name);
|
||||
@@ -2115,7 +2110,6 @@ void xrevrangeCommand(client *c);
|
||||
void xlenCommand(client *c);
|
||||
void xreadCommand(client *c);
|
||||
void xgroupCommand(client *c);
|
||||
void xsetidCommand(client *c);
|
||||
void xackCommand(client *c);
|
||||
void xpendingCommand(client *c);
|
||||
void xclaimCommand(client *c);
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@
|
||||
#include <errno.h> /* errno program_invocation_name program_invocation_short_name */
|
||||
|
||||
#if !defined(HAVE_SETPROCTITLE)
|
||||
#if (defined __NetBSD__ || defined __FreeBSD__ || defined __OpenBSD__ || defined __DragonFly__)
|
||||
#if (defined __NetBSD__ || defined __FreeBSD__ || defined __OpenBSD__)
|
||||
#define HAVE_SETPROCTITLE 1
|
||||
#else
|
||||
#define HAVE_SETPROCTITLE 0
|
||||
|
||||
+57
-204
@@ -777,8 +777,8 @@ int streamDeleteItem(stream *s, streamID *id) {
|
||||
* in the standard <ms>-<seq> format, using the simple string protocol
|
||||
* of REPL. */
|
||||
void addReplyStreamID(client *c, streamID *id) {
|
||||
sds replyid = sdscatfmt(sdsempty(),"%U-%U",id->ms,id->seq);
|
||||
addReplyBulkSds(c,replyid);
|
||||
sds replyid = sdscatfmt(sdsempty(),"+%U-%U\r\n",id->ms,id->seq);
|
||||
addReplySds(c,replyid);
|
||||
}
|
||||
|
||||
/* Similar to the above function, but just creates an object, usually useful
|
||||
@@ -791,18 +791,18 @@ robj *createObjectFromStreamID(streamID *id) {
|
||||
/* As a result of an explicit XCLAIM or XREADGROUP command, new entries
|
||||
* are created in the pending list of the stream and consumers. We need
|
||||
* to propagate this changes in the form of XCLAIM commands. */
|
||||
void streamPropagateXCLAIM(client *c, robj *key, streamCG *group, robj *groupname, robj *id, streamNACK *nack) {
|
||||
void streamPropagateXCLAIM(client *c, robj *key, robj *group, robj *id, streamNACK *nack) {
|
||||
/* We need to generate an XCLAIM that will work in a idempotent fashion:
|
||||
*
|
||||
* XCLAIM <key> <group> <consumer> 0 <id> TIME <milliseconds-unix-time>
|
||||
* RETRYCOUNT <count> FORCE JUSTID LASTID <id>.
|
||||
* RETRYCOUNT <count> FORCE JUSTID.
|
||||
*
|
||||
* Note that JUSTID is useful in order to avoid that XCLAIM will do
|
||||
* useless work in the slave side, trying to fetch the stream item. */
|
||||
robj *argv[14];
|
||||
robj *argv[12];
|
||||
argv[0] = createStringObject("XCLAIM",6);
|
||||
argv[1] = key;
|
||||
argv[2] = groupname;
|
||||
argv[2] = group;
|
||||
argv[3] = createStringObject(nack->consumer->name,sdslen(nack->consumer->name));
|
||||
argv[4] = createStringObjectFromLongLong(0);
|
||||
argv[5] = id;
|
||||
@@ -812,9 +812,7 @@ void streamPropagateXCLAIM(client *c, robj *key, streamCG *group, robj *groupnam
|
||||
argv[9] = createStringObjectFromLongLong(nack->delivery_count);
|
||||
argv[10] = createStringObject("FORCE",5);
|
||||
argv[11] = createStringObject("JUSTID",6);
|
||||
argv[12] = createStringObject("LASTID",6);
|
||||
argv[13] = createObjectFromStreamID(&group->last_id);
|
||||
propagate(server.xclaimCommand,c->db->id,argv,14,PROPAGATE_AOF|PROPAGATE_REPL);
|
||||
propagate(server.xclaimCommand,c->db->id,argv,12,PROPAGATE_AOF|PROPAGATE_REPL);
|
||||
decrRefCount(argv[0]);
|
||||
decrRefCount(argv[3]);
|
||||
decrRefCount(argv[4]);
|
||||
@@ -824,38 +822,13 @@ void streamPropagateXCLAIM(client *c, robj *key, streamCG *group, robj *groupnam
|
||||
decrRefCount(argv[9]);
|
||||
decrRefCount(argv[10]);
|
||||
decrRefCount(argv[11]);
|
||||
decrRefCount(argv[12]);
|
||||
decrRefCount(argv[13]);
|
||||
}
|
||||
|
||||
/* We need this when we want to propoagate the new last-id of a consumer group
|
||||
* that was consumed by XREADGROUP with the NOACK option: in that case we can't
|
||||
* propagate the last ID just using the XCLAIM LASTID option, so we emit
|
||||
*
|
||||
* XGROUP SETID <key> <groupname> <id>
|
||||
*/
|
||||
void streamPropagateGroupID(client *c, robj *key, streamCG *group, robj *groupname) {
|
||||
robj *argv[5];
|
||||
argv[0] = createStringObject("XGROUP",6);
|
||||
argv[1] = createStringObject("SETID",5);
|
||||
argv[2] = key;
|
||||
argv[3] = groupname;
|
||||
argv[4] = createObjectFromStreamID(&group->last_id);
|
||||
propagate(server.xgroupCommand,c->db->id,argv,5,PROPAGATE_AOF|PROPAGATE_REPL);
|
||||
decrRefCount(argv[0]);
|
||||
decrRefCount(argv[1]);
|
||||
decrRefCount(argv[4]);
|
||||
}
|
||||
|
||||
/* Send the stream items in the specified range to the client 'c'. The range
|
||||
* the client will receive is between start and end inclusive, if 'count' is
|
||||
* non zero, no more than 'count' elements are sent.
|
||||
*
|
||||
* The 'end' pointer can be NULL to mean that we want all the elements from
|
||||
* 'start' till the end of the stream. If 'rev' is non zero, elements are
|
||||
* produced in reversed order from end to start.
|
||||
*
|
||||
* The function returns the number of entries emitted.
|
||||
/* Send the specified range to the client 'c'. The range the client will
|
||||
* receive is between start and end inclusive, if 'count' is non zero, no more
|
||||
* than 'count' elemnets are sent. The 'end' pointer can be NULL to mean that
|
||||
* we want all the elements from 'start' till the end of the stream. If 'rev'
|
||||
* is non zero, elements are produced in reversed order from end to start.
|
||||
*
|
||||
* If group and consumer are not NULL, the function performs additional work:
|
||||
* 1. It updates the last delivered ID in the group in case we are
|
||||
@@ -894,21 +867,22 @@ void streamPropagateGroupID(client *c, robj *key, streamCG *group, robj *groupna
|
||||
#define STREAM_RWR_NOACK (1<<0) /* Do not create entries in the PEL. */
|
||||
#define STREAM_RWR_RAWENTRIES (1<<1) /* Do not emit protocol for array
|
||||
boundaries, just the entries. */
|
||||
#define STREAM_RWR_HISTORY (1<<2) /* Only serve consumer local PEL. */
|
||||
size_t streamReplyWithRange(client *c, stream *s, streamID *start, streamID *end, size_t count, int rev, streamCG *group, streamConsumer *consumer, int flags, streamPropInfo *spi) {
|
||||
void *arraylen_ptr = NULL;
|
||||
size_t arraylen = 0;
|
||||
streamIterator si;
|
||||
int64_t numfields;
|
||||
streamID id;
|
||||
int propagate_last_id = 0;
|
||||
|
||||
/* If the client is asking for some history, we serve it using a
|
||||
* different function, so that we return entries *solely* from its
|
||||
* own PEL. This ensures each consumer will always and only see
|
||||
* the history of messages delivered to it and not yet confirmed
|
||||
/* If a group was passed, we check if the request is about messages
|
||||
* never delivered so far (normally this happens when ">" ID is passed).
|
||||
*
|
||||
* If instead the client is asking for some history, we serve it
|
||||
* using a different function, so that we return entries *solely*
|
||||
* from its own PEL. This ensures each consumer will always and only
|
||||
* see the history of messages delivered to it and not yet confirmed
|
||||
* as delivered. */
|
||||
if (group && (flags & STREAM_RWR_HISTORY)) {
|
||||
if (group && streamCompareID(start,&group->last_id) <= 0) {
|
||||
return streamReplyWithRangeFromConsumerPEL(c,s,start,end,count,
|
||||
consumer);
|
||||
}
|
||||
@@ -918,10 +892,8 @@ size_t streamReplyWithRange(client *c, stream *s, streamID *start, streamID *end
|
||||
streamIteratorStart(&si,s,start,end,rev);
|
||||
while(streamIteratorGetID(&si,&id,&numfields)) {
|
||||
/* Update the group last_id if needed. */
|
||||
if (group && streamCompareID(&id,&group->last_id) > 0) {
|
||||
if (group && streamCompareID(&id,&group->last_id) > 0)
|
||||
group->last_id = id;
|
||||
propagate_last_id = 1;
|
||||
}
|
||||
|
||||
/* Emit a two elements array for each item. The first is
|
||||
* the ID, the second is an array of field-value pairs. */
|
||||
@@ -981,12 +953,9 @@ size_t streamReplyWithRange(client *c, stream *s, streamID *start, streamID *end
|
||||
/* Propagate as XCLAIM. */
|
||||
if (spi) {
|
||||
robj *idarg = createObjectFromStreamID(&id);
|
||||
streamPropagateXCLAIM(c,spi->keyname,group,spi->groupname,idarg,nack);
|
||||
streamPropagateXCLAIM(c,spi->keyname,spi->groupname,idarg,nack);
|
||||
decrRefCount(idarg);
|
||||
}
|
||||
} else {
|
||||
if (propagate_last_id)
|
||||
streamPropagateGroupID(c,spi->keyname,group,spi->groupname);
|
||||
}
|
||||
|
||||
arraylen++;
|
||||
@@ -1025,7 +994,7 @@ size_t streamReplyWithRangeFromConsumerPEL(client *c, stream *s, streamID *start
|
||||
if (end && memcmp(ri.key,end,ri.key_len) > 0) break;
|
||||
streamID thisid;
|
||||
streamDecodeID(ri.key,&thisid);
|
||||
if (streamReplyWithRange(c,s,&thisid,&thisid,1,0,NULL,NULL,
|
||||
if (streamReplyWithRange(c,s,&thisid,NULL,1,0,NULL,NULL,
|
||||
STREAM_RWR_RAWENTRIES,NULL) == 0)
|
||||
{
|
||||
/* Note that we may have a not acknowledged entry in the PEL
|
||||
@@ -1262,7 +1231,7 @@ void xrangeGenericCommand(client *c, int rev) {
|
||||
robj *o;
|
||||
stream *s;
|
||||
streamID startid, endid;
|
||||
long long count = -1;
|
||||
long long count = 0;
|
||||
robj *startarg = rev ? c->argv[3] : c->argv[2];
|
||||
robj *endarg = rev ? c->argv[2] : c->argv[3];
|
||||
|
||||
@@ -1289,13 +1258,7 @@ void xrangeGenericCommand(client *c, int rev) {
|
||||
if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.emptymultibulk)) == NULL
|
||||
|| checkType(c,o,OBJ_STREAM)) return;
|
||||
s = o->ptr;
|
||||
|
||||
if (count == 0) {
|
||||
addReply(c,shared.nullmultibulk);
|
||||
} else {
|
||||
if (count == -1) count = 0;
|
||||
streamReplyWithRange(c,s,&startid,&endid,count,rev,NULL,NULL,0,NULL);
|
||||
}
|
||||
streamReplyWithRange(c,s,&startid,&endid,count,rev,NULL,NULL,0,NULL);
|
||||
}
|
||||
|
||||
/* XRANGE key start end [COUNT <n>] */
|
||||
@@ -1472,10 +1435,8 @@ void xreadCommand(client *c) {
|
||||
stream *s = o->ptr;
|
||||
streamID *gt = ids+i; /* ID must be greater than this. */
|
||||
int serve_synchronously = 0;
|
||||
int serve_history = 0; /* True for XREADGROUP with ID != ">". */
|
||||
|
||||
/* Check if there are the conditions to serve the client
|
||||
* synchronously. */
|
||||
/* Check if there are the conditions to serve the client synchronously. */
|
||||
if (groups) {
|
||||
/* If the consumer is blocked on a group, we always serve it
|
||||
* synchronously (serving its local history) if the ID specified
|
||||
@@ -1484,7 +1445,6 @@ void xreadCommand(client *c) {
|
||||
gt->seq != UINT64_MAX)
|
||||
{
|
||||
serve_synchronously = 1;
|
||||
serve_history = 1;
|
||||
} else {
|
||||
/* We also want to serve a consumer in a consumer group
|
||||
* synchronously in case the group top item delivered is smaller
|
||||
@@ -1520,12 +1480,9 @@ void xreadCommand(client *c) {
|
||||
if (groups) consumer = streamLookupConsumer(groups[i],
|
||||
consumername->ptr,1);
|
||||
streamPropInfo spi = {c->argv[i+streams_arg],groupname};
|
||||
int flags = 0;
|
||||
if (noack) flags |= STREAM_RWR_NOACK;
|
||||
if (serve_history) flags |= STREAM_RWR_HISTORY;
|
||||
streamReplyWithRange(c,s,&start,NULL,count,0,
|
||||
groups ? groups[i] : NULL,
|
||||
consumer, flags, &spi);
|
||||
consumer, noack, &spi);
|
||||
if (groups) server.dirty++;
|
||||
}
|
||||
}
|
||||
@@ -1697,14 +1654,13 @@ uint64_t streamDelConsumer(streamCG *cg, sds name) {
|
||||
* Consumer groups commands
|
||||
* ----------------------------------------------------------------------- */
|
||||
|
||||
/* XGROUP CREATE <key> <groupname> <id or $> [MKSTREAM]
|
||||
/* XGROUP CREATE <key> <groupname> <id or $>
|
||||
* XGROUP SETID <key> <groupname> <id or $>
|
||||
* XGROUP DESTROY <key> <groupname>
|
||||
* XGROUP DELCONSUMER <key> <groupname> <consumername> */
|
||||
void xgroupCommand(client *c) {
|
||||
const char *help[] = {
|
||||
"CREATE <key> <groupname> <id or $> [opt] -- Create a new consumer group.",
|
||||
" option MKSTREAM: create the empty stream if it does not exist.",
|
||||
"CREATE <key> <groupname> <id or $> -- Create a new consumer group.",
|
||||
"SETID <key> <groupname> <id or $> -- Set the current group ID.",
|
||||
"DESTROY <key> <groupname> -- Remove the specified group.",
|
||||
"DELCONSUMER <key> <groupname> <consumer> -- Remove the specified consumer.",
|
||||
@@ -1715,39 +1671,13 @@ NULL
|
||||
sds grpname = NULL;
|
||||
streamCG *cg = NULL;
|
||||
char *opt = c->argv[1]->ptr; /* Subcommand name. */
|
||||
int mkstream = 0;
|
||||
robj *o;
|
||||
|
||||
/* CREATE has an MKSTREAM option that creates the stream if it
|
||||
* does not exist. */
|
||||
if (c->argc == 6 && !strcasecmp(opt,"CREATE")) {
|
||||
if (strcasecmp(c->argv[5]->ptr,"MKSTREAM")) {
|
||||
addReplySubcommandSyntaxError(c);
|
||||
return;
|
||||
}
|
||||
mkstream = 1;
|
||||
grpname = c->argv[3]->ptr;
|
||||
}
|
||||
|
||||
/* Everything but the "HELP" option requires a key and group name. */
|
||||
/* Lookup the key now, this is common for all the subcommands but HELP. */
|
||||
if (c->argc >= 4) {
|
||||
o = lookupKeyWrite(c->db,c->argv[2]);
|
||||
if (o) s = o->ptr;
|
||||
robj *o = lookupKeyWriteOrReply(c,c->argv[2],shared.nokeyerr);
|
||||
if (o == NULL || checkType(c,o,OBJ_STREAM)) return;
|
||||
s = o->ptr;
|
||||
grpname = c->argv[3]->ptr;
|
||||
}
|
||||
|
||||
/* Check for missing key/group. */
|
||||
if (c->argc >= 4 && !mkstream) {
|
||||
/* At this point key must exist, or there is an error. */
|
||||
if (o == NULL) {
|
||||
addReplyError(c,
|
||||
"The XGROUP subcommand requires the key to exist. "
|
||||
"Note that for CREATE you may want to use the MKSTREAM "
|
||||
"option to create an empty stream automatically.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkType(c,o,OBJ_STREAM)) return;
|
||||
|
||||
/* Certain subcommands require the group to exist. */
|
||||
if ((cg = streamLookupCG(s,grpname)) == NULL &&
|
||||
@@ -1762,26 +1692,13 @@ NULL
|
||||
}
|
||||
|
||||
/* Dispatch the different subcommands. */
|
||||
if (!strcasecmp(opt,"CREATE") && (c->argc == 5 || c->argc == 6)) {
|
||||
if (!strcasecmp(opt,"CREATE") && c->argc == 5) {
|
||||
streamID id;
|
||||
if (!strcmp(c->argv[4]->ptr,"$")) {
|
||||
if (s) {
|
||||
id = s->last_id;
|
||||
} else {
|
||||
id.ms = 0;
|
||||
id.seq = 0;
|
||||
}
|
||||
id = s->last_id;
|
||||
} else if (streamParseStrictIDOrReply(c,c->argv[4],&id,0) != C_OK) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Handle the MKSTREAM option now that the command can no longer fail. */
|
||||
if (s == NULL && mkstream) {
|
||||
o = createStreamObject();
|
||||
dbAdd(c->db,c->argv[2],o);
|
||||
s = o->ptr;
|
||||
}
|
||||
|
||||
streamCG *cg = streamCreateCG(s,grpname,sdslen(grpname),&id);
|
||||
if (cg) {
|
||||
addReply(c,shared.ok);
|
||||
@@ -1829,40 +1746,6 @@ NULL
|
||||
}
|
||||
}
|
||||
|
||||
/* XSETID <stream> <groupname> <id>
|
||||
*
|
||||
* Set the internal "last ID" of a stream. */
|
||||
void xsetidCommand(client *c) {
|
||||
robj *o = lookupKeyWriteOrReply(c,c->argv[1],shared.nokeyerr);
|
||||
if (o == NULL || checkType(c,o,OBJ_STREAM)) return;
|
||||
|
||||
stream *s = o->ptr;
|
||||
streamID id;
|
||||
if (streamParseStrictIDOrReply(c,c->argv[2],&id,0) != C_OK) return;
|
||||
|
||||
/* If the stream has at least one item, we want to check that the user
|
||||
* is setting a last ID that is equal or greater than the current top
|
||||
* item, otherwise the fundamental ID monotonicity assumption is violated. */
|
||||
if (s->length > 0) {
|
||||
streamID maxid;
|
||||
streamIterator si;
|
||||
streamIteratorStart(&si,s,NULL,NULL,1);
|
||||
int64_t numfields;
|
||||
streamIteratorGetID(&si,&maxid,&numfields);
|
||||
streamIteratorStop(&si);
|
||||
|
||||
if (streamCompareID(&id,&maxid) < 0) {
|
||||
addReplyError(c,"The ID specified in XSETID is smaller than the "
|
||||
"target stream top item");
|
||||
return;
|
||||
}
|
||||
}
|
||||
s->last_id = id;
|
||||
addReply(c,shared.ok);
|
||||
server.dirty++;
|
||||
notifyKeyspaceEvent(NOTIFY_STREAM,"xsetid",c->argv[1],c->db->id);
|
||||
}
|
||||
|
||||
/* XACK <key> <group> <id> <id> ... <id>
|
||||
*
|
||||
* Acknowledge a message as processed. In practical terms we just check the
|
||||
@@ -1908,7 +1791,7 @@ void xackCommand(client *c) {
|
||||
addReplyLongLong(c,acknowledged);
|
||||
}
|
||||
|
||||
/* XPENDING <key> <group> [<start> <stop> <count> [<consumer>]]
|
||||
/* XPENDING <key> <group> [<start> <stop> <count>] [<consumer>]
|
||||
*
|
||||
* If start and stop are omitted, the command just outputs information about
|
||||
* the amount of pending messages for the key/group pair, together with
|
||||
@@ -1937,7 +1820,6 @@ void xpendingCommand(client *c) {
|
||||
if (c->argc >= 6) {
|
||||
if (getLongLongFromObjectOrReply(c,c->argv[5],&count,NULL) == C_ERR)
|
||||
return;
|
||||
if (count < 0) count = 0;
|
||||
if (streamParseIDOrReply(c,c->argv[3],&startid,0) == C_ERR)
|
||||
return;
|
||||
if (streamParseIDOrReply(c,c->argv[4],&endid,UINT64_MAX) == C_ERR)
|
||||
@@ -2111,14 +1993,6 @@ void xpendingCommand(client *c) {
|
||||
* Return just an array of IDs of messages successfully claimed,
|
||||
* without returning the actual message.
|
||||
*
|
||||
* 6. LASTID <id>:
|
||||
* Update the consumer group last ID with the specified ID if the
|
||||
* current last ID is smaller than the provided one.
|
||||
* This is used for replication / AOF, so that when we read from a
|
||||
* consumer group, the XCLAIM that gets propagated to give ownership
|
||||
* to the consumer, is also used in order to update the group current
|
||||
* ID.
|
||||
*
|
||||
* The command returns an array of messages that the user
|
||||
* successfully claimed, so that the caller is able to understand
|
||||
* what messages it is now in charge of. */
|
||||
@@ -2163,9 +2037,7 @@ void xclaimCommand(client *c) {
|
||||
|
||||
/* If we stopped because some IDs cannot be parsed, perhaps they
|
||||
* are trailing options. */
|
||||
mstime_t now = mstime();
|
||||
streamID last_id = {0,0};
|
||||
int propagate_last_id = 0;
|
||||
time_t now = mstime();
|
||||
for (; j < c->argc; j++) {
|
||||
int moreargs = (c->argc-1) - j; /* Number of additional arguments. */
|
||||
char *opt = c->argv[j]->ptr;
|
||||
@@ -2189,20 +2061,12 @@ void xclaimCommand(client *c) {
|
||||
if (getLongLongFromObjectOrReply(c,c->argv[j],&retrycount,
|
||||
"Invalid RETRYCOUNT option argument for XCLAIM")
|
||||
!= C_OK) return;
|
||||
} else if (!strcasecmp(opt,"LASTID") && moreargs) {
|
||||
j++;
|
||||
if (streamParseStrictIDOrReply(c,c->argv[j],&last_id,0) != C_OK) return;
|
||||
} else {
|
||||
addReplyErrorFormat(c,"Unrecognized XCLAIM option '%s'",opt);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (streamCompareID(&last_id,&group->last_id) > 0) {
|
||||
group->last_id = last_id;
|
||||
propagate_last_id = 1;
|
||||
}
|
||||
|
||||
if (deliverytime != -1) {
|
||||
/* If a delivery time was passed, either with IDLE or TIME, we
|
||||
* do some sanity check on it, and set the deliverytime to now
|
||||
@@ -2226,8 +2090,7 @@ void xclaimCommand(client *c) {
|
||||
for (int j = 5; j <= last_id_arg; j++) {
|
||||
streamID id;
|
||||
unsigned char buf[sizeof(streamID)];
|
||||
if (streamParseStrictIDOrReply(c,c->argv[j],&id,0) != C_OK)
|
||||
serverPanic("StreamID invalid after check. Should not be possible.");
|
||||
if (streamParseStrictIDOrReply(c,c->argv[j],&id,0) != C_OK) return;
|
||||
streamEncodeID(buf,&id);
|
||||
|
||||
/* Lookup the ID in the group PEL. */
|
||||
@@ -2257,12 +2120,8 @@ void xclaimCommand(client *c) {
|
||||
|
||||
if (nack != raxNotFound) {
|
||||
/* We need to check if the minimum idle time requested
|
||||
* by the caller is satisfied by this entry.
|
||||
*
|
||||
* Note that the nack could be created by FORCE, in this
|
||||
* case there was no pre-existing entry and minidle should
|
||||
* be ignored, but in that case nick->consumer is NULL. */
|
||||
if (nack->consumer && minidle) {
|
||||
* by the caller is satisfied by this entry. */
|
||||
if (minidle) {
|
||||
mstime_t this_idle = now - nack->delivery_time;
|
||||
if (this_idle < minidle) continue;
|
||||
}
|
||||
@@ -2282,22 +2141,16 @@ void xclaimCommand(client *c) {
|
||||
if (justid) {
|
||||
addReplyStreamID(c,&id);
|
||||
} else {
|
||||
size_t emitted = streamReplyWithRange(c,o->ptr,&id,&id,1,0,
|
||||
NULL,NULL,STREAM_RWR_RAWENTRIES,NULL);
|
||||
if (!emitted) addReply(c,shared.nullbulk);
|
||||
streamReplyWithRange(c,o->ptr,&id,NULL,1,0,NULL,NULL,
|
||||
STREAM_RWR_RAWENTRIES,NULL);
|
||||
}
|
||||
arraylen++;
|
||||
|
||||
/* Propagate this change. */
|
||||
streamPropagateXCLAIM(c,c->argv[1],group,c->argv[2],c->argv[j],nack);
|
||||
propagate_last_id = 0; /* Will be propagated by XCLAIM itself. */
|
||||
streamPropagateXCLAIM(c,c->argv[1],c->argv[3],c->argv[j],nack);
|
||||
server.dirty++;
|
||||
}
|
||||
}
|
||||
if (propagate_last_id) {
|
||||
streamPropagateGroupID(c,c->argv[1],group,c->argv[2]);
|
||||
server.dirty++;
|
||||
}
|
||||
setDeferredMultiBulkLength(c,arraylenptr,arraylen);
|
||||
preventCommandPropagation(c);
|
||||
}
|
||||
@@ -2474,11 +2327,11 @@ NULL
|
||||
if (idle < 0) idle = 0;
|
||||
|
||||
addReplyMultiBulkLen(c,6);
|
||||
addReplyBulkCString(c,"name");
|
||||
addReplyStatus(c,"name");
|
||||
addReplyBulkCBuffer(c,consumer->name,sdslen(consumer->name));
|
||||
addReplyBulkCString(c,"pending");
|
||||
addReplyStatus(c,"pending");
|
||||
addReplyLongLong(c,raxSize(consumer->pel));
|
||||
addReplyBulkCString(c,"idle");
|
||||
addReplyStatus(c,"idle");
|
||||
addReplyLongLong(c,idle);
|
||||
}
|
||||
raxStop(&ri);
|
||||
@@ -2496,28 +2349,28 @@ NULL
|
||||
while(raxNext(&ri)) {
|
||||
streamCG *cg = ri.data;
|
||||
addReplyMultiBulkLen(c,8);
|
||||
addReplyBulkCString(c,"name");
|
||||
addReplyStatus(c,"name");
|
||||
addReplyBulkCBuffer(c,ri.key,ri.key_len);
|
||||
addReplyBulkCString(c,"consumers");
|
||||
addReplyStatus(c,"consumers");
|
||||
addReplyLongLong(c,raxSize(cg->consumers));
|
||||
addReplyBulkCString(c,"pending");
|
||||
addReplyStatus(c,"pending");
|
||||
addReplyLongLong(c,raxSize(cg->pel));
|
||||
addReplyBulkCString(c,"last-delivered-id");
|
||||
addReplyStatus(c,"last-delivered-id");
|
||||
addReplyStreamID(c,&cg->last_id);
|
||||
}
|
||||
raxStop(&ri);
|
||||
} else if (!strcasecmp(opt,"STREAM") && c->argc == 3) {
|
||||
/* XINFO STREAM <key> (or the alias XINFO <key>). */
|
||||
addReplyMultiBulkLen(c,14);
|
||||
addReplyBulkCString(c,"length");
|
||||
addReplyStatus(c,"length");
|
||||
addReplyLongLong(c,s->length);
|
||||
addReplyBulkCString(c,"radix-tree-keys");
|
||||
addReplyStatus(c,"radix-tree-keys");
|
||||
addReplyLongLong(c,raxSize(s->rax));
|
||||
addReplyBulkCString(c,"radix-tree-nodes");
|
||||
addReplyStatus(c,"radix-tree-nodes");
|
||||
addReplyLongLong(c,s->rax->numnodes);
|
||||
addReplyBulkCString(c,"groups");
|
||||
addReplyStatus(c,"groups");
|
||||
addReplyLongLong(c,s->cgroups ? raxSize(s->cgroups) : 0);
|
||||
addReplyBulkCString(c,"last-generated-id");
|
||||
addReplyStatus(c,"last-generated-id");
|
||||
addReplyStreamID(c,&s->last_id);
|
||||
|
||||
/* To emit the first/last entry we us the streamReplyWithRange()
|
||||
@@ -2526,11 +2379,11 @@ NULL
|
||||
streamID start, end;
|
||||
start.ms = start.seq = 0;
|
||||
end.ms = end.seq = UINT64_MAX;
|
||||
addReplyBulkCString(c,"first-entry");
|
||||
addReplyStatus(c,"first-entry");
|
||||
count = streamReplyWithRange(c,s,&start,&end,1,0,NULL,NULL,
|
||||
STREAM_RWR_RAWENTRIES,NULL);
|
||||
if (!count) addReply(c,shared.nullbulk);
|
||||
addReplyBulkCString(c,"last-entry");
|
||||
addReplyStatus(c,"last-entry");
|
||||
count = streamReplyWithRange(c,s,&start,&end,1,1,NULL,NULL,
|
||||
STREAM_RWR_RAWENTRIES,NULL);
|
||||
if (!count) addReply(c,shared.nullbulk);
|
||||
|
||||
+7
-5
@@ -301,22 +301,24 @@ void mgetCommand(client *c) {
|
||||
}
|
||||
|
||||
void msetGenericCommand(client *c, int nx) {
|
||||
int j;
|
||||
int j, busykeys = 0;
|
||||
|
||||
if ((c->argc % 2) == 0) {
|
||||
addReplyError(c,"wrong number of arguments for MSET");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Handle the NX flag. The MSETNX semantic is to return zero and don't
|
||||
* set anything if at least one key alerady exists. */
|
||||
* set nothing at all if at least one already key exists. */
|
||||
if (nx) {
|
||||
for (j = 1; j < c->argc; j += 2) {
|
||||
if (lookupKeyWrite(c->db,c->argv[j]) != NULL) {
|
||||
addReply(c, shared.czero);
|
||||
return;
|
||||
busykeys++;
|
||||
}
|
||||
}
|
||||
if (busykeys) {
|
||||
addReply(c, shared.czero);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (j = 1; j < c->argc; j += 2) {
|
||||
|
||||
+8
-6
@@ -574,12 +574,12 @@ int zslParseLexRangeItem(robj *item, sds *dest, int *ex) {
|
||||
switch(c[0]) {
|
||||
case '+':
|
||||
if (c[1] != '\0') return C_ERR;
|
||||
*ex = 1;
|
||||
*ex = 0;
|
||||
*dest = shared.maxstring;
|
||||
return C_OK;
|
||||
case '-':
|
||||
if (c[1] != '\0') return C_ERR;
|
||||
*ex = 1;
|
||||
*ex = 0;
|
||||
*dest = shared.minstring;
|
||||
return C_OK;
|
||||
case '(':
|
||||
@@ -652,8 +652,9 @@ int zslIsInLexRange(zskiplist *zsl, zlexrangespec *range) {
|
||||
zskiplistNode *x;
|
||||
|
||||
/* Test for ranges that will always be empty. */
|
||||
int cmp = sdscmplex(range->min,range->max);
|
||||
if (cmp > 0 || (cmp == 0 && (range->minex || range->maxex)))
|
||||
if (sdscmplex(range->min,range->max) > 1 ||
|
||||
(sdscmp(range->min,range->max) == 0 &&
|
||||
(range->minex || range->maxex)))
|
||||
return 0;
|
||||
x = zsl->tail;
|
||||
if (x == NULL || !zslLexValueGteMin(x->ele,range))
|
||||
@@ -926,8 +927,9 @@ int zzlIsInLexRange(unsigned char *zl, zlexrangespec *range) {
|
||||
unsigned char *p;
|
||||
|
||||
/* Test for ranges that will always be empty. */
|
||||
int cmp = sdscmplex(range->min,range->max);
|
||||
if (cmp > 0 || (cmp == 0 && (range->minex || range->maxex)))
|
||||
if (sdscmplex(range->min,range->max) > 1 ||
|
||||
(sdscmp(range->min,range->max) == 0 &&
|
||||
(range->minex || range->maxex)))
|
||||
return 0;
|
||||
|
||||
p = ziplistIndex(zl,-2); /* Last element. */
|
||||
|
||||
+2
-37
@@ -39,7 +39,6 @@
|
||||
#include <float.h>
|
||||
#include <stdint.h>
|
||||
#include <errno.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "util.h"
|
||||
#include "sha1.h"
|
||||
@@ -48,7 +47,7 @@
|
||||
int stringmatchlen(const char *pattern, int patternLen,
|
||||
const char *string, int stringLen, int nocase)
|
||||
{
|
||||
while(patternLen && stringLen) {
|
||||
while(patternLen) {
|
||||
switch(pattern[0]) {
|
||||
case '*':
|
||||
while (pattern[1] == '*') {
|
||||
@@ -171,22 +170,6 @@ int stringmatch(const char *pattern, const char *string, int nocase) {
|
||||
return stringmatchlen(pattern,strlen(pattern),string,strlen(string),nocase);
|
||||
}
|
||||
|
||||
/* Fuzz stringmatchlen() trying to crash it with bad input. */
|
||||
int stringmatchlen_fuzz_test(void) {
|
||||
char str[32];
|
||||
char pat[32];
|
||||
int cycles = 10000000;
|
||||
int total_matches = 0;
|
||||
while(cycles--) {
|
||||
int strlen = rand() % sizeof(str);
|
||||
int patlen = rand() % sizeof(pat);
|
||||
for (int j = 0; j < strlen; j++) str[j] = rand() % 128;
|
||||
for (int j = 0; j < patlen; j++) pat[j] = rand() % 128;
|
||||
total_matches += stringmatchlen(pat, patlen, str, strlen, 0);
|
||||
}
|
||||
return total_matches;
|
||||
}
|
||||
|
||||
/* Convert a string representing an amount of memory into the number of
|
||||
* bytes, so for instance memtoll("1Gb") will return 1073741824 that is
|
||||
* (1024*1024*1024).
|
||||
@@ -622,7 +605,7 @@ void getRandomHexChars(char *p, size_t len) {
|
||||
* already, this will be detected and handled correctly.
|
||||
*
|
||||
* The function does not try to normalize everything, but only the obvious
|
||||
* case of one or more "../" appearing at the start of "filename"
|
||||
* case of one or more "../" appearning at the start of "filename"
|
||||
* relative path. */
|
||||
sds getAbsolutePath(char *filename) {
|
||||
char cwd[1024];
|
||||
@@ -669,24 +652,6 @@ sds getAbsolutePath(char *filename) {
|
||||
return abspath;
|
||||
}
|
||||
|
||||
/*
|
||||
* Gets the proper timezone in a more portable fashion
|
||||
* i.e timezone variables are linux specific.
|
||||
*/
|
||||
|
||||
unsigned long getTimeZone(void) {
|
||||
#ifdef __linux__
|
||||
return timezone;
|
||||
#else
|
||||
struct timeval tv;
|
||||
struct timezone tz;
|
||||
|
||||
gettimeofday(&tv, &tz);
|
||||
|
||||
return tz.tz_minuteswest * 60UL;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Return true if the specified path is just a file basename without any
|
||||
* relative or absolute path. This function just checks that no / or \
|
||||
* character exists inside the specified path, that's enough in the
|
||||
|
||||
@@ -40,7 +40,6 @@
|
||||
|
||||
int stringmatchlen(const char *p, int plen, const char *s, int slen, int nocase);
|
||||
int stringmatch(const char *p, const char *s, int nocase);
|
||||
int stringmatchlen_fuzz_test(void);
|
||||
long long memtoll(const char *p, int *err);
|
||||
uint32_t digits10(uint64_t v);
|
||||
uint32_t sdigits10(int64_t v);
|
||||
@@ -51,7 +50,6 @@ int string2ld(const char *s, size_t slen, long double *dp);
|
||||
int d2string(char *buf, size_t len, double value);
|
||||
int ld2string(char *buf, size_t len, long double value, int humanfriendly);
|
||||
sds getAbsolutePath(char *filename);
|
||||
unsigned long getTimeZone(void);
|
||||
int pathIsBaseName(char *path);
|
||||
|
||||
#ifdef REDIS_TEST
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
#define REDIS_VERSION "5.0.3"
|
||||
#define REDIS_VERSION "999.999.999"
|
||||
|
||||
@@ -4,7 +4,7 @@ set ::valgrind_errors {}
|
||||
|
||||
proc start_server_error {config_file error} {
|
||||
set err {}
|
||||
append err "Cant' start the Redis server\n"
|
||||
append err "Can't start the Redis server\n"
|
||||
append err "CONFIGURATION:"
|
||||
append err [exec cat $config_file]
|
||||
append err "\nERROR:"
|
||||
|
||||
@@ -91,14 +91,6 @@ proc wait_for_sync r {
|
||||
}
|
||||
}
|
||||
|
||||
proc wait_for_ofs_sync {r1 r2} {
|
||||
wait_for_condition 50 100 {
|
||||
[status $r1 master_repl_offset] eq [status $r2 master_repl_offset]
|
||||
} else {
|
||||
fail "replica didn't sync in time"
|
||||
}
|
||||
}
|
||||
|
||||
# Random integer between 0 and max (excluded).
|
||||
proc randomInt {max} {
|
||||
expr {int(rand()*$max)}
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ start_server {tags {"dump"}} {
|
||||
set now [clock milliseconds]
|
||||
r restore foo [expr $now+3000] $encoded absttl
|
||||
set ttl [r pttl foo]
|
||||
assert {$ttl >= 2900 && $ttl <= 3100}
|
||||
assert {$ttl >= 2990 && $ttl <= 3000}
|
||||
r get foo
|
||||
} {bar}
|
||||
|
||||
|
||||
@@ -90,7 +90,6 @@ start_server {tags {"defrag"}} {
|
||||
test "Active defrag big keys" {
|
||||
r flushdb
|
||||
r config resetstat
|
||||
r config set save "" ;# prevent bgsave from interfereing with save below
|
||||
r config set activedefrag no
|
||||
r config set active-defrag-max-scan-fields 1000
|
||||
r config set active-defrag-threshold-lower 5
|
||||
|
||||
@@ -9,17 +9,6 @@ start_server {
|
||||
set err
|
||||
} {BUSYGROUP*}
|
||||
|
||||
test {XGROUP CREATE: automatic stream creation fails without MKSTREAM} {
|
||||
r DEL mystream
|
||||
catch {r XGROUP CREATE mystream mygroup $} err
|
||||
set err
|
||||
} {ERR*}
|
||||
|
||||
test {XGROUP CREATE: automatic stream creation works with MKSTREAM} {
|
||||
r DEL mystream
|
||||
r XGROUP CREATE mystream mygroup $ MKSTREAM
|
||||
} {OK}
|
||||
|
||||
test {XREADGROUP will return only new elements} {
|
||||
r XADD mystream * a 1
|
||||
r XADD mystream * b 2
|
||||
@@ -107,142 +96,4 @@ start_server {
|
||||
set c [llength [lindex [r xreadgroup group g1 c2 streams events >] 0 1]]
|
||||
assert {$c == 5}
|
||||
}
|
||||
|
||||
test {XREADGROUP will not report data on empty history. Bug #5577} {
|
||||
r del events
|
||||
r xadd events * a 1
|
||||
r xadd events * b 2
|
||||
r xadd events * c 3
|
||||
r xgroup create events mygroup 0
|
||||
|
||||
# Current local PEL should be empty
|
||||
set res [r xpending events mygroup - + 10]
|
||||
assert {[llength $res] == 0}
|
||||
|
||||
# So XREADGROUP should read an empty history as well
|
||||
set res [r xreadgroup group mygroup myconsumer count 3 streams events 0]
|
||||
assert {[llength [lindex $res 0 1]] == 0}
|
||||
|
||||
# We should fetch all the elements in the stream asking for >
|
||||
set res [r xreadgroup group mygroup myconsumer count 3 streams events >]
|
||||
assert {[llength [lindex $res 0 1]] == 3}
|
||||
|
||||
# Now the history is populated with three not acked entries
|
||||
set res [r xreadgroup group mygroup myconsumer count 3 streams events 0]
|
||||
assert {[llength [lindex $res 0 1]] == 3}
|
||||
}
|
||||
|
||||
test {XREADGROUP history reporting of deleted entries. Bug #5570} {
|
||||
r del mystream
|
||||
r XGROUP CREATE mystream mygroup $ MKSTREAM
|
||||
r XADD mystream 1 field1 A
|
||||
r XREADGROUP GROUP mygroup myconsumer STREAMS mystream >
|
||||
r XADD mystream MAXLEN 1 2 field1 B
|
||||
r XREADGROUP GROUP mygroup myconsumer STREAMS mystream >
|
||||
|
||||
# Now we have two pending entries, however one should be deleted
|
||||
# and one should be ok (we should only see "B")
|
||||
set res [r XREADGROUP GROUP mygroup myconsumer STREAMS mystream 0-1]
|
||||
assert {[lindex $res 0 1 0] == {1-0 {}}}
|
||||
assert {[lindex $res 0 1 1] == {2-0 {field1 B}}}
|
||||
}
|
||||
|
||||
test {XCLAIM can claim PEL items from another consumer} {
|
||||
# Add 3 items into the stream, and create a consumer group
|
||||
r del mystream
|
||||
set id1 [r XADD mystream * a 1]
|
||||
set id2 [r XADD mystream * b 2]
|
||||
set id3 [r XADD mystream * c 3]
|
||||
r XGROUP CREATE mystream mygroup 0
|
||||
|
||||
# Client 1 reads item 1 from the stream without acknowledgements.
|
||||
# Client 2 then claims pending item 1 from the PEL of client 1
|
||||
set reply [
|
||||
r XREADGROUP GROUP mygroup client1 count 1 STREAMS mystream >
|
||||
]
|
||||
assert {[llength [lindex $reply 0 1 0 1]] == 2}
|
||||
assert {[lindex $reply 0 1 0 1] eq {a 1}}
|
||||
r debug sleep 0.2
|
||||
set reply [
|
||||
r XCLAIM mystream mygroup client2 10 $id1
|
||||
]
|
||||
assert {[llength [lindex $reply 0 1]] == 2}
|
||||
assert {[lindex $reply 0 1] eq {a 1}}
|
||||
|
||||
# Client 1 reads another 2 items from stream
|
||||
r XREADGROUP GROUP mygroup client1 count 2 STREAMS mystream >
|
||||
r debug sleep 0.2
|
||||
|
||||
# Delete item 2 from the stream. Now client 1 has PEL that contains
|
||||
# only item 3. Try to use client 2 to claim the deleted item 2
|
||||
# from the PEL of client 1, this should return nil
|
||||
r XDEL mystream $id2
|
||||
set reply [
|
||||
r XCLAIM mystream mygroup client2 10 $id2
|
||||
]
|
||||
assert {[llength $reply] == 1}
|
||||
assert_equal "" [lindex $reply 0]
|
||||
|
||||
# Delete item 3 from the stream. Now client 1 has PEL that is empty.
|
||||
# Try to use client 2 to claim the deleted item 3 from the PEL
|
||||
# of client 1, this should return nil
|
||||
r debug sleep 0.2
|
||||
r XDEL mystream $id3
|
||||
set reply [
|
||||
r XCLAIM mystream mygroup client2 10 $id3
|
||||
]
|
||||
assert {[llength $reply] == 1}
|
||||
assert_equal "" [lindex $reply 0]
|
||||
}
|
||||
|
||||
start_server {} {
|
||||
set master [srv -1 client]
|
||||
set master_host [srv -1 host]
|
||||
set master_port [srv -1 port]
|
||||
set slave [srv 0 client]
|
||||
|
||||
foreach noack {0 1} {
|
||||
test "Consumer group last ID propagation to slave (NOACK=$noack)" {
|
||||
$slave slaveof $master_host $master_port
|
||||
wait_for_condition 50 100 {
|
||||
[s 0 master_link_status] eq {up}
|
||||
} else {
|
||||
fail "Replication not started."
|
||||
}
|
||||
|
||||
$master del stream
|
||||
$master xadd stream * a 1
|
||||
$master xadd stream * a 2
|
||||
$master xadd stream * a 3
|
||||
$master xgroup create stream mygroup 0
|
||||
|
||||
# Consume the first two items on the master
|
||||
for {set j 0} {$j < 2} {incr j} {
|
||||
if {$noack} {
|
||||
set item [$master xreadgroup group mygroup \
|
||||
myconsumer COUNT 1 NOACK STREAMS stream >]
|
||||
} else {
|
||||
set item [$master xreadgroup group mygroup \
|
||||
myconsumer COUNT 1 STREAMS stream >]
|
||||
}
|
||||
set id [lindex $item 0 1 0 0]
|
||||
if {$noack == 0} {
|
||||
assert {[$master xack stream mygroup $id] eq "1"}
|
||||
}
|
||||
}
|
||||
|
||||
wait_for_ofs_sync $master $slave
|
||||
|
||||
# Turn slave into master
|
||||
$slave slaveof no one
|
||||
|
||||
set item [$slave xreadgroup group mygroup myconsumer \
|
||||
COUNT 1 STREAMS stream >]
|
||||
|
||||
# The consumed enty should be the third
|
||||
set myentry [lindex $item 0 1 0 1]
|
||||
assert {$myentry eq {a 3}}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,51 +363,3 @@ start_server {tags {"stream"} overrides {appendonly yes stream-node-max-entries
|
||||
assert {[r xlen mystream] == 90}
|
||||
}
|
||||
}
|
||||
|
||||
start_server {tags {"xsetid"}} {
|
||||
test {XADD can CREATE an empty stream} {
|
||||
r XADD mystream MAXLEN 0 * a b
|
||||
assert {[dict get [r xinfo stream mystream] length] == 0}
|
||||
}
|
||||
|
||||
test {XSETID can set a specific ID} {
|
||||
r XSETID mystream "200-0"
|
||||
assert {[dict get [r xinfo stream mystream] last-generated-id] == "200-0"}
|
||||
}
|
||||
|
||||
test {XSETID cannot SETID with smaller ID} {
|
||||
r XADD mystream * a b
|
||||
catch {r XSETID mystream "1-1"} err
|
||||
r XADD mystream MAXLEN 0 * a b
|
||||
set err
|
||||
} {ERR*smaller*}
|
||||
|
||||
test {XSETID cannot SETID on non-existent key} {
|
||||
catch {r XSETID stream 1-1} err
|
||||
set _ $err
|
||||
} {ERR no such key}
|
||||
}
|
||||
|
||||
start_server {tags {"stream"} overrides {appendonly yes aof-use-rdb-preamble no}} {
|
||||
test {Empty stream can be rewrite into AOF correctly} {
|
||||
r XADD mystream MAXLEN 0 * a b
|
||||
assert {[dict get [r xinfo stream mystream] length] == 0}
|
||||
r bgrewriteaof
|
||||
waitForBgrewriteaof r
|
||||
r debug loadaof
|
||||
assert {[dict get [r xinfo stream mystream] length] == 0}
|
||||
}
|
||||
|
||||
test {Stream can be rewrite into AOF correctly after XDEL lastid} {
|
||||
r XSETID mystream 0-0
|
||||
r XADD mystream 1-1 a b
|
||||
r XADD mystream 2-2 a b
|
||||
assert {[dict get [r xinfo stream mystream] length] == 2}
|
||||
r XDEL mystream 2-2
|
||||
r bgrewriteaof
|
||||
waitForBgrewriteaof r
|
||||
r debug loadaof
|
||||
assert {[dict get [r xinfo stream mystream] length] == 1}
|
||||
assert {[dict get [r xinfo stream mystream] last-generated-id] == "2-2"}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,7 +388,7 @@ start_server {tags {"zset"}} {
|
||||
0 omega}
|
||||
}
|
||||
|
||||
test "ZRANGEBYLEX/ZREVRANGEBYLEX/ZLEXCOUNT basics" {
|
||||
test "ZRANGEBYLEX/ZREVRANGEBYLEX/ZCOUNT basics" {
|
||||
create_default_lex_zset
|
||||
|
||||
# inclusive range
|
||||
@@ -416,22 +416,6 @@ start_server {tags {"zset"}} {
|
||||
assert_equal {} [r zrevrangebylex zset \[elez \[elex]
|
||||
assert_equal {} [r zrevrangebylex zset (hill (omega]
|
||||
}
|
||||
|
||||
test "ZLEXCOUNT advanced" {
|
||||
create_default_lex_zset
|
||||
|
||||
assert_equal 9 [r zlexcount zset - +]
|
||||
assert_equal 0 [r zlexcount zset + -]
|
||||
assert_equal 0 [r zlexcount zset + \[c]
|
||||
assert_equal 0 [r zlexcount zset \[c -]
|
||||
assert_equal 8 [r zlexcount zset \[bar +]
|
||||
assert_equal 5 [r zlexcount zset \[bar \[foo]
|
||||
assert_equal 4 [r zlexcount zset \[bar (foo]
|
||||
assert_equal 4 [r zlexcount zset (bar \[foo]
|
||||
assert_equal 3 [r zlexcount zset (bar (foo]
|
||||
assert_equal 5 [r zlexcount zset - (foo]
|
||||
assert_equal 1 [r zlexcount zset (maxstring +]
|
||||
}
|
||||
|
||||
test "ZRANGEBYSLEX with LIMIT" {
|
||||
create_default_lex_zset
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
#!/usr/bin/env tclsh
|
||||
|
||||
if {[llength $::argv] != 2 && [llength $::argv] != 3} {
|
||||
puts "Usage: $::argv0 <branch> <version> \[<num-commits>\]"
|
||||
if {[llength $::argv] != 2} {
|
||||
puts "Usage: $::argv0 <branch> <version>"
|
||||
exit 1
|
||||
}
|
||||
|
||||
set branch [lindex $::argv 0]
|
||||
set ver [lindex $::argv 1]
|
||||
if {[llength $::argv] == 3} {
|
||||
set count [lindex ::$argv 2]
|
||||
} else {
|
||||
set count 100
|
||||
}
|
||||
|
||||
set template {
|
||||
================================================================================
|
||||
@@ -26,7 +21,7 @@ append template "\n\n"
|
||||
set date [clock format [clock seconds]]
|
||||
set template [string map [list %ver% $ver %date% $date] $template]
|
||||
|
||||
append template [exec git log $branch~$count..$branch "--format=format:%an in commit %h:%n %s" --shortstat]
|
||||
append template [exec git log $branch~100..$branch "--format=format:%an in commit %h:%n %s" --shortstat]
|
||||
|
||||
#Older, more verbose version.
|
||||
#
|
||||
|
||||
Reference in New Issue
Block a user