Compare commits
521
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
145ab1dc98 | ||
|
|
238c9bd086 | ||
|
|
ee724cac49 | ||
|
|
d745e8dfb3 | ||
|
|
398b2084af | ||
|
|
12e65a424c | ||
|
|
5182dcd645 | ||
|
|
77653ebe8a | ||
|
|
de276b6a43 | ||
|
|
f43eb5adcf | ||
|
|
522760fac7 | ||
|
|
482d678e95 | ||
|
|
b23c8babed | ||
|
|
8b51121998 | ||
|
|
62a4b817c6 | ||
|
|
03cfc8bf3a | ||
|
|
c56fbb246c | ||
|
|
479ea57797 | ||
|
|
7c6ddbc37d | ||
|
|
d1176b582c | ||
|
|
6830fa25e3 | ||
|
|
e4903ce586 | ||
|
|
42387d6c1a | ||
|
|
de809666f8 | ||
|
|
3f232ebfb8 | ||
|
|
e6c3bcf9e0 | ||
|
|
b9491b65d9 | ||
|
|
664bbfe760 | ||
|
|
2869284e44 | ||
|
|
60d26acfc8 | ||
|
|
c6eca690ee | ||
|
|
68681f2bcf | ||
|
|
6f0b19bc5b | ||
|
|
6a1bf07a46 | ||
|
|
51eb6cb395 | ||
|
|
bd5af03dbd | ||
|
|
ee3884e63c | ||
|
|
d884ba4bc9 | ||
|
|
65a9740fa8 | ||
|
|
8ac76be5f2 | ||
|
|
f42df6f43a | ||
|
|
45fe1f5e00 | ||
|
|
115d076d65 | ||
|
|
9bb18e5438 | ||
|
|
9dc79c039a | ||
|
|
6919280cc5 | ||
|
|
ee3490ec48 | ||
|
|
3c5d773f82 | ||
|
|
0381931b4c | ||
|
|
020fe26bd6 | ||
|
|
abab0b7817 | ||
|
|
671b1f6a9d | ||
|
|
1898c50573 | ||
|
|
5082ec6419 | ||
|
|
50595a5889 | ||
|
|
a4e6aae6b8 | ||
|
|
e53c90308b | ||
|
|
0248a6b125 | ||
|
|
0540803288 | ||
|
|
0c00fd7834 | ||
|
|
f24d3a7de0 | ||
|
|
cea421a021 | ||
|
|
7d0d9693c1 | ||
|
|
3f2d7e277e | ||
|
|
8f00cf85a7 | ||
|
|
ae9065d808 | ||
|
|
eb1230c999 | ||
|
|
fa707ca154 | ||
|
|
7a41b402c1 | ||
|
|
26d4f8e3ec | ||
|
|
01ea018c40 | ||
|
|
9ed40f0fc3 | ||
|
|
a58733cacf | ||
|
|
b1ec333633 | ||
|
|
1a603e1a87 | ||
|
|
94af55c5ea | ||
|
|
3a0b78bc52 | ||
|
|
19b06935d5 | ||
|
|
db89f7474d | ||
|
|
c128190026 | ||
|
|
6468cb2e82 | ||
|
|
b5be5093fe | ||
|
|
2cacdcd6f8 | ||
|
|
0adb43b68f | ||
|
|
6a1c92d52d | ||
|
|
a7d898334a | ||
|
|
110041825c | ||
|
|
fa61720d30 | ||
|
|
e65b4825f0 | ||
|
|
4086dff477 | ||
|
|
f80dfbf464 | ||
|
|
4a377cecd8 | ||
|
|
439120c620 | ||
|
|
ec9bbe96bf | ||
|
|
98d184db12 | ||
|
|
cd18f06e9c | ||
|
|
edd70c1993 | ||
|
|
485014cc74 | ||
|
|
100d43c1ac | ||
|
|
79866a6361 | ||
|
|
045d65c3af | ||
|
|
452ad2e928 | ||
|
|
28dfdca733 | ||
|
|
f11a7585a8 | ||
|
|
6fb04d4637 | ||
|
|
3b9be93fda | ||
|
|
d8f8701032 | ||
|
|
43be967690 | ||
|
|
1b5f56d042 | ||
|
|
2d73cf2367 | ||
|
|
0752a834f9 | ||
|
|
2785d6caa0 | ||
|
|
51d42c88a6 | ||
|
|
59d52f7fab | ||
|
|
565e139a56 | ||
|
|
b998e2035c | ||
|
|
923502a70b | ||
|
|
26826329f5 | ||
|
|
3508b9c440 | ||
|
|
4e55df4b5c | ||
|
|
ad171f5ce0 | ||
|
|
851e9fc48b | ||
|
|
aeee039a87 | ||
|
|
c44732ac58 | ||
|
|
c3806f5b72 | ||
|
|
a13106e001 | ||
|
|
bf71b120f1 | ||
|
|
8c7f90e91e | ||
|
|
06ca9d6839 | ||
|
|
9f131c9a89 | ||
|
|
583c314725 | ||
|
|
53cea97204 | ||
|
|
dfc42ec447 | ||
|
|
75fa7879e6 | ||
|
|
b412c544fd | ||
|
|
29252391c4 | ||
|
|
1c08220022 | ||
|
|
02d38f6b51 | ||
|
|
b28fb3d753 | ||
|
|
dc2df135b3 | ||
|
|
c508cb6793 | ||
|
|
7229fa8d6d | ||
|
|
4d063bb6ba | ||
|
|
9d86ae4597 | ||
|
|
f739c27229 | ||
|
|
58ac57f665 | ||
|
|
adf2701cc9 | ||
|
|
de914ede93 | ||
|
|
ada206559d | ||
|
|
2c70d28295 | ||
|
|
ea2e51c630 | ||
|
|
93037f7642 | ||
|
|
7a808fd8a7 | ||
|
|
57bd8feb8d | ||
|
|
62689ef0cf | ||
|
|
cf9a3f7048 | ||
|
|
30db805548 | ||
|
|
2564963dc8 | ||
|
|
a1944c3e4d | ||
|
|
34d5804d4c | ||
|
|
b8579c225c | ||
|
|
885c4f856e | ||
|
|
6ddf0ea293 | ||
|
|
ffcf7d5ab1 | ||
|
|
b2e295971f | ||
|
|
de474186bd | ||
|
|
6eb996540c | ||
|
|
2bf8c2c130 | ||
|
|
54ad626cce | ||
|
|
6dffc1b7a3 | ||
|
|
cb9dde3280 | ||
|
|
7393fd814e | ||
|
|
b246635d6d | ||
|
|
616c546b01 | ||
|
|
474adba9fa | ||
|
|
fa2c0582cf | ||
|
|
269760edbb | ||
|
|
bb3b5ddd19 | ||
|
|
b541ccef25 | ||
|
|
c1c99e9f4e | ||
|
|
a4152119c6 | ||
|
|
b75ae0bbea | ||
|
|
2d13bf4c59 | ||
|
|
9b4cb4addc | ||
|
|
8885921dfb | ||
|
|
b122cadc66 | ||
|
|
6b9f02ac12 | ||
|
|
447b373fc9 | ||
|
|
202c2ebec4 | ||
|
|
a8c08b9b76 | ||
|
|
34a79c353f | ||
|
|
0c6ea46f2b | ||
|
|
bc64df9a66 | ||
|
|
f06f10e66b | ||
|
|
2e6f285009 | ||
|
|
00954f4d48 | ||
|
|
005d9fa861 | ||
|
|
22bcee5178 | ||
|
|
81fe7a4733 | ||
|
|
dc997755bf | ||
|
|
6b64cc47a0 | ||
|
|
964224b77f | ||
|
|
ae40e5f362 | ||
|
|
25c231c4c1 | ||
|
|
d9565379da | ||
|
|
7c3c472b5e | ||
|
|
ffbbe5a720 | ||
|
|
314043552b | ||
|
|
5bfdfbe174 | ||
|
|
a3778f3b0f | ||
|
|
b1c2e1a19c | ||
|
|
9e7a8c0207 | ||
|
|
2cdf4cc656 | ||
|
|
c32c690de6 | ||
|
|
7560d347da | ||
|
|
b80e467023 | ||
|
|
bd1782fa0a | ||
|
|
8eefc9323d | ||
|
|
f03947a676 | ||
|
|
43aaf96163 | ||
|
|
e74f0aa6d1 | ||
|
|
e1b8b4b6da | ||
|
|
5bd46d33db | ||
|
|
e203a46cf3 | ||
|
|
14c32c3569 | ||
|
|
54e4bbeabd | ||
|
|
58104d8327 | ||
|
|
11182a1a58 | ||
|
|
dfb68cd235 | ||
|
|
fc7ecd8d35 | ||
|
|
6b0670daad | ||
|
|
38dd30af42 | ||
|
|
2d5aa00959 | ||
|
|
87f771bff1 | ||
|
|
2b36950e9b | ||
|
|
51ffd062d3 | ||
|
|
10db81af71 | ||
|
|
ea31a4eae3 | ||
|
|
f9fac7f777 | ||
|
|
413c2bc180 | ||
|
|
09dd7b5ff0 | ||
|
|
eddd8d34c4 | ||
|
|
86e564e9ff | ||
|
|
b2cd9fcab6 | ||
|
|
26e638a8e9 | ||
|
|
7bad78bd2f | ||
|
|
f8547e53f0 | ||
|
|
01a4b9892d | ||
|
|
634c64dd18 | ||
|
|
365dd037dc | ||
|
|
c3998728a2 | ||
|
|
c9097393bf | ||
|
|
936ade80b2 | ||
|
|
f85f36f50d | ||
|
|
d303bca587 | ||
|
|
8b768e8ea4 | ||
|
|
6476f1a979 | ||
|
|
86e9f48a0c | ||
|
|
ef446bf16d | ||
|
|
1d857a99d5 | ||
|
|
0a03187ac4 | ||
|
|
a2146c7592 | ||
|
|
2a84927f35 | ||
|
|
ee5044eef0 | ||
|
|
530fcf8687 | ||
|
|
53cb27b1d7 | ||
|
|
ab9d398835 | ||
|
|
4740424049 | ||
|
|
3fc4bf07cc | ||
|
|
d3b32ca48d | ||
|
|
33035cad04 | ||
|
|
5877c02c51 | ||
|
|
2d1d57eb47 | ||
|
|
591dba8055 | ||
|
|
a3e53cf9bc | ||
|
|
e91b81c612 | ||
|
|
e498d9ee3e | ||
|
|
78211aaaaf | ||
|
|
58f79e2ff4 | ||
|
|
1f598fc2bb | ||
|
|
de786186a5 | ||
|
|
6eb51bf1ec | ||
|
|
9390c384b8 | ||
|
|
ece658713b | ||
|
|
2a51bac44e | ||
|
|
52bc74f221 | ||
|
|
7d9326b1f3 | ||
|
|
9b01b64430 | ||
|
|
e67fb915eb | ||
|
|
79226cb9fa | ||
|
|
6798736909 | ||
|
|
1ed2ff5570 | ||
|
|
7127f15ebe | ||
|
|
3fcf959e60 | ||
|
|
ba4a5a3255 | ||
|
|
275905b328 | ||
|
|
9c500b89fb | ||
|
|
59b06b14c9 | ||
|
|
a4c7f34d3a | ||
|
|
c180bc7d98 | ||
|
|
469d6e2b37 | ||
|
|
c861e1e1ee | ||
|
|
a17390853d | ||
|
|
0fb5c4ebd8 | ||
|
|
e3b8492e83 | ||
|
|
238cebdd5e | ||
|
|
3773c06d28 | ||
|
|
7d9dd80db3 | ||
|
|
189a12afb4 | ||
|
|
22be435efe | ||
|
|
27fe8e9fb2 | ||
|
|
02d02a3754 | ||
|
|
8b7b4d6734 | ||
|
|
da2f9cd186 | ||
|
|
332a05dc33 | ||
|
|
5f88bd320e | ||
|
|
c33493277a | ||
|
|
0a942f1751 | ||
|
|
c4ad4765b0 | ||
|
|
23ec36909e | ||
|
|
271733f4f8 | ||
|
|
3f068b92b9 | ||
|
|
02777bb252 | ||
|
|
8c829d9e43 | ||
|
|
6878a3fedd | ||
|
|
8f7bf2841a | ||
|
|
c5d6f577f0 | ||
|
|
b8286d1fc9 | ||
|
|
104584b95e | ||
|
|
1210af3804 | ||
|
|
4a850be4dc | ||
|
|
f47607af02 | ||
|
|
74720ea993 | ||
|
|
aa5b4be02e | ||
|
|
69ce5c5d10 | ||
|
|
531647bb1b | ||
|
|
01b6966afc | ||
|
|
ffefc9f92d | ||
|
|
91999fce40 | ||
|
|
98db5739cc | ||
|
|
3f9e2322ec | ||
|
|
1409c545da | ||
|
|
94751543b0 | ||
|
|
4acb4da1d1 | ||
|
|
a62f786344 | ||
|
|
59bdd08214 | ||
|
|
443f279a3a | ||
|
|
4b2229e4b8 | ||
|
|
9cc83d2ad9 | ||
|
|
ed7e331051 | ||
|
|
b3391fd853 | ||
|
|
d7826823c0 | ||
|
|
95883313b5 | ||
|
|
2b36706a48 | ||
|
|
7c8ddab4f8 | ||
|
|
06263485d4 | ||
|
|
e084b5a39f | ||
|
|
0285c2714b | ||
|
|
cd90389b30 | ||
|
|
84fa8230e5 | ||
|
|
05ea8c6122 | ||
|
|
adeed29a99 | ||
|
|
9b05aafb50 | ||
|
|
76d87f47c7 | ||
|
|
7329cc3981 | ||
|
|
4e9cf4cc7e | ||
|
|
72d6d64771 | ||
|
|
1e272a6b52 | ||
|
|
de07deb4d2 | ||
|
|
1f72ec7dad | ||
|
|
f917e0da4c | ||
|
|
0dbfb1d154 | ||
|
|
c495d095ae | ||
|
|
3a7410a8a6 | ||
|
|
27e29f4fe6 | ||
|
|
713fe0b7d7 | ||
|
|
3c9b817217 | ||
|
|
2e4f53a711 | ||
|
|
3ec1a001fb | ||
|
|
41d16f7a4a | ||
|
|
432699845c | ||
|
|
17ac46ea78 | ||
|
|
53b8bf2c89 | ||
|
|
2cd1ae736f | ||
|
|
d21aabcedc | ||
|
|
636c693f44 | ||
|
|
3f79b2f883 | ||
|
|
baa9898821 | ||
|
|
634b096610 | ||
|
|
6ad34a4b78 | ||
|
|
86192f3038 | ||
|
|
4186879675 | ||
|
|
e91f0ea1b3 | ||
|
|
173d692bc2 | ||
|
|
afaaa91885 | ||
|
|
5ab6a54cc6 | ||
|
|
7aa9e6d2ae | ||
|
|
b2da5ea773 | ||
|
|
6712bce92c | ||
|
|
0b7691201e | ||
|
|
43bccbc3f4 | ||
|
|
a764c87b67 | ||
|
|
619317da6f | ||
|
|
af0284926c | ||
|
|
0f72257049 | ||
|
|
913070a9e8 | ||
|
|
7c64e88963 | ||
|
|
8444b46d20 | ||
|
|
074383f850 | ||
|
|
06bfeb482d | ||
|
|
90a6f7fc98 | ||
|
|
2988889db1 | ||
|
|
ce9e36eb01 | ||
|
|
6cf1a325d6 | ||
|
|
8e390a62ad | ||
|
|
ca4ca5073e | ||
|
|
151af73118 | ||
|
|
87538cb7fe | ||
|
|
0224be8811 | ||
|
|
47dea01c85 | ||
|
|
c55e3fbae5 | ||
|
|
5ad2a94a16 | ||
|
|
d634c36253 | ||
|
|
ac61f90625 | ||
|
|
7f9b9512b8 | ||
|
|
b53e73e159 | ||
|
|
c9f0456d81 | ||
|
|
dee11ebab8 | ||
|
|
b6f871cf42 | ||
|
|
d1adc85aa6 | ||
|
|
5b9ba26403 | ||
|
|
04542cff92 | ||
|
|
39f5c0713e | ||
|
|
b6ab4d04b6 | ||
|
|
2a32f0371e | ||
|
|
978a790b85 | ||
|
|
2d91fce970 | ||
|
|
94fe98666c | ||
|
|
16cce320c4 | ||
|
|
37b6e16ae1 | ||
|
|
3c85a88888 | ||
|
|
b1fc06f7eb | ||
|
|
001138aec3 | ||
|
|
e5db99ad4a | ||
|
|
5dc4fe1529 | ||
|
|
434e6b2da3 | ||
|
|
6eb0c52d4c | ||
|
|
ff1d548738 | ||
|
|
5f5b4f1508 | ||
|
|
3c4fe59e09 | ||
|
|
71e8d15e49 | ||
|
|
8521cde570 | ||
|
|
ce1f9cf81d | ||
|
|
93c5198c17 | ||
|
|
f6e42f0e3f | ||
|
|
6eb720ff2d | ||
|
|
c8f0690255 | ||
|
|
8abf9729f0 | ||
|
|
eab865a0a1 | ||
|
|
16559a02fc | ||
|
|
790310d894 | ||
|
|
e09e31b12e | ||
|
|
1f55170b9c | ||
|
|
5b7d42fff3 | ||
|
|
73dd51c044 | ||
|
|
f115461f4e | ||
|
|
5b83fa482c | ||
|
|
8fb3ad2444 | ||
|
|
e3a61950a2 | ||
|
|
59f2e7c162 | ||
|
|
cfdb3a2214 | ||
|
|
13a92a5bb1 | ||
|
|
0b57a19c1b | ||
|
|
dbb6cb442a | ||
|
|
a1a07225b3 | ||
|
|
28c96d73b2 | ||
|
|
4e5e366ed2 | ||
|
|
2669fb8364 | ||
|
|
b399288ff5 | ||
|
|
18d32c7e1c | ||
|
|
fa2dc4b60c | ||
|
|
25811bc983 | ||
|
|
b3e707339d | ||
|
|
c17fc39243 | ||
|
|
ec8fd6e5e4 | ||
|
|
9749e96f42 | ||
|
|
77b1abf185 | ||
|
|
f48ca5581e | ||
|
|
fbfa0a12ee | ||
|
|
8b070b5d12 | ||
|
|
7f1bb22ef3 | ||
|
|
a06d59b583 | ||
|
|
20feafda09 | ||
|
|
949a274817 | ||
|
|
f39e7d4d7e | ||
|
|
a9f50a389b | ||
|
|
ea95262d8d | ||
|
|
2000abc86f | ||
|
|
f633212073 | ||
|
|
c7a4e694ad | ||
|
|
a3b3ca7c21 | ||
|
|
95c17c0cb2 | ||
|
|
58601c8f7d | ||
|
|
553aa0e259 | ||
|
|
870274bea8 | ||
|
|
c76b9b644c | ||
|
|
16f65068b0 | ||
|
|
7dde8bf3ab | ||
|
|
34599691b3 | ||
|
|
f156038db8 | ||
|
|
ffb00fbcbe | ||
|
|
3aa816e61a | ||
|
|
3879923db8 | ||
|
|
8fadfe52a2 | ||
|
|
ca6fc4f031 | ||
|
|
27d08fb8f0 | ||
|
|
0f36e5e5d8 | ||
|
|
413d8239df | ||
|
|
04607b5350 | ||
|
|
087b19237c | ||
|
|
367035a983 |
@@ -1,4 +1,4 @@
|
||||
This README is just a fast *quick start* document. You can find more detailed documentation at http://redis.io.
|
||||
This README is just a fast *quick start* document. You can find more detailed documentation at [redis.io](https://redis.io).
|
||||
|
||||
What is Redis?
|
||||
--------------
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
/*.so
|
||||
/*.dylib
|
||||
/*.a
|
||||
/*.pc
|
||||
|
||||
Vendored
+34
-1
@@ -1,6 +1,39 @@
|
||||
language: c
|
||||
sudo: false
|
||||
compiler:
|
||||
- gcc
|
||||
- clang
|
||||
|
||||
script: make && make check
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
before_script:
|
||||
- if [ "$TRAVIS_OS_NAME" == "osx" ] ; then brew update; brew install redis; fi
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- libc6-dbg
|
||||
- libc6-dev
|
||||
- libc6:i386
|
||||
- libc6-dev-i386
|
||||
- libc6-dbg:i386
|
||||
- gcc-multilib
|
||||
- valgrind
|
||||
|
||||
env:
|
||||
- CFLAGS="-Werror"
|
||||
- PRE="valgrind --track-origins=yes --leak-check=full"
|
||||
- TARGET="32bit" TARGET_VARS="32bit-vars" CFLAGS="-Werror"
|
||||
- TARGET="32bit" TARGET_VARS="32bit-vars" PRE="valgrind --track-origins=yes --leak-check=full"
|
||||
|
||||
matrix:
|
||||
exclude:
|
||||
- os: osx
|
||||
env: PRE="valgrind --track-origins=yes --leak-check=full"
|
||||
|
||||
- os: osx
|
||||
env: TARGET="32bit" TARGET_VARS="32bit-vars" PRE="valgrind --track-origins=yes --leak-check=full"
|
||||
|
||||
script: make $TARGET CFLAGS="$CFLAGS" && make check PRE="$PRE" && make $TARGET_VARS hiredis-example
|
||||
|
||||
Vendored
+125
@@ -1,3 +1,128 @@
|
||||
### 1.0.0 (unreleased)
|
||||
|
||||
**Fixes**:
|
||||
|
||||
* Catch a buffer overflow when formatting the error message
|
||||
* Import latest upstream sds. This breaks applications that are linked against the old hiredis v0.13
|
||||
* Fix warnings, when compiled with -Wshadow
|
||||
* Make hiredis compile in Cygwin on Windows, now CI-tested
|
||||
|
||||
**BREAKING CHANGES**:
|
||||
|
||||
* Change `redisReply.len` to `size_t`, as it denotes the the size of a string
|
||||
|
||||
User code should compare this to `size_t` values as well.
|
||||
If it was used to compare to other values, casting might be necessary or can be removed, if casting was applied before.
|
||||
|
||||
* Remove backwards compatibility macro's
|
||||
|
||||
This removes the following old function aliases, use the new name now:
|
||||
|
||||
| Old | New |
|
||||
| --------------------------- | ---------------------- |
|
||||
| redisReplyReaderCreate | redisReaderCreate |
|
||||
| redisReplyReaderCreate | redisReaderCreate |
|
||||
| redisReplyReaderFree | redisReaderFree |
|
||||
| redisReplyReaderFeed | redisReaderFeed |
|
||||
| redisReplyReaderGetReply | redisReaderGetReply |
|
||||
| redisReplyReaderSetPrivdata | redisReaderSetPrivdata |
|
||||
| redisReplyReaderGetObject | redisReaderGetObject |
|
||||
| redisReplyReaderGetError | redisReaderGetError |
|
||||
|
||||
* The `DEBUG` variable in the Makefile was renamed to `DEBUG_FLAGS`
|
||||
|
||||
Previously it broke some builds for people that had `DEBUG` set to some arbitrary value,
|
||||
due to debugging other software.
|
||||
By renaming we avoid unintentional name clashes.
|
||||
|
||||
Simply rename `DEBUG` to `DEBUG_FLAGS` in your environment to make it working again.
|
||||
|
||||
### 0.13.3 (2015-09-16)
|
||||
|
||||
* Revert "Clear `REDIS_CONNECTED` flag when connection is closed".
|
||||
* Make tests pass on FreeBSD (Thanks, Giacomo Olgeni)
|
||||
|
||||
|
||||
If the `REDIS_CONNECTED` flag is cleared,
|
||||
the async onDisconnect callback function will never be called.
|
||||
This causes problems as the disconnect is never reported back to the user.
|
||||
|
||||
### 0.13.2 (2015-08-25)
|
||||
|
||||
* Prevent crash on pending replies in async code (Thanks, @switch-st)
|
||||
* Clear `REDIS_CONNECTED` flag when connection is closed (Thanks, Jerry Jacobs)
|
||||
* Add MacOS X addapter (Thanks, @dizzus)
|
||||
* Add Qt adapter (Thanks, Pietro Cerutti)
|
||||
* Add Ivykis adapter (Thanks, Gergely Nagy)
|
||||
|
||||
All adapters are provided as is and are only tested where possible.
|
||||
|
||||
### 0.13.1 (2015-05-03)
|
||||
|
||||
This is a bug fix release.
|
||||
The new `reconnect` method introduced new struct members, which clashed with pre-defined names in pre-C99 code.
|
||||
Another commit forced C99 compilation just to make it work, but of course this is not desirable for outside projects.
|
||||
Other non-C99 code can now use hiredis as usual again.
|
||||
Sorry for the inconvenience.
|
||||
|
||||
* Fix memory leak in async reply handling (Salvatore Sanfilippo)
|
||||
* Rename struct member to avoid name clash with pre-c99 code (Alex Balashov, ncopa)
|
||||
|
||||
### 0.13.0 (2015-04-16)
|
||||
|
||||
This release adds a minimal Windows compatibility layer.
|
||||
The parser, standalone since v0.12.0, can now be compiled on Windows
|
||||
(and thus used in other client libraries as well)
|
||||
|
||||
* Windows compatibility layer for parser code (tzickel)
|
||||
* Properly escape data printed to PKGCONF file (Dan Skorupski)
|
||||
* Fix tests when assert() undefined (Keith Bennett, Matt Stancliff)
|
||||
* Implement a reconnect method for the client context, this changes the structure of `redisContext` (Aaron Bedra)
|
||||
|
||||
### 0.12.1 (2015-01-26)
|
||||
|
||||
* Fix `make install`: DESTDIR support, install all required files, install PKGCONF in proper location
|
||||
* Fix `make test` as 32 bit build on 64 bit platform
|
||||
|
||||
### 0.12.0 (2015-01-22)
|
||||
|
||||
* Add optional KeepAlive support
|
||||
|
||||
* Try again on EINTR errors
|
||||
|
||||
* Add libuv adapter
|
||||
|
||||
* Add IPv6 support
|
||||
|
||||
* Remove possiblity of multiple close on same fd
|
||||
|
||||
* Add ability to bind source address on connect
|
||||
|
||||
* Add redisConnectFd() and redisFreeKeepFd()
|
||||
|
||||
* Fix getaddrinfo() memory leak
|
||||
|
||||
* Free string if it is unused (fixes memory leak)
|
||||
|
||||
* Improve redisAppendCommandArgv performance 2.5x
|
||||
|
||||
* Add support for SO_REUSEADDR
|
||||
|
||||
* Fix redisvFormatCommand format parsing
|
||||
|
||||
* Add GLib 2.0 adapter
|
||||
|
||||
* Refactor reading code into read.c
|
||||
|
||||
* Fix errno error buffers to not clobber errors
|
||||
|
||||
* Generate pkgconf during build
|
||||
|
||||
* Silence _BSD_SOURCE warnings
|
||||
|
||||
* Improve digit counting for multibulk creation
|
||||
|
||||
|
||||
### 0.11.0
|
||||
|
||||
* Increase the maximum multi-bulk reply depth to 7.
|
||||
|
||||
Vendored
+79
-28
@@ -3,13 +3,25 @@
|
||||
# Copyright (C) 2010-2011 Pieter Noordhuis <pcnoordhuis at gmail dot com>
|
||||
# This file is released under the BSD license, see the COPYING file
|
||||
|
||||
OBJ=net.o hiredis.o sds.o async.o
|
||||
EXAMPLES=hiredis-example hiredis-example-libevent hiredis-example-libev
|
||||
OBJ=net.o hiredis.o sds.o async.o read.o
|
||||
EXAMPLES=hiredis-example hiredis-example-libevent hiredis-example-libev hiredis-example-glib
|
||||
TESTS=hiredis-test
|
||||
LIBNAME=libhiredis
|
||||
PKGCONFNAME=hiredis.pc
|
||||
|
||||
HIREDIS_MAJOR=0
|
||||
HIREDIS_MINOR=11
|
||||
HIREDIS_MAJOR=$(shell grep HIREDIS_MAJOR hiredis.h | awk '{print $$3}')
|
||||
HIREDIS_MINOR=$(shell grep HIREDIS_MINOR hiredis.h | awk '{print $$3}')
|
||||
HIREDIS_PATCH=$(shell grep HIREDIS_PATCH hiredis.h | awk '{print $$3}')
|
||||
HIREDIS_SONAME=$(shell grep HIREDIS_SONAME hiredis.h | awk '{print $$3}')
|
||||
|
||||
# Installation related variables and target
|
||||
PREFIX?=/usr/local
|
||||
INCLUDE_PATH?=include/hiredis
|
||||
LIBRARY_PATH?=lib
|
||||
PKGCONF_PATH?=pkgconfig
|
||||
INSTALL_INCLUDE_PATH= $(DESTDIR)$(PREFIX)/$(INCLUDE_PATH)
|
||||
INSTALL_LIBRARY_PATH= $(DESTDIR)$(PREFIX)/$(LIBRARY_PATH)
|
||||
INSTALL_PKGCONF_PATH= $(INSTALL_LIBRARY_PATH)/$(PKGCONF_PATH)
|
||||
|
||||
# redis-server configuration used for testing
|
||||
REDIS_PORT=56379
|
||||
@@ -25,15 +37,16 @@ export REDIS_TEST_CONFIG
|
||||
|
||||
# Fallback to gcc when $CC is not in $PATH.
|
||||
CC:=$(shell sh -c 'type $(CC) >/dev/null 2>/dev/null && echo $(CC) || echo gcc')
|
||||
CXX:=$(shell sh -c 'type $(CXX) >/dev/null 2>/dev/null && echo $(CXX) || echo g++')
|
||||
OPTIMIZATION?=-O3
|
||||
WARNINGS=-Wall -W -Wstrict-prototypes -Wwrite-strings
|
||||
DEBUG?= -g -ggdb
|
||||
REAL_CFLAGS=$(OPTIMIZATION) -fPIC $(CFLAGS) $(WARNINGS) $(DEBUG) $(ARCH)
|
||||
DEBUG_FLAGS?= -g -ggdb
|
||||
REAL_CFLAGS=$(OPTIMIZATION) -fPIC $(CFLAGS) $(WARNINGS) $(DEBUG_FLAGS) $(ARCH)
|
||||
REAL_LDFLAGS=$(LDFLAGS) $(ARCH)
|
||||
|
||||
DYLIBSUFFIX=so
|
||||
STLIBSUFFIX=a
|
||||
DYLIB_MINOR_NAME=$(LIBNAME).$(DYLIBSUFFIX).$(HIREDIS_MAJOR).$(HIREDIS_MINOR)
|
||||
DYLIB_MINOR_NAME=$(LIBNAME).$(DYLIBSUFFIX).$(HIREDIS_SONAME)
|
||||
DYLIB_MAJOR_NAME=$(LIBNAME).$(DYLIBSUFFIX).$(HIREDIS_MAJOR)
|
||||
DYLIBNAME=$(LIBNAME).$(DYLIBSUFFIX)
|
||||
DYLIB_MAKE_CMD=$(CC) -shared -Wl,-soname,$(DYLIB_MINOR_NAME) -o $(DYLIBNAME) $(LDFLAGS)
|
||||
@@ -49,19 +62,20 @@ ifeq ($(uname_S),SunOS)
|
||||
endif
|
||||
ifeq ($(uname_S),Darwin)
|
||||
DYLIBSUFFIX=dylib
|
||||
DYLIB_MINOR_NAME=$(LIBNAME).$(HIREDIS_MAJOR).$(HIREDIS_MINOR).$(DYLIBSUFFIX)
|
||||
DYLIB_MAJOR_NAME=$(LIBNAME).$(HIREDIS_MAJOR).$(DYLIBSUFFIX)
|
||||
DYLIB_MINOR_NAME=$(LIBNAME).$(HIREDIS_SONAME).$(DYLIBSUFFIX)
|
||||
DYLIB_MAKE_CMD=$(CC) -shared -Wl,-install_name,$(DYLIB_MINOR_NAME) -o $(DYLIBNAME) $(LDFLAGS)
|
||||
endif
|
||||
|
||||
all: $(DYLIBNAME)
|
||||
all: $(DYLIBNAME) $(STLIBNAME) hiredis-test $(PKGCONFNAME)
|
||||
|
||||
# Deps (use make dep to generate this)
|
||||
net.o: net.c fmacros.h net.h hiredis.h
|
||||
async.o: async.c async.h hiredis.h sds.h dict.c dict.h
|
||||
hiredis.o: hiredis.c fmacros.h hiredis.h net.h sds.h
|
||||
async.o: async.c fmacros.h async.h hiredis.h read.h sds.h net.h dict.c dict.h
|
||||
dict.o: dict.c fmacros.h dict.h
|
||||
hiredis.o: hiredis.c fmacros.h hiredis.h read.h sds.h net.h
|
||||
net.o: net.c fmacros.h net.h hiredis.h read.h sds.h
|
||||
read.o: read.c fmacros.h read.h sds.h
|
||||
sds.o: sds.c sds.h
|
||||
test.o: test.c hiredis.h
|
||||
test.o: test.c fmacros.h hiredis.h read.h sds.h
|
||||
|
||||
$(DYLIBNAME): $(OBJ)
|
||||
$(DYLIB_MAKE_CMD) $(OBJ)
|
||||
@@ -79,6 +93,15 @@ hiredis-example-libevent: examples/example-libevent.c adapters/libevent.h $(STLI
|
||||
hiredis-example-libev: examples/example-libev.c adapters/libev.h $(STLIBNAME)
|
||||
$(CC) -o examples/$@ $(REAL_CFLAGS) $(REAL_LDFLAGS) -I. $< -lev $(STLIBNAME)
|
||||
|
||||
hiredis-example-glib: examples/example-glib.c adapters/glib.h $(STLIBNAME)
|
||||
$(CC) -o examples/$@ $(REAL_CFLAGS) $(REAL_LDFLAGS) $(shell pkg-config --cflags --libs glib-2.0) -I. $< $(STLIBNAME)
|
||||
|
||||
hiredis-example-ivykis: examples/example-ivykis.c adapters/ivykis.h $(STLIBNAME)
|
||||
$(CC) -o examples/$@ $(REAL_CFLAGS) $(REAL_LDFLAGS) -I. $< -livykis $(STLIBNAME)
|
||||
|
||||
hiredis-example-macosx: examples/example-macosx.c adapters/macosx.h $(STLIBNAME)
|
||||
$(CC) -o examples/$@ $(REAL_CFLAGS) $(REAL_LDFLAGS) -I. $< -framework CoreFoundation $(STLIBNAME)
|
||||
|
||||
ifndef AE_DIR
|
||||
hiredis-example-ae:
|
||||
@echo "Please specify AE_DIR (e.g. <redis repository>/src)"
|
||||
@@ -94,7 +117,20 @@ hiredis-example-libuv:
|
||||
@false
|
||||
else
|
||||
hiredis-example-libuv: examples/example-libuv.c adapters/libuv.h $(STLIBNAME)
|
||||
$(CC) -o examples/$@ $(REAL_CFLAGS) $(REAL_LDFLAGS) -I. -I$(LIBUV_DIR)/include $< $(LIBUV_DIR)/.libs/libuv.a -lpthread $(STLIBNAME)
|
||||
$(CC) -o examples/$@ $(REAL_CFLAGS) $(REAL_LDFLAGS) -I. -I$(LIBUV_DIR)/include $< $(LIBUV_DIR)/.libs/libuv.a -lpthread -lrt $(STLIBNAME)
|
||||
endif
|
||||
|
||||
ifeq ($(and $(QT_MOC),$(QT_INCLUDE_DIR),$(QT_LIBRARY_DIR)),)
|
||||
hiredis-example-qt:
|
||||
@echo "Please specify QT_MOC, QT_INCLUDE_DIR AND QT_LIBRARY_DIR"
|
||||
@false
|
||||
else
|
||||
hiredis-example-qt: examples/example-qt.cpp adapters/qt.h $(STLIBNAME)
|
||||
$(QT_MOC) adapters/qt.h -I. -I$(QT_INCLUDE_DIR) -I$(QT_INCLUDE_DIR)/QtCore | \
|
||||
$(CXX) -x c++ -o qt-adapter-moc.o -c - $(REAL_CFLAGS) -I. -I$(QT_INCLUDE_DIR) -I$(QT_INCLUDE_DIR)/QtCore
|
||||
$(QT_MOC) examples/example-qt.h -I. -I$(QT_INCLUDE_DIR) -I$(QT_INCLUDE_DIR)/QtCore | \
|
||||
$(CXX) -x c++ -o qt-example-moc.o -c - $(REAL_CFLAGS) -I. -I$(QT_INCLUDE_DIR) -I$(QT_INCLUDE_DIR)/QtCore
|
||||
$(CXX) -o examples/$@ $(REAL_CFLAGS) $(REAL_LDFLAGS) -I. -I$(QT_INCLUDE_DIR) -I$(QT_INCLUDE_DIR)/QtCore -L$(QT_LIBRARY_DIR) qt-adapter-moc.o qt-example-moc.o $< -pthread $(STLIBNAME) -lQtCore
|
||||
endif
|
||||
|
||||
hiredis-example: examples/example.c $(STLIBNAME)
|
||||
@@ -103,14 +139,16 @@ hiredis-example: examples/example.c $(STLIBNAME)
|
||||
examples: $(EXAMPLES)
|
||||
|
||||
hiredis-test: test.o $(STLIBNAME)
|
||||
$(CC) -o $@ $(REAL_LDFLAGS) $< $(STLIBNAME)
|
||||
|
||||
hiredis-%: %.o $(STLIBNAME)
|
||||
$(CC) $(REAL_CFLAGS) -o $@ $(REAL_LDFLAGS) $< $(STLIBNAME)
|
||||
|
||||
test: hiredis-test
|
||||
./hiredis-test
|
||||
|
||||
check: hiredis-test
|
||||
@echo "$$REDIS_TEST_CONFIG" | $(REDIS_SERVER) -
|
||||
./hiredis-test -h 127.0.0.1 -p $(REDIS_PORT) -s /tmp/hiredis-test-redis.sock || \
|
||||
$(PRE) ./hiredis-test -h 127.0.0.1 -p $(REDIS_PORT) -s /tmp/hiredis-test-redis.sock || \
|
||||
( kill `cat /tmp/hiredis-test-redis.pid` && false )
|
||||
kill `cat /tmp/hiredis-test-redis.pid`
|
||||
|
||||
@@ -118,29 +156,38 @@ check: hiredis-test
|
||||
$(CC) -std=c99 -pedantic -c $(REAL_CFLAGS) $<
|
||||
|
||||
clean:
|
||||
rm -rf $(DYLIBNAME) $(STLIBNAME) $(TESTS) examples/hiredis-example* *.o *.gcda *.gcno *.gcov
|
||||
rm -rf $(DYLIBNAME) $(STLIBNAME) $(TESTS) $(PKGCONFNAME) examples/hiredis-example* *.o *.gcda *.gcno *.gcov
|
||||
|
||||
dep:
|
||||
$(CC) -MM *.c
|
||||
|
||||
# Installation related variables and target
|
||||
PREFIX?=/usr/local
|
||||
INSTALL_INCLUDE_PATH= $(PREFIX)/include/hiredis
|
||||
INSTALL_LIBRARY_PATH= $(PREFIX)/lib
|
||||
|
||||
ifeq ($(uname_S),SunOS)
|
||||
INSTALL?= cp -r
|
||||
endif
|
||||
|
||||
INSTALL?= cp -a
|
||||
|
||||
install: $(DYLIBNAME) $(STLIBNAME)
|
||||
$(PKGCONFNAME): hiredis.h
|
||||
@echo "Generating $@ for pkgconfig..."
|
||||
@echo prefix=$(PREFIX) > $@
|
||||
@echo exec_prefix=\$${prefix} >> $@
|
||||
@echo libdir=$(PREFIX)/$(LIBRARY_PATH) >> $@
|
||||
@echo includedir=$(PREFIX)/$(INCLUDE_PATH) >> $@
|
||||
@echo >> $@
|
||||
@echo Name: hiredis >> $@
|
||||
@echo Description: Minimalistic C client library for Redis. >> $@
|
||||
@echo Version: $(HIREDIS_MAJOR).$(HIREDIS_MINOR).$(HIREDIS_PATCH) >> $@
|
||||
@echo Libs: -L\$${libdir} -lhiredis >> $@
|
||||
@echo Cflags: -I\$${includedir} -D_FILE_OFFSET_BITS=64 >> $@
|
||||
|
||||
install: $(DYLIBNAME) $(STLIBNAME) $(PKGCONFNAME)
|
||||
mkdir -p $(INSTALL_INCLUDE_PATH) $(INSTALL_LIBRARY_PATH)
|
||||
$(INSTALL) hiredis.h async.h adapters $(INSTALL_INCLUDE_PATH)
|
||||
$(INSTALL) hiredis.h async.h read.h sds.h adapters $(INSTALL_INCLUDE_PATH)
|
||||
$(INSTALL) $(DYLIBNAME) $(INSTALL_LIBRARY_PATH)/$(DYLIB_MINOR_NAME)
|
||||
cd $(INSTALL_LIBRARY_PATH) && ln -sf $(DYLIB_MINOR_NAME) $(DYLIB_MAJOR_NAME)
|
||||
cd $(INSTALL_LIBRARY_PATH) && ln -sf $(DYLIB_MAJOR_NAME) $(DYLIBNAME)
|
||||
cd $(INSTALL_LIBRARY_PATH) && ln -sf $(DYLIB_MINOR_NAME) $(DYLIBNAME)
|
||||
$(INSTALL) $(STLIBNAME) $(INSTALL_LIBRARY_PATH)
|
||||
mkdir -p $(INSTALL_PKGCONF_PATH)
|
||||
$(INSTALL) $(PKGCONFNAME) $(INSTALL_PKGCONF_PATH)
|
||||
|
||||
32bit:
|
||||
@echo ""
|
||||
@@ -148,6 +195,10 @@ install: $(DYLIBNAME) $(STLIBNAME)
|
||||
@echo ""
|
||||
$(MAKE) CFLAGS="-m32" LDFLAGS="-m32"
|
||||
|
||||
32bit-vars:
|
||||
$(eval CFLAGS=-m32)
|
||||
$(eval LDFLAGS=-m32)
|
||||
|
||||
gprof:
|
||||
$(MAKE) CFLAGS="-pg" LDFLAGS="-pg"
|
||||
|
||||
@@ -163,4 +214,4 @@ coverage: gcov
|
||||
noopt:
|
||||
$(MAKE) OPTIMIZATION=""
|
||||
|
||||
.PHONY: all test check clean dep install 32bit gprof gcov noopt
|
||||
.PHONY: all test check clean dep install 32bit 32bit-vars gprof gcov noopt
|
||||
|
||||
Vendored
+115
-88
@@ -1,11 +1,13 @@
|
||||
[](https://travis-ci.org/redis/hiredis)
|
||||
|
||||
**This Readme reflects the latest changed in the master branch. See [v0.13.3](https://github.com/redis/hiredis/tree/v0.13.3) for the Readme and documentation for the latest release.**
|
||||
|
||||
# HIREDIS
|
||||
|
||||
Hiredis is a minimalistic C client library for the [Redis](http://redis.io/) database.
|
||||
|
||||
It is minimalistic because it just adds minimal support for the protocol, but
|
||||
at the same time it uses an high level printf-alike API in order to make it
|
||||
at the same time it uses a high level printf-alike API in order to make it
|
||||
much higher level than otherwise suggested by its minimal code base and the
|
||||
lack of explicit bindings for every Redis command.
|
||||
|
||||
@@ -20,7 +22,15 @@ Redis version >= 1.2.0.
|
||||
The library comes with multiple APIs. There is the
|
||||
*synchronous API*, the *asynchronous API* and the *reply parsing API*.
|
||||
|
||||
## UPGRADING
|
||||
## Upgrading to `1.0.0`
|
||||
|
||||
Version 1.0.0 marks a stable release of hiredis.
|
||||
It includes some minor breaking changes, mostly to make the exposed API more uniform and self-explanatory.
|
||||
It also bundles the updated `sds` library, to sync up with upstream and Redis.
|
||||
For most applications a recompile against the new hiredis should be enough.
|
||||
For code changes see the [Changelog](CHANGELOG.md).
|
||||
|
||||
## Upgrading from `<0.9.0`
|
||||
|
||||
Version 0.9.0 is a major overhaul of hiredis in every aspect. However, upgrading existing
|
||||
code using hiredis should not be a big pain. The key thing to keep in mind when
|
||||
@@ -31,51 +41,62 @@ the stateless 0.0.1 that only has a file descriptor to work with.
|
||||
|
||||
To consume the synchronous API, there are only a few function calls that need to be introduced:
|
||||
|
||||
redisContext *redisConnect(const char *ip, int port);
|
||||
void *redisCommand(redisContext *c, const char *format, ...);
|
||||
void freeReplyObject(void *reply);
|
||||
```c
|
||||
redisContext *redisConnect(const char *ip, int port);
|
||||
void *redisCommand(redisContext *c, const char *format, ...);
|
||||
void freeReplyObject(void *reply);
|
||||
```
|
||||
|
||||
### Connecting
|
||||
|
||||
The function `redisConnect` is used to create a so-called `redisContext`. The
|
||||
context is where Hiredis holds state for a connection. The `redisContext`
|
||||
struct has an integer `err` field that is non-zero when an the connection is in
|
||||
struct has an integer `err` field that is non-zero when the connection is in
|
||||
an error state. The field `errstr` will contain a string with a description of
|
||||
the error. More information on errors can be found in the **Errors** section.
|
||||
After trying to connect to Redis using `redisConnect` you should
|
||||
check the `err` field to see if establishing the connection was successful:
|
||||
|
||||
redisContext *c = redisConnect("127.0.0.1", 6379);
|
||||
if (c != NULL && c->err) {
|
||||
```c
|
||||
redisContext *c = redisConnect("127.0.0.1", 6379);
|
||||
if (c == NULL || c->err) {
|
||||
if (c) {
|
||||
printf("Error: %s\n", c->errstr);
|
||||
// handle error
|
||||
} else {
|
||||
printf("Can't allocate redis context\n");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
*Note: A `redisContext` is not thread-safe.*
|
||||
|
||||
### Sending commands
|
||||
|
||||
There are several ways to issue commands to Redis. The first that will be introduced is
|
||||
`redisCommand`. This function takes a format similar to printf. In the simplest form,
|
||||
it is used like this:
|
||||
|
||||
reply = redisCommand(context, "SET foo bar");
|
||||
```c
|
||||
reply = redisCommand(context, "SET foo bar");
|
||||
```
|
||||
|
||||
The specifier `%s` interpolates a string in the command, and uses `strlen` to
|
||||
determine the length of the string:
|
||||
|
||||
reply = redisCommand(context, "SET foo %s", value);
|
||||
|
||||
```c
|
||||
reply = redisCommand(context, "SET foo %s", value);
|
||||
```
|
||||
When you need to pass binary safe strings in a command, the `%b` specifier can be
|
||||
used. Together with a pointer to the string, it requires a `size_t` length argument
|
||||
of the string:
|
||||
|
||||
reply = redisCommand(context, "SET foo %b", value, (size_t) valuelen);
|
||||
|
||||
```c
|
||||
reply = redisCommand(context, "SET foo %b", value, (size_t) valuelen);
|
||||
```
|
||||
Internally, Hiredis splits the command in different arguments and will
|
||||
convert it to the protocol used to communicate with Redis.
|
||||
One or more spaces separates arguments, so you can use the specifiers
|
||||
anywhere in an argument:
|
||||
|
||||
reply = redisCommand(context, "SET key:%s %s", myid, value);
|
||||
```c
|
||||
reply = redisCommand(context, "SET key:%s %s", myid, value);
|
||||
```
|
||||
|
||||
### Using replies
|
||||
|
||||
@@ -114,11 +135,11 @@ was received:
|
||||
Redis may reply with nested arrays but this is fully supported.
|
||||
|
||||
Replies should be freed using the `freeReplyObject()` function.
|
||||
Note that this function will take care of freeing sub-replies objects
|
||||
Note that this function will take care of freeing sub-reply objects
|
||||
contained in arrays and nested arrays, so there is no need for the user to
|
||||
free the sub replies (it is actually harmful and will corrupt the memory).
|
||||
|
||||
**Important:** the current version of hiredis (0.10.0) free's replies when the
|
||||
**Important:** the current version of hiredis (0.10.0) frees replies when the
|
||||
asynchronous API is used. This means you should not call `freeReplyObject` when
|
||||
you use this API. The reply is cleaned up by hiredis _after_ the callback
|
||||
returns. This behavior will probably change in future releases, so make sure to
|
||||
@@ -127,19 +148,19 @@ keep an eye on the changelog when upgrading (see issue #39).
|
||||
### Cleaning up
|
||||
|
||||
To disconnect and free the context the following function can be used:
|
||||
|
||||
void redisFree(redisContext *c);
|
||||
|
||||
This function immediately closes the socket and then free's the allocations done in
|
||||
```c
|
||||
void redisFree(redisContext *c);
|
||||
```
|
||||
This function immediately closes the socket and then frees the allocations done in
|
||||
creating the context.
|
||||
|
||||
### Sending commands (cont'd)
|
||||
|
||||
Together with `redisCommand`, the function `redisCommandArgv` can be used to issue commands.
|
||||
It has the following prototype:
|
||||
|
||||
void *redisCommandArgv(redisContext *c, int argc, const char **argv, const size_t *argvlen);
|
||||
|
||||
```c
|
||||
void *redisCommandArgv(redisContext *c, int argc, const char **argv, const size_t *argvlen);
|
||||
```
|
||||
It takes the number of arguments `argc`, an array of strings `argv` and the lengths of the
|
||||
arguments `argvlen`. For convenience, `argvlen` may be set to `NULL` and the function will
|
||||
use `strlen(3)` on every argument to determine its length. Obviously, when any of the arguments
|
||||
@@ -169,10 +190,10 @@ The function `redisGetReply` is exported as part of the Hiredis API and can be u
|
||||
is expected on the socket. To pipeline commands, the only things that needs to be done is
|
||||
filling up the output buffer. For this cause, two commands can be used that are identical
|
||||
to the `redisCommand` family, apart from not returning a reply:
|
||||
|
||||
void redisAppendCommand(redisContext *c, const char *format, ...);
|
||||
void redisAppendCommandArgv(redisContext *c, int argc, const char **argv, const size_t *argvlen);
|
||||
|
||||
```c
|
||||
void redisAppendCommand(redisContext *c, const char *format, ...);
|
||||
void redisAppendCommandArgv(redisContext *c, int argc, const char **argv, const size_t *argvlen);
|
||||
```
|
||||
After calling either function one or more times, `redisGetReply` can be used to receive the
|
||||
subsequent replies. The return value for this function is either `REDIS_OK` or `REDIS_ERR`, where
|
||||
the latter means an error occurred while reading a reply. Just as with the other commands,
|
||||
@@ -180,24 +201,24 @@ the `err` field in the context can be used to find out what the cause of this er
|
||||
|
||||
The following examples shows a simple pipeline (resulting in only a single call to `write(2)` and
|
||||
a single call to `read(2)`):
|
||||
|
||||
redisReply *reply;
|
||||
redisAppendCommand(context,"SET foo bar");
|
||||
redisAppendCommand(context,"GET foo");
|
||||
redisGetReply(context,&reply); // reply for SET
|
||||
freeReplyObject(reply);
|
||||
redisGetReply(context,&reply); // reply for GET
|
||||
freeReplyObject(reply);
|
||||
|
||||
```c
|
||||
redisReply *reply;
|
||||
redisAppendCommand(context,"SET foo bar");
|
||||
redisAppendCommand(context,"GET foo");
|
||||
redisGetReply(context,&reply); // reply for SET
|
||||
freeReplyObject(reply);
|
||||
redisGetReply(context,&reply); // reply for GET
|
||||
freeReplyObject(reply);
|
||||
```
|
||||
This API can also be used to implement a blocking subscriber:
|
||||
|
||||
reply = redisCommand(context,"SUBSCRIBE foo");
|
||||
```c
|
||||
reply = redisCommand(context,"SUBSCRIBE foo");
|
||||
freeReplyObject(reply);
|
||||
while(redisGetReply(context,&reply) == REDIS_OK) {
|
||||
// consume message
|
||||
freeReplyObject(reply);
|
||||
while(redisGetReply(context,&reply) == REDIS_OK) {
|
||||
// consume message
|
||||
freeReplyObject(reply);
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
### Errors
|
||||
|
||||
When a function call is not successful, depending on the function either `NULL` or `REDIS_ERR` is
|
||||
@@ -237,58 +258,62 @@ should be checked after creation to see if there were errors creating the connec
|
||||
Because the connection that will be created is non-blocking, the kernel is not able to
|
||||
instantly return if the specified host and port is able to accept a connection.
|
||||
|
||||
redisAsyncContext *c = redisAsyncConnect("127.0.0.1", 6379);
|
||||
if (c->err) {
|
||||
printf("Error: %s\n", c->errstr);
|
||||
// handle error
|
||||
}
|
||||
*Note: A `redisAsyncContext` is not thread-safe.*
|
||||
|
||||
```c
|
||||
redisAsyncContext *c = redisAsyncConnect("127.0.0.1", 6379);
|
||||
if (c->err) {
|
||||
printf("Error: %s\n", c->errstr);
|
||||
// handle error
|
||||
}
|
||||
```
|
||||
|
||||
The asynchronous context can hold a disconnect callback function that is called when the
|
||||
connection is disconnected (either because of an error or per user request). This function should
|
||||
have the following prototype:
|
||||
|
||||
void(const redisAsyncContext *c, int status);
|
||||
|
||||
```c
|
||||
void(const redisAsyncContext *c, int status);
|
||||
```
|
||||
On a disconnect, the `status` argument is set to `REDIS_OK` when disconnection was initiated by the
|
||||
user, or `REDIS_ERR` when the disconnection was caused by an error. When it is `REDIS_ERR`, the `err`
|
||||
field in the context can be accessed to find out the cause of the error.
|
||||
|
||||
The context object is always free'd after the disconnect callback fired. When a reconnect is needed,
|
||||
The context object is always freed after the disconnect callback fired. When a reconnect is needed,
|
||||
the disconnect callback is a good point to do so.
|
||||
|
||||
Setting the disconnect callback can only be done once per context. For subsequent calls it will
|
||||
return `REDIS_ERR`. The function to set the disconnect callback has the following prototype:
|
||||
|
||||
int redisAsyncSetDisconnectCallback(redisAsyncContext *ac, redisDisconnectCallback *fn);
|
||||
|
||||
```c
|
||||
int redisAsyncSetDisconnectCallback(redisAsyncContext *ac, redisDisconnectCallback *fn);
|
||||
```
|
||||
### Sending commands and their callbacks
|
||||
|
||||
In an asynchronous context, commands are automatically pipelined due to the nature of an event loop.
|
||||
Therefore, unlike the synchronous API, there is only a single way to send commands.
|
||||
Because commands are sent to Redis asynchronously, issuing a command requires a callback function
|
||||
that is called when the reply is received. Reply callbacks should have the following prototype:
|
||||
|
||||
void(redisAsyncContext *c, void *reply, void *privdata);
|
||||
|
||||
```c
|
||||
void(redisAsyncContext *c, void *reply, void *privdata);
|
||||
```
|
||||
The `privdata` argument can be used to curry arbitrary data to the callback from the point where
|
||||
the command is initially queued for execution.
|
||||
|
||||
The functions that can be used to issue commands in an asynchronous context are:
|
||||
|
||||
int redisAsyncCommand(
|
||||
redisAsyncContext *ac, redisCallbackFn *fn, void *privdata,
|
||||
const char *format, ...);
|
||||
int redisAsyncCommandArgv(
|
||||
redisAsyncContext *ac, redisCallbackFn *fn, void *privdata,
|
||||
int argc, const char **argv, const size_t *argvlen);
|
||||
|
||||
```c
|
||||
int redisAsyncCommand(
|
||||
redisAsyncContext *ac, redisCallbackFn *fn, void *privdata,
|
||||
const char *format, ...);
|
||||
int redisAsyncCommandArgv(
|
||||
redisAsyncContext *ac, redisCallbackFn *fn, void *privdata,
|
||||
int argc, const char **argv, const size_t *argvlen);
|
||||
```
|
||||
Both functions work like their blocking counterparts. The return value is `REDIS_OK` when the command
|
||||
was successfully added to the output buffer and `REDIS_ERR` otherwise. Example: when the connection
|
||||
is being disconnected per user-request, no new commands may be added to the output buffer and `REDIS_ERR` is
|
||||
returned on calls to the `redisAsyncCommand` family.
|
||||
|
||||
If the reply for a command with a `NULL` callback is read, it is immediately free'd. When the callback
|
||||
for a command is non-`NULL`, the memory is free'd immediately following the callback: the reply is only
|
||||
If the reply for a command with a `NULL` callback is read, it is immediately freed. When the callback
|
||||
for a command is non-`NULL`, the memory is freed immediately following the callback: the reply is only
|
||||
valid for the duration of the callback.
|
||||
|
||||
All pending callbacks are called with a `NULL` reply when the context encountered an error.
|
||||
@@ -296,14 +321,14 @@ All pending callbacks are called with a `NULL` reply when the context encountere
|
||||
### Disconnecting
|
||||
|
||||
An asynchronous connection can be terminated using:
|
||||
|
||||
void redisAsyncDisconnect(redisAsyncContext *ac);
|
||||
|
||||
```c
|
||||
void redisAsyncDisconnect(redisAsyncContext *ac);
|
||||
```
|
||||
When this function is called, the connection is **not** immediately terminated. Instead, new
|
||||
commands are no longer accepted and the connection is only terminated when all pending commands
|
||||
have been written to the socket, their respective replies have been read and their respective
|
||||
callbacks have been executed. After this, the disconnection callback is executed with the
|
||||
`REDIS_OK` status and the context object is free'd.
|
||||
`REDIS_OK` status and the context object is freed.
|
||||
|
||||
### Hooking it up to event library *X*
|
||||
|
||||
@@ -316,12 +341,12 @@ Hiredis comes with a reply parsing API that makes it easy for writing higher
|
||||
level language bindings.
|
||||
|
||||
The reply parsing API consists of the following functions:
|
||||
|
||||
redisReader *redisReaderCreate(void);
|
||||
void redisReaderFree(redisReader *reader);
|
||||
int redisReaderFeed(redisReader *reader, const char *buf, size_t len);
|
||||
int redisReaderGetReply(redisReader *reader, void **reply);
|
||||
|
||||
```c
|
||||
redisReader *redisReaderCreate(void);
|
||||
void redisReaderFree(redisReader *reader);
|
||||
int redisReaderFeed(redisReader *reader, const char *buf, size_t len);
|
||||
int redisReaderGetReply(redisReader *reader, void **reply);
|
||||
```
|
||||
The same set of functions are used internally by hiredis when creating a
|
||||
normal Redis context, the above API just exposes it to the user for a direct
|
||||
usage.
|
||||
@@ -361,7 +386,7 @@ Both when using the Reader API directly or when using it indirectly via a
|
||||
normal Redis context, the redisReader structure uses a buffer in order to
|
||||
accumulate data from the server.
|
||||
Usually this buffer is destroyed when it is empty and is larger than 16
|
||||
kb in order to avoid wasting memory in unused buffers
|
||||
KiB in order to avoid wasting memory in unused buffers
|
||||
|
||||
However when working with very big payloads destroying the buffer may slow
|
||||
down performances considerably, so it is possible to modify the max size of
|
||||
@@ -371,9 +396,9 @@ value for an idle buffer, so the buffer will never get freed.
|
||||
|
||||
For instance if you have a normal Redis context you can set the maximum idle
|
||||
buffer to zero (unlimited) just with:
|
||||
|
||||
context->reader->maxbuf = 0;
|
||||
|
||||
```c
|
||||
context->reader->maxbuf = 0;
|
||||
```
|
||||
This should be done only in order to maximize performances when working with
|
||||
large payloads. The context should be set back to `REDIS_READER_MAX_BUF` again
|
||||
as soon as possible in order to prevent allocation of useless memory.
|
||||
@@ -381,4 +406,6 @@ as soon as possible in order to prevent allocation of useless memory.
|
||||
## AUTHORS
|
||||
|
||||
Hiredis was written by Salvatore Sanfilippo (antirez at gmail) and
|
||||
Pieter Noordhuis (pcnoordhuis at gmail) and is released under the BSD license.
|
||||
Pieter Noordhuis (pcnoordhuis at gmail) and is released under the BSD license.
|
||||
Hiredis is currently maintained by Matt Stancliff (matt at genges dot com) and
|
||||
Jan-Erik Rediger (janerik at fnordig dot com)
|
||||
|
||||
Vendored
+153
@@ -0,0 +1,153 @@
|
||||
#ifndef __HIREDIS_GLIB_H__
|
||||
#define __HIREDIS_GLIB_H__
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
#include "../hiredis.h"
|
||||
#include "../async.h"
|
||||
|
||||
typedef struct
|
||||
{
|
||||
GSource source;
|
||||
redisAsyncContext *ac;
|
||||
GPollFD poll_fd;
|
||||
} RedisSource;
|
||||
|
||||
static void
|
||||
redis_source_add_read (gpointer data)
|
||||
{
|
||||
RedisSource *source = (RedisSource *)data;
|
||||
g_return_if_fail(source);
|
||||
source->poll_fd.events |= G_IO_IN;
|
||||
g_main_context_wakeup(g_source_get_context((GSource *)data));
|
||||
}
|
||||
|
||||
static void
|
||||
redis_source_del_read (gpointer data)
|
||||
{
|
||||
RedisSource *source = (RedisSource *)data;
|
||||
g_return_if_fail(source);
|
||||
source->poll_fd.events &= ~G_IO_IN;
|
||||
g_main_context_wakeup(g_source_get_context((GSource *)data));
|
||||
}
|
||||
|
||||
static void
|
||||
redis_source_add_write (gpointer data)
|
||||
{
|
||||
RedisSource *source = (RedisSource *)data;
|
||||
g_return_if_fail(source);
|
||||
source->poll_fd.events |= G_IO_OUT;
|
||||
g_main_context_wakeup(g_source_get_context((GSource *)data));
|
||||
}
|
||||
|
||||
static void
|
||||
redis_source_del_write (gpointer data)
|
||||
{
|
||||
RedisSource *source = (RedisSource *)data;
|
||||
g_return_if_fail(source);
|
||||
source->poll_fd.events &= ~G_IO_OUT;
|
||||
g_main_context_wakeup(g_source_get_context((GSource *)data));
|
||||
}
|
||||
|
||||
static void
|
||||
redis_source_cleanup (gpointer data)
|
||||
{
|
||||
RedisSource *source = (RedisSource *)data;
|
||||
|
||||
g_return_if_fail(source);
|
||||
|
||||
redis_source_del_read(source);
|
||||
redis_source_del_write(source);
|
||||
/*
|
||||
* It is not our responsibility to remove ourself from the
|
||||
* current main loop. However, we will remove the GPollFD.
|
||||
*/
|
||||
if (source->poll_fd.fd >= 0) {
|
||||
g_source_remove_poll((GSource *)data, &source->poll_fd);
|
||||
source->poll_fd.fd = -1;
|
||||
}
|
||||
}
|
||||
|
||||
static gboolean
|
||||
redis_source_prepare (GSource *source,
|
||||
gint *timeout_)
|
||||
{
|
||||
RedisSource *redis = (RedisSource *)source;
|
||||
*timeout_ = -1;
|
||||
return !!(redis->poll_fd.events & redis->poll_fd.revents);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
redis_source_check (GSource *source)
|
||||
{
|
||||
RedisSource *redis = (RedisSource *)source;
|
||||
return !!(redis->poll_fd.events & redis->poll_fd.revents);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
redis_source_dispatch (GSource *source,
|
||||
GSourceFunc callback,
|
||||
gpointer user_data)
|
||||
{
|
||||
RedisSource *redis = (RedisSource *)source;
|
||||
|
||||
if ((redis->poll_fd.revents & G_IO_OUT)) {
|
||||
redisAsyncHandleWrite(redis->ac);
|
||||
redis->poll_fd.revents &= ~G_IO_OUT;
|
||||
}
|
||||
|
||||
if ((redis->poll_fd.revents & G_IO_IN)) {
|
||||
redisAsyncHandleRead(redis->ac);
|
||||
redis->poll_fd.revents &= ~G_IO_IN;
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
return callback(user_data);
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void
|
||||
redis_source_finalize (GSource *source)
|
||||
{
|
||||
RedisSource *redis = (RedisSource *)source;
|
||||
|
||||
if (redis->poll_fd.fd >= 0) {
|
||||
g_source_remove_poll(source, &redis->poll_fd);
|
||||
redis->poll_fd.fd = -1;
|
||||
}
|
||||
}
|
||||
|
||||
static GSource *
|
||||
redis_source_new (redisAsyncContext *ac)
|
||||
{
|
||||
static GSourceFuncs source_funcs = {
|
||||
.prepare = redis_source_prepare,
|
||||
.check = redis_source_check,
|
||||
.dispatch = redis_source_dispatch,
|
||||
.finalize = redis_source_finalize,
|
||||
};
|
||||
redisContext *c = &ac->c;
|
||||
RedisSource *source;
|
||||
|
||||
g_return_val_if_fail(ac != NULL, NULL);
|
||||
|
||||
source = (RedisSource *)g_source_new(&source_funcs, sizeof *source);
|
||||
source->ac = ac;
|
||||
source->poll_fd.fd = c->fd;
|
||||
source->poll_fd.events = 0;
|
||||
source->poll_fd.revents = 0;
|
||||
g_source_add_poll((GSource *)source, &source->poll_fd);
|
||||
|
||||
ac->ev.addRead = redis_source_add_read;
|
||||
ac->ev.delRead = redis_source_del_read;
|
||||
ac->ev.addWrite = redis_source_add_write;
|
||||
ac->ev.delWrite = redis_source_del_write;
|
||||
ac->ev.cleanup = redis_source_cleanup;
|
||||
ac->ev.data = source;
|
||||
|
||||
return (GSource *)source;
|
||||
}
|
||||
|
||||
#endif /* __HIREDIS_GLIB_H__ */
|
||||
Vendored
+81
@@ -0,0 +1,81 @@
|
||||
#ifndef __HIREDIS_IVYKIS_H__
|
||||
#define __HIREDIS_IVYKIS_H__
|
||||
#include <iv.h>
|
||||
#include "../hiredis.h"
|
||||
#include "../async.h"
|
||||
|
||||
typedef struct redisIvykisEvents {
|
||||
redisAsyncContext *context;
|
||||
struct iv_fd fd;
|
||||
} redisIvykisEvents;
|
||||
|
||||
static void redisIvykisReadEvent(void *arg) {
|
||||
redisAsyncContext *context = (redisAsyncContext *)arg;
|
||||
redisAsyncHandleRead(context);
|
||||
}
|
||||
|
||||
static void redisIvykisWriteEvent(void *arg) {
|
||||
redisAsyncContext *context = (redisAsyncContext *)arg;
|
||||
redisAsyncHandleWrite(context);
|
||||
}
|
||||
|
||||
static void redisIvykisAddRead(void *privdata) {
|
||||
redisIvykisEvents *e = (redisIvykisEvents*)privdata;
|
||||
iv_fd_set_handler_in(&e->fd, redisIvykisReadEvent);
|
||||
}
|
||||
|
||||
static void redisIvykisDelRead(void *privdata) {
|
||||
redisIvykisEvents *e = (redisIvykisEvents*)privdata;
|
||||
iv_fd_set_handler_in(&e->fd, NULL);
|
||||
}
|
||||
|
||||
static void redisIvykisAddWrite(void *privdata) {
|
||||
redisIvykisEvents *e = (redisIvykisEvents*)privdata;
|
||||
iv_fd_set_handler_out(&e->fd, redisIvykisWriteEvent);
|
||||
}
|
||||
|
||||
static void redisIvykisDelWrite(void *privdata) {
|
||||
redisIvykisEvents *e = (redisIvykisEvents*)privdata;
|
||||
iv_fd_set_handler_out(&e->fd, NULL);
|
||||
}
|
||||
|
||||
static void redisIvykisCleanup(void *privdata) {
|
||||
redisIvykisEvents *e = (redisIvykisEvents*)privdata;
|
||||
|
||||
iv_fd_unregister(&e->fd);
|
||||
free(e);
|
||||
}
|
||||
|
||||
static int redisIvykisAttach(redisAsyncContext *ac) {
|
||||
redisContext *c = &(ac->c);
|
||||
redisIvykisEvents *e;
|
||||
|
||||
/* Nothing should be attached when something is already attached */
|
||||
if (ac->ev.data != NULL)
|
||||
return REDIS_ERR;
|
||||
|
||||
/* Create container for context and r/w events */
|
||||
e = (redisIvykisEvents*)malloc(sizeof(*e));
|
||||
e->context = ac;
|
||||
|
||||
/* Register functions to start/stop listening for events */
|
||||
ac->ev.addRead = redisIvykisAddRead;
|
||||
ac->ev.delRead = redisIvykisDelRead;
|
||||
ac->ev.addWrite = redisIvykisAddWrite;
|
||||
ac->ev.delWrite = redisIvykisDelWrite;
|
||||
ac->ev.cleanup = redisIvykisCleanup;
|
||||
ac->ev.data = e;
|
||||
|
||||
/* Initialize and install read/write events */
|
||||
IV_FD_INIT(&e->fd);
|
||||
e->fd.fd = c->fd;
|
||||
e->fd.handler_in = redisIvykisReadEvent;
|
||||
e->fd.handler_out = redisIvykisWriteEvent;
|
||||
e->fd.handler_err = NULL;
|
||||
e->fd.cookie = e->context;
|
||||
|
||||
iv_fd_register(&e->fd);
|
||||
|
||||
return REDIS_OK;
|
||||
}
|
||||
#endif
|
||||
Vendored
+12
-12
@@ -30,13 +30,13 @@
|
||||
|
||||
#ifndef __HIREDIS_LIBEVENT_H__
|
||||
#define __HIREDIS_LIBEVENT_H__
|
||||
#include <event.h>
|
||||
#include <event2/event.h>
|
||||
#include "../hiredis.h"
|
||||
#include "../async.h"
|
||||
|
||||
typedef struct redisLibeventEvents {
|
||||
redisAsyncContext *context;
|
||||
struct event rev, wev;
|
||||
struct event *rev, *wev;
|
||||
} redisLibeventEvents;
|
||||
|
||||
static void redisLibeventReadEvent(int fd, short event, void *arg) {
|
||||
@@ -53,28 +53,28 @@ static void redisLibeventWriteEvent(int fd, short event, void *arg) {
|
||||
|
||||
static void redisLibeventAddRead(void *privdata) {
|
||||
redisLibeventEvents *e = (redisLibeventEvents*)privdata;
|
||||
event_add(&e->rev,NULL);
|
||||
event_add(e->rev,NULL);
|
||||
}
|
||||
|
||||
static void redisLibeventDelRead(void *privdata) {
|
||||
redisLibeventEvents *e = (redisLibeventEvents*)privdata;
|
||||
event_del(&e->rev);
|
||||
event_del(e->rev);
|
||||
}
|
||||
|
||||
static void redisLibeventAddWrite(void *privdata) {
|
||||
redisLibeventEvents *e = (redisLibeventEvents*)privdata;
|
||||
event_add(&e->wev,NULL);
|
||||
event_add(e->wev,NULL);
|
||||
}
|
||||
|
||||
static void redisLibeventDelWrite(void *privdata) {
|
||||
redisLibeventEvents *e = (redisLibeventEvents*)privdata;
|
||||
event_del(&e->wev);
|
||||
event_del(e->wev);
|
||||
}
|
||||
|
||||
static void redisLibeventCleanup(void *privdata) {
|
||||
redisLibeventEvents *e = (redisLibeventEvents*)privdata;
|
||||
event_del(&e->rev);
|
||||
event_del(&e->wev);
|
||||
event_del(e->rev);
|
||||
event_del(e->wev);
|
||||
free(e);
|
||||
}
|
||||
|
||||
@@ -99,10 +99,10 @@ static int redisLibeventAttach(redisAsyncContext *ac, struct event_base *base) {
|
||||
ac->ev.data = e;
|
||||
|
||||
/* Initialize and install read/write events */
|
||||
event_set(&e->rev,c->fd,EV_READ,redisLibeventReadEvent,e);
|
||||
event_set(&e->wev,c->fd,EV_WRITE,redisLibeventWriteEvent,e);
|
||||
event_base_set(base,&e->rev);
|
||||
event_base_set(base,&e->wev);
|
||||
e->rev = event_new(base, c->fd, EV_READ, redisLibeventReadEvent, e);
|
||||
e->wev = event_new(base, c->fd, EV_WRITE, redisLibeventWriteEvent, e);
|
||||
event_add(e->rev, NULL);
|
||||
event_add(e->wev, NULL);
|
||||
return REDIS_OK;
|
||||
}
|
||||
#endif
|
||||
|
||||
Vendored
+4
-3
@@ -1,5 +1,6 @@
|
||||
#ifndef __HIREDIS_LIBUV_H__
|
||||
#define __HIREDIS_LIBUV_H__
|
||||
#include <stdlib.h>
|
||||
#include <uv.h>
|
||||
#include "../hiredis.h"
|
||||
#include "../async.h"
|
||||
@@ -11,7 +12,6 @@ typedef struct redisLibuvEvents {
|
||||
int events;
|
||||
} redisLibuvEvents;
|
||||
|
||||
int redisLibuvAttach(redisAsyncContext*, uv_loop_t*);
|
||||
|
||||
static void redisLibuvPoll(uv_poll_t* handle, int status, int events) {
|
||||
redisLibuvEvents* p = (redisLibuvEvents*)handle->data;
|
||||
@@ -20,10 +20,10 @@ static void redisLibuvPoll(uv_poll_t* handle, int status, int events) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (events & UV_READABLE) {
|
||||
if (p->context != NULL && (events & UV_READABLE)) {
|
||||
redisAsyncHandleRead(p->context);
|
||||
}
|
||||
if (events & UV_WRITABLE) {
|
||||
if (p->context != NULL && (events & UV_WRITABLE)) {
|
||||
redisAsyncHandleWrite(p->context);
|
||||
}
|
||||
}
|
||||
@@ -83,6 +83,7 @@ static void on_close(uv_handle_t* handle) {
|
||||
static void redisLibuvCleanup(void *privdata) {
|
||||
redisLibuvEvents* p = (redisLibuvEvents*)privdata;
|
||||
|
||||
p->context = NULL; // indicate that context might no longer exist
|
||||
uv_close((uv_handle_t*)&p->handle, on_close);
|
||||
}
|
||||
|
||||
|
||||
Vendored
+114
@@ -0,0 +1,114 @@
|
||||
//
|
||||
// Created by Дмитрий Бахвалов on 13.07.15.
|
||||
// Copyright (c) 2015 Dmitry Bakhvalov. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef __HIREDIS_MACOSX_H__
|
||||
#define __HIREDIS_MACOSX_H__
|
||||
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
|
||||
#include "../hiredis.h"
|
||||
#include "../async.h"
|
||||
|
||||
typedef struct {
|
||||
redisAsyncContext *context;
|
||||
CFSocketRef socketRef;
|
||||
CFRunLoopSourceRef sourceRef;
|
||||
} RedisRunLoop;
|
||||
|
||||
static int freeRedisRunLoop(RedisRunLoop* redisRunLoop) {
|
||||
if( redisRunLoop != NULL ) {
|
||||
if( redisRunLoop->sourceRef != NULL ) {
|
||||
CFRunLoopSourceInvalidate(redisRunLoop->sourceRef);
|
||||
CFRelease(redisRunLoop->sourceRef);
|
||||
}
|
||||
if( redisRunLoop->socketRef != NULL ) {
|
||||
CFSocketInvalidate(redisRunLoop->socketRef);
|
||||
CFRelease(redisRunLoop->socketRef);
|
||||
}
|
||||
free(redisRunLoop);
|
||||
}
|
||||
return REDIS_ERR;
|
||||
}
|
||||
|
||||
static void redisMacOSAddRead(void *privdata) {
|
||||
RedisRunLoop *redisRunLoop = (RedisRunLoop*)privdata;
|
||||
CFSocketEnableCallBacks(redisRunLoop->socketRef, kCFSocketReadCallBack);
|
||||
}
|
||||
|
||||
static void redisMacOSDelRead(void *privdata) {
|
||||
RedisRunLoop *redisRunLoop = (RedisRunLoop*)privdata;
|
||||
CFSocketDisableCallBacks(redisRunLoop->socketRef, kCFSocketReadCallBack);
|
||||
}
|
||||
|
||||
static void redisMacOSAddWrite(void *privdata) {
|
||||
RedisRunLoop *redisRunLoop = (RedisRunLoop*)privdata;
|
||||
CFSocketEnableCallBacks(redisRunLoop->socketRef, kCFSocketWriteCallBack);
|
||||
}
|
||||
|
||||
static void redisMacOSDelWrite(void *privdata) {
|
||||
RedisRunLoop *redisRunLoop = (RedisRunLoop*)privdata;
|
||||
CFSocketDisableCallBacks(redisRunLoop->socketRef, kCFSocketWriteCallBack);
|
||||
}
|
||||
|
||||
static void redisMacOSCleanup(void *privdata) {
|
||||
RedisRunLoop *redisRunLoop = (RedisRunLoop*)privdata;
|
||||
freeRedisRunLoop(redisRunLoop);
|
||||
}
|
||||
|
||||
static void redisMacOSAsyncCallback(CFSocketRef __unused s, CFSocketCallBackType callbackType, CFDataRef __unused address, const void __unused *data, void *info) {
|
||||
redisAsyncContext* context = (redisAsyncContext*) info;
|
||||
|
||||
switch (callbackType) {
|
||||
case kCFSocketReadCallBack:
|
||||
redisAsyncHandleRead(context);
|
||||
break;
|
||||
|
||||
case kCFSocketWriteCallBack:
|
||||
redisAsyncHandleWrite(context);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static int redisMacOSAttach(redisAsyncContext *redisAsyncCtx, CFRunLoopRef runLoop) {
|
||||
redisContext *redisCtx = &(redisAsyncCtx->c);
|
||||
|
||||
/* Nothing should be attached when something is already attached */
|
||||
if( redisAsyncCtx->ev.data != NULL ) return REDIS_ERR;
|
||||
|
||||
RedisRunLoop* redisRunLoop = (RedisRunLoop*) calloc(1, sizeof(RedisRunLoop));
|
||||
if( !redisRunLoop ) return REDIS_ERR;
|
||||
|
||||
/* Setup redis stuff */
|
||||
redisRunLoop->context = redisAsyncCtx;
|
||||
|
||||
redisAsyncCtx->ev.addRead = redisMacOSAddRead;
|
||||
redisAsyncCtx->ev.delRead = redisMacOSDelRead;
|
||||
redisAsyncCtx->ev.addWrite = redisMacOSAddWrite;
|
||||
redisAsyncCtx->ev.delWrite = redisMacOSDelWrite;
|
||||
redisAsyncCtx->ev.cleanup = redisMacOSCleanup;
|
||||
redisAsyncCtx->ev.data = redisRunLoop;
|
||||
|
||||
/* Initialize and install read/write events */
|
||||
CFSocketContext socketCtx = { 0, redisAsyncCtx, NULL, NULL, NULL };
|
||||
|
||||
redisRunLoop->socketRef = CFSocketCreateWithNative(NULL, redisCtx->fd,
|
||||
kCFSocketReadCallBack | kCFSocketWriteCallBack,
|
||||
redisMacOSAsyncCallback,
|
||||
&socketCtx);
|
||||
if( !redisRunLoop->socketRef ) return freeRedisRunLoop(redisRunLoop);
|
||||
|
||||
redisRunLoop->sourceRef = CFSocketCreateRunLoopSource(NULL, redisRunLoop->socketRef, 0);
|
||||
if( !redisRunLoop->sourceRef ) return freeRedisRunLoop(redisRunLoop);
|
||||
|
||||
CFRunLoopAddSource(runLoop, redisRunLoop->sourceRef, kCFRunLoopDefaultMode);
|
||||
|
||||
return REDIS_OK;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
Vendored
+135
@@ -0,0 +1,135 @@
|
||||
/*-
|
||||
* Copyright (C) 2014 Pietro Cerutti <gahr@gahr.ch>
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. 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.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY AUTHOR 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 AUTHOR 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 __HIREDIS_QT_H__
|
||||
#define __HIREDIS_QT_H__
|
||||
#include <QSocketNotifier>
|
||||
#include "../async.h"
|
||||
|
||||
static void RedisQtAddRead(void *);
|
||||
static void RedisQtDelRead(void *);
|
||||
static void RedisQtAddWrite(void *);
|
||||
static void RedisQtDelWrite(void *);
|
||||
static void RedisQtCleanup(void *);
|
||||
|
||||
class RedisQtAdapter : public QObject {
|
||||
|
||||
Q_OBJECT
|
||||
|
||||
friend
|
||||
void RedisQtAddRead(void * adapter) {
|
||||
RedisQtAdapter * a = static_cast<RedisQtAdapter *>(adapter);
|
||||
a->addRead();
|
||||
}
|
||||
|
||||
friend
|
||||
void RedisQtDelRead(void * adapter) {
|
||||
RedisQtAdapter * a = static_cast<RedisQtAdapter *>(adapter);
|
||||
a->delRead();
|
||||
}
|
||||
|
||||
friend
|
||||
void RedisQtAddWrite(void * adapter) {
|
||||
RedisQtAdapter * a = static_cast<RedisQtAdapter *>(adapter);
|
||||
a->addWrite();
|
||||
}
|
||||
|
||||
friend
|
||||
void RedisQtDelWrite(void * adapter) {
|
||||
RedisQtAdapter * a = static_cast<RedisQtAdapter *>(adapter);
|
||||
a->delWrite();
|
||||
}
|
||||
|
||||
friend
|
||||
void RedisQtCleanup(void * adapter) {
|
||||
RedisQtAdapter * a = static_cast<RedisQtAdapter *>(adapter);
|
||||
a->cleanup();
|
||||
}
|
||||
|
||||
public:
|
||||
RedisQtAdapter(QObject * parent = 0)
|
||||
: QObject(parent), m_ctx(0), m_read(0), m_write(0) { }
|
||||
|
||||
~RedisQtAdapter() {
|
||||
if (m_ctx != 0) {
|
||||
m_ctx->ev.data = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
int setContext(redisAsyncContext * ac) {
|
||||
if (ac->ev.data != NULL) {
|
||||
return REDIS_ERR;
|
||||
}
|
||||
m_ctx = ac;
|
||||
m_ctx->ev.data = this;
|
||||
m_ctx->ev.addRead = RedisQtAddRead;
|
||||
m_ctx->ev.delRead = RedisQtDelRead;
|
||||
m_ctx->ev.addWrite = RedisQtAddWrite;
|
||||
m_ctx->ev.delWrite = RedisQtDelWrite;
|
||||
m_ctx->ev.cleanup = RedisQtCleanup;
|
||||
return REDIS_OK;
|
||||
}
|
||||
|
||||
private:
|
||||
void addRead() {
|
||||
if (m_read) return;
|
||||
m_read = new QSocketNotifier(m_ctx->c.fd, QSocketNotifier::Read, 0);
|
||||
connect(m_read, SIGNAL(activated(int)), this, SLOT(read()));
|
||||
}
|
||||
|
||||
void delRead() {
|
||||
if (!m_read) return;
|
||||
delete m_read;
|
||||
m_read = 0;
|
||||
}
|
||||
|
||||
void addWrite() {
|
||||
if (m_write) return;
|
||||
m_write = new QSocketNotifier(m_ctx->c.fd, QSocketNotifier::Write, 0);
|
||||
connect(m_write, SIGNAL(activated(int)), this, SLOT(write()));
|
||||
}
|
||||
|
||||
void delWrite() {
|
||||
if (!m_write) return;
|
||||
delete m_write;
|
||||
m_write = 0;
|
||||
}
|
||||
|
||||
void cleanup() {
|
||||
delRead();
|
||||
delWrite();
|
||||
}
|
||||
|
||||
private slots:
|
||||
void read() { redisAsyncHandleRead(m_ctx); }
|
||||
void write() { redisAsyncHandleWrite(m_ctx); }
|
||||
|
||||
private:
|
||||
redisAsyncContext * m_ctx;
|
||||
QSocketNotifier * m_read;
|
||||
QSocketNotifier * m_write;
|
||||
};
|
||||
|
||||
#endif /* !__HIREDIS_QT_H__ */
|
||||
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
# Appveyor configuration file for CI build of hiredis on Windows (under Cygwin)
|
||||
environment:
|
||||
matrix:
|
||||
- CYG_ROOT: C:\cygwin64
|
||||
CYG_SETUP: setup-x86_64.exe
|
||||
CYG_MIRROR: http://cygwin.mirror.constant.com
|
||||
CYG_CACHE: C:\cygwin64\var\cache\setup
|
||||
CYG_BASH: C:\cygwin64\bin\bash
|
||||
CC: gcc
|
||||
- CYG_ROOT: C:\cygwin
|
||||
CYG_SETUP: setup-x86.exe
|
||||
CYG_MIRROR: http://cygwin.mirror.constant.com
|
||||
CYG_CACHE: C:\cygwin\var\cache\setup
|
||||
CYG_BASH: C:\cygwin\bin\bash
|
||||
CC: gcc
|
||||
TARGET: 32bit
|
||||
TARGET_VARS: 32bit-vars
|
||||
|
||||
# Cache Cygwin files to speed up build
|
||||
cache:
|
||||
- '%CYG_CACHE%'
|
||||
clone_depth: 1
|
||||
|
||||
# Attempt to ensure we don't try to convert line endings to Win32 CRLF as this will cause build to fail
|
||||
init:
|
||||
- git config --global core.autocrlf input
|
||||
|
||||
# Install needed build dependencies
|
||||
install:
|
||||
- ps: 'Start-FileDownload "http://cygwin.com/$env:CYG_SETUP" -FileName "$env:CYG_SETUP"'
|
||||
- '%CYG_SETUP% --quiet-mode --no-shortcuts --only-site --root "%CYG_ROOT%" --site "%CYG_MIRROR%" --local-package-dir "%CYG_CACHE%" --packages automake,bison,gcc-core,libtool,make,gettext-devel,gettext,intltool,pkg-config,clang,llvm > NUL 2>&1'
|
||||
- '%CYG_BASH% -lc "cygcheck -dc cygwin"'
|
||||
|
||||
build_script:
|
||||
- 'echo building...'
|
||||
- '%CYG_BASH% -lc "cd $APPVEYOR_BUILD_FOLDER; exec 0</dev/null; make LDFLAGS=$LDFLAGS CC=$CC $TARGET CFLAGS=$CFLAGS && make LDFLAGS=$LDFLAGS CC=$CC $TARGET_VARS hiredis-example"'
|
||||
Vendored
+38
-13
@@ -58,7 +58,7 @@
|
||||
} while(0);
|
||||
|
||||
/* Forward declaration of function in hiredis.c */
|
||||
void __redisAppendCommand(redisContext *c, char *cmd, size_t len);
|
||||
int __redisAppendCommand(redisContext *c, const char *cmd, size_t len);
|
||||
|
||||
/* Functions managing dictionary of callbacks for pub/sub. */
|
||||
static unsigned int callbackHash(const void *key) {
|
||||
@@ -142,6 +142,9 @@ static redisAsyncContext *redisAsyncInitialize(redisContext *c) {
|
||||
/* We want the error field to be accessible directly instead of requiring
|
||||
* an indirection to the redisContext struct. */
|
||||
static void __redisAsyncCopyError(redisAsyncContext *ac) {
|
||||
if (!ac)
|
||||
return;
|
||||
|
||||
redisContext *c = &(ac->c);
|
||||
ac->err = c->err;
|
||||
ac->errstr = c->errstr;
|
||||
@@ -173,6 +176,14 @@ redisAsyncContext *redisAsyncConnectBind(const char *ip, int port,
|
||||
return ac;
|
||||
}
|
||||
|
||||
redisAsyncContext *redisAsyncConnectBindWithReuse(const char *ip, int port,
|
||||
const char *source_addr) {
|
||||
redisContext *c = redisConnectBindNonBlockWithReuse(ip,port,source_addr);
|
||||
redisAsyncContext *ac = redisAsyncInitialize(c);
|
||||
__redisAsyncCopyError(ac);
|
||||
return ac;
|
||||
}
|
||||
|
||||
redisAsyncContext *redisAsyncConnectUnix(const char *path) {
|
||||
redisContext *c;
|
||||
redisAsyncContext *ac;
|
||||
@@ -407,7 +418,8 @@ void redisProcessCallbacks(redisAsyncContext *ac) {
|
||||
if (reply == NULL) {
|
||||
/* When the connection is being disconnected and there are
|
||||
* no more replies, this is the cue to really disconnect. */
|
||||
if (c->flags & REDIS_DISCONNECTING && sdslen(c->obuf) == 0) {
|
||||
if (c->flags & REDIS_DISCONNECTING && sdslen(c->obuf) == 0
|
||||
&& ac->replies.head == NULL) {
|
||||
__redisAsyncDisconnect(ac);
|
||||
return;
|
||||
}
|
||||
@@ -477,7 +489,7 @@ void redisProcessCallbacks(redisAsyncContext *ac) {
|
||||
}
|
||||
|
||||
/* Internal helper function to detect socket status the first time a read or
|
||||
* write event fires. When connecting was not succesful, the connect callback
|
||||
* write event fires. When connecting was not successful, the connect callback
|
||||
* is called with a REDIS_ERR status and the context is free'd. */
|
||||
static int __redisAsyncHandleConnect(redisAsyncContext *ac) {
|
||||
redisContext *c = &(ac->c);
|
||||
@@ -551,8 +563,8 @@ void redisAsyncHandleWrite(redisAsyncContext *ac) {
|
||||
|
||||
/* Sets a pointer to the first argument and its length starting at p. Returns
|
||||
* the number of bytes to skip to get to the following argument. */
|
||||
static char *nextArgument(char *start, char **str, size_t *len) {
|
||||
char *p = start;
|
||||
static const char *nextArgument(const char *start, const char **str, size_t *len) {
|
||||
const char *p = start;
|
||||
if (p[0] != '$') {
|
||||
p = strchr(p,'$');
|
||||
if (p == NULL) return NULL;
|
||||
@@ -568,14 +580,15 @@ static char *nextArgument(char *start, char **str, size_t *len) {
|
||||
/* Helper function for the redisAsyncCommand* family of functions. Writes a
|
||||
* formatted command to the output buffer and registers the provided callback
|
||||
* function with the context. */
|
||||
static int __redisAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, char *cmd, size_t len) {
|
||||
static int __redisAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, const char *cmd, size_t len) {
|
||||
redisContext *c = &(ac->c);
|
||||
redisCallback cb;
|
||||
int pvariant, hasnext;
|
||||
char *cstr, *astr;
|
||||
const char *cstr, *astr;
|
||||
size_t clen, alen;
|
||||
char *p;
|
||||
const char *p;
|
||||
sds sname;
|
||||
int ret;
|
||||
|
||||
/* Don't accept new commands when the connection is about to be closed. */
|
||||
if (c->flags & (REDIS_DISCONNECTING | REDIS_FREEING)) return REDIS_ERR;
|
||||
@@ -599,9 +612,11 @@ static int __redisAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void
|
||||
while ((p = nextArgument(p,&astr,&alen)) != NULL) {
|
||||
sname = sdsnewlen(astr,alen);
|
||||
if (pvariant)
|
||||
dictReplace(ac->sub.patterns,sname,&cb);
|
||||
ret = dictReplace(ac->sub.patterns,sname,&cb);
|
||||
else
|
||||
dictReplace(ac->sub.channels,sname,&cb);
|
||||
ret = dictReplace(ac->sub.channels,sname,&cb);
|
||||
|
||||
if (ret == 0) sdsfree(sname);
|
||||
}
|
||||
} else if (strncasecmp(cstr,"unsubscribe\r\n",13) == 0) {
|
||||
/* It is only useful to call (P)UNSUBSCRIBE when the context is
|
||||
@@ -637,6 +652,11 @@ int redisvAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdat
|
||||
int len;
|
||||
int status;
|
||||
len = redisvFormatCommand(&cmd,format,ap);
|
||||
|
||||
/* We don't want to pass -1 or -2 to future functions as a length. */
|
||||
if (len < 0)
|
||||
return REDIS_ERR;
|
||||
|
||||
status = __redisAsyncCommand(ac,fn,privdata,cmd,len);
|
||||
free(cmd);
|
||||
return status;
|
||||
@@ -652,11 +672,16 @@ int redisAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata
|
||||
}
|
||||
|
||||
int redisAsyncCommandArgv(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, int argc, const char **argv, const size_t *argvlen) {
|
||||
char *cmd;
|
||||
sds cmd;
|
||||
int len;
|
||||
int status;
|
||||
len = redisFormatCommandArgv(&cmd,argc,argv,argvlen);
|
||||
len = redisFormatSdsCommandArgv(&cmd,argc,argv,argvlen);
|
||||
status = __redisAsyncCommand(ac,fn,privdata,cmd,len);
|
||||
free(cmd);
|
||||
sdsfree(cmd);
|
||||
return status;
|
||||
}
|
||||
|
||||
int redisAsyncFormattedCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, const char *cmd, size_t len) {
|
||||
int status = __redisAsyncCommand(ac,fn,privdata,cmd,len);
|
||||
return status;
|
||||
}
|
||||
|
||||
Vendored
+3
@@ -103,6 +103,8 @@ typedef struct redisAsyncContext {
|
||||
/* Functions that proxy to hiredis */
|
||||
redisAsyncContext *redisAsyncConnect(const char *ip, int port);
|
||||
redisAsyncContext *redisAsyncConnectBind(const char *ip, int port, const char *source_addr);
|
||||
redisAsyncContext *redisAsyncConnectBindWithReuse(const char *ip, int port,
|
||||
const char *source_addr);
|
||||
redisAsyncContext *redisAsyncConnectUnix(const char *path);
|
||||
int redisAsyncSetConnectCallback(redisAsyncContext *ac, redisConnectCallback *fn);
|
||||
int redisAsyncSetDisconnectCallback(redisAsyncContext *ac, redisDisconnectCallback *fn);
|
||||
@@ -118,6 +120,7 @@ void redisAsyncHandleWrite(redisAsyncContext *ac);
|
||||
int redisvAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, const char *format, va_list ap);
|
||||
int redisAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, const char *format, ...);
|
||||
int redisAsyncCommandArgv(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, int argc, const char **argv, const size_t *argvlen);
|
||||
int redisAsyncFormattedCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, const char *cmd, size_t len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
Vendored
+2
-2
@@ -161,7 +161,7 @@ static int dictReplace(dict *ht, void *key, void *val) {
|
||||
dictEntry *entry, auxentry;
|
||||
|
||||
/* Try to add the element. If the key
|
||||
* does not exists dictAdd will suceed. */
|
||||
* does not exists dictAdd will succeed. */
|
||||
if (dictAdd(ht, key, val) == DICT_OK)
|
||||
return 1;
|
||||
/* It already exists, get the entry */
|
||||
@@ -293,7 +293,7 @@ static void dictReleaseIterator(dictIterator *iter) {
|
||||
|
||||
/* Expand the hash table if needed */
|
||||
static int _dictExpandIfNeeded(dict *ht) {
|
||||
/* If the hash table is empty expand it to the intial size,
|
||||
/* If the hash table is empty expand it to the initial size,
|
||||
* if the table is "full" dobule its size. */
|
||||
if (ht->size == 0)
|
||||
return dictExpand(ht, DICT_HT_INITIAL_SIZE);
|
||||
|
||||
Vendored
+73
@@ -0,0 +1,73 @@
|
||||
#include <stdlib.h>
|
||||
|
||||
#include <hiredis.h>
|
||||
#include <async.h>
|
||||
#include <adapters/glib.h>
|
||||
|
||||
static GMainLoop *mainloop;
|
||||
|
||||
static void
|
||||
connect_cb (const redisAsyncContext *ac G_GNUC_UNUSED,
|
||||
int status)
|
||||
{
|
||||
if (status != REDIS_OK) {
|
||||
g_printerr("Failed to connect: %s\n", ac->errstr);
|
||||
g_main_loop_quit(mainloop);
|
||||
} else {
|
||||
g_printerr("Connected...\n");
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
disconnect_cb (const redisAsyncContext *ac G_GNUC_UNUSED,
|
||||
int status)
|
||||
{
|
||||
if (status != REDIS_OK) {
|
||||
g_error("Failed to disconnect: %s", ac->errstr);
|
||||
} else {
|
||||
g_printerr("Disconnected...\n");
|
||||
g_main_loop_quit(mainloop);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
command_cb(redisAsyncContext *ac,
|
||||
gpointer r,
|
||||
gpointer user_data G_GNUC_UNUSED)
|
||||
{
|
||||
redisReply *reply = r;
|
||||
|
||||
if (reply) {
|
||||
g_print("REPLY: %s\n", reply->str);
|
||||
}
|
||||
|
||||
redisAsyncDisconnect(ac);
|
||||
}
|
||||
|
||||
gint
|
||||
main (gint argc G_GNUC_UNUSED,
|
||||
gchar *argv[] G_GNUC_UNUSED)
|
||||
{
|
||||
redisAsyncContext *ac;
|
||||
GMainContext *context = NULL;
|
||||
GSource *source;
|
||||
|
||||
ac = redisAsyncConnect("127.0.0.1", 6379);
|
||||
if (ac->err) {
|
||||
g_printerr("%s\n", ac->errstr);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
source = redis_source_new(ac);
|
||||
mainloop = g_main_loop_new(context, FALSE);
|
||||
g_source_attach(source, context);
|
||||
|
||||
redisAsyncSetConnectCallback(ac, connect_cb);
|
||||
redisAsyncSetDisconnectCallback(ac, disconnect_cb);
|
||||
redisAsyncCommand(ac, command_cb, NULL, "SET key 1234");
|
||||
redisAsyncCommand(ac, command_cb, NULL, "GET key");
|
||||
|
||||
g_main_loop_run(mainloop);
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <signal.h>
|
||||
|
||||
#include <hiredis.h>
|
||||
#include <async.h>
|
||||
#include <adapters/ivykis.h>
|
||||
|
||||
void getCallback(redisAsyncContext *c, void *r, void *privdata) {
|
||||
redisReply *reply = r;
|
||||
if (reply == NULL) return;
|
||||
printf("argv[%s]: %s\n", (char*)privdata, reply->str);
|
||||
|
||||
/* Disconnect after receiving the reply to GET */
|
||||
redisAsyncDisconnect(c);
|
||||
}
|
||||
|
||||
void connectCallback(const redisAsyncContext *c, int status) {
|
||||
if (status != REDIS_OK) {
|
||||
printf("Error: %s\n", c->errstr);
|
||||
return;
|
||||
}
|
||||
printf("Connected...\n");
|
||||
}
|
||||
|
||||
void disconnectCallback(const redisAsyncContext *c, int status) {
|
||||
if (status != REDIS_OK) {
|
||||
printf("Error: %s\n", c->errstr);
|
||||
return;
|
||||
}
|
||||
printf("Disconnected...\n");
|
||||
}
|
||||
|
||||
int main (int argc, char **argv) {
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
|
||||
iv_init();
|
||||
|
||||
redisAsyncContext *c = redisAsyncConnect("127.0.0.1", 6379);
|
||||
if (c->err) {
|
||||
/* Let *c leak for now... */
|
||||
printf("Error: %s\n", c->errstr);
|
||||
return 1;
|
||||
}
|
||||
|
||||
redisIvykisAttach(c);
|
||||
redisAsyncSetConnectCallback(c,connectCallback);
|
||||
redisAsyncSetDisconnectCallback(c,disconnectCallback);
|
||||
redisAsyncCommand(c, NULL, NULL, "SET key %b", argv[argc-1], strlen(argv[argc-1]));
|
||||
redisAsyncCommand(c, getCallback, (char*)"end-1", "GET key");
|
||||
|
||||
iv_main();
|
||||
|
||||
iv_deinit();
|
||||
|
||||
return 0;
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// Created by Дмитрий Бахвалов on 13.07.15.
|
||||
// Copyright (c) 2015 Dmitry Bakhvalov. All rights reserved.
|
||||
//
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#include <hiredis.h>
|
||||
#include <async.h>
|
||||
#include <adapters/macosx.h>
|
||||
|
||||
void getCallback(redisAsyncContext *c, void *r, void *privdata) {
|
||||
redisReply *reply = r;
|
||||
if (reply == NULL) return;
|
||||
printf("argv[%s]: %s\n", (char*)privdata, reply->str);
|
||||
|
||||
/* Disconnect after receiving the reply to GET */
|
||||
redisAsyncDisconnect(c);
|
||||
}
|
||||
|
||||
void connectCallback(const redisAsyncContext *c, int status) {
|
||||
if (status != REDIS_OK) {
|
||||
printf("Error: %s\n", c->errstr);
|
||||
return;
|
||||
}
|
||||
printf("Connected...\n");
|
||||
}
|
||||
|
||||
void disconnectCallback(const redisAsyncContext *c, int status) {
|
||||
if (status != REDIS_OK) {
|
||||
printf("Error: %s\n", c->errstr);
|
||||
return;
|
||||
}
|
||||
CFRunLoopStop(CFRunLoopGetCurrent());
|
||||
printf("Disconnected...\n");
|
||||
}
|
||||
|
||||
int main (int argc, char **argv) {
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
|
||||
CFRunLoopRef loop = CFRunLoopGetCurrent();
|
||||
if( !loop ) {
|
||||
printf("Error: Cannot get current run loop\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
redisAsyncContext *c = redisAsyncConnect("127.0.0.1", 6379);
|
||||
if (c->err) {
|
||||
/* Let *c leak for now... */
|
||||
printf("Error: %s\n", c->errstr);
|
||||
return 1;
|
||||
}
|
||||
|
||||
redisMacOSAttach(c, loop);
|
||||
|
||||
redisAsyncSetConnectCallback(c,connectCallback);
|
||||
redisAsyncSetDisconnectCallback(c,disconnectCallback);
|
||||
|
||||
redisAsyncCommand(c, NULL, NULL, "SET key %b", argv[argc-1], strlen(argv[argc-1]));
|
||||
redisAsyncCommand(c, getCallback, (char*)"end-1", "GET key");
|
||||
|
||||
CFRunLoopRun();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
Vendored
+46
@@ -0,0 +1,46 @@
|
||||
#include <iostream>
|
||||
using namespace std;
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QTimer>
|
||||
|
||||
#include "example-qt.h"
|
||||
|
||||
void getCallback(redisAsyncContext *, void * r, void * privdata) {
|
||||
|
||||
redisReply * reply = static_cast<redisReply *>(r);
|
||||
ExampleQt * ex = static_cast<ExampleQt *>(privdata);
|
||||
if (reply == nullptr || ex == nullptr) return;
|
||||
|
||||
cout << "key: " << reply->str << endl;
|
||||
|
||||
ex->finish();
|
||||
}
|
||||
|
||||
void ExampleQt::run() {
|
||||
|
||||
m_ctx = redisAsyncConnect("localhost", 6379);
|
||||
|
||||
if (m_ctx->err) {
|
||||
cerr << "Error: " << m_ctx->errstr << endl;
|
||||
redisAsyncFree(m_ctx);
|
||||
emit finished();
|
||||
}
|
||||
|
||||
m_adapter.setContext(m_ctx);
|
||||
|
||||
redisAsyncCommand(m_ctx, NULL, NULL, "SET key %s", m_value);
|
||||
redisAsyncCommand(m_ctx, getCallback, this, "GET key");
|
||||
}
|
||||
|
||||
int main (int argc, char **argv) {
|
||||
|
||||
QCoreApplication app(argc, argv);
|
||||
|
||||
ExampleQt example(argv[argc-1]);
|
||||
|
||||
QObject::connect(&example, SIGNAL(finished()), &app, SLOT(quit()));
|
||||
QTimer::singleShot(0, &example, SLOT(run()));
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
Vendored
+32
@@ -0,0 +1,32 @@
|
||||
#ifndef __HIREDIS_EXAMPLE_QT_H
|
||||
#define __HIREDIS_EXAMPLE_QT_H
|
||||
|
||||
#include <adapters/qt.h>
|
||||
|
||||
class ExampleQt : public QObject {
|
||||
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ExampleQt(const char * value, QObject * parent = 0)
|
||||
: QObject(parent), m_value(value) {}
|
||||
|
||||
signals:
|
||||
void finished();
|
||||
|
||||
public slots:
|
||||
void run();
|
||||
|
||||
private:
|
||||
void finish() { emit finished(); }
|
||||
|
||||
private:
|
||||
const char * m_value;
|
||||
redisAsyncContext * m_ctx;
|
||||
RedisQtAdapter m_adapter;
|
||||
|
||||
friend
|
||||
void getCallback(redisAsyncContext *, void *, void *);
|
||||
};
|
||||
|
||||
#endif /* !__HIREDIS_EXAMPLE_QT_H */
|
||||
Vendored
+1
-1
@@ -57,7 +57,7 @@ int main(int argc, char **argv) {
|
||||
for (j = 0; j < 10; j++) {
|
||||
char buf[64];
|
||||
|
||||
snprintf(buf,64,"%d",j);
|
||||
snprintf(buf,64,"%u",j);
|
||||
reply = redisCommand(c,"LPUSH mylist element-%s", buf);
|
||||
freeReplyObject(reply);
|
||||
}
|
||||
|
||||
Vendored
+8
-7
@@ -1,23 +1,24 @@
|
||||
#ifndef __HIREDIS_FMACRO_H
|
||||
#define __HIREDIS_FMACRO_H
|
||||
|
||||
#if !defined(_BSD_SOURCE)
|
||||
#if defined(__linux__)
|
||||
#define _BSD_SOURCE
|
||||
#define _DEFAULT_SOURCE
|
||||
#endif
|
||||
|
||||
#if defined(_AIX)
|
||||
#define _ALL_SOURCE
|
||||
#if defined(__CYGWIN__)
|
||||
#include <sys/cdefs.h>
|
||||
#endif
|
||||
|
||||
#if defined(__sun__)
|
||||
#define _POSIX_C_SOURCE 200112L
|
||||
#elif defined(__linux__) || defined(__OpenBSD__) || defined(__NetBSD__)
|
||||
#define _XOPEN_SOURCE 600
|
||||
#else
|
||||
#define _XOPEN_SOURCE
|
||||
#if !(defined(__APPLE__) && defined(__MACH__))
|
||||
#define _XOPEN_SOURCE 600
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if __APPLE__ && __MACH__
|
||||
#if defined(__APPLE__) && defined(__MACH__)
|
||||
#define _OSX
|
||||
#endif
|
||||
|
||||
|
||||
Vendored
+191
-528
@@ -1,6 +1,8 @@
|
||||
/*
|
||||
* Copyright (c) 2009-2011, Salvatore Sanfilippo <antirez at gmail dot com>
|
||||
* Copyright (c) 2010-2011, Pieter Noordhuis <pcnoordhuis at gmail dot com>
|
||||
* Copyright (c) 2010-2014, Pieter Noordhuis <pcnoordhuis at gmail dot com>
|
||||
* Copyright (c) 2015, Matt Stancliff <matt at genges dot com>,
|
||||
* Jan-Erik Rediger <janerik at fnordig dot com>
|
||||
*
|
||||
* All rights reserved.
|
||||
*
|
||||
@@ -73,6 +75,9 @@ void freeReplyObject(void *reply) {
|
||||
redisReply *r = reply;
|
||||
size_t j;
|
||||
|
||||
if (r == NULL)
|
||||
return;
|
||||
|
||||
switch(r->type) {
|
||||
case REDIS_REPLY_INTEGER:
|
||||
break; /* Nothing to free */
|
||||
@@ -183,504 +188,23 @@ static void *createNilObject(const redisReadTask *task) {
|
||||
return r;
|
||||
}
|
||||
|
||||
static void __redisReaderSetError(redisReader *r, int type, const char *str) {
|
||||
size_t len;
|
||||
|
||||
if (r->reply != NULL && r->fn && r->fn->freeObject) {
|
||||
r->fn->freeObject(r->reply);
|
||||
r->reply = NULL;
|
||||
}
|
||||
|
||||
/* Clear input buffer on errors. */
|
||||
if (r->buf != NULL) {
|
||||
sdsfree(r->buf);
|
||||
r->buf = NULL;
|
||||
r->pos = r->len = 0;
|
||||
}
|
||||
|
||||
/* Reset task stack. */
|
||||
r->ridx = -1;
|
||||
|
||||
/* Set error. */
|
||||
r->err = type;
|
||||
len = strlen(str);
|
||||
len = len < (sizeof(r->errstr)-1) ? len : (sizeof(r->errstr)-1);
|
||||
memcpy(r->errstr,str,len);
|
||||
r->errstr[len] = '\0';
|
||||
}
|
||||
|
||||
static size_t chrtos(char *buf, size_t size, char byte) {
|
||||
size_t len = 0;
|
||||
|
||||
switch(byte) {
|
||||
case '\\':
|
||||
case '"':
|
||||
len = snprintf(buf,size,"\"\\%c\"",byte);
|
||||
break;
|
||||
case '\n': len = snprintf(buf,size,"\"\\n\""); break;
|
||||
case '\r': len = snprintf(buf,size,"\"\\r\""); break;
|
||||
case '\t': len = snprintf(buf,size,"\"\\t\""); break;
|
||||
case '\a': len = snprintf(buf,size,"\"\\a\""); break;
|
||||
case '\b': len = snprintf(buf,size,"\"\\b\""); break;
|
||||
default:
|
||||
if (isprint(byte))
|
||||
len = snprintf(buf,size,"\"%c\"",byte);
|
||||
else
|
||||
len = snprintf(buf,size,"\"\\x%02x\"",(unsigned char)byte);
|
||||
break;
|
||||
}
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
static void __redisReaderSetErrorProtocolByte(redisReader *r, char byte) {
|
||||
char cbuf[8], sbuf[128];
|
||||
|
||||
chrtos(cbuf,sizeof(cbuf),byte);
|
||||
snprintf(sbuf,sizeof(sbuf),
|
||||
"Protocol error, got %s as reply type byte", cbuf);
|
||||
__redisReaderSetError(r,REDIS_ERR_PROTOCOL,sbuf);
|
||||
}
|
||||
|
||||
static void __redisReaderSetErrorOOM(redisReader *r) {
|
||||
__redisReaderSetError(r,REDIS_ERR_OOM,"Out of memory");
|
||||
}
|
||||
|
||||
static char *readBytes(redisReader *r, unsigned int bytes) {
|
||||
char *p;
|
||||
if (r->len-r->pos >= bytes) {
|
||||
p = r->buf+r->pos;
|
||||
r->pos += bytes;
|
||||
return p;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Find pointer to \r\n. */
|
||||
static char *seekNewline(char *s, size_t len) {
|
||||
int pos = 0;
|
||||
int _len = len-1;
|
||||
|
||||
/* Position should be < len-1 because the character at "pos" should be
|
||||
* followed by a \n. Note that strchr cannot be used because it doesn't
|
||||
* allow to search a limited length and the buffer that is being searched
|
||||
* might not have a trailing NULL character. */
|
||||
while (pos < _len) {
|
||||
while(pos < _len && s[pos] != '\r') pos++;
|
||||
if (s[pos] != '\r') {
|
||||
/* Not found. */
|
||||
return NULL;
|
||||
} else {
|
||||
if (s[pos+1] == '\n') {
|
||||
/* Found. */
|
||||
return s+pos;
|
||||
} else {
|
||||
/* Continue searching. */
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Read a long long value starting at *s, under the assumption that it will be
|
||||
* terminated by \r\n. Ambiguously returns -1 for unexpected input. */
|
||||
static long long readLongLong(char *s) {
|
||||
long long v = 0;
|
||||
int dec, mult = 1;
|
||||
char c;
|
||||
|
||||
if (*s == '-') {
|
||||
mult = -1;
|
||||
s++;
|
||||
} else if (*s == '+') {
|
||||
mult = 1;
|
||||
s++;
|
||||
}
|
||||
|
||||
while ((c = *(s++)) != '\r') {
|
||||
dec = c - '0';
|
||||
if (dec >= 0 && dec < 10) {
|
||||
v *= 10;
|
||||
v += dec;
|
||||
} else {
|
||||
/* Should not happen... */
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return mult*v;
|
||||
}
|
||||
|
||||
static char *readLine(redisReader *r, int *_len) {
|
||||
char *p, *s;
|
||||
int len;
|
||||
|
||||
p = r->buf+r->pos;
|
||||
s = seekNewline(p,(r->len-r->pos));
|
||||
if (s != NULL) {
|
||||
len = s-(r->buf+r->pos);
|
||||
r->pos += len+2; /* skip \r\n */
|
||||
if (_len) *_len = len;
|
||||
return p;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void moveToNextTask(redisReader *r) {
|
||||
redisReadTask *cur, *prv;
|
||||
while (r->ridx >= 0) {
|
||||
/* Return a.s.a.p. when the stack is now empty. */
|
||||
if (r->ridx == 0) {
|
||||
r->ridx--;
|
||||
return;
|
||||
}
|
||||
|
||||
cur = &(r->rstack[r->ridx]);
|
||||
prv = &(r->rstack[r->ridx-1]);
|
||||
assert(prv->type == REDIS_REPLY_ARRAY);
|
||||
if (cur->idx == prv->elements-1) {
|
||||
r->ridx--;
|
||||
} else {
|
||||
/* Reset the type because the next item can be anything */
|
||||
assert(cur->idx < prv->elements);
|
||||
cur->type = -1;
|
||||
cur->elements = -1;
|
||||
cur->idx++;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int processLineItem(redisReader *r) {
|
||||
redisReadTask *cur = &(r->rstack[r->ridx]);
|
||||
void *obj;
|
||||
char *p;
|
||||
int len;
|
||||
|
||||
if ((p = readLine(r,&len)) != NULL) {
|
||||
if (cur->type == REDIS_REPLY_INTEGER) {
|
||||
if (r->fn && r->fn->createInteger)
|
||||
obj = r->fn->createInteger(cur,readLongLong(p));
|
||||
else
|
||||
obj = (void*)REDIS_REPLY_INTEGER;
|
||||
} else {
|
||||
/* Type will be error or status. */
|
||||
if (r->fn && r->fn->createString)
|
||||
obj = r->fn->createString(cur,p,len);
|
||||
else
|
||||
obj = (void*)(size_t)(cur->type);
|
||||
}
|
||||
|
||||
if (obj == NULL) {
|
||||
__redisReaderSetErrorOOM(r);
|
||||
return REDIS_ERR;
|
||||
}
|
||||
|
||||
/* Set reply if this is the root object. */
|
||||
if (r->ridx == 0) r->reply = obj;
|
||||
moveToNextTask(r);
|
||||
return REDIS_OK;
|
||||
}
|
||||
|
||||
return REDIS_ERR;
|
||||
}
|
||||
|
||||
static int processBulkItem(redisReader *r) {
|
||||
redisReadTask *cur = &(r->rstack[r->ridx]);
|
||||
void *obj = NULL;
|
||||
char *p, *s;
|
||||
long len;
|
||||
unsigned long bytelen;
|
||||
int success = 0;
|
||||
|
||||
p = r->buf+r->pos;
|
||||
s = seekNewline(p,r->len-r->pos);
|
||||
if (s != NULL) {
|
||||
p = r->buf+r->pos;
|
||||
bytelen = s-(r->buf+r->pos)+2; /* include \r\n */
|
||||
len = readLongLong(p);
|
||||
|
||||
if (len < 0) {
|
||||
/* The nil object can always be created. */
|
||||
if (r->fn && r->fn->createNil)
|
||||
obj = r->fn->createNil(cur);
|
||||
else
|
||||
obj = (void*)REDIS_REPLY_NIL;
|
||||
success = 1;
|
||||
} else {
|
||||
/* Only continue when the buffer contains the entire bulk item. */
|
||||
bytelen += len+2; /* include \r\n */
|
||||
if (r->pos+bytelen <= r->len) {
|
||||
if (r->fn && r->fn->createString)
|
||||
obj = r->fn->createString(cur,s+2,len);
|
||||
else
|
||||
obj = (void*)REDIS_REPLY_STRING;
|
||||
success = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Proceed when obj was created. */
|
||||
if (success) {
|
||||
if (obj == NULL) {
|
||||
__redisReaderSetErrorOOM(r);
|
||||
return REDIS_ERR;
|
||||
}
|
||||
|
||||
r->pos += bytelen;
|
||||
|
||||
/* Set reply if this is the root object. */
|
||||
if (r->ridx == 0) r->reply = obj;
|
||||
moveToNextTask(r);
|
||||
return REDIS_OK;
|
||||
}
|
||||
}
|
||||
|
||||
return REDIS_ERR;
|
||||
}
|
||||
|
||||
static int processMultiBulkItem(redisReader *r) {
|
||||
redisReadTask *cur = &(r->rstack[r->ridx]);
|
||||
void *obj;
|
||||
char *p;
|
||||
long elements;
|
||||
int root = 0;
|
||||
|
||||
/* Set error for nested multi bulks with depth > 7 */
|
||||
if (r->ridx == 8) {
|
||||
__redisReaderSetError(r,REDIS_ERR_PROTOCOL,
|
||||
"No support for nested multi bulk replies with depth > 7");
|
||||
return REDIS_ERR;
|
||||
}
|
||||
|
||||
if ((p = readLine(r,NULL)) != NULL) {
|
||||
elements = readLongLong(p);
|
||||
root = (r->ridx == 0);
|
||||
|
||||
if (elements == -1) {
|
||||
if (r->fn && r->fn->createNil)
|
||||
obj = r->fn->createNil(cur);
|
||||
else
|
||||
obj = (void*)REDIS_REPLY_NIL;
|
||||
|
||||
if (obj == NULL) {
|
||||
__redisReaderSetErrorOOM(r);
|
||||
return REDIS_ERR;
|
||||
}
|
||||
|
||||
moveToNextTask(r);
|
||||
} else {
|
||||
if (r->fn && r->fn->createArray)
|
||||
obj = r->fn->createArray(cur,elements);
|
||||
else
|
||||
obj = (void*)REDIS_REPLY_ARRAY;
|
||||
|
||||
if (obj == NULL) {
|
||||
__redisReaderSetErrorOOM(r);
|
||||
return REDIS_ERR;
|
||||
}
|
||||
|
||||
/* Modify task stack when there are more than 0 elements. */
|
||||
if (elements > 0) {
|
||||
cur->elements = elements;
|
||||
cur->obj = obj;
|
||||
r->ridx++;
|
||||
r->rstack[r->ridx].type = -1;
|
||||
r->rstack[r->ridx].elements = -1;
|
||||
r->rstack[r->ridx].idx = 0;
|
||||
r->rstack[r->ridx].obj = NULL;
|
||||
r->rstack[r->ridx].parent = cur;
|
||||
r->rstack[r->ridx].privdata = r->privdata;
|
||||
} else {
|
||||
moveToNextTask(r);
|
||||
}
|
||||
}
|
||||
|
||||
/* Set reply if this is the root object. */
|
||||
if (root) r->reply = obj;
|
||||
return REDIS_OK;
|
||||
}
|
||||
|
||||
return REDIS_ERR;
|
||||
}
|
||||
|
||||
static int processItem(redisReader *r) {
|
||||
redisReadTask *cur = &(r->rstack[r->ridx]);
|
||||
char *p;
|
||||
|
||||
/* check if we need to read type */
|
||||
if (cur->type < 0) {
|
||||
if ((p = readBytes(r,1)) != NULL) {
|
||||
switch (p[0]) {
|
||||
case '-':
|
||||
cur->type = REDIS_REPLY_ERROR;
|
||||
break;
|
||||
case '+':
|
||||
cur->type = REDIS_REPLY_STATUS;
|
||||
break;
|
||||
case ':':
|
||||
cur->type = REDIS_REPLY_INTEGER;
|
||||
break;
|
||||
case '$':
|
||||
cur->type = REDIS_REPLY_STRING;
|
||||
break;
|
||||
case '*':
|
||||
cur->type = REDIS_REPLY_ARRAY;
|
||||
break;
|
||||
default:
|
||||
__redisReaderSetErrorProtocolByte(r,*p);
|
||||
return REDIS_ERR;
|
||||
}
|
||||
} else {
|
||||
/* could not consume 1 byte */
|
||||
return REDIS_ERR;
|
||||
}
|
||||
}
|
||||
|
||||
/* process typed item */
|
||||
switch(cur->type) {
|
||||
case REDIS_REPLY_ERROR:
|
||||
case REDIS_REPLY_STATUS:
|
||||
case REDIS_REPLY_INTEGER:
|
||||
return processLineItem(r);
|
||||
case REDIS_REPLY_STRING:
|
||||
return processBulkItem(r);
|
||||
case REDIS_REPLY_ARRAY:
|
||||
return processMultiBulkItem(r);
|
||||
default:
|
||||
assert(NULL);
|
||||
return REDIS_ERR; /* Avoid warning. */
|
||||
}
|
||||
}
|
||||
|
||||
redisReader *redisReaderCreate(void) {
|
||||
redisReader *r;
|
||||
|
||||
r = calloc(sizeof(redisReader),1);
|
||||
if (r == NULL)
|
||||
return NULL;
|
||||
|
||||
r->err = 0;
|
||||
r->errstr[0] = '\0';
|
||||
r->fn = &defaultFunctions;
|
||||
r->buf = sdsempty();
|
||||
r->maxbuf = REDIS_READER_MAX_BUF;
|
||||
if (r->buf == NULL) {
|
||||
free(r);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
r->ridx = -1;
|
||||
return r;
|
||||
}
|
||||
|
||||
void redisReaderFree(redisReader *r) {
|
||||
if (r->reply != NULL && r->fn && r->fn->freeObject)
|
||||
r->fn->freeObject(r->reply);
|
||||
if (r->buf != NULL)
|
||||
sdsfree(r->buf);
|
||||
free(r);
|
||||
}
|
||||
|
||||
int redisReaderFeed(redisReader *r, const char *buf, size_t len) {
|
||||
sds newbuf;
|
||||
|
||||
/* Return early when this reader is in an erroneous state. */
|
||||
if (r->err)
|
||||
return REDIS_ERR;
|
||||
|
||||
/* Copy the provided buffer. */
|
||||
if (buf != NULL && len >= 1) {
|
||||
/* Destroy internal buffer when it is empty and is quite large. */
|
||||
if (r->len == 0 && r->maxbuf != 0 && sdsavail(r->buf) > r->maxbuf) {
|
||||
sdsfree(r->buf);
|
||||
r->buf = sdsempty();
|
||||
r->pos = 0;
|
||||
|
||||
/* r->buf should not be NULL since we just free'd a larger one. */
|
||||
assert(r->buf != NULL);
|
||||
}
|
||||
|
||||
newbuf = sdscatlen(r->buf,buf,len);
|
||||
if (newbuf == NULL) {
|
||||
__redisReaderSetErrorOOM(r);
|
||||
return REDIS_ERR;
|
||||
}
|
||||
|
||||
r->buf = newbuf;
|
||||
r->len = sdslen(r->buf);
|
||||
}
|
||||
|
||||
return REDIS_OK;
|
||||
}
|
||||
|
||||
int redisReaderGetReply(redisReader *r, void **reply) {
|
||||
/* Default target pointer to NULL. */
|
||||
if (reply != NULL)
|
||||
*reply = NULL;
|
||||
|
||||
/* Return early when this reader is in an erroneous state. */
|
||||
if (r->err)
|
||||
return REDIS_ERR;
|
||||
|
||||
/* When the buffer is empty, there will never be a reply. */
|
||||
if (r->len == 0)
|
||||
return REDIS_OK;
|
||||
|
||||
/* Set first item to process when the stack is empty. */
|
||||
if (r->ridx == -1) {
|
||||
r->rstack[0].type = -1;
|
||||
r->rstack[0].elements = -1;
|
||||
r->rstack[0].idx = -1;
|
||||
r->rstack[0].obj = NULL;
|
||||
r->rstack[0].parent = NULL;
|
||||
r->rstack[0].privdata = r->privdata;
|
||||
r->ridx = 0;
|
||||
}
|
||||
|
||||
/* Process items in reply. */
|
||||
while (r->ridx >= 0)
|
||||
if (processItem(r) != REDIS_OK)
|
||||
break;
|
||||
|
||||
/* Return ASAP when an error occurred. */
|
||||
if (r->err)
|
||||
return REDIS_ERR;
|
||||
|
||||
/* Discard part of the buffer when we've consumed at least 1k, to avoid
|
||||
* doing unnecessary calls to memmove() in sds.c. */
|
||||
if (r->pos >= 1024) {
|
||||
sdsrange(r->buf,r->pos,-1);
|
||||
r->pos = 0;
|
||||
r->len = sdslen(r->buf);
|
||||
}
|
||||
|
||||
/* Emit a reply when there is one. */
|
||||
if (r->ridx == -1) {
|
||||
if (reply != NULL)
|
||||
*reply = r->reply;
|
||||
r->reply = NULL;
|
||||
}
|
||||
return REDIS_OK;
|
||||
}
|
||||
|
||||
/* Calculate the number of bytes needed to represent an integer as string. */
|
||||
static int intlen(int i) {
|
||||
int len = 0;
|
||||
if (i < 0) {
|
||||
len++;
|
||||
i = -i;
|
||||
}
|
||||
do {
|
||||
len++;
|
||||
i /= 10;
|
||||
} while(i);
|
||||
return len;
|
||||
/* Return the number of digits of 'v' when converted to string in radix 10.
|
||||
* Implementation borrowed from link in redis/src/util.c:string2ll(). */
|
||||
static uint32_t countDigits(uint64_t v) {
|
||||
uint32_t result = 1;
|
||||
for (;;) {
|
||||
if (v < 10) return result;
|
||||
if (v < 100) return result + 1;
|
||||
if (v < 1000) return result + 2;
|
||||
if (v < 10000) return result + 3;
|
||||
v /= 10000U;
|
||||
result += 4;
|
||||
}
|
||||
}
|
||||
|
||||
/* Helper that calculates the bulk length given a certain string length. */
|
||||
static size_t bulklen(size_t len) {
|
||||
return 1+intlen(len)+2+len+2;
|
||||
return 1+countDigits(len)+2+len+2;
|
||||
}
|
||||
|
||||
int redisvFormatCommand(char **target, const char *format, va_list ap) {
|
||||
@@ -692,6 +216,7 @@ int redisvFormatCommand(char **target, const char *format, va_list ap) {
|
||||
char **curargv = NULL, **newargv = NULL;
|
||||
int argc = 0;
|
||||
int totlen = 0;
|
||||
int error_type = 0; /* 0 = no error; -1 = memory error; -2 = format error */
|
||||
int j;
|
||||
|
||||
/* Abort if there is not target to set */
|
||||
@@ -708,19 +233,19 @@ int redisvFormatCommand(char **target, const char *format, va_list ap) {
|
||||
if (*c == ' ') {
|
||||
if (touched) {
|
||||
newargv = realloc(curargv,sizeof(char*)*(argc+1));
|
||||
if (newargv == NULL) goto err;
|
||||
if (newargv == NULL) goto memory_err;
|
||||
curargv = newargv;
|
||||
curargv[argc++] = curarg;
|
||||
totlen += bulklen(sdslen(curarg));
|
||||
|
||||
/* curarg is put in argv so it can be overwritten. */
|
||||
curarg = sdsempty();
|
||||
if (curarg == NULL) goto err;
|
||||
if (curarg == NULL) goto memory_err;
|
||||
touched = 0;
|
||||
}
|
||||
} else {
|
||||
newarg = sdscatlen(curarg,c,1);
|
||||
if (newarg == NULL) goto err;
|
||||
if (newarg == NULL) goto memory_err;
|
||||
curarg = newarg;
|
||||
touched = 1;
|
||||
}
|
||||
@@ -751,17 +276,14 @@ int redisvFormatCommand(char **target, const char *format, va_list ap) {
|
||||
/* Try to detect printf format */
|
||||
{
|
||||
static const char intfmts[] = "diouxX";
|
||||
static const char flags[] = "#0-+ ";
|
||||
char _format[16];
|
||||
const char *_p = c+1;
|
||||
size_t _l = 0;
|
||||
va_list _cpy;
|
||||
|
||||
/* Flags */
|
||||
if (*_p != '\0' && *_p == '#') _p++;
|
||||
if (*_p != '\0' && *_p == '0') _p++;
|
||||
if (*_p != '\0' && *_p == '-') _p++;
|
||||
if (*_p != '\0' && *_p == ' ') _p++;
|
||||
if (*_p != '\0' && *_p == '+') _p++;
|
||||
while (*_p != '\0' && strchr(flags,*_p) != NULL) _p++;
|
||||
|
||||
/* Field width */
|
||||
while (*_p != '\0' && isdigit(*_p)) _p++;
|
||||
@@ -829,7 +351,7 @@ int redisvFormatCommand(char **target, const char *format, va_list ap) {
|
||||
|
||||
fmt_invalid:
|
||||
va_end(_cpy);
|
||||
goto err;
|
||||
goto format_err;
|
||||
|
||||
fmt_valid:
|
||||
_l = (_p+1)-c;
|
||||
@@ -848,7 +370,7 @@ int redisvFormatCommand(char **target, const char *format, va_list ap) {
|
||||
}
|
||||
}
|
||||
|
||||
if (newarg == NULL) goto err;
|
||||
if (newarg == NULL) goto memory_err;
|
||||
curarg = newarg;
|
||||
|
||||
touched = 1;
|
||||
@@ -860,7 +382,7 @@ int redisvFormatCommand(char **target, const char *format, va_list ap) {
|
||||
/* Add the last argument if needed */
|
||||
if (touched) {
|
||||
newargv = realloc(curargv,sizeof(char*)*(argc+1));
|
||||
if (newargv == NULL) goto err;
|
||||
if (newargv == NULL) goto memory_err;
|
||||
curargv = newargv;
|
||||
curargv[argc++] = curarg;
|
||||
totlen += bulklen(sdslen(curarg));
|
||||
@@ -872,11 +394,11 @@ int redisvFormatCommand(char **target, const char *format, va_list ap) {
|
||||
curarg = NULL;
|
||||
|
||||
/* Add bytes needed to hold multi bulk count */
|
||||
totlen += 1+intlen(argc)+2;
|
||||
totlen += 1+countDigits(argc)+2;
|
||||
|
||||
/* Build the command at protocol level */
|
||||
cmd = malloc(totlen+1);
|
||||
if (cmd == NULL) goto err;
|
||||
if (cmd == NULL) goto memory_err;
|
||||
|
||||
pos = sprintf(cmd,"*%d\r\n",argc);
|
||||
for (j = 0; j < argc; j++) {
|
||||
@@ -894,20 +416,29 @@ int redisvFormatCommand(char **target, const char *format, va_list ap) {
|
||||
*target = cmd;
|
||||
return totlen;
|
||||
|
||||
err:
|
||||
while(argc--)
|
||||
sdsfree(curargv[argc]);
|
||||
free(curargv);
|
||||
format_err:
|
||||
error_type = -2;
|
||||
goto cleanup;
|
||||
|
||||
if (curarg != NULL)
|
||||
sdsfree(curarg);
|
||||
memory_err:
|
||||
error_type = -1;
|
||||
goto cleanup;
|
||||
|
||||
cleanup:
|
||||
if (curargv) {
|
||||
while(argc--)
|
||||
sdsfree(curargv[argc]);
|
||||
free(curargv);
|
||||
}
|
||||
|
||||
sdsfree(curarg);
|
||||
|
||||
/* No need to check cmd since it is the last statement that can fail,
|
||||
* but do it anyway to be as defensive as possible. */
|
||||
if (cmd != NULL)
|
||||
free(cmd);
|
||||
|
||||
return -1;
|
||||
return error_type;
|
||||
}
|
||||
|
||||
/* Format a command according to the Redis protocol. This function
|
||||
@@ -928,9 +459,69 @@ int redisFormatCommand(char **target, const char *format, ...) {
|
||||
va_start(ap,format);
|
||||
len = redisvFormatCommand(target,format,ap);
|
||||
va_end(ap);
|
||||
|
||||
/* The API says "-1" means bad result, but we now also return "-2" in some
|
||||
* cases. Force the return value to always be -1. */
|
||||
if (len < 0)
|
||||
len = -1;
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
/* Format a command according to the Redis protocol using an sds string and
|
||||
* sdscatfmt for the processing of arguments. This function takes the
|
||||
* number of arguments, an array with arguments and an array with their
|
||||
* lengths. If the latter is set to NULL, strlen will be used to compute the
|
||||
* argument lengths.
|
||||
*/
|
||||
int redisFormatSdsCommandArgv(sds *target, int argc, const char **argv,
|
||||
const size_t *argvlen)
|
||||
{
|
||||
sds cmd;
|
||||
unsigned long long totlen;
|
||||
int j;
|
||||
size_t len;
|
||||
|
||||
/* Abort on a NULL target */
|
||||
if (target == NULL)
|
||||
return -1;
|
||||
|
||||
/* Calculate our total size */
|
||||
totlen = 1+countDigits(argc)+2;
|
||||
for (j = 0; j < argc; j++) {
|
||||
len = argvlen ? argvlen[j] : strlen(argv[j]);
|
||||
totlen += bulklen(len);
|
||||
}
|
||||
|
||||
/* Use an SDS string for command construction */
|
||||
cmd = sdsempty();
|
||||
if (cmd == NULL)
|
||||
return -1;
|
||||
|
||||
/* We already know how much storage we need */
|
||||
cmd = sdsMakeRoomFor(cmd, totlen);
|
||||
if (cmd == NULL)
|
||||
return -1;
|
||||
|
||||
/* Construct command */
|
||||
cmd = sdscatfmt(cmd, "*%i\r\n", argc);
|
||||
for (j=0; j < argc; j++) {
|
||||
len = argvlen ? argvlen[j] : strlen(argv[j]);
|
||||
cmd = sdscatfmt(cmd, "$%u\r\n", len);
|
||||
cmd = sdscatlen(cmd, argv[j], len);
|
||||
cmd = sdscatlen(cmd, "\r\n", sizeof("\r\n")-1);
|
||||
}
|
||||
|
||||
assert(sdslen(cmd)==totlen);
|
||||
|
||||
*target = cmd;
|
||||
return totlen;
|
||||
}
|
||||
|
||||
void redisFreeSdsCommand(sds cmd) {
|
||||
sdsfree(cmd);
|
||||
}
|
||||
|
||||
/* Format a command according to the Redis protocol. This function takes the
|
||||
* number of arguments, an array with arguments and an array with their
|
||||
* lengths. If the latter is set to NULL, strlen will be used to compute the
|
||||
@@ -942,8 +533,12 @@ int redisFormatCommandArgv(char **target, int argc, const char **argv, const siz
|
||||
size_t len;
|
||||
int totlen, j;
|
||||
|
||||
/* Abort on a NULL target */
|
||||
if (target == NULL)
|
||||
return -1;
|
||||
|
||||
/* Calculate number of bytes needed for the command */
|
||||
totlen = 1+intlen(argc)+2;
|
||||
totlen = 1+countDigits(argc)+2;
|
||||
for (j = 0; j < argc; j++) {
|
||||
len = argvlen ? argvlen[j] : strlen(argv[j]);
|
||||
totlen += bulklen(len);
|
||||
@@ -970,6 +565,10 @@ int redisFormatCommandArgv(char **target, int argc, const char **argv, const siz
|
||||
return totlen;
|
||||
}
|
||||
|
||||
void redisFreeCommand(char *cmd) {
|
||||
free(cmd);
|
||||
}
|
||||
|
||||
void __redisSetError(redisContext *c, int type, const char *str) {
|
||||
size_t len;
|
||||
|
||||
@@ -982,10 +581,14 @@ void __redisSetError(redisContext *c, int type, const char *str) {
|
||||
} else {
|
||||
/* Only REDIS_ERR_IO may lack a description! */
|
||||
assert(type == REDIS_ERR_IO);
|
||||
strerror_r(errno,c->errstr,sizeof(c->errstr));
|
||||
__redis_strerror_r(errno, c->errstr, sizeof(c->errstr));
|
||||
}
|
||||
}
|
||||
|
||||
redisReader *redisReaderCreate(void) {
|
||||
return redisReaderCreateWithFunctions(&defaultFunctions);
|
||||
}
|
||||
|
||||
static redisContext *redisContextInit(void) {
|
||||
redisContext *c;
|
||||
|
||||
@@ -997,24 +600,72 @@ static redisContext *redisContextInit(void) {
|
||||
c->errstr[0] = '\0';
|
||||
c->obuf = sdsempty();
|
||||
c->reader = redisReaderCreate();
|
||||
c->tcp.host = NULL;
|
||||
c->tcp.source_addr = NULL;
|
||||
c->unix_sock.path = NULL;
|
||||
c->timeout = NULL;
|
||||
|
||||
if (c->obuf == NULL || c->reader == NULL) {
|
||||
redisFree(c);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
void redisFree(redisContext *c) {
|
||||
if (c == NULL)
|
||||
return;
|
||||
if (c->fd > 0)
|
||||
close(c->fd);
|
||||
if (c->obuf != NULL)
|
||||
sdsfree(c->obuf);
|
||||
if (c->reader != NULL)
|
||||
redisReaderFree(c->reader);
|
||||
if (c->tcp.host)
|
||||
free(c->tcp.host);
|
||||
if (c->tcp.source_addr)
|
||||
free(c->tcp.source_addr);
|
||||
if (c->unix_sock.path)
|
||||
free(c->unix_sock.path);
|
||||
if (c->timeout)
|
||||
free(c->timeout);
|
||||
free(c);
|
||||
}
|
||||
|
||||
int redisFreeKeepFd(redisContext *c) {
|
||||
int fd = c->fd;
|
||||
c->fd = -1;
|
||||
redisFree(c);
|
||||
return fd;
|
||||
int fd = c->fd;
|
||||
c->fd = -1;
|
||||
redisFree(c);
|
||||
return fd;
|
||||
}
|
||||
|
||||
int redisReconnect(redisContext *c) {
|
||||
c->err = 0;
|
||||
memset(c->errstr, '\0', strlen(c->errstr));
|
||||
|
||||
if (c->fd > 0) {
|
||||
close(c->fd);
|
||||
}
|
||||
|
||||
sdsfree(c->obuf);
|
||||
redisReaderFree(c->reader);
|
||||
|
||||
c->obuf = sdsempty();
|
||||
c->reader = redisReaderCreate();
|
||||
|
||||
if (c->connection_type == REDIS_CONN_TCP) {
|
||||
return redisContextConnectBindTcp(c, c->tcp.host, c->tcp.port,
|
||||
c->timeout, c->tcp.source_addr);
|
||||
} else if (c->connection_type == REDIS_CONN_UNIX) {
|
||||
return redisContextConnectUnix(c, c->unix_sock.path, c->timeout);
|
||||
} else {
|
||||
/* Something bad happened here and shouldn't have. There isn't
|
||||
enough information in the context to reconnect. */
|
||||
__redisSetError(c,REDIS_ERR_OTHER,"Not enough information to reconnect");
|
||||
}
|
||||
|
||||
return REDIS_ERR;
|
||||
}
|
||||
|
||||
/* Connect to a Redis instance. On error the field error in the returned
|
||||
@@ -1064,6 +715,15 @@ redisContext *redisConnectBindNonBlock(const char *ip, int port,
|
||||
return c;
|
||||
}
|
||||
|
||||
redisContext *redisConnectBindNonBlockWithReuse(const char *ip, int port,
|
||||
const char *source_addr) {
|
||||
redisContext *c = redisContextInit();
|
||||
c->flags &= ~REDIS_BLOCK;
|
||||
c->flags |= REDIS_REUSEADDR;
|
||||
redisContextConnectBindTcp(c,ip,port,NULL,source_addr);
|
||||
return c;
|
||||
}
|
||||
|
||||
redisContext *redisConnectUnix(const char *path) {
|
||||
redisContext *c;
|
||||
|
||||
@@ -1162,10 +822,10 @@ int redisBufferRead(redisContext *c) {
|
||||
/* Write the output buffer to the socket.
|
||||
*
|
||||
* Returns REDIS_OK when the buffer is empty, or (a part of) the buffer was
|
||||
* succesfully written to the socket. When the buffer is empty after the
|
||||
* successfully written to the socket. When the buffer is empty after the
|
||||
* write operation, "done" is set to 1 (if given).
|
||||
*
|
||||
* Returns REDIS_ERR if an error occured trying to write and sets
|
||||
* Returns REDIS_ERR if an error occurred trying to write and sets
|
||||
* c->errstr to hold the appropriate error string.
|
||||
*/
|
||||
int redisBufferWrite(redisContext *c, int *done) {
|
||||
@@ -1274,6 +934,9 @@ int redisvAppendCommand(redisContext *c, const char *format, va_list ap) {
|
||||
if (len == -1) {
|
||||
__redisSetError(c,REDIS_ERR_OOM,"Out of memory");
|
||||
return REDIS_ERR;
|
||||
} else if (len == -2) {
|
||||
__redisSetError(c,REDIS_ERR_OTHER,"Invalid format string");
|
||||
return REDIS_ERR;
|
||||
}
|
||||
|
||||
if (__redisAppendCommand(c,cmd,len) != REDIS_OK) {
|
||||
@@ -1296,21 +959,21 @@ int redisAppendCommand(redisContext *c, const char *format, ...) {
|
||||
}
|
||||
|
||||
int redisAppendCommandArgv(redisContext *c, int argc, const char **argv, const size_t *argvlen) {
|
||||
char *cmd;
|
||||
sds cmd;
|
||||
int len;
|
||||
|
||||
len = redisFormatCommandArgv(&cmd,argc,argv,argvlen);
|
||||
len = redisFormatSdsCommandArgv(&cmd,argc,argv,argvlen);
|
||||
if (len == -1) {
|
||||
__redisSetError(c,REDIS_ERR_OOM,"Out of memory");
|
||||
return REDIS_ERR;
|
||||
}
|
||||
|
||||
if (__redisAppendCommand(c,cmd,len) != REDIS_OK) {
|
||||
free(cmd);
|
||||
sdsfree(cmd);
|
||||
return REDIS_ERR;
|
||||
}
|
||||
|
||||
free(cmd);
|
||||
sdsfree(cmd);
|
||||
return REDIS_OK;
|
||||
}
|
||||
|
||||
@@ -1321,7 +984,7 @@ int redisAppendCommandArgv(redisContext *c, int argc, const char **argv, const s
|
||||
* context is non-blocking, the "reply" pointer will not be used and the
|
||||
* command is simply appended to the write buffer.
|
||||
*
|
||||
* Returns the reply when a reply was succesfully retrieved. Returns NULL
|
||||
* Returns the reply when a reply was successfully retrieved. Returns NULL
|
||||
* otherwise. When NULL is returned in a blocking context, the error field
|
||||
* in the context will be set.
|
||||
*/
|
||||
|
||||
Vendored
+78
-75
@@ -1,6 +1,8 @@
|
||||
/*
|
||||
* Copyright (c) 2009-2011, Salvatore Sanfilippo <antirez at gmail dot com>
|
||||
* Copyright (c) 2010-2011, Pieter Noordhuis <pcnoordhuis at gmail dot com>
|
||||
* Copyright (c) 2010-2014, Pieter Noordhuis <pcnoordhuis at gmail dot com>
|
||||
* Copyright (c) 2015, Matt Stancliff <matt at genges dot com>,
|
||||
* Jan-Erik Rediger <janerik at fnordig dot com>
|
||||
*
|
||||
* All rights reserved.
|
||||
*
|
||||
@@ -31,26 +33,16 @@
|
||||
|
||||
#ifndef __HIREDIS_H
|
||||
#define __HIREDIS_H
|
||||
#include <stdio.h> /* for size_t */
|
||||
#include "read.h"
|
||||
#include <stdarg.h> /* for va_list */
|
||||
#include <sys/time.h> /* for struct timeval */
|
||||
#include <stdint.h> /* uintXX_t, etc */
|
||||
#include "sds.h" /* for sds */
|
||||
|
||||
#define HIREDIS_MAJOR 0
|
||||
#define HIREDIS_MINOR 11
|
||||
#define HIREDIS_PATCH 0
|
||||
|
||||
#define REDIS_ERR -1
|
||||
#define REDIS_OK 0
|
||||
|
||||
/* When an error occurs, the err flag in a context is set to hold the type of
|
||||
* error that occured. REDIS_ERR_IO means there was an I/O error and you
|
||||
* should use the "errno" variable to find out what is wrong.
|
||||
* For other values, the "errstr" field will hold a description. */
|
||||
#define REDIS_ERR_IO 1 /* Error in read or write */
|
||||
#define REDIS_ERR_EOF 3 /* End of file */
|
||||
#define REDIS_ERR_PROTOCOL 4 /* Protocol error */
|
||||
#define REDIS_ERR_OOM 5 /* Out of memory */
|
||||
#define REDIS_ERR_OTHER 2 /* Everything else... */
|
||||
#define HIREDIS_MINOR 13
|
||||
#define HIREDIS_PATCH 3
|
||||
#define HIREDIS_SONAME 0.13
|
||||
|
||||
/* Connection type can be blocking or non-blocking and is set in the
|
||||
* least significant bit of the flags field in redisContext. */
|
||||
@@ -79,17 +71,39 @@
|
||||
/* Flag that is set when monitor mode is active */
|
||||
#define REDIS_MONITORING 0x40
|
||||
|
||||
#define REDIS_REPLY_STRING 1
|
||||
#define REDIS_REPLY_ARRAY 2
|
||||
#define REDIS_REPLY_INTEGER 3
|
||||
#define REDIS_REPLY_NIL 4
|
||||
#define REDIS_REPLY_STATUS 5
|
||||
#define REDIS_REPLY_ERROR 6
|
||||
|
||||
#define REDIS_READER_MAX_BUF (1024*16) /* Default max unused reader buffer. */
|
||||
/* Flag that is set when we should set SO_REUSEADDR before calling bind() */
|
||||
#define REDIS_REUSEADDR 0x80
|
||||
|
||||
#define REDIS_KEEPALIVE_INTERVAL 15 /* seconds */
|
||||
|
||||
/* number of times we retry to connect in the case of EADDRNOTAVAIL and
|
||||
* SO_REUSEADDR is being used. */
|
||||
#define REDIS_CONNECT_RETRIES 10
|
||||
|
||||
/* strerror_r has two completely different prototypes and behaviors
|
||||
* depending on system issues, so we need to operate on the error buffer
|
||||
* differently depending on which strerror_r we're using. */
|
||||
#ifndef _GNU_SOURCE
|
||||
/* "regular" POSIX strerror_r that does the right thing. */
|
||||
#define __redis_strerror_r(errno, buf, len) \
|
||||
do { \
|
||||
strerror_r((errno), (buf), (len)); \
|
||||
} while (0)
|
||||
#else
|
||||
/* "bad" GNU strerror_r we need to clean up after. */
|
||||
#define __redis_strerror_r(errno, buf, len) \
|
||||
do { \
|
||||
char *err_str = strerror_r((errno), (buf), (len)); \
|
||||
/* If return value _isn't_ the start of the buffer we passed in, \
|
||||
* then GNU strerror_r returned an internal static buffer and we \
|
||||
* need to copy the result into our private buffer. */ \
|
||||
if (err_str != (buf)) { \
|
||||
strncpy((buf), err_str, ((len) - 1)); \
|
||||
buf[(len)-1] = '\0'; \
|
||||
} \
|
||||
} while (0)
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
@@ -98,61 +112,13 @@ extern "C" {
|
||||
typedef struct redisReply {
|
||||
int type; /* REDIS_REPLY_* */
|
||||
long long integer; /* The integer when type is REDIS_REPLY_INTEGER */
|
||||
int len; /* Length of string */
|
||||
size_t len; /* Length of string */
|
||||
char *str; /* Used for both REDIS_REPLY_ERROR and REDIS_REPLY_STRING */
|
||||
size_t elements; /* number of elements, for REDIS_REPLY_ARRAY */
|
||||
struct redisReply **element; /* elements vector for REDIS_REPLY_ARRAY */
|
||||
} redisReply;
|
||||
|
||||
typedef struct redisReadTask {
|
||||
int type;
|
||||
int elements; /* number of elements in multibulk container */
|
||||
int idx; /* index in parent (array) object */
|
||||
void *obj; /* holds user-generated value for a read task */
|
||||
struct redisReadTask *parent; /* parent task */
|
||||
void *privdata; /* user-settable arbitrary field */
|
||||
} redisReadTask;
|
||||
|
||||
typedef struct redisReplyObjectFunctions {
|
||||
void *(*createString)(const redisReadTask*, char*, size_t);
|
||||
void *(*createArray)(const redisReadTask*, int);
|
||||
void *(*createInteger)(const redisReadTask*, long long);
|
||||
void *(*createNil)(const redisReadTask*);
|
||||
void (*freeObject)(void*);
|
||||
} redisReplyObjectFunctions;
|
||||
|
||||
/* State for the protocol parser */
|
||||
typedef struct redisReader {
|
||||
int err; /* Error flags, 0 when there is no error */
|
||||
char errstr[128]; /* String representation of error when applicable */
|
||||
|
||||
char *buf; /* Read buffer */
|
||||
size_t pos; /* Buffer cursor */
|
||||
size_t len; /* Buffer length */
|
||||
size_t maxbuf; /* Max length of unused buffer */
|
||||
|
||||
redisReadTask rstack[9];
|
||||
int ridx; /* Index of current read task */
|
||||
void *reply; /* Temporary reply pointer */
|
||||
|
||||
redisReplyObjectFunctions *fn;
|
||||
void *privdata;
|
||||
} redisReader;
|
||||
|
||||
/* Public API for the protocol parser. */
|
||||
redisReader *redisReaderCreate(void);
|
||||
void redisReaderFree(redisReader *r);
|
||||
int redisReaderFeed(redisReader *r, const char *buf, size_t len);
|
||||
int redisReaderGetReply(redisReader *r, void **reply);
|
||||
|
||||
/* Backwards compatibility, can be removed on big version bump. */
|
||||
#define redisReplyReaderCreate redisReaderCreate
|
||||
#define redisReplyReaderFree redisReaderFree
|
||||
#define redisReplyReaderFeed redisReaderFeed
|
||||
#define redisReplyReaderGetReply redisReaderGetReply
|
||||
#define redisReplyReaderSetPrivdata(_r, _p) (int)(((redisReader*)(_r))->privdata = (_p))
|
||||
#define redisReplyReaderGetObject(_r) (((redisReader*)(_r))->reply)
|
||||
#define redisReplyReaderGetError(_r) (((redisReader*)(_r))->errstr)
|
||||
|
||||
/* Function to free the reply objects hiredis returns by default. */
|
||||
void freeReplyObject(void *reply);
|
||||
@@ -161,6 +127,14 @@ void freeReplyObject(void *reply);
|
||||
int redisvFormatCommand(char **target, const char *format, va_list ap);
|
||||
int redisFormatCommand(char **target, const char *format, ...);
|
||||
int redisFormatCommandArgv(char **target, int argc, const char **argv, const size_t *argvlen);
|
||||
int redisFormatSdsCommandArgv(sds *target, int argc, const char ** argv, const size_t *argvlen);
|
||||
void redisFreeCommand(char *cmd);
|
||||
void redisFreeSdsCommand(sds cmd);
|
||||
|
||||
enum redisConnectionType {
|
||||
REDIS_CONN_TCP,
|
||||
REDIS_CONN_UNIX
|
||||
};
|
||||
|
||||
/* Context for a connection to Redis */
|
||||
typedef struct redisContext {
|
||||
@@ -170,16 +144,45 @@ typedef struct redisContext {
|
||||
int flags;
|
||||
char *obuf; /* Write buffer */
|
||||
redisReader *reader; /* Protocol reader */
|
||||
|
||||
enum redisConnectionType connection_type;
|
||||
struct timeval *timeout;
|
||||
|
||||
struct {
|
||||
char *host;
|
||||
char *source_addr;
|
||||
int port;
|
||||
} tcp;
|
||||
|
||||
struct {
|
||||
char *path;
|
||||
} unix_sock;
|
||||
|
||||
} redisContext;
|
||||
|
||||
redisContext *redisConnect(const char *ip, int port);
|
||||
redisContext *redisConnectWithTimeout(const char *ip, int port, const struct timeval tv);
|
||||
redisContext *redisConnectNonBlock(const char *ip, int port);
|
||||
redisContext *redisConnectBindNonBlock(const char *ip, int port, const char *source_addr);
|
||||
redisContext *redisConnectBindNonBlock(const char *ip, int port,
|
||||
const char *source_addr);
|
||||
redisContext *redisConnectBindNonBlockWithReuse(const char *ip, int port,
|
||||
const char *source_addr);
|
||||
redisContext *redisConnectUnix(const char *path);
|
||||
redisContext *redisConnectUnixWithTimeout(const char *path, const struct timeval tv);
|
||||
redisContext *redisConnectUnixNonBlock(const char *path);
|
||||
redisContext *redisConnectFd(int fd);
|
||||
|
||||
/**
|
||||
* Reconnect the given context using the saved information.
|
||||
*
|
||||
* This re-uses the exact same connect options as in the initial connection.
|
||||
* host, ip (or path), timeout and bind address are reused,
|
||||
* flags are used unmodified from the existing context.
|
||||
*
|
||||
* Returns REDIS_OK on successful connect or REDIS_ERR otherwise.
|
||||
*/
|
||||
int redisReconnect(redisContext *c);
|
||||
|
||||
int redisSetTimeout(redisContext *c, const struct timeval tv);
|
||||
int redisEnableKeepAlive(redisContext *c);
|
||||
void redisFree(redisContext *c);
|
||||
|
||||
Vendored
+113
-19
@@ -1,7 +1,9 @@
|
||||
/* Extracted from anet.c to work properly with Hiredis error reporting.
|
||||
*
|
||||
* Copyright (c) 2006-2011, Salvatore Sanfilippo <antirez at gmail dot com>
|
||||
* Copyright (c) 2010-2011, Pieter Noordhuis <pcnoordhuis at gmail dot com>
|
||||
* Copyright (c) 2009-2011, Salvatore Sanfilippo <antirez at gmail dot com>
|
||||
* Copyright (c) 2010-2014, Pieter Noordhuis <pcnoordhuis at gmail dot com>
|
||||
* Copyright (c) 2015, Matt Stancliff <matt at genges dot com>,
|
||||
* Jan-Erik Rediger <janerik at fnordig dot com>
|
||||
*
|
||||
* All rights reserved.
|
||||
*
|
||||
@@ -47,6 +49,7 @@
|
||||
#include <stdio.h>
|
||||
#include <poll.h>
|
||||
#include <limits.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "net.h"
|
||||
#include "sds.h"
|
||||
@@ -67,7 +70,7 @@ static void __redisSetErrorFromErrno(redisContext *c, int type, const char *pref
|
||||
|
||||
if (prefix != NULL)
|
||||
len = snprintf(buf,sizeof(buf),"%s: ",prefix);
|
||||
strerror_r(errno,buf+len,sizeof(buf)-len);
|
||||
__redis_strerror_r(errno, (char *)(buf + len), sizeof(buf) - len);
|
||||
__redisSetError(c,type,buf);
|
||||
}
|
||||
|
||||
@@ -138,7 +141,7 @@ int redisKeepAlive(redisContext *c, int interval) {
|
||||
return REDIS_ERR;
|
||||
}
|
||||
#else
|
||||
#ifndef __sun
|
||||
#if defined(__GLIBC__) && !defined(__FreeBSD_kernel__)
|
||||
val = interval;
|
||||
if (setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &val, sizeof(val)) < 0) {
|
||||
__redisSetError(c,REDIS_ERR_OTHER,strerror(errno));
|
||||
@@ -175,19 +178,15 @@ static int redisSetTcpNoDelay(redisContext *c) {
|
||||
|
||||
#define __MAX_MSEC (((LONG_MAX) - 999) / 1000)
|
||||
|
||||
static int redisContextWaitReady(redisContext *c, const struct timeval *timeout) {
|
||||
struct pollfd wfd[1];
|
||||
long msec;
|
||||
|
||||
msec = -1;
|
||||
wfd[0].fd = c->fd;
|
||||
wfd[0].events = POLLOUT;
|
||||
static int redisContextTimeoutMsec(redisContext *c, long *result)
|
||||
{
|
||||
const struct timeval *timeout = c->timeout;
|
||||
long msec = -1;
|
||||
|
||||
/* Only use timeout when not NULL. */
|
||||
if (timeout != NULL) {
|
||||
if (timeout->tv_usec > 1000000 || timeout->tv_sec > __MAX_MSEC) {
|
||||
__redisSetErrorFromErrno(c, REDIS_ERR_IO, NULL);
|
||||
redisContextCloseFd(c);
|
||||
*result = msec;
|
||||
return REDIS_ERR;
|
||||
}
|
||||
|
||||
@@ -198,6 +197,16 @@ static int redisContextWaitReady(redisContext *c, const struct timeval *timeout)
|
||||
}
|
||||
}
|
||||
|
||||
*result = msec;
|
||||
return REDIS_OK;
|
||||
}
|
||||
|
||||
static int redisContextWaitReady(redisContext *c, long msec) {
|
||||
struct pollfd wfd[1];
|
||||
|
||||
wfd[0].fd = c->fd;
|
||||
wfd[0].events = POLLOUT;
|
||||
|
||||
if (errno == EINPROGRESS) {
|
||||
int res;
|
||||
|
||||
@@ -256,10 +265,57 @@ int redisContextSetTimeout(redisContext *c, const struct timeval tv) {
|
||||
static int _redisContextConnectTcp(redisContext *c, const char *addr, int port,
|
||||
const struct timeval *timeout,
|
||||
const char *source_addr) {
|
||||
int s, rv;
|
||||
int s, rv, n;
|
||||
char _port[6]; /* strlen("65535"); */
|
||||
struct addrinfo hints, *servinfo, *bservinfo, *p, *b;
|
||||
int blocking = (c->flags & REDIS_BLOCK);
|
||||
int reuseaddr = (c->flags & REDIS_REUSEADDR);
|
||||
int reuses = 0;
|
||||
long timeout_msec = -1;
|
||||
|
||||
servinfo = NULL;
|
||||
c->connection_type = REDIS_CONN_TCP;
|
||||
c->tcp.port = port;
|
||||
|
||||
/* We need to take possession of the passed parameters
|
||||
* to make them reusable for a reconnect.
|
||||
* We also carefully check we don't free data we already own,
|
||||
* as in the case of the reconnect method.
|
||||
*
|
||||
* This is a bit ugly, but atleast it works and doesn't leak memory.
|
||||
**/
|
||||
if (c->tcp.host != addr) {
|
||||
if (c->tcp.host)
|
||||
free(c->tcp.host);
|
||||
|
||||
c->tcp.host = strdup(addr);
|
||||
}
|
||||
|
||||
if (timeout) {
|
||||
if (c->timeout != timeout) {
|
||||
if (c->timeout == NULL)
|
||||
c->timeout = malloc(sizeof(struct timeval));
|
||||
|
||||
memcpy(c->timeout, timeout, sizeof(struct timeval));
|
||||
}
|
||||
} else {
|
||||
if (c->timeout)
|
||||
free(c->timeout);
|
||||
c->timeout = NULL;
|
||||
}
|
||||
|
||||
if (redisContextTimeoutMsec(c, &timeout_msec) != REDIS_OK) {
|
||||
__redisSetError(c, REDIS_ERR_IO, "Invalid timeout specified");
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (source_addr == NULL) {
|
||||
free(c->tcp.source_addr);
|
||||
c->tcp.source_addr = NULL;
|
||||
} else if (c->tcp.source_addr != source_addr) {
|
||||
free(c->tcp.source_addr);
|
||||
c->tcp.source_addr = strdup(source_addr);
|
||||
}
|
||||
|
||||
snprintf(_port, 6, "%d", port);
|
||||
memset(&hints,0,sizeof(hints));
|
||||
@@ -271,7 +327,7 @@ static int _redisContextConnectTcp(redisContext *c, const char *addr, int port,
|
||||
* as this would add latency to every connect. Otherwise a more sensible
|
||||
* route could be: Use IPv6 if both addresses are available and there is IPv6
|
||||
* connectivity. */
|
||||
if ((rv = getaddrinfo(addr,_port,&hints,&servinfo)) != 0) {
|
||||
if ((rv = getaddrinfo(c->tcp.host,_port,&hints,&servinfo)) != 0) {
|
||||
hints.ai_family = AF_INET6;
|
||||
if ((rv = getaddrinfo(addr,_port,&hints,&servinfo)) != 0) {
|
||||
__redisSetError(c,REDIS_ERR_OTHER,gai_strerror(rv));
|
||||
@@ -279,21 +335,31 @@ static int _redisContextConnectTcp(redisContext *c, const char *addr, int port,
|
||||
}
|
||||
}
|
||||
for (p = servinfo; p != NULL; p = p->ai_next) {
|
||||
addrretry:
|
||||
if ((s = socket(p->ai_family,p->ai_socktype,p->ai_protocol)) == -1)
|
||||
continue;
|
||||
|
||||
c->fd = s;
|
||||
if (redisSetBlocking(c,0) != REDIS_OK)
|
||||
goto error;
|
||||
if (source_addr) {
|
||||
if (c->tcp.source_addr) {
|
||||
int bound = 0;
|
||||
/* Using getaddrinfo saves us from self-determining IPv4 vs IPv6 */
|
||||
if ((rv = getaddrinfo(source_addr, NULL, &hints, &bservinfo)) != 0) {
|
||||
if ((rv = getaddrinfo(c->tcp.source_addr, NULL, &hints, &bservinfo)) != 0) {
|
||||
char buf[128];
|
||||
snprintf(buf,sizeof(buf),"Can't get addr: %s",gai_strerror(rv));
|
||||
__redisSetError(c,REDIS_ERR_OTHER,buf);
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (reuseaddr) {
|
||||
n = 1;
|
||||
if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char*) &n,
|
||||
sizeof(n)) < 0) {
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
for (b = bservinfo; b != NULL; b = b->ai_next) {
|
||||
if (bind(s,b->ai_addr,b->ai_addrlen) != -1) {
|
||||
bound = 1;
|
||||
@@ -314,8 +380,15 @@ static int _redisContextConnectTcp(redisContext *c, const char *addr, int port,
|
||||
continue;
|
||||
} else if (errno == EINPROGRESS && !blocking) {
|
||||
/* This is ok. */
|
||||
} else if (errno == EADDRNOTAVAIL && reuseaddr) {
|
||||
if (++reuses >= REDIS_CONNECT_RETRIES) {
|
||||
goto error;
|
||||
} else {
|
||||
redisContextCloseFd(c);
|
||||
goto addrretry;
|
||||
}
|
||||
} else {
|
||||
if (redisContextWaitReady(c,timeout) != REDIS_OK)
|
||||
if (redisContextWaitReady(c,timeout_msec) != REDIS_OK)
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
@@ -356,19 +429,40 @@ int redisContextConnectBindTcp(redisContext *c, const char *addr, int port,
|
||||
int redisContextConnectUnix(redisContext *c, const char *path, const struct timeval *timeout) {
|
||||
int blocking = (c->flags & REDIS_BLOCK);
|
||||
struct sockaddr_un sa;
|
||||
long timeout_msec = -1;
|
||||
|
||||
if (redisCreateSocket(c,AF_LOCAL) < 0)
|
||||
return REDIS_ERR;
|
||||
if (redisSetBlocking(c,0) != REDIS_OK)
|
||||
return REDIS_ERR;
|
||||
|
||||
c->connection_type = REDIS_CONN_UNIX;
|
||||
if (c->unix_sock.path != path)
|
||||
c->unix_sock.path = strdup(path);
|
||||
|
||||
if (timeout) {
|
||||
if (c->timeout != timeout) {
|
||||
if (c->timeout == NULL)
|
||||
c->timeout = malloc(sizeof(struct timeval));
|
||||
|
||||
memcpy(c->timeout, timeout, sizeof(struct timeval));
|
||||
}
|
||||
} else {
|
||||
if (c->timeout)
|
||||
free(c->timeout);
|
||||
c->timeout = NULL;
|
||||
}
|
||||
|
||||
if (redisContextTimeoutMsec(c,&timeout_msec) != REDIS_OK)
|
||||
return REDIS_ERR;
|
||||
|
||||
sa.sun_family = AF_LOCAL;
|
||||
strncpy(sa.sun_path,path,sizeof(sa.sun_path)-1);
|
||||
if (connect(c->fd, (struct sockaddr*)&sa, sizeof(sa)) == -1) {
|
||||
if (errno == EINPROGRESS && !blocking) {
|
||||
/* This is ok. */
|
||||
} else {
|
||||
if (redisContextWaitReady(c,timeout) != REDIS_OK)
|
||||
if (redisContextWaitReady(c,timeout_msec) != REDIS_OK)
|
||||
return REDIS_ERR;
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+5
-3
@@ -1,7 +1,9 @@
|
||||
/* Extracted from anet.c to work properly with Hiredis error reporting.
|
||||
*
|
||||
* Copyright (c) 2006-2011, Salvatore Sanfilippo <antirez at gmail dot com>
|
||||
* Copyright (c) 2010-2011, Pieter Noordhuis <pcnoordhuis at gmail dot com>
|
||||
* Copyright (c) 2009-2011, Salvatore Sanfilippo <antirez at gmail dot com>
|
||||
* Copyright (c) 2010-2014, Pieter Noordhuis <pcnoordhuis at gmail dot com>
|
||||
* Copyright (c) 2015, Matt Stancliff <matt at genges dot com>,
|
||||
* Jan-Erik Rediger <janerik at fnordig dot com>
|
||||
*
|
||||
* All rights reserved.
|
||||
*
|
||||
@@ -35,7 +37,7 @@
|
||||
|
||||
#include "hiredis.h"
|
||||
|
||||
#if defined(__sun) || defined(_AIX)
|
||||
#if defined(__sun)
|
||||
#define AF_LOCAL AF_UNIX
|
||||
#endif
|
||||
|
||||
|
||||
Vendored
+525
@@ -0,0 +1,525 @@
|
||||
/*
|
||||
* Copyright (c) 2009-2011, Salvatore Sanfilippo <antirez at gmail dot com>
|
||||
* Copyright (c) 2010-2011, Pieter Noordhuis <pcnoordhuis 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.
|
||||
*/
|
||||
|
||||
|
||||
#include "fmacros.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#ifndef _MSC_VER
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
#include <ctype.h>
|
||||
|
||||
#include "read.h"
|
||||
#include "sds.h"
|
||||
|
||||
static void __redisReaderSetError(redisReader *r, int type, const char *str) {
|
||||
size_t len;
|
||||
|
||||
if (r->reply != NULL && r->fn && r->fn->freeObject) {
|
||||
r->fn->freeObject(r->reply);
|
||||
r->reply = NULL;
|
||||
}
|
||||
|
||||
/* Clear input buffer on errors. */
|
||||
if (r->buf != NULL) {
|
||||
sdsfree(r->buf);
|
||||
r->buf = NULL;
|
||||
r->pos = r->len = 0;
|
||||
}
|
||||
|
||||
/* Reset task stack. */
|
||||
r->ridx = -1;
|
||||
|
||||
/* Set error. */
|
||||
r->err = type;
|
||||
len = strlen(str);
|
||||
len = len < (sizeof(r->errstr)-1) ? len : (sizeof(r->errstr)-1);
|
||||
memcpy(r->errstr,str,len);
|
||||
r->errstr[len] = '\0';
|
||||
}
|
||||
|
||||
static size_t chrtos(char *buf, size_t size, char byte) {
|
||||
size_t len = 0;
|
||||
|
||||
switch(byte) {
|
||||
case '\\':
|
||||
case '"':
|
||||
len = snprintf(buf,size,"\"\\%c\"",byte);
|
||||
break;
|
||||
case '\n': len = snprintf(buf,size,"\"\\n\""); break;
|
||||
case '\r': len = snprintf(buf,size,"\"\\r\""); break;
|
||||
case '\t': len = snprintf(buf,size,"\"\\t\""); break;
|
||||
case '\a': len = snprintf(buf,size,"\"\\a\""); break;
|
||||
case '\b': len = snprintf(buf,size,"\"\\b\""); break;
|
||||
default:
|
||||
if (isprint(byte))
|
||||
len = snprintf(buf,size,"\"%c\"",byte);
|
||||
else
|
||||
len = snprintf(buf,size,"\"\\x%02x\"",(unsigned char)byte);
|
||||
break;
|
||||
}
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
static void __redisReaderSetErrorProtocolByte(redisReader *r, char byte) {
|
||||
char cbuf[8], sbuf[128];
|
||||
|
||||
chrtos(cbuf,sizeof(cbuf),byte);
|
||||
snprintf(sbuf,sizeof(sbuf),
|
||||
"Protocol error, got %s as reply type byte", cbuf);
|
||||
__redisReaderSetError(r,REDIS_ERR_PROTOCOL,sbuf);
|
||||
}
|
||||
|
||||
static void __redisReaderSetErrorOOM(redisReader *r) {
|
||||
__redisReaderSetError(r,REDIS_ERR_OOM,"Out of memory");
|
||||
}
|
||||
|
||||
static char *readBytes(redisReader *r, unsigned int bytes) {
|
||||
char *p;
|
||||
if (r->len-r->pos >= bytes) {
|
||||
p = r->buf+r->pos;
|
||||
r->pos += bytes;
|
||||
return p;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Find pointer to \r\n. */
|
||||
static char *seekNewline(char *s, size_t len) {
|
||||
int pos = 0;
|
||||
int _len = len-1;
|
||||
|
||||
/* Position should be < len-1 because the character at "pos" should be
|
||||
* followed by a \n. Note that strchr cannot be used because it doesn't
|
||||
* allow to search a limited length and the buffer that is being searched
|
||||
* might not have a trailing NULL character. */
|
||||
while (pos < _len) {
|
||||
while(pos < _len && s[pos] != '\r') pos++;
|
||||
if (pos==_len) {
|
||||
/* Not found. */
|
||||
return NULL;
|
||||
} else {
|
||||
if (s[pos+1] == '\n') {
|
||||
/* Found. */
|
||||
return s+pos;
|
||||
} else {
|
||||
/* Continue searching. */
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Read a long long value starting at *s, under the assumption that it will be
|
||||
* terminated by \r\n. Ambiguously returns -1 for unexpected input. */
|
||||
static long long readLongLong(char *s) {
|
||||
long long v = 0;
|
||||
int dec, mult = 1;
|
||||
char c;
|
||||
|
||||
if (*s == '-') {
|
||||
mult = -1;
|
||||
s++;
|
||||
} else if (*s == '+') {
|
||||
mult = 1;
|
||||
s++;
|
||||
}
|
||||
|
||||
while ((c = *(s++)) != '\r') {
|
||||
dec = c - '0';
|
||||
if (dec >= 0 && dec < 10) {
|
||||
v *= 10;
|
||||
v += dec;
|
||||
} else {
|
||||
/* Should not happen... */
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return mult*v;
|
||||
}
|
||||
|
||||
static char *readLine(redisReader *r, int *_len) {
|
||||
char *p, *s;
|
||||
int len;
|
||||
|
||||
p = r->buf+r->pos;
|
||||
s = seekNewline(p,(r->len-r->pos));
|
||||
if (s != NULL) {
|
||||
len = s-(r->buf+r->pos);
|
||||
r->pos += len+2; /* skip \r\n */
|
||||
if (_len) *_len = len;
|
||||
return p;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void moveToNextTask(redisReader *r) {
|
||||
redisReadTask *cur, *prv;
|
||||
while (r->ridx >= 0) {
|
||||
/* Return a.s.a.p. when the stack is now empty. */
|
||||
if (r->ridx == 0) {
|
||||
r->ridx--;
|
||||
return;
|
||||
}
|
||||
|
||||
cur = &(r->rstack[r->ridx]);
|
||||
prv = &(r->rstack[r->ridx-1]);
|
||||
assert(prv->type == REDIS_REPLY_ARRAY);
|
||||
if (cur->idx == prv->elements-1) {
|
||||
r->ridx--;
|
||||
} else {
|
||||
/* Reset the type because the next item can be anything */
|
||||
assert(cur->idx < prv->elements);
|
||||
cur->type = -1;
|
||||
cur->elements = -1;
|
||||
cur->idx++;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int processLineItem(redisReader *r) {
|
||||
redisReadTask *cur = &(r->rstack[r->ridx]);
|
||||
void *obj;
|
||||
char *p;
|
||||
int len;
|
||||
|
||||
if ((p = readLine(r,&len)) != NULL) {
|
||||
if (cur->type == REDIS_REPLY_INTEGER) {
|
||||
if (r->fn && r->fn->createInteger)
|
||||
obj = r->fn->createInteger(cur,readLongLong(p));
|
||||
else
|
||||
obj = (void*)REDIS_REPLY_INTEGER;
|
||||
} else {
|
||||
/* Type will be error or status. */
|
||||
if (r->fn && r->fn->createString)
|
||||
obj = r->fn->createString(cur,p,len);
|
||||
else
|
||||
obj = (void*)(size_t)(cur->type);
|
||||
}
|
||||
|
||||
if (obj == NULL) {
|
||||
__redisReaderSetErrorOOM(r);
|
||||
return REDIS_ERR;
|
||||
}
|
||||
|
||||
/* Set reply if this is the root object. */
|
||||
if (r->ridx == 0) r->reply = obj;
|
||||
moveToNextTask(r);
|
||||
return REDIS_OK;
|
||||
}
|
||||
|
||||
return REDIS_ERR;
|
||||
}
|
||||
|
||||
static int processBulkItem(redisReader *r) {
|
||||
redisReadTask *cur = &(r->rstack[r->ridx]);
|
||||
void *obj = NULL;
|
||||
char *p, *s;
|
||||
long len;
|
||||
unsigned long bytelen;
|
||||
int success = 0;
|
||||
|
||||
p = r->buf+r->pos;
|
||||
s = seekNewline(p,r->len-r->pos);
|
||||
if (s != NULL) {
|
||||
p = r->buf+r->pos;
|
||||
bytelen = s-(r->buf+r->pos)+2; /* include \r\n */
|
||||
len = readLongLong(p);
|
||||
|
||||
if (len < 0) {
|
||||
/* The nil object can always be created. */
|
||||
if (r->fn && r->fn->createNil)
|
||||
obj = r->fn->createNil(cur);
|
||||
else
|
||||
obj = (void*)REDIS_REPLY_NIL;
|
||||
success = 1;
|
||||
} else {
|
||||
/* Only continue when the buffer contains the entire bulk item. */
|
||||
bytelen += len+2; /* include \r\n */
|
||||
if (r->pos+bytelen <= r->len) {
|
||||
if (r->fn && r->fn->createString)
|
||||
obj = r->fn->createString(cur,s+2,len);
|
||||
else
|
||||
obj = (void*)REDIS_REPLY_STRING;
|
||||
success = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Proceed when obj was created. */
|
||||
if (success) {
|
||||
if (obj == NULL) {
|
||||
__redisReaderSetErrorOOM(r);
|
||||
return REDIS_ERR;
|
||||
}
|
||||
|
||||
r->pos += bytelen;
|
||||
|
||||
/* Set reply if this is the root object. */
|
||||
if (r->ridx == 0) r->reply = obj;
|
||||
moveToNextTask(r);
|
||||
return REDIS_OK;
|
||||
}
|
||||
}
|
||||
|
||||
return REDIS_ERR;
|
||||
}
|
||||
|
||||
static int processMultiBulkItem(redisReader *r) {
|
||||
redisReadTask *cur = &(r->rstack[r->ridx]);
|
||||
void *obj;
|
||||
char *p;
|
||||
long elements;
|
||||
int root = 0;
|
||||
|
||||
/* Set error for nested multi bulks with depth > 7 */
|
||||
if (r->ridx == 8) {
|
||||
__redisReaderSetError(r,REDIS_ERR_PROTOCOL,
|
||||
"No support for nested multi bulk replies with depth > 7");
|
||||
return REDIS_ERR;
|
||||
}
|
||||
|
||||
if ((p = readLine(r,NULL)) != NULL) {
|
||||
elements = readLongLong(p);
|
||||
root = (r->ridx == 0);
|
||||
|
||||
if (elements == -1) {
|
||||
if (r->fn && r->fn->createNil)
|
||||
obj = r->fn->createNil(cur);
|
||||
else
|
||||
obj = (void*)REDIS_REPLY_NIL;
|
||||
|
||||
if (obj == NULL) {
|
||||
__redisReaderSetErrorOOM(r);
|
||||
return REDIS_ERR;
|
||||
}
|
||||
|
||||
moveToNextTask(r);
|
||||
} else {
|
||||
if (r->fn && r->fn->createArray)
|
||||
obj = r->fn->createArray(cur,elements);
|
||||
else
|
||||
obj = (void*)REDIS_REPLY_ARRAY;
|
||||
|
||||
if (obj == NULL) {
|
||||
__redisReaderSetErrorOOM(r);
|
||||
return REDIS_ERR;
|
||||
}
|
||||
|
||||
/* Modify task stack when there are more than 0 elements. */
|
||||
if (elements > 0) {
|
||||
cur->elements = elements;
|
||||
cur->obj = obj;
|
||||
r->ridx++;
|
||||
r->rstack[r->ridx].type = -1;
|
||||
r->rstack[r->ridx].elements = -1;
|
||||
r->rstack[r->ridx].idx = 0;
|
||||
r->rstack[r->ridx].obj = NULL;
|
||||
r->rstack[r->ridx].parent = cur;
|
||||
r->rstack[r->ridx].privdata = r->privdata;
|
||||
} else {
|
||||
moveToNextTask(r);
|
||||
}
|
||||
}
|
||||
|
||||
/* Set reply if this is the root object. */
|
||||
if (root) r->reply = obj;
|
||||
return REDIS_OK;
|
||||
}
|
||||
|
||||
return REDIS_ERR;
|
||||
}
|
||||
|
||||
static int processItem(redisReader *r) {
|
||||
redisReadTask *cur = &(r->rstack[r->ridx]);
|
||||
char *p;
|
||||
|
||||
/* check if we need to read type */
|
||||
if (cur->type < 0) {
|
||||
if ((p = readBytes(r,1)) != NULL) {
|
||||
switch (p[0]) {
|
||||
case '-':
|
||||
cur->type = REDIS_REPLY_ERROR;
|
||||
break;
|
||||
case '+':
|
||||
cur->type = REDIS_REPLY_STATUS;
|
||||
break;
|
||||
case ':':
|
||||
cur->type = REDIS_REPLY_INTEGER;
|
||||
break;
|
||||
case '$':
|
||||
cur->type = REDIS_REPLY_STRING;
|
||||
break;
|
||||
case '*':
|
||||
cur->type = REDIS_REPLY_ARRAY;
|
||||
break;
|
||||
default:
|
||||
__redisReaderSetErrorProtocolByte(r,*p);
|
||||
return REDIS_ERR;
|
||||
}
|
||||
} else {
|
||||
/* could not consume 1 byte */
|
||||
return REDIS_ERR;
|
||||
}
|
||||
}
|
||||
|
||||
/* process typed item */
|
||||
switch(cur->type) {
|
||||
case REDIS_REPLY_ERROR:
|
||||
case REDIS_REPLY_STATUS:
|
||||
case REDIS_REPLY_INTEGER:
|
||||
return processLineItem(r);
|
||||
case REDIS_REPLY_STRING:
|
||||
return processBulkItem(r);
|
||||
case REDIS_REPLY_ARRAY:
|
||||
return processMultiBulkItem(r);
|
||||
default:
|
||||
assert(NULL);
|
||||
return REDIS_ERR; /* Avoid warning. */
|
||||
}
|
||||
}
|
||||
|
||||
redisReader *redisReaderCreateWithFunctions(redisReplyObjectFunctions *fn) {
|
||||
redisReader *r;
|
||||
|
||||
r = calloc(sizeof(redisReader),1);
|
||||
if (r == NULL)
|
||||
return NULL;
|
||||
|
||||
r->err = 0;
|
||||
r->errstr[0] = '\0';
|
||||
r->fn = fn;
|
||||
r->buf = sdsempty();
|
||||
r->maxbuf = REDIS_READER_MAX_BUF;
|
||||
if (r->buf == NULL) {
|
||||
free(r);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
r->ridx = -1;
|
||||
return r;
|
||||
}
|
||||
|
||||
void redisReaderFree(redisReader *r) {
|
||||
if (r->reply != NULL && r->fn && r->fn->freeObject)
|
||||
r->fn->freeObject(r->reply);
|
||||
if (r->buf != NULL)
|
||||
sdsfree(r->buf);
|
||||
free(r);
|
||||
}
|
||||
|
||||
int redisReaderFeed(redisReader *r, const char *buf, size_t len) {
|
||||
sds newbuf;
|
||||
|
||||
/* Return early when this reader is in an erroneous state. */
|
||||
if (r->err)
|
||||
return REDIS_ERR;
|
||||
|
||||
/* Copy the provided buffer. */
|
||||
if (buf != NULL && len >= 1) {
|
||||
/* Destroy internal buffer when it is empty and is quite large. */
|
||||
if (r->len == 0 && r->maxbuf != 0 && sdsavail(r->buf) > r->maxbuf) {
|
||||
sdsfree(r->buf);
|
||||
r->buf = sdsempty();
|
||||
r->pos = 0;
|
||||
|
||||
/* r->buf should not be NULL since we just free'd a larger one. */
|
||||
assert(r->buf != NULL);
|
||||
}
|
||||
|
||||
newbuf = sdscatlen(r->buf,buf,len);
|
||||
if (newbuf == NULL) {
|
||||
__redisReaderSetErrorOOM(r);
|
||||
return REDIS_ERR;
|
||||
}
|
||||
|
||||
r->buf = newbuf;
|
||||
r->len = sdslen(r->buf);
|
||||
}
|
||||
|
||||
return REDIS_OK;
|
||||
}
|
||||
|
||||
int redisReaderGetReply(redisReader *r, void **reply) {
|
||||
/* Default target pointer to NULL. */
|
||||
if (reply != NULL)
|
||||
*reply = NULL;
|
||||
|
||||
/* Return early when this reader is in an erroneous state. */
|
||||
if (r->err)
|
||||
return REDIS_ERR;
|
||||
|
||||
/* When the buffer is empty, there will never be a reply. */
|
||||
if (r->len == 0)
|
||||
return REDIS_OK;
|
||||
|
||||
/* Set first item to process when the stack is empty. */
|
||||
if (r->ridx == -1) {
|
||||
r->rstack[0].type = -1;
|
||||
r->rstack[0].elements = -1;
|
||||
r->rstack[0].idx = -1;
|
||||
r->rstack[0].obj = NULL;
|
||||
r->rstack[0].parent = NULL;
|
||||
r->rstack[0].privdata = r->privdata;
|
||||
r->ridx = 0;
|
||||
}
|
||||
|
||||
/* Process items in reply. */
|
||||
while (r->ridx >= 0)
|
||||
if (processItem(r) != REDIS_OK)
|
||||
break;
|
||||
|
||||
/* Return ASAP when an error occurred. */
|
||||
if (r->err)
|
||||
return REDIS_ERR;
|
||||
|
||||
/* Discard part of the buffer when we've consumed at least 1k, to avoid
|
||||
* doing unnecessary calls to memmove() in sds.c. */
|
||||
if (r->pos >= 1024) {
|
||||
sdsrange(r->buf,r->pos,-1);
|
||||
r->pos = 0;
|
||||
r->len = sdslen(r->buf);
|
||||
}
|
||||
|
||||
/* Emit a reply when there is one. */
|
||||
if (r->ridx == -1) {
|
||||
if (reply != NULL)
|
||||
*reply = r->reply;
|
||||
r->reply = NULL;
|
||||
}
|
||||
return REDIS_OK;
|
||||
}
|
||||
Vendored
+111
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright (c) 2009-2011, Salvatore Sanfilippo <antirez at gmail dot com>
|
||||
* Copyright (c) 2010-2011, Pieter Noordhuis <pcnoordhuis 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 __HIREDIS_READ_H
|
||||
#define __HIREDIS_READ_H
|
||||
#include <stdio.h> /* for size_t */
|
||||
|
||||
#define REDIS_ERR -1
|
||||
#define REDIS_OK 0
|
||||
|
||||
/* When an error occurs, the err flag in a context is set to hold the type of
|
||||
* error that occurred. REDIS_ERR_IO means there was an I/O error and you
|
||||
* should use the "errno" variable to find out what is wrong.
|
||||
* For other values, the "errstr" field will hold a description. */
|
||||
#define REDIS_ERR_IO 1 /* Error in read or write */
|
||||
#define REDIS_ERR_EOF 3 /* End of file */
|
||||
#define REDIS_ERR_PROTOCOL 4 /* Protocol error */
|
||||
#define REDIS_ERR_OOM 5 /* Out of memory */
|
||||
#define REDIS_ERR_OTHER 2 /* Everything else... */
|
||||
|
||||
#define REDIS_REPLY_STRING 1
|
||||
#define REDIS_REPLY_ARRAY 2
|
||||
#define REDIS_REPLY_INTEGER 3
|
||||
#define REDIS_REPLY_NIL 4
|
||||
#define REDIS_REPLY_STATUS 5
|
||||
#define REDIS_REPLY_ERROR 6
|
||||
|
||||
#define REDIS_READER_MAX_BUF (1024*16) /* Default max unused reader buffer. */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct redisReadTask {
|
||||
int type;
|
||||
int elements; /* number of elements in multibulk container */
|
||||
int idx; /* index in parent (array) object */
|
||||
void *obj; /* holds user-generated value for a read task */
|
||||
struct redisReadTask *parent; /* parent task */
|
||||
void *privdata; /* user-settable arbitrary field */
|
||||
} redisReadTask;
|
||||
|
||||
typedef struct redisReplyObjectFunctions {
|
||||
void *(*createString)(const redisReadTask*, char*, size_t);
|
||||
void *(*createArray)(const redisReadTask*, int);
|
||||
void *(*createInteger)(const redisReadTask*, long long);
|
||||
void *(*createNil)(const redisReadTask*);
|
||||
void (*freeObject)(void*);
|
||||
} redisReplyObjectFunctions;
|
||||
|
||||
typedef struct redisReader {
|
||||
int err; /* Error flags, 0 when there is no error */
|
||||
char errstr[128]; /* String representation of error when applicable */
|
||||
|
||||
char *buf; /* Read buffer */
|
||||
size_t pos; /* Buffer cursor */
|
||||
size_t len; /* Buffer length */
|
||||
size_t maxbuf; /* Max length of unused buffer */
|
||||
|
||||
redisReadTask rstack[9];
|
||||
int ridx; /* Index of current read task */
|
||||
void *reply; /* Temporary reply pointer */
|
||||
|
||||
redisReplyObjectFunctions *fn;
|
||||
void *privdata;
|
||||
} redisReader;
|
||||
|
||||
/* Public API for the protocol parser. */
|
||||
redisReader *redisReaderCreateWithFunctions(redisReplyObjectFunctions *fn);
|
||||
void redisReaderFree(redisReader *r);
|
||||
int redisReaderFeed(redisReader *r, const char *buf, size_t len);
|
||||
int redisReaderGetReply(redisReader *r, void **reply);
|
||||
|
||||
#define redisReaderSetPrivdata(_r, _p) (int)(((redisReader*)(_r))->privdata = (_p))
|
||||
#define redisReaderGetObject(_r) (((redisReader*)(_r))->reply)
|
||||
#define redisReaderGetError(_r) (((redisReader*)(_r))->errstr)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
Vendored
+2
-4
@@ -89,9 +89,9 @@ sds sdsnewlen(const void *init, size_t initlen) {
|
||||
unsigned char *fp; /* flags pointer. */
|
||||
|
||||
sh = s_malloc(hdrlen+initlen+1);
|
||||
if (sh == NULL) return NULL;
|
||||
if (!init)
|
||||
memset(sh, 0, hdrlen+initlen+1);
|
||||
if (sh == NULL) return NULL;
|
||||
s = (char*)sh+hdrlen;
|
||||
fp = ((unsigned char*)s)-1;
|
||||
switch(type) {
|
||||
@@ -577,14 +577,12 @@ sds sdscatprintf(sds s, const char *fmt, ...) {
|
||||
* %% - Verbatim "%" character.
|
||||
*/
|
||||
sds sdscatfmt(sds s, char const *fmt, ...) {
|
||||
size_t initlen = sdslen(s);
|
||||
const char *f = fmt;
|
||||
int i;
|
||||
va_list ap;
|
||||
|
||||
va_start(ap,fmt);
|
||||
f = fmt; /* Next format specifier byte to process. */
|
||||
i = initlen; /* Position of the next byte to write to dest str. */
|
||||
i = sdslen(s); /* Position of the next byte to write to dest str. */
|
||||
while(*f) {
|
||||
char next, *str;
|
||||
size_t l;
|
||||
|
||||
Vendored
+1
-1
@@ -79,7 +79,7 @@ struct __attribute__ ((__packed__)) sdshdr64 {
|
||||
#define SDS_TYPE_64 4
|
||||
#define SDS_TYPE_MASK 7
|
||||
#define SDS_TYPE_BITS 3
|
||||
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
|
||||
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T)));
|
||||
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
|
||||
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)
|
||||
|
||||
|
||||
Vendored
+4
-4
@@ -1,6 +1,7 @@
|
||||
/* SDSLib 2.0 -- A C dynamic strings library
|
||||
*
|
||||
* Copyright (c) 2006-2015, Salvatore Sanfilippo <antirez at gmail dot com>
|
||||
* Copyright (c) 2015, Oran Agra
|
||||
* Copyright (c) 2015, Redis Labs, Inc
|
||||
* All rights reserved.
|
||||
*
|
||||
@@ -36,7 +37,6 @@
|
||||
* the include of your alternate allocator if needed (not needed in order
|
||||
* to use the default libc allocator). */
|
||||
|
||||
#include "zmalloc.h"
|
||||
#define s_malloc zmalloc
|
||||
#define s_realloc zrealloc
|
||||
#define s_free zfree
|
||||
#define s_malloc malloc
|
||||
#define s_realloc realloc
|
||||
#define s_free free
|
||||
|
||||
Vendored
+105
-15
@@ -11,6 +11,7 @@
|
||||
#include <limits.h>
|
||||
|
||||
#include "hiredis.h"
|
||||
#include "net.h"
|
||||
|
||||
enum connection_type {
|
||||
CONN_TCP,
|
||||
@@ -29,7 +30,7 @@ struct config {
|
||||
|
||||
struct {
|
||||
const char *path;
|
||||
} unix;
|
||||
} unix_sock;
|
||||
};
|
||||
|
||||
/* The following lines make up our testing "framework" :) */
|
||||
@@ -43,6 +44,13 @@ static long long usec(void) {
|
||||
return (((long long)tv.tv_sec)*1000000)+tv.tv_usec;
|
||||
}
|
||||
|
||||
/* The assert() calls below have side effects, so we need assert()
|
||||
* even if we are compiling without asserts (-DNDEBUG). */
|
||||
#ifdef NDEBUG
|
||||
#undef assert
|
||||
#define assert(e) (void)(e)
|
||||
#endif
|
||||
|
||||
static redisContext *select_database(redisContext *c) {
|
||||
redisReply *reply;
|
||||
|
||||
@@ -51,7 +59,7 @@ static redisContext *select_database(redisContext *c) {
|
||||
assert(reply != NULL);
|
||||
freeReplyObject(reply);
|
||||
|
||||
/* Make sure the DB is empty */
|
||||
/* Make sure the DB is emtpy */
|
||||
reply = redisCommand(c,"DBSIZE");
|
||||
assert(reply != NULL);
|
||||
if (reply->type == REDIS_REPLY_INTEGER && reply->integer == 0) {
|
||||
@@ -89,10 +97,10 @@ static redisContext *connect(struct config config) {
|
||||
if (config.type == CONN_TCP) {
|
||||
c = redisConnect(config.tcp.host, config.tcp.port);
|
||||
} else if (config.type == CONN_UNIX) {
|
||||
c = redisConnectUnix(config.unix.path);
|
||||
c = redisConnectUnix(config.unix_sock.path);
|
||||
} else if (config.type == CONN_FD) {
|
||||
/* Create a dummy connection just to get an fd to inherit */
|
||||
redisContext *dummy_ctx = redisConnectUnix(config.unix.path);
|
||||
redisContext *dummy_ctx = redisConnectUnix(config.unix_sock.path);
|
||||
if (dummy_ctx) {
|
||||
int fd = disconnect(dummy_ctx, 1);
|
||||
printf("Connecting to inherited fd %d\n", fd);
|
||||
@@ -107,6 +115,7 @@ static redisContext *connect(struct config config) {
|
||||
exit(1);
|
||||
} else if (c->err) {
|
||||
printf("Connection error: %s\n", c->errstr);
|
||||
redisFree(c);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
@@ -215,6 +224,22 @@ static void test_format_commands(void) {
|
||||
test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$7\r\nfoo\0xxx\r\n$3\r\nbar\r\n",len) == 0 &&
|
||||
len == 4+4+(3+2)+4+(7+2)+4+(3+2));
|
||||
free(cmd);
|
||||
|
||||
sds sds_cmd;
|
||||
|
||||
sds_cmd = sdsempty();
|
||||
test("Format command into sds by passing argc/argv without lengths: ");
|
||||
len = redisFormatSdsCommandArgv(&sds_cmd,argc,argv,NULL);
|
||||
test_cond(strncmp(sds_cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n",len) == 0 &&
|
||||
len == 4+4+(3+2)+4+(3+2)+4+(3+2));
|
||||
sdsfree(sds_cmd);
|
||||
|
||||
sds_cmd = sdsempty();
|
||||
test("Format command into sds by passing argc/argv with lengths: ");
|
||||
len = redisFormatSdsCommandArgv(&sds_cmd,argc,argv,lens);
|
||||
test_cond(strncmp(sds_cmd,"*3\r\n$3\r\nSET\r\n$7\r\nfoo\0xxx\r\n$3\r\nbar\r\n",len) == 0 &&
|
||||
len == 4+4+(3+2)+4+(7+2)+4+(3+2));
|
||||
sdsfree(sds_cmd);
|
||||
}
|
||||
|
||||
static void test_append_formatted_commands(struct config config) {
|
||||
@@ -318,16 +343,31 @@ static void test_reply_reader(void) {
|
||||
redisReaderFree(reader);
|
||||
}
|
||||
|
||||
static void test_free_null(void) {
|
||||
void *redisCtx = NULL;
|
||||
void *reply = NULL;
|
||||
|
||||
test("Don't fail when redisFree is passed a NULL value: ");
|
||||
redisFree(redisCtx);
|
||||
test_cond(redisCtx == NULL);
|
||||
|
||||
test("Don't fail when freeReplyObject is passed a NULL value: ");
|
||||
freeReplyObject(reply);
|
||||
test_cond(reply == NULL);
|
||||
}
|
||||
|
||||
static void test_blocking_connection_errors(void) {
|
||||
redisContext *c;
|
||||
|
||||
test("Returns error when host cannot be resolved: ");
|
||||
c = redisConnect((char*)"idontexist.local", 6379);
|
||||
c = redisConnect((char*)"idontexist.test", 6379);
|
||||
test_cond(c->err == REDIS_ERR_OTHER &&
|
||||
(strcmp(c->errstr,"Name or service not known") == 0 ||
|
||||
strcmp(c->errstr,"Can't resolve: idontexist.local") == 0 ||
|
||||
strcmp(c->errstr,"Can't resolve: idontexist.test") == 0 ||
|
||||
strcmp(c->errstr,"nodename nor servname provided, or not known") == 0 ||
|
||||
strcmp(c->errstr,"No address associated with hostname") == 0 ||
|
||||
strcmp(c->errstr,"Temporary failure in name resolution") == 0 ||
|
||||
strcmp(c->errstr,"hostname nor servname provided, or not known") == 0 ||
|
||||
strcmp(c->errstr,"no address associated with name") == 0));
|
||||
redisFree(c);
|
||||
|
||||
@@ -337,7 +377,7 @@ static void test_blocking_connection_errors(void) {
|
||||
strcmp(c->errstr,"Connection refused") == 0);
|
||||
redisFree(c);
|
||||
|
||||
test("Returns error when the unix socket path doesn't accept connections: ");
|
||||
test("Returns error when the unix_sock socket path doesn't accept connections: ");
|
||||
c = redisConnectUnix((char*)"/tmp/idontexist.sock");
|
||||
test_cond(c->err == REDIS_ERR_IO); /* Don't care about the message... */
|
||||
redisFree(c);
|
||||
@@ -421,6 +461,52 @@ static void test_blocking_connection(struct config config) {
|
||||
disconnect(c, 0);
|
||||
}
|
||||
|
||||
static void test_blocking_connection_timeouts(struct config config) {
|
||||
redisContext *c;
|
||||
redisReply *reply;
|
||||
ssize_t s;
|
||||
const char *cmd = "DEBUG SLEEP 3\r\n";
|
||||
struct timeval tv;
|
||||
|
||||
c = connect(config);
|
||||
test("Successfully completes a command when the timeout is not exceeded: ");
|
||||
reply = redisCommand(c,"SET foo fast");
|
||||
freeReplyObject(reply);
|
||||
tv.tv_sec = 0;
|
||||
tv.tv_usec = 10000;
|
||||
redisSetTimeout(c, tv);
|
||||
reply = redisCommand(c, "GET foo");
|
||||
test_cond(reply != NULL && reply->type == REDIS_REPLY_STRING && memcmp(reply->str, "fast", 4) == 0);
|
||||
freeReplyObject(reply);
|
||||
disconnect(c, 0);
|
||||
|
||||
c = connect(config);
|
||||
test("Does not return a reply when the command times out: ");
|
||||
s = write(c->fd, cmd, strlen(cmd));
|
||||
tv.tv_sec = 0;
|
||||
tv.tv_usec = 10000;
|
||||
redisSetTimeout(c, tv);
|
||||
reply = redisCommand(c, "GET foo");
|
||||
test_cond(s > 0 && reply == NULL && c->err == REDIS_ERR_IO && strcmp(c->errstr, "Resource temporarily unavailable") == 0);
|
||||
freeReplyObject(reply);
|
||||
|
||||
test("Reconnect properly reconnects after a timeout: ");
|
||||
redisReconnect(c);
|
||||
reply = redisCommand(c, "PING");
|
||||
test_cond(reply != NULL && reply->type == REDIS_REPLY_STATUS && strcmp(reply->str, "PONG") == 0);
|
||||
freeReplyObject(reply);
|
||||
|
||||
test("Reconnect properly uses owned parameters: ");
|
||||
config.tcp.host = "foo";
|
||||
config.unix_sock.path = "foo";
|
||||
redisReconnect(c);
|
||||
reply = redisCommand(c, "PING");
|
||||
test_cond(reply != NULL && reply->type == REDIS_REPLY_STATUS && strcmp(reply->str, "PONG") == 0);
|
||||
freeReplyObject(reply);
|
||||
|
||||
disconnect(c, 0);
|
||||
}
|
||||
|
||||
static void test_blocking_io_errors(struct config config) {
|
||||
redisContext *c;
|
||||
redisReply *reply;
|
||||
@@ -444,7 +530,7 @@ static void test_blocking_io_errors(struct config config) {
|
||||
|
||||
test("Returns I/O error when the connection is lost: ");
|
||||
reply = redisCommand(c,"QUIT");
|
||||
if (major >= 2 && minor > 0) {
|
||||
if (major > 2 || (major == 2 && minor > 0)) {
|
||||
/* > 2.0 returns OK on QUIT and read() should be issued once more
|
||||
* to know the descriptor is at EOF. */
|
||||
test_cond(strcasecmp(reply->str,"OK") == 0 &&
|
||||
@@ -482,7 +568,8 @@ static void test_invalid_timeout_errors(struct config config) {
|
||||
|
||||
c = redisConnectWithTimeout(config.tcp.host, config.tcp.port, config.tcp.timeout);
|
||||
|
||||
test_cond(c->err == REDIS_ERR_IO);
|
||||
test_cond(c->err == REDIS_ERR_IO && strcmp(c->errstr, "Invalid timeout specified") == 0);
|
||||
redisFree(c);
|
||||
|
||||
test("Set error when an invalid timeout sec value is given to redisConnectWithTimeout: ");
|
||||
|
||||
@@ -491,8 +578,7 @@ static void test_invalid_timeout_errors(struct config config) {
|
||||
|
||||
c = redisConnectWithTimeout(config.tcp.host, config.tcp.port, config.tcp.timeout);
|
||||
|
||||
test_cond(c->err == REDIS_ERR_IO);
|
||||
|
||||
test_cond(c->err == REDIS_ERR_IO && strcmp(c->errstr, "Invalid timeout specified") == 0);
|
||||
redisFree(c);
|
||||
}
|
||||
|
||||
@@ -666,7 +752,7 @@ int main(int argc, char **argv) {
|
||||
.host = "127.0.0.1",
|
||||
.port = 6379
|
||||
},
|
||||
.unix = {
|
||||
.unix_sock = {
|
||||
.path = "/tmp/redis.sock"
|
||||
}
|
||||
};
|
||||
@@ -687,7 +773,7 @@ int main(int argc, char **argv) {
|
||||
cfg.tcp.port = atoi(argv[0]);
|
||||
} else if (argc >= 2 && !strcmp(argv[0],"-s")) {
|
||||
argv++; argc--;
|
||||
cfg.unix.path = argv[0];
|
||||
cfg.unix_sock.path = argv[0];
|
||||
} else if (argc >= 1 && !strcmp(argv[0],"--skip-throughput")) {
|
||||
throughput = 0;
|
||||
} else if (argc >= 1 && !strcmp(argv[0],"--skip-inherit-fd")) {
|
||||
@@ -702,27 +788,31 @@ int main(int argc, char **argv) {
|
||||
test_format_commands();
|
||||
test_reply_reader();
|
||||
test_blocking_connection_errors();
|
||||
test_free_null();
|
||||
|
||||
printf("\nTesting against TCP connection (%s:%d):\n", cfg.tcp.host, cfg.tcp.port);
|
||||
cfg.type = CONN_TCP;
|
||||
test_blocking_connection(cfg);
|
||||
test_blocking_connection_timeouts(cfg);
|
||||
test_blocking_io_errors(cfg);
|
||||
test_invalid_timeout_errors(cfg);
|
||||
test_append_formatted_commands(cfg);
|
||||
if (throughput) test_throughput(cfg);
|
||||
|
||||
printf("\nTesting against Unix socket connection (%s):\n", cfg.unix.path);
|
||||
printf("\nTesting against Unix socket connection (%s):\n", cfg.unix_sock.path);
|
||||
cfg.type = CONN_UNIX;
|
||||
test_blocking_connection(cfg);
|
||||
test_blocking_connection_timeouts(cfg);
|
||||
test_blocking_io_errors(cfg);
|
||||
if (throughput) test_throughput(cfg);
|
||||
|
||||
if (test_inherit_fd) {
|
||||
printf("\nTesting against inherited fd (%s):\n", cfg.unix.path);
|
||||
printf("\nTesting against inherited fd (%s):\n", cfg.unix_sock.path);
|
||||
cfg.type = CONN_FD;
|
||||
test_blocking_connection(cfg);
|
||||
}
|
||||
|
||||
|
||||
if (fails) {
|
||||
printf("*** %d TESTS FAILED ***\n", fails);
|
||||
return 1;
|
||||
|
||||
Vendored
+42
@@ -0,0 +1,42 @@
|
||||
#ifndef _WIN32_HELPER_INCLUDE
|
||||
#define _WIN32_HELPER_INCLUDE
|
||||
#ifdef _MSC_VER
|
||||
|
||||
#ifndef inline
|
||||
#define inline __inline
|
||||
#endif
|
||||
|
||||
#ifndef va_copy
|
||||
#define va_copy(d,s) ((d) = (s))
|
||||
#endif
|
||||
|
||||
#ifndef snprintf
|
||||
#define snprintf c99_snprintf
|
||||
|
||||
__inline int c99_vsnprintf(char* str, size_t size, const char* format, va_list ap)
|
||||
{
|
||||
int count = -1;
|
||||
|
||||
if (size != 0)
|
||||
count = _vsnprintf_s(str, size, _TRUNCATE, format, ap);
|
||||
if (count == -1)
|
||||
count = _vscprintf(format, ap);
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
__inline int c99_snprintf(char* str, size_t size, const char* format, ...)
|
||||
{
|
||||
int count;
|
||||
va_list ap;
|
||||
|
||||
va_start(ap, format);
|
||||
count = c99_vsnprintf(str, size, format, ap);
|
||||
va_end(ap);
|
||||
|
||||
return count;
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
#endif
|
||||
Vendored
-13
@@ -1,13 +0,0 @@
|
||||
/* Drop in replacement for zmalloc.h in order to just use libc malloc without
|
||||
* any wrappering. */
|
||||
|
||||
#ifndef ZMALLOC_H
|
||||
#define ZMALLOC_H
|
||||
|
||||
#define zmalloc malloc
|
||||
#define zrealloc realloc
|
||||
#define zcalloc(x) calloc(x,1)
|
||||
#define zfree free
|
||||
#define zstrdup strdup
|
||||
|
||||
#endif
|
||||
@@ -100,3 +100,7 @@
|
||||
# define JEMALLOC_RESTRICT_RETURN
|
||||
# define JEMALLOC_ALLOCATOR
|
||||
#endif
|
||||
|
||||
/* This version of Jemalloc, modified for Redis, has the je_get_defrag_hint()
|
||||
* function. */
|
||||
#define JEMALLOC_FRAG_HINT
|
||||
|
||||
Vendored
+32
@@ -2591,3 +2591,35 @@ jemalloc_postfork_child(void)
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
/* Helps the application decide if a pointer is worth re-allocating in order to reduce fragmentation.
|
||||
* returns 0 if the allocation is in the currently active run,
|
||||
* or when it is not causing any frag issue (large or huge bin)
|
||||
* returns the bin utilization and run utilization both in fixed point 16:16.
|
||||
* If the application decides to re-allocate it should use MALLOCX_TCACHE_NONE when doing so. */
|
||||
JEMALLOC_EXPORT int JEMALLOC_NOTHROW
|
||||
je_get_defrag_hint(void* ptr, int *bin_util, int *run_util) {
|
||||
int defrag = 0;
|
||||
arena_chunk_t *chunk = (arena_chunk_t *)CHUNK_ADDR2BASE(ptr);
|
||||
if (likely(chunk != ptr)) { /* indication that this is not a HUGE alloc */
|
||||
size_t pageind = ((uintptr_t)ptr - (uintptr_t)chunk) >> LG_PAGE;
|
||||
size_t mapbits = arena_mapbits_get(chunk, pageind);
|
||||
if (likely((mapbits & CHUNK_MAP_LARGE) == 0)) { /* indication that this is not a LARGE alloc */
|
||||
arena_t *arena = extent_node_arena_get(&chunk->node);
|
||||
size_t rpages_ind = pageind - arena_mapbits_small_runind_get(chunk, pageind);
|
||||
arena_run_t *run = &arena_miscelm_get(chunk, rpages_ind)->run;
|
||||
arena_bin_t *bin = &arena->bins[run->binind];
|
||||
malloc_mutex_lock(&bin->lock);
|
||||
/* runs that are in the same chunk in as the current chunk, are likely to be the next currun */
|
||||
if (chunk != (arena_chunk_t *)CHUNK_ADDR2BASE(bin->runcur)) {
|
||||
arena_bin_info_t *bin_info = &arena_bin_info[run->binind];
|
||||
size_t availregs = bin_info->nregs * bin->stats.curruns;
|
||||
*bin_util = (bin->stats.curregs<<16) / availregs;
|
||||
*run_util = ((bin_info->nregs - run->nfree)<<16) / bin_info->nregs;
|
||||
defrag = 1;
|
||||
}
|
||||
malloc_mutex_unlock(&bin->lock);
|
||||
}
|
||||
}
|
||||
return defrag;
|
||||
}
|
||||
|
||||
+84
-7
@@ -59,7 +59,7 @@
|
||||
# internet, binding to all the interfaces is dangerous and will expose the
|
||||
# instance to everybody on the internet. So by default we uncomment the
|
||||
# following bind directive, that will force Redis to listen only into
|
||||
# the IPv4 lookback interface address (this means Redis will be able to
|
||||
# the IPv4 loopback interface address (this means Redis will be able to
|
||||
# accept connections only from clients running into the same computer it
|
||||
# is running).
|
||||
#
|
||||
@@ -185,6 +185,14 @@ logfile ""
|
||||
# dbid is a number between 0 and 'databases'-1
|
||||
databases 16
|
||||
|
||||
# By default Redis shows an ASCII art logo only when started to log to the
|
||||
# standard output and if the standard output is a TTY. Basically this means
|
||||
# that normally a logo is displayed only in interactive sessions.
|
||||
#
|
||||
# However it is possible to force the pre-4.0 behavior and always show a
|
||||
# ASCII art logo in startup logs by setting the following option to yes.
|
||||
always-show-logo yes
|
||||
|
||||
################################ SNAPSHOTTING ################################
|
||||
#
|
||||
# Save the DB on disk:
|
||||
@@ -288,7 +296,9 @@ dir ./
|
||||
#
|
||||
# 2) if slave-serve-stale-data is set to 'no' the slave will reply with
|
||||
# an error "SYNC with master in progress" to all the kind of commands
|
||||
# but to INFO and SLAVEOF.
|
||||
# but to INFO, SLAVEOF, AUTH, PING, SHUTDOWN, REPLCONF, ROLE, CONFIG,
|
||||
# SUBSCRIBE, UNSUBSCRIBE, PSUBSCRIBE, PUNSUBSCRIBE, PUBLISH, PUBSUB,
|
||||
# COMMAND, POST, HOST: and LATENCY.
|
||||
#
|
||||
slave-serve-stale-data yes
|
||||
|
||||
@@ -402,6 +412,10 @@ repl-disable-tcp-nodelay no
|
||||
# need to elapse, starting from the time the last slave disconnected, for
|
||||
# the backlog buffer to be freed.
|
||||
#
|
||||
# Note that slaves never free the backlog for timeout, since they may be
|
||||
# promoted to masters later, and should be able to correctly "partially
|
||||
# resynchronize" with the slaves: hence they should always accumulate backlog.
|
||||
#
|
||||
# A value of 0 means to never release the backlog.
|
||||
#
|
||||
# repl-backlog-ttl 3600
|
||||
@@ -448,7 +462,7 @@ slave-priority 100
|
||||
# offers this information, which is used, among other tools, by
|
||||
# Redis Sentinel in order to discover slave instances.
|
||||
# Another place where this info is available is in the output of the
|
||||
# "ROLE" command of a masteer.
|
||||
# "ROLE" command of a master.
|
||||
#
|
||||
# The listed IP and address normally reported by a slave is obtained
|
||||
# in the following way:
|
||||
@@ -594,7 +608,7 @@ slave-priority 100
|
||||
# deletion of the object. It means that the server stops processing new commands
|
||||
# in order to reclaim all the memory associated with an object in a synchronous
|
||||
# way. If the key deleted is associated with a small object, the time needed
|
||||
# in order to execute th DEL command is very small and comparable to most other
|
||||
# in order to execute the DEL command is very small and comparable to most other
|
||||
# O(1) or O(log_N) commands in Redis. However if the key is associated with an
|
||||
# aggregated value containing millions of elements, the server can block for
|
||||
# a long time (even seconds) in order to complete the operation.
|
||||
@@ -609,7 +623,7 @@ slave-priority 100
|
||||
# It's up to the design of the application to understand when it is a good
|
||||
# idea to use one or the other. However the Redis server sometimes has to
|
||||
# delete keys or flush the whole database as a side effect of other operations.
|
||||
# Specifically Redis deletes objects independently of an user call in the
|
||||
# Specifically Redis deletes objects independently of a user call in the
|
||||
# following scenarios:
|
||||
#
|
||||
# 1) On eviction, because of the maxmemory and maxmemory policy configurations,
|
||||
@@ -818,7 +832,7 @@ lua-time-limit 5000
|
||||
# A slave of a failing master will avoid to start a failover if its data
|
||||
# looks too old.
|
||||
#
|
||||
# There is no simple way for a slave to actually have a exact measure of
|
||||
# There is no simple way for a slave to actually have an exact measure of
|
||||
# its "data age", so the following two checks are performed:
|
||||
#
|
||||
# 1) If there are multiple slaves able to failover, they exchange messages
|
||||
@@ -902,7 +916,7 @@ lua-time-limit 5000
|
||||
# Docker and other containers).
|
||||
#
|
||||
# In order to make Redis Cluster working in such environments, a static
|
||||
# configuration where each node known its public address is needed. The
|
||||
# configuration where each node knows its public address is needed. The
|
||||
# following two options are used for this scope, and are:
|
||||
#
|
||||
# * cluster-announce-ip
|
||||
@@ -1216,3 +1230,66 @@ aof-rewrite-incremental-fsync yes
|
||||
#
|
||||
# lfu-log-factor 10
|
||||
# lfu-decay-time 1
|
||||
|
||||
########################### ACTIVE DEFRAGMENTATION #######################
|
||||
#
|
||||
# WARNING THIS FEATURE IS EXPERIMENTAL. However it was stress tested
|
||||
# even in production and manually tested by multiple engineers for some
|
||||
# time.
|
||||
#
|
||||
# What is active defragmentation?
|
||||
# -------------------------------
|
||||
#
|
||||
# Active (online) defragmentation allows a Redis server to compact the
|
||||
# spaces left between small allocations and deallocations of data in memory,
|
||||
# thus allowing to reclaim back memory.
|
||||
#
|
||||
# Fragmentation is a natural process that happens with every allocator (but
|
||||
# less so with Jemalloc, fortunately) and certain workloads. Normally a server
|
||||
# restart is needed in order to lower the fragmentation, or at least to flush
|
||||
# away all the data and create it again. However thanks to this feature
|
||||
# implemented by Oran Agra for Redis 4.0 this process can happen at runtime
|
||||
# in an "hot" way, while the server is running.
|
||||
#
|
||||
# Basically when the fragmentation is over a certain level (see the
|
||||
# configuration options below) Redis will start to create new copies of the
|
||||
# values in contiguous memory regions by exploiting certain specific Jemalloc
|
||||
# features (in order to understand if an allocation is causing fragmentation
|
||||
# and to allocate it in a better place), and at the same time, will release the
|
||||
# old copies of the data. This process, repeated incrementally for all the keys
|
||||
# will cause the fragmentation to drop back to normal values.
|
||||
#
|
||||
# Important things to understand:
|
||||
#
|
||||
# 1. This feature is disabled by default, and only works if you compiled Redis
|
||||
# to use the copy of Jemalloc we ship with the source code of Redis.
|
||||
# This is the default with Linux builds.
|
||||
#
|
||||
# 2. You never need to enable this feature if you don't have fragmentation
|
||||
# issues.
|
||||
#
|
||||
# 3. Once you experience fragmentation, you can enable this feature when
|
||||
# needed with the command "CONFIG SET activedefrag yes".
|
||||
#
|
||||
# The configuration parameters are able to fine tune the behavior of the
|
||||
# defragmentation process. If you are not sure about what they mean it is
|
||||
# a good idea to leave the defaults untouched.
|
||||
|
||||
# Enabled active defragmentation
|
||||
# activedefrag yes
|
||||
|
||||
# Minimum amount of fragmentation waste to start active defrag
|
||||
# active-defrag-ignore-bytes 100mb
|
||||
|
||||
# Minimum percentage of fragmentation to start active defrag
|
||||
# active-defrag-threshold-lower 10
|
||||
|
||||
# Maximum percentage of fragmentation at which we use maximum effort
|
||||
# active-defrag-threshold-upper 100
|
||||
|
||||
# Minimal effort for defrag in CPU percentage
|
||||
# active-defrag-cycle-min 25
|
||||
|
||||
# Maximal effort for defrag in CPU percentage
|
||||
# active-defrag-cycle-max 75
|
||||
|
||||
|
||||
+30
-12
@@ -14,6 +14,7 @@
|
||||
|
||||
release_hdr := $(shell sh -c './mkreleasehdr.sh')
|
||||
uname_S := $(shell sh -c 'uname -s 2>/dev/null || echo not')
|
||||
uname_M := $(shell sh -c 'uname -m 2>/dev/null || echo not')
|
||||
OPTIMIZATION?=-O2
|
||||
DEPENDENCY_TARGETS=hiredis linenoise lua
|
||||
NODEPS:=clean distclean
|
||||
@@ -27,11 +28,19 @@ PREFIX?=/usr/local
|
||||
INSTALL_BIN=$(PREFIX)/bin
|
||||
INSTALL=install
|
||||
|
||||
# Default allocator
|
||||
# Default allocator defaults to Jemalloc if it's not an ARM
|
||||
MALLOC=libc
|
||||
ifneq ($(uname_M),armv6l)
|
||||
ifneq ($(uname_M),armv7l)
|
||||
ifeq ($(uname_S),Linux)
|
||||
MALLOC=jemalloc
|
||||
else
|
||||
MALLOC=libc
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
# To get ARM stack traces if Redis crashes we need a special C flag.
|
||||
ifneq (,$(findstring armv,$(uname_M)))
|
||||
CFLAGS+=-funwind-tables
|
||||
endif
|
||||
|
||||
# Backwards compatibility for selecting an allocator
|
||||
@@ -61,6 +70,13 @@ DEBUG=-g -ggdb
|
||||
|
||||
ifeq ($(uname_S),SunOS)
|
||||
# SunOS
|
||||
ifneq ($(@@),32bit)
|
||||
CFLAGS+= -m64
|
||||
LDFLAGS+= -m64
|
||||
endif
|
||||
DEBUG=-g
|
||||
DEBUG_FLAGS=-g
|
||||
export CFLAGS LDFLAGS DEBUG DEBUG_FLAGS
|
||||
INSTALL=cp -pf
|
||||
FINAL_CFLAGS+= -D__EXTENSIONS__ -D_XPG6
|
||||
FINAL_LIBS+= -ldl -lnsl -lsocket -lresolv -lpthread -lrt
|
||||
@@ -128,14 +144,13 @@ endif
|
||||
|
||||
REDIS_SERVER_NAME=redis-server
|
||||
REDIS_SENTINEL_NAME=redis-sentinel
|
||||
REDIS_SERVER_OBJ=adlist.o quicklist.o ae.o anet.o dict.o server.o sds.o zmalloc.o lzf_c.o lzf_d.o pqsort.o zipmap.o sha1.o ziplist.o release.o networking.o util.o object.o db.o replication.o rdb.o t_string.o t_list.o t_set.o t_zset.o t_hash.o config.o aof.o pubsub.o multi.o debug.o sort.o intset.o syncio.o cluster.o crc16.o endianconv.o slowlog.o scripting.o bio.o rio.o rand.o memtest.o crc64.o bitops.o sentinel.o notify.o setproctitle.o blocked.o hyperloglog.o latency.o sparkline.o redis-check-rdb.o geo.o lazyfree.o module.o evict.o expire.o geohash.o geohash_helper.o childinfo.o
|
||||
REDIS_SERVER_OBJ=adlist.o quicklist.o ae.o anet.o dict.o server.o sds.o zmalloc.o lzf_c.o lzf_d.o pqsort.o zipmap.o sha1.o ziplist.o release.o networking.o util.o object.o db.o replication.o rdb.o t_string.o t_list.o t_set.o t_zset.o t_hash.o config.o aof.o pubsub.o multi.o debug.o sort.o intset.o syncio.o cluster.o crc16.o endianconv.o slowlog.o scripting.o bio.o rio.o rand.o memtest.o crc64.o bitops.o sentinel.o notify.o setproctitle.o blocked.o hyperloglog.o latency.o sparkline.o redis-check-rdb.o redis-check-aof.o geo.o lazyfree.o module.o evict.o expire.o geohash.o geohash_helper.o childinfo.o defrag.o siphash.o rax.o t_stream.o listpack.c
|
||||
REDIS_CLI_NAME=redis-cli
|
||||
REDIS_CLI_OBJ=anet.o adlist.o redis-cli.o zmalloc.o release.o anet.o ae.o crc64.o
|
||||
REDIS_BENCHMARK_NAME=redis-benchmark
|
||||
REDIS_BENCHMARK_OBJ=ae.o anet.o redis-benchmark.o adlist.o zmalloc.o redis-benchmark.o
|
||||
REDIS_CHECK_RDB_NAME=redis-check-rdb
|
||||
REDIS_CHECK_AOF_NAME=redis-check-aof
|
||||
REDIS_CHECK_AOF_OBJ=redis-check-aof.o
|
||||
|
||||
all: $(REDIS_SERVER_NAME) $(REDIS_SENTINEL_NAME) $(REDIS_CLI_NAME) $(REDIS_BENCHMARK_NAME) $(REDIS_CHECK_RDB_NAME) $(REDIS_CHECK_AOF_NAME)
|
||||
@echo ""
|
||||
@@ -191,6 +206,10 @@ $(REDIS_SENTINEL_NAME): $(REDIS_SERVER_NAME)
|
||||
$(REDIS_CHECK_RDB_NAME): $(REDIS_SERVER_NAME)
|
||||
$(REDIS_INSTALL) $(REDIS_SERVER_NAME) $(REDIS_CHECK_RDB_NAME)
|
||||
|
||||
# redis-check-aof
|
||||
$(REDIS_CHECK_AOF_NAME): $(REDIS_SERVER_NAME)
|
||||
$(REDIS_INSTALL) $(REDIS_SERVER_NAME) $(REDIS_CHECK_AOF_NAME)
|
||||
|
||||
# redis-cli
|
||||
$(REDIS_CLI_NAME): $(REDIS_CLI_OBJ)
|
||||
$(REDIS_LD) -o $@ $^ ../deps/hiredis/libhiredis.a ../deps/linenoise/linenoise.o $(FINAL_LIBS)
|
||||
@@ -199,12 +218,8 @@ $(REDIS_CLI_NAME): $(REDIS_CLI_OBJ)
|
||||
$(REDIS_BENCHMARK_NAME): $(REDIS_BENCHMARK_OBJ)
|
||||
$(REDIS_LD) -o $@ $^ ../deps/hiredis/libhiredis.a $(FINAL_LIBS)
|
||||
|
||||
# redis-check-aof
|
||||
$(REDIS_CHECK_AOF_NAME): $(REDIS_CHECK_AOF_OBJ)
|
||||
$(REDIS_LD) -o $@ $^ $(FINAL_LIBS)
|
||||
|
||||
dict-benchmark: dict.c zmalloc.c sds.c
|
||||
$(REDIS_CC) $(FINAL_CFLAGS) dict.c zmalloc.c sds.c -D DICT_BENCHMARK_MAIN -o dict-benchmark
|
||||
dict-benchmark: dict.c zmalloc.c sds.c siphash.c
|
||||
$(REDIS_CC) $(FINAL_CFLAGS) $^ -D DICT_BENCHMARK_MAIN -o $@ $(FINAL_LIBS)
|
||||
|
||||
# Because the jemalloc.h header is generated as a part of the jemalloc build,
|
||||
# building it should complete before building any other object. Instead of
|
||||
@@ -238,7 +253,7 @@ lcov:
|
||||
@genhtml --legend -o lcov-html redis.info
|
||||
|
||||
test-sds: sds.c sds.h
|
||||
$(REDIS_CC) sds.c zmalloc.c -DSDS_TEST_MAIN -o /tmp/sds_test
|
||||
$(REDIS_CC) sds.c zmalloc.c -DSDS_TEST_MAIN $(FINAL_LIBS) -o /tmp/sds_test
|
||||
/tmp/sds_test
|
||||
|
||||
.PHONY: lcov
|
||||
@@ -261,6 +276,9 @@ noopt:
|
||||
valgrind:
|
||||
$(MAKE) OPTIMIZATION="-O0" MALLOC="libc"
|
||||
|
||||
helgrind:
|
||||
$(MAKE) OPTIMIZATION="-O0" MALLOC="libc" CFLAGS="-D__ATOMIC_VAR_FORCE_SYNC_MACROS"
|
||||
|
||||
src/help.h:
|
||||
@../utils/generate-command-help.rb > help.h
|
||||
|
||||
|
||||
+31
-4
@@ -52,10 +52,8 @@ list *listCreate(void)
|
||||
return list;
|
||||
}
|
||||
|
||||
/* Free the whole list.
|
||||
*
|
||||
* This function can't fail. */
|
||||
void listRelease(list *list)
|
||||
/* Remove all the elements from the list without destroying the list itself. */
|
||||
void listEmpty(list *list)
|
||||
{
|
||||
unsigned long len;
|
||||
listNode *current, *next;
|
||||
@@ -68,6 +66,16 @@ void listRelease(list *list)
|
||||
zfree(current);
|
||||
current = next;
|
||||
}
|
||||
list->head = list->tail = NULL;
|
||||
list->len = 0;
|
||||
}
|
||||
|
||||
/* Free the whole list.
|
||||
*
|
||||
* This function can't fail. */
|
||||
void listRelease(list *list)
|
||||
{
|
||||
listEmpty(list);
|
||||
zfree(list);
|
||||
}
|
||||
|
||||
@@ -333,3 +341,22 @@ void listRotate(list *list) {
|
||||
tail->next = list->head;
|
||||
list->head = tail;
|
||||
}
|
||||
|
||||
/* Add all the elements of the list 'o' at the end of the
|
||||
* list 'l'. The list 'other' remains empty but otherwise valid. */
|
||||
void listJoin(list *l, list *o) {
|
||||
if (o->head)
|
||||
o->head->prev = l->tail;
|
||||
|
||||
if (l->tail)
|
||||
l->tail->next = o->head;
|
||||
else
|
||||
l->head = o->head;
|
||||
|
||||
if (o->tail) l->tail = o->tail;
|
||||
l->len += o->len;
|
||||
|
||||
/* Setup other as an empty list. */
|
||||
o->head = o->tail = NULL;
|
||||
o->len = 0;
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ typedef struct list {
|
||||
/* Prototypes */
|
||||
list *listCreate(void);
|
||||
void listRelease(list *list);
|
||||
void listEmpty(list *list);
|
||||
list *listAddNodeHead(list *list, void *value);
|
||||
list *listAddNodeTail(list *list, void *value);
|
||||
list *listInsertNode(list *list, listNode *old_node, void *value, int after);
|
||||
@@ -85,6 +86,7 @@ listNode *listIndex(list *list, long index);
|
||||
void listRewind(list *list, listIter *li);
|
||||
void listRewindTail(list *list, listIter *li);
|
||||
void listRotate(list *list);
|
||||
void listJoin(list *l, list *o);
|
||||
|
||||
/* Directions for iterators */
|
||||
#define AL_START_HEAD 0
|
||||
|
||||
@@ -75,6 +75,7 @@ aeEventLoop *aeCreateEventLoop(int setsize) {
|
||||
eventLoop->stop = 0;
|
||||
eventLoop->maxfd = -1;
|
||||
eventLoop->beforesleep = NULL;
|
||||
eventLoop->aftersleep = NULL;
|
||||
if (aeApiCreate(eventLoop) == -1) goto err;
|
||||
/* Events with mask == AE_NONE are not set. So let's initialize the
|
||||
* vector with it. */
|
||||
@@ -343,6 +344,7 @@ static int processTimeEvents(aeEventLoop *eventLoop) {
|
||||
* if flags has AE_FILE_EVENTS set, file events are processed.
|
||||
* if flags has AE_TIME_EVENTS set, time events are processed.
|
||||
* if flags has AE_DONT_WAIT set the function returns ASAP until all
|
||||
* if flags has AE_CALL_AFTER_SLEEP set, the aftersleep callback is called.
|
||||
* the events that's possible to process without to wait are processed.
|
||||
*
|
||||
* The function returns the number of events processed. */
|
||||
@@ -397,7 +399,14 @@ int aeProcessEvents(aeEventLoop *eventLoop, int flags)
|
||||
}
|
||||
}
|
||||
|
||||
/* Call the multiplexing API, will return only on timeout or when
|
||||
* some event fires. */
|
||||
numevents = aeApiPoll(eventLoop, tvp);
|
||||
|
||||
/* After sleep callback. */
|
||||
if (eventLoop->aftersleep != NULL && flags & AE_CALL_AFTER_SLEEP)
|
||||
eventLoop->aftersleep(eventLoop);
|
||||
|
||||
for (j = 0; j < numevents; j++) {
|
||||
aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
|
||||
int mask = eventLoop->fired[j].mask;
|
||||
@@ -452,7 +461,7 @@ void aeMain(aeEventLoop *eventLoop) {
|
||||
while (!eventLoop->stop) {
|
||||
if (eventLoop->beforesleep != NULL)
|
||||
eventLoop->beforesleep(eventLoop);
|
||||
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
|
||||
aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,3 +472,7 @@ char *aeGetApiName(void) {
|
||||
void aeSetBeforeSleepProc(aeEventLoop *eventLoop, aeBeforeSleepProc *beforesleep) {
|
||||
eventLoop->beforesleep = beforesleep;
|
||||
}
|
||||
|
||||
void aeSetAfterSleepProc(aeEventLoop *eventLoop, aeBeforeSleepProc *aftersleep) {
|
||||
eventLoop->aftersleep = aftersleep;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
#define AE_TIME_EVENTS 2
|
||||
#define AE_ALL_EVENTS (AE_FILE_EVENTS|AE_TIME_EVENTS)
|
||||
#define AE_DONT_WAIT 4
|
||||
#define AE_CALL_AFTER_SLEEP 8
|
||||
|
||||
#define AE_NOMORE -1
|
||||
#define AE_DELETED_EVENT_ID -1
|
||||
@@ -98,6 +99,7 @@ typedef struct aeEventLoop {
|
||||
int stop;
|
||||
void *apidata; /* This is used for polling API specific data */
|
||||
aeBeforeSleepProc *beforesleep;
|
||||
aeBeforeSleepProc *aftersleep;
|
||||
} aeEventLoop;
|
||||
|
||||
/* Prototypes */
|
||||
@@ -117,6 +119,7 @@ int aeWait(int fd, int mask, long long milliseconds);
|
||||
void aeMain(aeEventLoop *eventLoop);
|
||||
char *aeGetApiName(void);
|
||||
void aeSetBeforeSleepProc(aeEventLoop *eventLoop, aeBeforeSleepProc *beforesleep);
|
||||
void aeSetAfterSleepProc(aeEventLoop *eventLoop, aeBeforeSleepProc *aftersleep);
|
||||
int aeGetSetSize(aeEventLoop *eventLoop);
|
||||
int aeResizeSetSize(aeEventLoop *eventLoop, int setsize);
|
||||
|
||||
|
||||
+6
-3
@@ -237,7 +237,7 @@ int anetResolveIP(char *err, char *host, char *ipbuf, size_t ipbuf_len) {
|
||||
|
||||
static int anetSetReuseAddr(char *err, int fd) {
|
||||
int yes = 1;
|
||||
/* Make sure connection-intensive things like the redis benckmark
|
||||
/* Make sure connection-intensive things like the redis benchmark
|
||||
* will be able to close/open sockets a zillion of times */
|
||||
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) == -1) {
|
||||
anetSetError(err, "setsockopt SO_REUSEADDR: %s", strerror(errno));
|
||||
@@ -380,8 +380,10 @@ int anetUnixGenericConnect(char *err, char *path, int flags)
|
||||
sa.sun_family = AF_LOCAL;
|
||||
strncpy(sa.sun_path,path,sizeof(sa.sun_path)-1);
|
||||
if (flags & ANET_CONNECT_NONBLOCK) {
|
||||
if (anetNonBlock(err,s) != ANET_OK)
|
||||
if (anetNonBlock(err,s) != ANET_OK) {
|
||||
close(s);
|
||||
return ANET_ERR;
|
||||
}
|
||||
}
|
||||
if (connect(s,(struct sockaddr*)&sa,sizeof(sa)) == -1) {
|
||||
if (errno == EINPROGRESS &&
|
||||
@@ -462,7 +464,7 @@ static int anetV6Only(char *err, int s) {
|
||||
|
||||
static int _anetTcpServer(char *err, int port, char *bindaddr, int af, int backlog)
|
||||
{
|
||||
int s, rv;
|
||||
int s = -1, rv;
|
||||
char _port[6]; /* strlen("65535") */
|
||||
struct addrinfo hints, *servinfo, *p;
|
||||
|
||||
@@ -491,6 +493,7 @@ static int _anetTcpServer(char *err, int port, char *bindaddr, int af, int backl
|
||||
}
|
||||
|
||||
error:
|
||||
if (s != -1) close(s);
|
||||
s = ANET_ERR;
|
||||
end:
|
||||
freeaddrinfo(servinfo);
|
||||
|
||||
@@ -115,6 +115,7 @@ void aofChildWriteDiffData(aeEventLoop *el, int fd, void *privdata, int mask) {
|
||||
if (nwritten <= 0) return;
|
||||
memmove(block->buf,block->buf+nwritten,block->used-nwritten);
|
||||
block->used -= nwritten;
|
||||
block->free += nwritten;
|
||||
}
|
||||
if (block->used == 0) listDelNode(server.aof_rewrite_buf_blocks,ln);
|
||||
}
|
||||
@@ -236,11 +237,11 @@ void stopAppendOnly(void) {
|
||||
* at runtime using the CONFIG command. */
|
||||
int startAppendOnly(void) {
|
||||
char cwd[MAXPATHLEN]; /* Current working dir path for error messages. */
|
||||
int newfd;
|
||||
|
||||
server.aof_last_fsync = server.unixtime;
|
||||
server.aof_fd = open(server.aof_filename,O_WRONLY|O_APPEND|O_CREAT,0644);
|
||||
newfd = open(server.aof_filename,O_WRONLY|O_APPEND|O_CREAT,0644);
|
||||
serverAssert(server.aof_state == AOF_OFF);
|
||||
if (server.aof_fd == -1) {
|
||||
if (newfd == -1) {
|
||||
char *cwdp = getcwd(cwd,MAXPATHLEN);
|
||||
|
||||
serverLog(LL_WARNING,
|
||||
@@ -255,16 +256,46 @@ int startAppendOnly(void) {
|
||||
server.aof_rewrite_scheduled = 1;
|
||||
serverLog(LL_WARNING,"AOF was enabled but there is already a child process saving an RDB file on disk. An AOF background was scheduled to start when possible.");
|
||||
} else if (rewriteAppendOnlyFileBackground() == C_ERR) {
|
||||
close(server.aof_fd);
|
||||
close(newfd);
|
||||
serverLog(LL_WARNING,"Redis needs to enable the AOF but can't trigger a background AOF rewrite operation. Check the above logs for more info about the error.");
|
||||
return C_ERR;
|
||||
}
|
||||
/* We correctly switched on AOF, now wait for the rewrite to be complete
|
||||
* in order to append data on disk. */
|
||||
server.aof_state = AOF_WAIT_REWRITE;
|
||||
server.aof_last_fsync = server.unixtime;
|
||||
server.aof_fd = newfd;
|
||||
return C_OK;
|
||||
}
|
||||
|
||||
/* This is a wrapper to the write syscall in order to retry on short writes
|
||||
* or if the syscall gets interrupted. It could look strange that we retry
|
||||
* on short writes given that we are writing to a block device: normally if
|
||||
* the first call is short, there is a end-of-space condition, so the next
|
||||
* is likely to fail. However apparently in modern systems this is no longer
|
||||
* true, and in general it looks just more resilient to retry the write. If
|
||||
* there is an actual error condition we'll get it at the next try. */
|
||||
ssize_t aofWrite(int fd, const char *buf, size_t len) {
|
||||
ssize_t nwritten = 0, totwritten = 0;
|
||||
|
||||
while(len) {
|
||||
nwritten = write(fd, buf, len);
|
||||
|
||||
if (nwritten < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
return totwritten ? totwritten : -1;
|
||||
}
|
||||
|
||||
len -= nwritten;
|
||||
buf += nwritten;
|
||||
totwritten += nwritten;
|
||||
}
|
||||
|
||||
return totwritten;
|
||||
}
|
||||
|
||||
/* Write the append only file buffer on disk.
|
||||
*
|
||||
* Since we are required to write the AOF before replying to the client,
|
||||
@@ -322,7 +353,7 @@ void flushAppendOnlyFile(int force) {
|
||||
* or alike */
|
||||
|
||||
latencyStartMonitor(latency);
|
||||
nwritten = write(server.aof_fd,server.aof_buf,sdslen(server.aof_buf));
|
||||
nwritten = aofWrite(server.aof_fd,server.aof_buf,sdslen(server.aof_buf));
|
||||
latencyEndMonitor(latency);
|
||||
/* We want to capture different events for delayed writes:
|
||||
* when the delay happens with a pending fsync, or with a saving child
|
||||
@@ -341,7 +372,7 @@ void flushAppendOnlyFile(int force) {
|
||||
/* We performed the write so reset the postponed flush sentinel to zero. */
|
||||
server.aof_flush_postponed_start = 0;
|
||||
|
||||
if (nwritten != (signed)sdslen(server.aof_buf)) {
|
||||
if (nwritten != (ssize_t)sdslen(server.aof_buf)) {
|
||||
static time_t last_write_error_log = 0;
|
||||
int can_log = 0;
|
||||
|
||||
@@ -535,6 +566,22 @@ void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int a
|
||||
buf = catAppendOnlyGenericCommand(buf,3,tmpargv);
|
||||
decrRefCount(tmpargv[0]);
|
||||
buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);
|
||||
} else if (cmd->proc == setCommand && argc > 3) {
|
||||
int i;
|
||||
robj *exarg = NULL, *pxarg = NULL;
|
||||
/* Translate SET [EX seconds][PX milliseconds] to SET and PEXPIREAT */
|
||||
buf = catAppendOnlyGenericCommand(buf,3,argv);
|
||||
for (i = 3; i < argc; i ++) {
|
||||
if (!strcasecmp(argv[i]->ptr, "ex")) exarg = argv[i+1];
|
||||
if (!strcasecmp(argv[i]->ptr, "px")) pxarg = argv[i+1];
|
||||
}
|
||||
serverAssert(!(exarg && pxarg));
|
||||
if (exarg)
|
||||
buf = catAppendOnlyExpireAtCommand(buf,server.expireCommand,argv[1],
|
||||
exarg);
|
||||
if (pxarg)
|
||||
buf = catAppendOnlyExpireAtCommand(buf,server.pexpireCommand,argv[1],
|
||||
pxarg);
|
||||
} else {
|
||||
/* All the other commands don't need translation or need the
|
||||
* same translation already operated in the command vector
|
||||
@@ -653,7 +700,7 @@ int loadAppendOnlyFile(char *filename) {
|
||||
serverLog(LL_NOTICE,"Reading RDB preamble from AOF file...");
|
||||
if (fseek(fp,0,SEEK_SET) == -1) goto readerr;
|
||||
rioInitWithFile(&rdb,fp);
|
||||
if (rdbLoadRio(&rdb) != C_OK) {
|
||||
if (rdbLoadRio(&rdb,NULL) != C_OK) {
|
||||
serverLog(LL_WARNING,"Error reading the RDB preamble of the AOF file, AOF loading aborted");
|
||||
goto readerr;
|
||||
} else {
|
||||
@@ -1014,6 +1061,37 @@ int rewriteHashObject(rio *r, robj *key, robj *o) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Emit the commands needed to rebuild a stream object.
|
||||
* The function returns 0 on error, 1 on success. */
|
||||
int rewriteStreamObject(rio *r, robj *key, robj *o) {
|
||||
streamIterator si;
|
||||
streamIteratorStart(&si,o->ptr,NULL,NULL,0);
|
||||
streamID id;
|
||||
int64_t numfields;
|
||||
|
||||
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;
|
||||
sds replyid = sdscatfmt(sdsempty(),"%U.%U",id.ms,id.seq);
|
||||
if (rioWriteBulkString(r,replyid,sdslen(replyid)) == 0) return 0;
|
||||
sdsfree(replyid);
|
||||
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;
|
||||
}
|
||||
}
|
||||
streamIteratorStop(&si);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Call the module type callback in order to rewrite a data type
|
||||
* that is exported by a module and is not handled by Redis itself.
|
||||
* The function returns 0 on error, 1 on success. */
|
||||
@@ -1094,6 +1172,8 @@ int rewriteAppendOnlyFileRio(rio *aof) {
|
||||
if (rewriteSortedSetObject(aof,&key,o) == 0) goto werr;
|
||||
} else if (o->type == OBJ_HASH) {
|
||||
if (rewriteHashObject(aof,&key,o) == 0) goto werr;
|
||||
} else if (o->type == OBJ_STREAM) {
|
||||
if (rewriteStreamObject(aof,&key,o) == 0) goto werr;
|
||||
} else if (o->type == OBJ_MODULE) {
|
||||
if (rewriteModuleObject(aof,&key,o) == 0) goto werr;
|
||||
} else {
|
||||
@@ -1152,7 +1232,7 @@ int rewriteAppendOnlyFile(char *filename) {
|
||||
|
||||
if (server.aof_use_rdb_preamble) {
|
||||
int error;
|
||||
if (rdbSaveRio(&aof,&error,RDB_SAVE_AOF_PREAMBLE) == C_ERR) {
|
||||
if (rdbSaveRio(&aof,&error,RDB_SAVE_AOF_PREAMBLE,NULL) == C_ERR) {
|
||||
errno = error;
|
||||
goto werr;
|
||||
}
|
||||
@@ -1357,6 +1437,7 @@ int rewriteAppendOnlyFileBackground(void) {
|
||||
serverLog(LL_WARNING,
|
||||
"Can't rewrite append only file in background: fork: %s",
|
||||
strerror(errno));
|
||||
aofClosePipes();
|
||||
return C_ERR;
|
||||
}
|
||||
serverLog(LL_NOTICE,
|
||||
|
||||
+65
-26
@@ -3,18 +3,29 @@
|
||||
*
|
||||
* The exported interaface is composed of three macros:
|
||||
*
|
||||
* atomicIncr(var,count,mutex) -- Increment the atomic counter
|
||||
* atomicDecr(var,count,mutex) -- Decrement the atomic counter
|
||||
* atomicGet(var,dstvar,mutex) -- Fetch the atomic counter value
|
||||
* atomicIncr(var,count) -- Increment the atomic counter
|
||||
* atomicGetIncr(var,oldvalue_var,count) -- Get and increment the atomic counter
|
||||
* atomicDecr(var,count) -- Decrement the atomic counter
|
||||
* atomicGet(var,dstvar) -- Fetch the atomic counter value
|
||||
* atomicSet(var,value) -- Set the atomic counter value
|
||||
*
|
||||
* The variable 'var' should also have a declared mutex with the same
|
||||
* name and the "_mutex" postfix, for instance:
|
||||
*
|
||||
* long myvar;
|
||||
* pthread_mutex_t myvar_mutex;
|
||||
* atomicSet(myvar,12345);
|
||||
*
|
||||
* If atomic primitives are availble (tested in config.h) the mutex
|
||||
* is not used.
|
||||
*
|
||||
* Never use return value from the macros. To update and get use instead:
|
||||
* Never use return value from the macros, instead use the AtomicGetIncr()
|
||||
* if you need to get the current value and increment it atomically, like
|
||||
* in the followign example:
|
||||
*
|
||||
* atomicIncr(mycounter,...);
|
||||
* atomicGet(mycounter,newvalue);
|
||||
* doSomethingWith(newvalue);
|
||||
* long oldvalue;
|
||||
* atomicGetIncr(myvar,oldvalue,1);
|
||||
* doSomethingWith(oldvalue);
|
||||
*
|
||||
* ----------------------------------------------------------------------------
|
||||
*
|
||||
@@ -51,44 +62,72 @@
|
||||
#ifndef __ATOMIC_VAR_H
|
||||
#define __ATOMIC_VAR_H
|
||||
|
||||
#if defined(__ATOMIC_RELAXED)
|
||||
/* To test Redis with Helgrind (a Valgrind tool) it is useful to define
|
||||
* the following macro, so that __sync macros are used: those can be detected
|
||||
* by Helgrind (even if they are less efficient) so that no false positive
|
||||
* is reported. */
|
||||
// #define __ATOMIC_VAR_FORCE_SYNC_MACROS
|
||||
|
||||
#if !defined(__ATOMIC_VAR_FORCE_SYNC_MACROS) && defined(__ATOMIC_RELAXED) && !defined(__sun) && (!defined(__clang__) || !defined(__APPLE__) || __apple_build_version__ > 4210057)
|
||||
/* Implementation using __atomic macros. */
|
||||
|
||||
#define atomicIncr(var,count,mutex) __atomic_add_fetch(&var,(count),__ATOMIC_RELAXED)
|
||||
#define atomicDecr(var,count,mutex) __atomic_sub_fetch(&var,(count),__ATOMIC_RELAXED)
|
||||
#define atomicGet(var,dstvar,mutex) do { \
|
||||
#define atomicIncr(var,count) __atomic_add_fetch(&var,(count),__ATOMIC_RELAXED)
|
||||
#define atomicGetIncr(var,oldvalue_var,count) do { \
|
||||
oldvalue_var = __atomic_fetch_add(&var,(count),__ATOMIC_RELAXED); \
|
||||
} while(0)
|
||||
#define atomicDecr(var,count) __atomic_sub_fetch(&var,(count),__ATOMIC_RELAXED)
|
||||
#define atomicGet(var,dstvar) do { \
|
||||
dstvar = __atomic_load_n(&var,__ATOMIC_RELAXED); \
|
||||
} while(0)
|
||||
#define atomicSet(var,value) __atomic_store_n(&var,value,__ATOMIC_RELAXED)
|
||||
#define REDIS_ATOMIC_API "atomic-builtin"
|
||||
|
||||
#elif defined(HAVE_ATOMIC)
|
||||
/* Implementation using __sync macros. */
|
||||
|
||||
#define atomicIncr(var,count,mutex) __sync_add_and_fetch(&var,(count))
|
||||
#define atomicDecr(var,count,mutex) __sync_sub_and_fetch(&var,(count))
|
||||
#define atomicGet(var,dstvar,mutex) do { \
|
||||
#define atomicIncr(var,count) __sync_add_and_fetch(&var,(count))
|
||||
#define atomicGetIncr(var,oldvalue_var,count) do { \
|
||||
oldvalue_var = __sync_fetch_and_add(&var,(count)); \
|
||||
} while(0)
|
||||
#define atomicDecr(var,count) __sync_sub_and_fetch(&var,(count))
|
||||
#define atomicGet(var,dstvar) do { \
|
||||
dstvar = __sync_sub_and_fetch(&var,0); \
|
||||
} while(0)
|
||||
#define atomicSet(var,value) do { \
|
||||
while(!__sync_bool_compare_and_swap(&var,var,value)); \
|
||||
} while(0)
|
||||
#define REDIS_ATOMIC_API "sync-builtin"
|
||||
|
||||
#else
|
||||
/* Implementation using pthread mutex. */
|
||||
|
||||
#define atomicIncr(var,count,mutex) do { \
|
||||
pthread_mutex_lock(&mutex); \
|
||||
#define atomicIncr(var,count) do { \
|
||||
pthread_mutex_lock(&var ## _mutex); \
|
||||
var += (count); \
|
||||
pthread_mutex_unlock(&mutex); \
|
||||
pthread_mutex_unlock(&var ## _mutex); \
|
||||
} while(0)
|
||||
|
||||
#define atomicDecr(var,count,mutex) do { \
|
||||
pthread_mutex_lock(&mutex); \
|
||||
#define atomicGetIncr(var,oldvalue_var,count) do { \
|
||||
pthread_mutex_lock(&var ## _mutex); \
|
||||
oldvalue_var = var; \
|
||||
var += (count); \
|
||||
pthread_mutex_unlock(&var ## _mutex); \
|
||||
} while(0)
|
||||
#define atomicDecr(var,count) do { \
|
||||
pthread_mutex_lock(&var ## _mutex); \
|
||||
var -= (count); \
|
||||
pthread_mutex_unlock(&mutex); \
|
||||
pthread_mutex_unlock(&var ## _mutex); \
|
||||
} while(0)
|
||||
|
||||
#define atomicGet(var,dstvar,mutex) do { \
|
||||
pthread_mutex_lock(&mutex); \
|
||||
#define atomicGet(var,dstvar) do { \
|
||||
pthread_mutex_lock(&var ## _mutex); \
|
||||
dstvar = var; \
|
||||
pthread_mutex_unlock(&mutex); \
|
||||
pthread_mutex_unlock(&var ## _mutex); \
|
||||
} while(0)
|
||||
#endif
|
||||
#define atomicSet(var,value) do { \
|
||||
pthread_mutex_lock(&var ## _mutex); \
|
||||
var = value; \
|
||||
pthread_mutex_unlock(&var ## _mutex); \
|
||||
} while(0)
|
||||
#define REDIS_ATOMIC_API "pthread-mutex"
|
||||
|
||||
#endif
|
||||
#endif /* __ATOMIC_VAR_H */
|
||||
|
||||
+22
-10
@@ -104,6 +104,7 @@ long redisBitpos(void *s, unsigned long count, int bit) {
|
||||
unsigned long skipval, word = 0, one;
|
||||
long pos = 0; /* Position of bit, to return to the caller. */
|
||||
unsigned long j;
|
||||
int found;
|
||||
|
||||
/* Process whole words first, seeking for first word that is not
|
||||
* all ones or all zeros respectively if we are lookig for zeros
|
||||
@@ -117,21 +118,27 @@ long redisBitpos(void *s, unsigned long count, int bit) {
|
||||
/* Skip initial bits not aligned to sizeof(unsigned long) byte by byte. */
|
||||
skipval = bit ? 0 : UCHAR_MAX;
|
||||
c = (unsigned char*) s;
|
||||
found = 0;
|
||||
while((unsigned long)c & (sizeof(*l)-1) && count) {
|
||||
if (*c != skipval) break;
|
||||
if (*c != skipval) {
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
c++;
|
||||
count--;
|
||||
pos += 8;
|
||||
}
|
||||
|
||||
/* Skip bits with full word step. */
|
||||
skipval = bit ? 0 : ULONG_MAX;
|
||||
l = (unsigned long*) c;
|
||||
while (count >= sizeof(*l)) {
|
||||
if (*l != skipval) break;
|
||||
l++;
|
||||
count -= sizeof(*l);
|
||||
pos += sizeof(*l)*8;
|
||||
if (!found) {
|
||||
skipval = bit ? 0 : ULONG_MAX;
|
||||
while (count >= sizeof(*l)) {
|
||||
if (*l != skipval) break;
|
||||
l++;
|
||||
count -= sizeof(*l);
|
||||
pos += sizeof(*l)*8;
|
||||
}
|
||||
}
|
||||
|
||||
/* Load bytes into "word" considering the first byte as the most significant
|
||||
@@ -654,8 +661,11 @@ void bitopCommand(client *c) {
|
||||
|
||||
/* Fast path: as far as we have data for all the input bitmaps we
|
||||
* can take a fast path that performs much better than the
|
||||
* vanilla algorithm. */
|
||||
* vanilla algorithm. On ARM we skip the fast path since it will
|
||||
* result in GCC compiling the code using multiple-words load/store
|
||||
* operations that are not supported even in ARM >= v6. */
|
||||
j = 0;
|
||||
#ifndef USE_ALIGNED_ACCESS
|
||||
if (minlen >= sizeof(unsigned long)*4 && numkeys <= 16) {
|
||||
unsigned long *lp[16];
|
||||
unsigned long *lres = (unsigned long*) res;
|
||||
@@ -716,6 +726,7 @@ void bitopCommand(client *c) {
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/* j is set to the next byte to process by the previous loop. */
|
||||
for (; j < maxlen; j++) {
|
||||
@@ -907,7 +918,7 @@ void bitfieldCommand(client *c) {
|
||||
struct bitfieldOp *ops = NULL; /* Array of ops to execute at end. */
|
||||
int owtype = BFOVERFLOW_WRAP; /* Overflow type. */
|
||||
int readonly = 1;
|
||||
long higest_write_offset = 0;
|
||||
size_t higest_write_offset = 0;
|
||||
|
||||
for (j = 2; j < c->argc; j++) {
|
||||
int remargs = c->argc-j-1; /* Remaining args other than current. */
|
||||
@@ -957,7 +968,8 @@ void bitfieldCommand(client *c) {
|
||||
|
||||
if (opcode != BITFIELDOP_GET) {
|
||||
readonly = 0;
|
||||
higest_write_offset = bitoffset + bits - 1;
|
||||
if (higest_write_offset < bitoffset + bits - 1)
|
||||
higest_write_offset = bitoffset + bits - 1;
|
||||
/* INCRBY and SET require another argument. */
|
||||
if (getLongLongFromObjectOrReply(c,c->argv[j+3],&i64,NULL) != C_OK){
|
||||
zfree(ops);
|
||||
|
||||
+294
-5
@@ -65,6 +65,8 @@
|
||||
|
||||
#include "server.h"
|
||||
|
||||
int serveClientBlockedOnList(client *receiver, robj *key, robj *dstkey, redisDb *db, robj *value, int where);
|
||||
|
||||
/* Get a timeout value from an object and store it into 'timeout'.
|
||||
* The final timeout is always stored as milliseconds as a time where the
|
||||
* timeout will expire, however the parsing is performed according to
|
||||
@@ -100,7 +102,8 @@ int getTimeoutFromObjectOrReply(client *c, robj *object, mstime_t *timeout, int
|
||||
void blockClient(client *c, int btype) {
|
||||
c->flags |= CLIENT_BLOCKED;
|
||||
c->btype = btype;
|
||||
server.bpop_blocked_clients++;
|
||||
server.blocked_clients++;
|
||||
server.blocked_clients_by_type[btype]++;
|
||||
}
|
||||
|
||||
/* This function is called in the beforeSleep() function of the event loop
|
||||
@@ -132,18 +135,21 @@ void processUnblockedClients(void) {
|
||||
/* Unblock a client calling the right function depending on the kind
|
||||
* of operation the client is blocking for. */
|
||||
void unblockClient(client *c) {
|
||||
if (c->btype == BLOCKED_LIST) {
|
||||
if (c->btype == BLOCKED_LIST || c->btype == BLOCKED_STREAM) {
|
||||
unblockClientWaitingData(c);
|
||||
} else if (c->btype == BLOCKED_WAIT) {
|
||||
unblockClientWaitingReplicas(c);
|
||||
} else if (c->btype == BLOCKED_MODULE) {
|
||||
unblockClientFromModule(c);
|
||||
} else {
|
||||
serverPanic("Unknown btype in unblockClient().");
|
||||
}
|
||||
/* Clear the flags, and put the client in the unblocked list so that
|
||||
* we'll process new commands in its query buffer ASAP. */
|
||||
server.blocked_clients--;
|
||||
server.blocked_clients_by_type[c->btype]--;
|
||||
c->flags &= ~CLIENT_BLOCKED;
|
||||
c->btype = BLOCKED_NONE;
|
||||
server.bpop_blocked_clients--;
|
||||
/* The client may already be into the unblocked list because of a previous
|
||||
* blocking operation, don't add back it into the list multiple times. */
|
||||
if (!(c->flags & CLIENT_UNBLOCKED)) {
|
||||
@@ -153,12 +159,15 @@ void unblockClient(client *c) {
|
||||
}
|
||||
|
||||
/* This function gets called when a blocked client timed out in order to
|
||||
* send it a reply of some kind. */
|
||||
* send it a reply of some kind. After this function is called,
|
||||
* unblockClient() will be called with the same client as argument. */
|
||||
void replyToBlockedClientTimedOut(client *c) {
|
||||
if (c->btype == BLOCKED_LIST) {
|
||||
if (c->btype == BLOCKED_LIST || c->btype == BLOCKED_STREAM) {
|
||||
addReply(c,shared.nullmultibulk);
|
||||
} else if (c->btype == BLOCKED_WAIT) {
|
||||
addReplyLongLong(c,replicationCountAcksByOffset(c->bpop.reploffset));
|
||||
} else if (c->btype == BLOCKED_MODULE) {
|
||||
moduleBlockedClientTimedOut(c);
|
||||
} else {
|
||||
serverPanic("Unknown btype in replyToBlockedClientTimedOut().");
|
||||
}
|
||||
@@ -188,3 +197,283 @@ void disconnectAllBlockedClients(void) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* This function should be called by Redis every time a single command,
|
||||
* a MULTI/EXEC block, or a Lua script, terminated its execution after
|
||||
* being called by a client.
|
||||
*
|
||||
* All the keys with at least one client blocked that received at least
|
||||
* one new element via some PUSH/XADD operation are accumulated into
|
||||
* the server.ready_keys list. This function will run the list and will
|
||||
* serve clients accordingly. Note that the function will iterate again and
|
||||
* again as a result of serving BRPOPLPUSH we can have new blocking clients
|
||||
* to serve because of the PUSH side of BRPOPLPUSH. */
|
||||
void handleClientsBlockedOnKeys(void) {
|
||||
while(listLength(server.ready_keys) != 0) {
|
||||
list *l;
|
||||
|
||||
/* Point server.ready_keys to a fresh list and save the current one
|
||||
* locally. This way as we run the old list we are free to call
|
||||
* signalKeyAsReady() that may push new elements in server.ready_keys
|
||||
* when handling clients blocked into BRPOPLPUSH. */
|
||||
l = server.ready_keys;
|
||||
server.ready_keys = listCreate();
|
||||
|
||||
while(listLength(l) != 0) {
|
||||
listNode *ln = listFirst(l);
|
||||
readyList *rl = ln->value;
|
||||
|
||||
/* First of all remove this key from db->ready_keys so that
|
||||
* we can safely call signalKeyAsReady() against this key. */
|
||||
dictDelete(rl->db->ready_keys,rl->key);
|
||||
|
||||
/* Serve clients blocked on list key. */
|
||||
robj *o = lookupKeyWrite(rl->db,rl->key);
|
||||
if (o != NULL && o->type == OBJ_LIST) {
|
||||
dictEntry *de;
|
||||
|
||||
/* We serve clients in the same order they blocked for
|
||||
* this key, from the first blocked to the last. */
|
||||
de = dictFind(rl->db->blocking_keys,rl->key);
|
||||
if (de) {
|
||||
list *clients = dictGetVal(de);
|
||||
int numclients = listLength(clients);
|
||||
|
||||
while(numclients--) {
|
||||
listNode *clientnode = listFirst(clients);
|
||||
client *receiver = clientnode->value;
|
||||
|
||||
if (receiver->btype != BLOCKED_LIST) {
|
||||
/* Put on the tail, so that at the next call
|
||||
* we'll not run into it again. */
|
||||
listDelNode(clients,clientnode);
|
||||
listAddNodeTail(clients,receiver);
|
||||
continue;
|
||||
}
|
||||
|
||||
robj *dstkey = receiver->bpop.target;
|
||||
int where = (receiver->lastcmd &&
|
||||
receiver->lastcmd->proc == blpopCommand) ?
|
||||
LIST_HEAD : LIST_TAIL;
|
||||
robj *value = listTypePop(o,where);
|
||||
|
||||
if (value) {
|
||||
/* Protect receiver->bpop.target, that will be
|
||||
* freed by the next unblockClient()
|
||||
* call. */
|
||||
if (dstkey) incrRefCount(dstkey);
|
||||
unblockClient(receiver);
|
||||
|
||||
if (serveClientBlockedOnList(receiver,
|
||||
rl->key,dstkey,rl->db,value,
|
||||
where) == C_ERR)
|
||||
{
|
||||
/* If we failed serving the client we need
|
||||
* to also undo the POP operation. */
|
||||
listTypePush(o,value,where);
|
||||
}
|
||||
|
||||
if (dstkey) decrRefCount(dstkey);
|
||||
decrRefCount(value);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (listTypeLength(o) == 0) {
|
||||
dbDelete(rl->db,rl->key);
|
||||
}
|
||||
/* We don't call signalModifiedKey() as it was already called
|
||||
* when an element was pushed on the list. */
|
||||
}
|
||||
|
||||
/* Serve clients blocked on stream key. */
|
||||
else if (o != NULL && o->type == OBJ_STREAM) {
|
||||
dictEntry *de = dictFind(rl->db->blocking_keys,rl->key);
|
||||
stream *s = o->ptr;
|
||||
|
||||
/* We need to provide the new data arrived on the stream
|
||||
* to all the clients that are waiting for an offset smaller
|
||||
* than the current top item. */
|
||||
if (de) {
|
||||
list *clients = dictGetVal(de);
|
||||
listNode *ln;
|
||||
listIter li;
|
||||
listRewind(clients,&li);
|
||||
|
||||
while((ln = listNext(&li))) {
|
||||
client *receiver = listNodeValue(ln);
|
||||
if (receiver->btype != BLOCKED_STREAM) continue;
|
||||
streamID *gt = dictFetchValue(receiver->bpop.keys,
|
||||
rl->key);
|
||||
if (s->last_id.ms > gt->ms ||
|
||||
(s->last_id.ms == gt->ms &&
|
||||
s->last_id.seq > gt->seq))
|
||||
{
|
||||
streamID start = *gt;
|
||||
start.seq++; /* Can't overflow, it's an uint64_t */
|
||||
/* Note that after we unblock the client, 'gt'
|
||||
* is no longer valid, so we must do it after
|
||||
* we copied the ID into the 'start' variable. */
|
||||
unblockClient(receiver);
|
||||
|
||||
/* Emit the two elements sub-array consisting of
|
||||
* the name of the stream and the data we
|
||||
* extracted from it. Wrapped in a single-item
|
||||
* array, since we have just one key. */
|
||||
addReplyMultiBulkLen(receiver,1);
|
||||
addReplyMultiBulkLen(receiver,2);
|
||||
addReplyBulk(receiver,rl->key);
|
||||
streamReplyWithRange(receiver,s,&start,NULL,
|
||||
receiver->bpop.xread_count,0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Free this item. */
|
||||
decrRefCount(rl->key);
|
||||
zfree(rl);
|
||||
listDelNode(l,ln);
|
||||
}
|
||||
listRelease(l); /* We have the new list on place at this point. */
|
||||
}
|
||||
}
|
||||
|
||||
/* This is how the current blocking lists/streams work, we use BLPOP as
|
||||
* example, but the concept is the same for other list ops and XREAD.
|
||||
* - If the user calls BLPOP and the key exists and contains a non empty list
|
||||
* then LPOP is called instead. So BLPOP is semantically the same as LPOP
|
||||
* if blocking is not required.
|
||||
* - If instead BLPOP is called and the key does not exists or the list is
|
||||
* empty we need to block. In order to do so we remove the notification for
|
||||
* new data to read in the client socket (so that we'll not serve new
|
||||
* requests if the blocking request is not served). Also we put the client
|
||||
* in a dictionary (db->blocking_keys) mapping keys to a list of clients
|
||||
* blocking for this keys.
|
||||
* - If a PUSH operation against a key with blocked clients waiting is
|
||||
* performed, we mark this key as "ready", and after the current command,
|
||||
* MULTI/EXEC block, or script, is executed, we serve all the clients waiting
|
||||
* for this list, from the one that blocked first, to the last, accordingly
|
||||
* to the number of elements we have in the ready list.
|
||||
*/
|
||||
|
||||
/* Set a client in blocking mode for the specified key (list or stream), with
|
||||
* the specified timeout. The 'type' argument is BLOCKED_LIST or BLOCKED_STREAM
|
||||
* depending on the kind of operation we are waiting for an empty key in
|
||||
* order to awake the client. The client is blocked for all the 'numkeys'
|
||||
* keys as in the 'keys' argument. When we block for stream keys, we also
|
||||
* provide an array of streamID structures: clients will be unblocked only
|
||||
* when items with an ID greater or equal to the specified one is appended
|
||||
* to the stream. */
|
||||
void blockForKeys(client *c, int btype, robj **keys, int numkeys, mstime_t timeout, robj *target, streamID *ids) {
|
||||
dictEntry *de;
|
||||
list *l;
|
||||
int j;
|
||||
|
||||
c->bpop.timeout = timeout;
|
||||
c->bpop.target = target;
|
||||
|
||||
if (target != NULL) incrRefCount(target);
|
||||
|
||||
for (j = 0; j < numkeys; j++) {
|
||||
/* The value associated with the key name in the bpop.keys dictionary
|
||||
* is NULL for lists, or the stream ID for streams. */
|
||||
void *key_data = NULL;
|
||||
if (btype == BLOCKED_STREAM) {
|
||||
key_data = zmalloc(sizeof(streamID));
|
||||
memcpy(key_data,ids+j,sizeof(streamID));
|
||||
}
|
||||
|
||||
/* If the key already exists in the dictionary ignore it. */
|
||||
if (dictAdd(c->bpop.keys,keys[j],key_data) != DICT_OK) {
|
||||
zfree(key_data);
|
||||
continue;
|
||||
}
|
||||
incrRefCount(keys[j]);
|
||||
|
||||
/* And in the other "side", to map keys -> clients */
|
||||
de = dictFind(c->db->blocking_keys,keys[j]);
|
||||
if (de == NULL) {
|
||||
int retval;
|
||||
|
||||
/* For every key we take a list of clients blocked for it */
|
||||
l = listCreate();
|
||||
retval = dictAdd(c->db->blocking_keys,keys[j],l);
|
||||
incrRefCount(keys[j]);
|
||||
serverAssertWithInfo(c,keys[j],retval == DICT_OK);
|
||||
} else {
|
||||
l = dictGetVal(de);
|
||||
}
|
||||
listAddNodeTail(l,c);
|
||||
}
|
||||
blockClient(c,btype);
|
||||
}
|
||||
|
||||
/* Unblock a client that's waiting in a blocking operation such as BLPOP.
|
||||
* You should never call this function directly, but unblockClient() instead. */
|
||||
void unblockClientWaitingData(client *c) {
|
||||
dictEntry *de;
|
||||
dictIterator *di;
|
||||
list *l;
|
||||
|
||||
serverAssertWithInfo(c,NULL,dictSize(c->bpop.keys) != 0);
|
||||
di = dictGetIterator(c->bpop.keys);
|
||||
/* The client may wait for multiple keys, so unblock it for every key. */
|
||||
while((de = dictNext(di)) != NULL) {
|
||||
robj *key = dictGetKey(de);
|
||||
|
||||
/* Remove this client from the list of clients waiting for this key. */
|
||||
l = dictFetchValue(c->db->blocking_keys,key);
|
||||
serverAssertWithInfo(c,key,l != NULL);
|
||||
listDelNode(l,listSearchKey(l,c));
|
||||
/* If the list is empty we need to remove it to avoid wasting memory */
|
||||
if (listLength(l) == 0)
|
||||
dictDelete(c->db->blocking_keys,key);
|
||||
}
|
||||
dictReleaseIterator(di);
|
||||
|
||||
/* Cleanup the client structure */
|
||||
dictEmpty(c->bpop.keys,NULL);
|
||||
if (c->bpop.target) {
|
||||
decrRefCount(c->bpop.target);
|
||||
c->bpop.target = NULL;
|
||||
}
|
||||
if (c->bpop.xread_group) {
|
||||
decrRefCount(c->bpop.xread_group);
|
||||
c->bpop.xread_group = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/* If the specified key has clients blocked waiting for list pushes, this
|
||||
* function will put the key reference into the server.ready_keys list.
|
||||
* Note that db->ready_keys is a hash table that allows us to avoid putting
|
||||
* the same key again and again in the list in case of multiple pushes
|
||||
* made by a script or in the context of MULTI/EXEC.
|
||||
*
|
||||
* The list will be finally processed by handleClientsBlockedOnLists() */
|
||||
void signalKeyAsReady(redisDb *db, robj *key) {
|
||||
readyList *rl;
|
||||
|
||||
/* No clients blocking for this key? No need to queue it. */
|
||||
if (dictFind(db->blocking_keys,key) == NULL) return;
|
||||
|
||||
/* Key was already signaled? No need to queue it again. */
|
||||
if (dictFind(db->ready_keys,key) != NULL) return;
|
||||
|
||||
/* Ok, we need to queue this key into server.ready_keys. */
|
||||
rl = zmalloc(sizeof(*rl));
|
||||
rl->key = key;
|
||||
rl->db = db;
|
||||
incrRefCount(key);
|
||||
listAddNodeTail(server.ready_keys,rl);
|
||||
|
||||
/* We also add the key in the db->ready_keys dictionary in order
|
||||
* to avoid adding it multiple times into a list with a simple O(1)
|
||||
* check. */
|
||||
incrRefCount(key);
|
||||
serverAssert(dictAdd(db->ready_keys,key,NULL) == DICT_OK);
|
||||
}
|
||||
|
||||
|
||||
|
||||
+248
-72
@@ -37,7 +37,6 @@
|
||||
#include <arpa/inet.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/file.h>
|
||||
#include <math.h>
|
||||
@@ -129,7 +128,7 @@ int clusterLoadConfig(char *filename) {
|
||||
/* Skip blank lines, they can be created either by users manually
|
||||
* editing nodes.conf or by the config writing process if stopped
|
||||
* before the truncate() call. */
|
||||
if (line[0] == '\n') continue;
|
||||
if (line[0] == '\n' || line[0] == '\0') continue;
|
||||
|
||||
/* Split the line into arguments for processing. */
|
||||
argv = sdssplitargs(line,&argc);
|
||||
@@ -244,6 +243,7 @@ int clusterLoadConfig(char *filename) {
|
||||
*p = '\0';
|
||||
direction = p[1]; /* Either '>' or '<' */
|
||||
slot = atoi(argv[j]+1);
|
||||
if (slot < 0 || slot >= CLUSTER_SLOTS) goto fmterr;
|
||||
p += 3;
|
||||
cn = clusterLookupNode(p);
|
||||
if (!cn) {
|
||||
@@ -263,6 +263,8 @@ int clusterLoadConfig(char *filename) {
|
||||
} else {
|
||||
start = stop = atoi(argv[j]);
|
||||
}
|
||||
if (start < 0 || start >= CLUSTER_SLOTS) goto fmterr;
|
||||
if (stop < 0 || stop >= CLUSTER_SLOTS) goto fmterr;
|
||||
while(start <= stop) clusterAddSlot(n, start++);
|
||||
}
|
||||
|
||||
@@ -423,8 +425,11 @@ void clusterInit(void) {
|
||||
server.cluster->failover_auth_epoch = 0;
|
||||
server.cluster->cant_failover_reason = CLUSTER_CANT_FAILOVER_NONE;
|
||||
server.cluster->lastVoteEpoch = 0;
|
||||
server.cluster->stats_bus_messages_sent = 0;
|
||||
server.cluster->stats_bus_messages_received = 0;
|
||||
for (int i = 0; i < CLUSTERMSG_TYPE_COUNT; i++) {
|
||||
server.cluster->stats_bus_messages_sent[i] = 0;
|
||||
server.cluster->stats_bus_messages_received[i] = 0;
|
||||
}
|
||||
server.cluster->stats_pfail_nodes = 0;
|
||||
memset(server.cluster->slots,0, sizeof(server.cluster->slots));
|
||||
clusterCloseAllSlots();
|
||||
|
||||
@@ -476,8 +481,10 @@ void clusterInit(void) {
|
||||
}
|
||||
}
|
||||
|
||||
/* The slots -> keys map is a sorted set. Init it. */
|
||||
server.cluster->slots_to_keys = zslCreate();
|
||||
/* The slots -> keys map is a radix tree. Initialize it here. */
|
||||
server.cluster->slots_to_keys = raxNew();
|
||||
memset(server.cluster->slots_keys_count,0,
|
||||
sizeof(server.cluster->slots_keys_count));
|
||||
|
||||
/* Set myself->port / cport to my listening ports, we'll just need to
|
||||
* discover the IP address via MEET messages. */
|
||||
@@ -646,7 +653,7 @@ unsigned int keyHashSlot(char *key, int keylen) {
|
||||
for (e = s+1; e < keylen; e++)
|
||||
if (key[e] == '}') break;
|
||||
|
||||
/* No '}' or nothing betweeen {} ? Hash the whole key. */
|
||||
/* No '}' or nothing between {} ? Hash the whole key. */
|
||||
if (e == keylen || e == s+1) return crc16(key,keylen) & 0x3FFF;
|
||||
|
||||
/* If we are here there is both a { and a } on its right. Hash
|
||||
@@ -1319,14 +1326,16 @@ void clusterProcessGossipSection(clusterMsg *hdr, clusterLink *link) {
|
||||
clusterNode *node;
|
||||
sds ci;
|
||||
|
||||
ci = representClusterNodeFlags(sdsempty(), flags);
|
||||
serverLog(LL_DEBUG,"GOSSIP %.40s %s:%d@%d %s",
|
||||
g->nodename,
|
||||
g->ip,
|
||||
ntohs(g->port),
|
||||
ntohs(g->cport),
|
||||
ci);
|
||||
sdsfree(ci);
|
||||
if (server.verbosity == LL_DEBUG) {
|
||||
ci = representClusterNodeFlags(sdsempty(), flags);
|
||||
serverLog(LL_DEBUG,"GOSSIP %.40s %s:%d@%d %s",
|
||||
g->nodename,
|
||||
g->ip,
|
||||
ntohs(g->port),
|
||||
ntohs(g->cport),
|
||||
ci);
|
||||
sdsfree(ci);
|
||||
}
|
||||
|
||||
/* Update our state accordingly to the gossip sections */
|
||||
node = clusterLookupNode(g->nodename);
|
||||
@@ -1350,6 +1359,28 @@ void clusterProcessGossipSection(clusterMsg *hdr, clusterLink *link) {
|
||||
}
|
||||
}
|
||||
|
||||
/* If from our POV the node is up (no failure flags are set),
|
||||
* we have no pending ping for the node, nor we have failure
|
||||
* reports for this node, update the last pong time with the
|
||||
* one we see from the other nodes. */
|
||||
if (!(flags & (CLUSTER_NODE_FAIL|CLUSTER_NODE_PFAIL)) &&
|
||||
node->ping_sent == 0 &&
|
||||
clusterNodeFailureReportsCount(node) == 0)
|
||||
{
|
||||
mstime_t pongtime = ntohl(g->pong_received);
|
||||
pongtime *= 1000; /* Convert back to milliseconds. */
|
||||
|
||||
/* Replace the pong time with the received one only if
|
||||
* it's greater than our view but is not in the future
|
||||
* (with 500 milliseconds tolerance) from the POV of our
|
||||
* clock. */
|
||||
if (pongtime <= (server.mstime+500) &&
|
||||
pongtime > node->pong_received)
|
||||
{
|
||||
node->pong_received = pongtime;
|
||||
}
|
||||
}
|
||||
|
||||
/* If we already know this node, but it is not reachable, and
|
||||
* we see a different address in the gossip section of a node that
|
||||
* can talk with this other node, update the address, disconnect
|
||||
@@ -1581,7 +1612,8 @@ int clusterProcessPacket(clusterLink *link) {
|
||||
uint32_t totlen = ntohl(hdr->totlen);
|
||||
uint16_t type = ntohs(hdr->type);
|
||||
|
||||
server.cluster->stats_bus_messages_received++;
|
||||
if (type < CLUSTERMSG_TYPE_COUNT)
|
||||
server.cluster->stats_bus_messages_received[type]++;
|
||||
serverLog(LL_DEBUG,"--- Processing packet of type %d, %lu bytes",
|
||||
type, (unsigned long) totlen);
|
||||
|
||||
@@ -2128,7 +2160,12 @@ void clusterSendMessage(clusterLink *link, unsigned char *msg, size_t msglen) {
|
||||
clusterWriteHandler,link);
|
||||
|
||||
link->sndbuf = sdscatlen(link->sndbuf, msg, msglen);
|
||||
server.cluster->stats_bus_messages_sent++;
|
||||
|
||||
/* Populate sent messages stats. */
|
||||
clusterMsg *hdr = (clusterMsg*) msg;
|
||||
uint16_t type = ntohs(hdr->type);
|
||||
if (type < CLUSTERMSG_TYPE_COUNT)
|
||||
server.cluster->stats_bus_messages_sent[type]++;
|
||||
}
|
||||
|
||||
/* Send a message to all the nodes that are part of the cluster having
|
||||
@@ -2229,6 +2266,33 @@ void clusterBuildMessageHdr(clusterMsg *hdr, int type) {
|
||||
/* For PING, PONG, and MEET, fixing the totlen field is up to the caller. */
|
||||
}
|
||||
|
||||
/* Return non zero if the node is already present in the gossip section of the
|
||||
* message pointed by 'hdr' and having 'count' gossip entries. Otherwise
|
||||
* zero is returned. Helper for clusterSendPing(). */
|
||||
int clusterNodeIsInGossipSection(clusterMsg *hdr, int count, clusterNode *n) {
|
||||
int j;
|
||||
for (j = 0; j < count; j++) {
|
||||
if (memcmp(hdr->data.ping.gossip[j].nodename,n->name,
|
||||
CLUSTER_NAMELEN) == 0) break;
|
||||
}
|
||||
return j != count;
|
||||
}
|
||||
|
||||
/* Set the i-th entry of the gossip section in the message pointed by 'hdr'
|
||||
* to the info of the specified node 'n'. */
|
||||
void clusterSetGossipEntry(clusterMsg *hdr, int i, clusterNode *n) {
|
||||
clusterMsgDataGossip *gossip;
|
||||
gossip = &(hdr->data.ping.gossip[i]);
|
||||
memcpy(gossip->nodename,n->name,CLUSTER_NAMELEN);
|
||||
gossip->ping_sent = htonl(n->ping_sent/1000);
|
||||
gossip->pong_received = htonl(n->pong_received/1000);
|
||||
memcpy(gossip->ip,n->ip,sizeof(n->ip));
|
||||
gossip->port = htons(n->port);
|
||||
gossip->cport = htons(n->cport);
|
||||
gossip->flags = htons(n->flags);
|
||||
gossip->notused1 = 0;
|
||||
}
|
||||
|
||||
/* Send a PING or PONG packet to the specified node, making sure to add enough
|
||||
* gossip informations. */
|
||||
void clusterSendPing(clusterLink *link, int type) {
|
||||
@@ -2273,11 +2337,15 @@ void clusterSendPing(clusterLink *link, int type) {
|
||||
if (wanted < 3) wanted = 3;
|
||||
if (wanted > freshnodes) wanted = freshnodes;
|
||||
|
||||
/* Include all the nodes in PFAIL state, so that failure reports are
|
||||
* faster to propagate to go from PFAIL to FAIL state. */
|
||||
int pfail_wanted = server.cluster->stats_pfail_nodes;
|
||||
|
||||
/* Compute the maxium totlen to allocate our buffer. We'll fix the totlen
|
||||
* later according to the number of gossip sections we really were able
|
||||
* to put inside the packet. */
|
||||
totlen = sizeof(clusterMsg)-sizeof(union clusterMsgData);
|
||||
totlen += (sizeof(clusterMsgDataGossip)*wanted);
|
||||
totlen += (sizeof(clusterMsgDataGossip)*(wanted+pfail_wanted));
|
||||
/* Note: clusterBuildMessageHdr() expects the buffer to be always at least
|
||||
* sizeof(clusterMsg) or more. */
|
||||
if (totlen < (int)sizeof(clusterMsg)) totlen = sizeof(clusterMsg);
|
||||
@@ -2294,17 +2362,13 @@ void clusterSendPing(clusterLink *link, int type) {
|
||||
while(freshnodes > 0 && gossipcount < wanted && maxiterations--) {
|
||||
dictEntry *de = dictGetRandomKey(server.cluster->nodes);
|
||||
clusterNode *this = dictGetVal(de);
|
||||
clusterMsgDataGossip *gossip;
|
||||
int j;
|
||||
|
||||
/* Don't include this node: the whole packet header is about us
|
||||
* already, so we just gossip about other nodes. */
|
||||
if (this == myself) continue;
|
||||
|
||||
/* Give a bias to FAIL/PFAIL nodes. */
|
||||
if (maxiterations > wanted*2 &&
|
||||
!(this->flags & (CLUSTER_NODE_PFAIL|CLUSTER_NODE_FAIL)))
|
||||
continue;
|
||||
/* PFAIL nodes will be added later. */
|
||||
if (this->flags & CLUSTER_NODE_PFAIL) continue;
|
||||
|
||||
/* In the gossip section don't include:
|
||||
* 1) Nodes in HANDSHAKE state.
|
||||
@@ -2318,27 +2382,37 @@ void clusterSendPing(clusterLink *link, int type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Check if we already added this node */
|
||||
for (j = 0; j < gossipcount; j++) {
|
||||
if (memcmp(hdr->data.ping.gossip[j].nodename,this->name,
|
||||
CLUSTER_NAMELEN) == 0) break;
|
||||
}
|
||||
if (j != gossipcount) continue;
|
||||
/* Do not add a node we already have. */
|
||||
if (clusterNodeIsInGossipSection(hdr,gossipcount,this)) continue;
|
||||
|
||||
/* Add it */
|
||||
clusterSetGossipEntry(hdr,gossipcount,this);
|
||||
freshnodes--;
|
||||
gossip = &(hdr->data.ping.gossip[gossipcount]);
|
||||
memcpy(gossip->nodename,this->name,CLUSTER_NAMELEN);
|
||||
gossip->ping_sent = htonl(this->ping_sent);
|
||||
gossip->pong_received = htonl(this->pong_received);
|
||||
memcpy(gossip->ip,this->ip,sizeof(this->ip));
|
||||
gossip->port = htons(this->port);
|
||||
gossip->cport = htons(this->cport);
|
||||
gossip->flags = htons(this->flags);
|
||||
gossip->notused1 = 0;
|
||||
gossipcount++;
|
||||
}
|
||||
|
||||
/* If there are PFAIL nodes, add them at the end. */
|
||||
if (pfail_wanted) {
|
||||
dictIterator *di;
|
||||
dictEntry *de;
|
||||
|
||||
di = dictGetSafeIterator(server.cluster->nodes);
|
||||
while((de = dictNext(di)) != NULL && pfail_wanted > 0) {
|
||||
clusterNode *node = dictGetVal(de);
|
||||
if (node->flags & CLUSTER_NODE_HANDSHAKE) continue;
|
||||
if (node->flags & CLUSTER_NODE_NOADDR) continue;
|
||||
if (!(node->flags & CLUSTER_NODE_PFAIL)) continue;
|
||||
clusterSetGossipEntry(hdr,gossipcount,node);
|
||||
freshnodes--;
|
||||
gossipcount++;
|
||||
/* We take the count of the slots we allocated, since the
|
||||
* PFAIL stats may not match perfectly with the current number
|
||||
* of PFAIL nodes. */
|
||||
pfail_wanted--;
|
||||
}
|
||||
dictReleaseIterator(di);
|
||||
}
|
||||
|
||||
/* Ready to send... fix the totlen fiend and queue the message in the
|
||||
* output buffer. */
|
||||
totlen = sizeof(clusterMsg)-sizeof(union clusterMsgData);
|
||||
@@ -2774,7 +2848,7 @@ void clusterHandleSlaveFailover(void) {
|
||||
* and wait for replies), and the failover retry time (the time to wait
|
||||
* before trying to get voted again).
|
||||
*
|
||||
* Timeout is MIN(NODE_TIMEOUT*2,2000) milliseconds.
|
||||
* Timeout is MAX(NODE_TIMEOUT*2,2000) milliseconds.
|
||||
* Retry is two times the Timeout.
|
||||
*/
|
||||
auth_timeout = server.cluster_node_timeout*2;
|
||||
@@ -3164,13 +3238,21 @@ void clusterCron(void) {
|
||||
handshake_timeout = server.cluster_node_timeout;
|
||||
if (handshake_timeout < 1000) handshake_timeout = 1000;
|
||||
|
||||
/* Check if we have disconnected nodes and re-establish the connection. */
|
||||
/* Check if we have disconnected nodes and re-establish the connection.
|
||||
* Also update a few stats while we are here, that can be used to make
|
||||
* better decisions in other part of the code. */
|
||||
di = dictGetSafeIterator(server.cluster->nodes);
|
||||
server.cluster->stats_pfail_nodes = 0;
|
||||
while((de = dictNext(di)) != NULL) {
|
||||
clusterNode *node = dictGetVal(de);
|
||||
|
||||
/* Not interested in reconnecting the link with myself or nodes
|
||||
* for which we have no address. */
|
||||
if (node->flags & (CLUSTER_NODE_MYSELF|CLUSTER_NODE_NOADDR)) continue;
|
||||
|
||||
if (node->flags & CLUSTER_NODE_PFAIL)
|
||||
server.cluster->stats_pfail_nodes++;
|
||||
|
||||
/* A Node in HANDSHAKE state has a limited lifespan equal to the
|
||||
* configured node timeout. */
|
||||
if (nodeInHandshake(node) && now - node->ctime > handshake_timeout) {
|
||||
@@ -3523,8 +3605,10 @@ int clusterDelNodeSlots(clusterNode *node) {
|
||||
int deleted = 0, j;
|
||||
|
||||
for (j = 0; j < CLUSTER_SLOTS; j++) {
|
||||
if (clusterNodeGetSlotBit(node,j)) clusterDelSlot(j);
|
||||
deleted++;
|
||||
if (clusterNodeGetSlotBit(node,j)) {
|
||||
clusterDelSlot(j);
|
||||
deleted++;
|
||||
}
|
||||
}
|
||||
return deleted;
|
||||
}
|
||||
@@ -3758,15 +3842,14 @@ static struct redisNodeFlags redisNodeFlagsTable[] = {
|
||||
/* Concatenate the comma separated list of node flags to the given SDS
|
||||
* string 'ci'. */
|
||||
sds representClusterNodeFlags(sds ci, uint16_t flags) {
|
||||
if (flags == 0) {
|
||||
ci = sdscat(ci,"noflags,");
|
||||
} else {
|
||||
int i, size = sizeof(redisNodeFlagsTable)/sizeof(struct redisNodeFlags);
|
||||
for (i = 0; i < size; i++) {
|
||||
struct redisNodeFlags *nodeflag = redisNodeFlagsTable + i;
|
||||
if (flags & nodeflag->flag) ci = sdscat(ci, nodeflag->name);
|
||||
}
|
||||
size_t orig_len = sdslen(ci);
|
||||
int i, size = sizeof(redisNodeFlagsTable)/sizeof(struct redisNodeFlags);
|
||||
for (i = 0; i < size; i++) {
|
||||
struct redisNodeFlags *nodeflag = redisNodeFlagsTable + i;
|
||||
if (flags & nodeflag->flag) ci = sdscat(ci, nodeflag->name);
|
||||
}
|
||||
/* If no flag was added, add the "noflags" special flag. */
|
||||
if (sdslen(ci) == orig_len) ci = sdscat(ci,"noflags,");
|
||||
sdsIncrLen(ci,-1); /* Remove trailing comma. */
|
||||
return ci;
|
||||
}
|
||||
@@ -3875,6 +3958,21 @@ sds clusterGenNodesDescription(int filter) {
|
||||
* CLUSTER command
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
const char *clusterGetMessageTypeString(int type) {
|
||||
switch(type) {
|
||||
case CLUSTERMSG_TYPE_PING: return "ping";
|
||||
case CLUSTERMSG_TYPE_PONG: return "pong";
|
||||
case CLUSTERMSG_TYPE_MEET: return "meet";
|
||||
case CLUSTERMSG_TYPE_FAIL: return "fail";
|
||||
case CLUSTERMSG_TYPE_PUBLISH: return "publish";
|
||||
case CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST: return "auth-req";
|
||||
case CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK: return "auth-ack";
|
||||
case CLUSTERMSG_TYPE_UPDATE: return "update";
|
||||
case CLUSTERMSG_TYPE_MFSTART: return "mfstart";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
int getSlotOrReply(client *c, robj *o) {
|
||||
long long slot;
|
||||
|
||||
@@ -3967,7 +4065,34 @@ void clusterCommand(client *c) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!strcasecmp(c->argv[1]->ptr,"meet") && (c->argc == 4 || c->argc == 5)) {
|
||||
if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"help")) {
|
||||
const char *help[] = {
|
||||
"addslots <slot> [slot ...] -- Assign slots to current node.",
|
||||
"bumpepoch -- Advance the cluster config epoch.",
|
||||
"count-failure-reports <node-id> -- Return number of failure reports for <node-id>.",
|
||||
"countkeysinslot <slot> - Return the number of keys in <slot>.",
|
||||
"delslots <slot> [slot ...] -- Delete slots information from current node.",
|
||||
"failover [force|takeover] -- Promote current slave node to being a master.",
|
||||
"forget <node-id> -- Remove a node from the cluster.",
|
||||
"getkeysinslot <slot> <count> -- Return key names stored by current node in a slot.",
|
||||
"flushslots -- Delete current node own slots information.",
|
||||
"info - Return onformation about the cluster.",
|
||||
"keyslot <key> -- Return the hash slot for <key>.",
|
||||
"meet <ip> <port> [bus-port] -- Connect nodes into a working cluster.",
|
||||
"myid -- Return the node id.",
|
||||
"nodes -- Return cluster configuration seen by node. Output format:",
|
||||
" <id> <ip:port> <flags> <master> <pings> <pongs> <epoch> <link> <slot> ... <slot>",
|
||||
"replicate <node-id> -- Configure current node as slave to <node-id>.",
|
||||
"reset [hard|soft] -- Reset current node (default: soft).",
|
||||
"set-config-epoch <epoch> - Set config epoch of current node.",
|
||||
"setslot <slot> (importing|migrating|stable|node <node-id>) -- Set slot state.",
|
||||
"slaves <node-id> -- Return <node-id> slaves.",
|
||||
"slots -- Return information about slots range mappings. Each range is made of:",
|
||||
" start, end, master and replicas IP addresses, ports and ids",
|
||||
NULL
|
||||
};
|
||||
addReplyHelp(c, help);
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"meet") && (c->argc == 4 || c->argc == 5)) {
|
||||
/* CLUSTER MEET <ip> <port> [cport] */
|
||||
long long port, cport;
|
||||
|
||||
@@ -4102,7 +4227,7 @@ void clusterCommand(client *c) {
|
||||
}
|
||||
if ((n = clusterLookupNode(c->argv[4]->ptr)) == NULL) {
|
||||
addReplyErrorFormat(c,"I don't know about node %s",
|
||||
(char*)c->argv[3]->ptr);
|
||||
(char*)c->argv[4]->ptr);
|
||||
return;
|
||||
}
|
||||
server.cluster->importing_slots_from[slot] = n;
|
||||
@@ -4160,7 +4285,7 @@ void clusterCommand(client *c) {
|
||||
clusterAddSlot(n,slot);
|
||||
} else {
|
||||
addReplyError(c,
|
||||
"Invalid CLUSTER SETSLOT action or number of arguments");
|
||||
"Invalid CLUSTER SETSLOT action or number of arguments. Try CLUSTER HELP");
|
||||
return;
|
||||
}
|
||||
clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG|CLUSTER_TODO_UPDATE_STATE);
|
||||
@@ -4206,8 +4331,6 @@ void clusterCommand(client *c) {
|
||||
"cluster_size:%d\r\n"
|
||||
"cluster_current_epoch:%llu\r\n"
|
||||
"cluster_my_epoch:%llu\r\n"
|
||||
"cluster_stats_messages_sent:%lld\r\n"
|
||||
"cluster_stats_messages_received:%lld\r\n"
|
||||
, statestr[server.cluster->state],
|
||||
slots_assigned,
|
||||
slots_ok,
|
||||
@@ -4216,10 +4339,36 @@ void clusterCommand(client *c) {
|
||||
dictSize(server.cluster->nodes),
|
||||
server.cluster->size,
|
||||
(unsigned long long) server.cluster->currentEpoch,
|
||||
(unsigned long long) myepoch,
|
||||
server.cluster->stats_bus_messages_sent,
|
||||
server.cluster->stats_bus_messages_received
|
||||
(unsigned long long) myepoch
|
||||
);
|
||||
|
||||
/* Show stats about messages sent and received. */
|
||||
long long tot_msg_sent = 0;
|
||||
long long tot_msg_received = 0;
|
||||
|
||||
for (int i = 0; i < CLUSTERMSG_TYPE_COUNT; i++) {
|
||||
if (server.cluster->stats_bus_messages_sent[i] == 0) continue;
|
||||
tot_msg_sent += server.cluster->stats_bus_messages_sent[i];
|
||||
info = sdscatprintf(info,
|
||||
"cluster_stats_messages_%s_sent:%lld\r\n",
|
||||
clusterGetMessageTypeString(i),
|
||||
server.cluster->stats_bus_messages_sent[i]);
|
||||
}
|
||||
info = sdscatprintf(info,
|
||||
"cluster_stats_messages_sent:%lld\r\n", tot_msg_sent);
|
||||
|
||||
for (int i = 0; i < CLUSTERMSG_TYPE_COUNT; i++) {
|
||||
if (server.cluster->stats_bus_messages_received[i] == 0) continue;
|
||||
tot_msg_received += server.cluster->stats_bus_messages_received[i];
|
||||
info = sdscatprintf(info,
|
||||
"cluster_stats_messages_%s_received:%lld\r\n",
|
||||
clusterGetMessageTypeString(i),
|
||||
server.cluster->stats_bus_messages_received[i]);
|
||||
}
|
||||
info = sdscatprintf(info,
|
||||
"cluster_stats_messages_received:%lld\r\n", tot_msg_received);
|
||||
|
||||
/* Produce the reply protocol. */
|
||||
addReplySds(c,sdscatprintf(sdsempty(),"$%lu\r\n",
|
||||
(unsigned long)sdslen(info)));
|
||||
addReplySds(c,info);
|
||||
@@ -4264,6 +4413,11 @@ void clusterCommand(client *c) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Avoid allocating more than needed in case of large COUNT argument
|
||||
* and smaller actual number of keys. */
|
||||
unsigned int keys_in_slot = countKeysInSlot(slot);
|
||||
if (maxkeys > keys_in_slot) maxkeys = keys_in_slot;
|
||||
|
||||
keys = zmalloc(sizeof(robj*)*maxkeys);
|
||||
numkeys = getKeysInSlot(slot, keys, maxkeys);
|
||||
addReplyMultiBulkLen(c,numkeys);
|
||||
@@ -4481,7 +4635,9 @@ void clusterCommand(client *c) {
|
||||
clusterReset(hard);
|
||||
addReply(c,shared.ok);
|
||||
} else {
|
||||
addReplyError(c,"Wrong CLUSTER subcommand or number of arguments");
|
||||
addReplyErrorFormat(c, "Unknown subcommand or wrong number of arguments for '%s'. Try CLUSTER HELP",
|
||||
(char*)c->argv[1]->ptr);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4617,7 +4773,7 @@ void restoreCommand(client *c) {
|
||||
|
||||
/* Create the key and set the TTL if any */
|
||||
dbAdd(c->db,c->argv[1],obj);
|
||||
if (ttl) setExpire(c->db,c->argv[1],mstime()+ttl);
|
||||
if (ttl) setExpire(c,c->db,c->argv[1],mstime()+ttl);
|
||||
signalModifiedKey(c->db,c->argv[1]);
|
||||
addReply(c,shared.ok);
|
||||
server.dirty++;
|
||||
@@ -4750,13 +4906,13 @@ void migrateCommand(client *c) {
|
||||
int copy, replace, j;
|
||||
long timeout;
|
||||
long dbid;
|
||||
long long ttl, expireat;
|
||||
robj **ov = NULL; /* Objects to migrate. */
|
||||
robj **kv = NULL; /* Key names. */
|
||||
robj **newargv = NULL; /* Used to rewrite the command as DEL ... keys ... */
|
||||
rio cmd, payload;
|
||||
int may_retry = 1;
|
||||
int write_error = 0;
|
||||
int argv_rewritten = 0;
|
||||
|
||||
/* To support the KEYS option we need the following additional state. */
|
||||
int first_key = 3; /* Argument index of the first key. */
|
||||
@@ -4765,7 +4921,6 @@ void migrateCommand(client *c) {
|
||||
/* Initialization */
|
||||
copy = 0;
|
||||
replace = 0;
|
||||
ttl = 0;
|
||||
|
||||
/* Parse additional options */
|
||||
for (j = 6; j < c->argc; j++) {
|
||||
@@ -4841,7 +4996,9 @@ try_again:
|
||||
|
||||
/* Create RESTORE payload and generate the protocol to call the command. */
|
||||
for (j = 0; j < num_keys; j++) {
|
||||
expireat = getExpire(c->db,kv[j]);
|
||||
long long ttl = 0;
|
||||
long long expireat = getExpire(c->db,kv[j]);
|
||||
|
||||
if (expireat != -1) {
|
||||
ttl = expireat-mstime();
|
||||
if (ttl < 1) ttl = 1;
|
||||
@@ -4939,12 +5096,20 @@ try_again:
|
||||
goto socket_err; /* A retry is guaranteed because of tested conditions.*/
|
||||
}
|
||||
|
||||
/* On socket errors, close the migration socket now that we still have
|
||||
* the original host/port in the ARGV. Later the original command may be
|
||||
* rewritten to DEL and will be too later. */
|
||||
if (socket_error) migrateCloseSocket(c->argv[1],c->argv[2]);
|
||||
|
||||
if (!copy) {
|
||||
/* Translate MIGRATE as DEL for replication/AOF. */
|
||||
/* Translate MIGRATE as DEL for replication/AOF. Note that we do
|
||||
* this only for the keys for which we received an acknowledgement
|
||||
* from the receiving Redis server, by using the del_idx index. */
|
||||
if (del_idx > 1) {
|
||||
newargv[0] = createStringObject("DEL",3);
|
||||
/* Note that the following call takes ownership of newargv. */
|
||||
replaceClientCommandVector(c,del_idx,newargv);
|
||||
argv_rewritten = 1;
|
||||
} else {
|
||||
/* No key transfer acknowledged, no need to rewrite as DEL. */
|
||||
zfree(newargv);
|
||||
@@ -4953,8 +5118,8 @@ try_again:
|
||||
}
|
||||
|
||||
/* If we are here and a socket error happened, we don't want to retry.
|
||||
* Just signal the problem to the client, but only do it if we don't
|
||||
* already queued a different error reported by the destination server. */
|
||||
* Just signal the problem to the client, but only do it if we did not
|
||||
* already queue a different error reported by the destination server. */
|
||||
if (!error_from_target && socket_error) {
|
||||
may_retry = 0;
|
||||
goto socket_err;
|
||||
@@ -4962,7 +5127,11 @@ try_again:
|
||||
|
||||
if (!error_from_target) {
|
||||
/* Success! Update the last_dbid in migrateCachedSocket, so that we can
|
||||
* avoid SELECT the next time if the target DB is the same. Reply +OK. */
|
||||
* avoid SELECT the next time if the target DB is the same. Reply +OK.
|
||||
*
|
||||
* Note: If we reached this point, even if socket_error is true
|
||||
* still the SELECT command succeeded (otherwise the code jumps to
|
||||
* socket_err label. */
|
||||
cs->last_dbid = dbid;
|
||||
addReply(c,shared.ok);
|
||||
} else {
|
||||
@@ -4972,7 +5141,6 @@ try_again:
|
||||
|
||||
sdsfree(cmd.io.buffer.ptr);
|
||||
zfree(ov); zfree(kv); zfree(newargv);
|
||||
if (socket_error) migrateCloseSocket(c->argv[1],c->argv[2]);
|
||||
return;
|
||||
|
||||
/* On socket errors we try to close the cached socket and try again.
|
||||
@@ -4982,7 +5150,12 @@ socket_err:
|
||||
/* Cleanup we want to perform in both the retry and no retry case.
|
||||
* Note: Closing the migrate socket will also force SELECT next time. */
|
||||
sdsfree(cmd.io.buffer.ptr);
|
||||
migrateCloseSocket(c->argv[1],c->argv[2]);
|
||||
|
||||
/* If the command was rewritten as DEL and there was a socket error,
|
||||
* we already closed the socket earlier. While migrateCloseSocket()
|
||||
* is idempotent, the host/port arguments are now gone, so don't do it
|
||||
* again. */
|
||||
if (!argv_rewritten) migrateCloseSocket(c->argv[1],c->argv[2]);
|
||||
zfree(newargv);
|
||||
newargv = NULL; /* This will get reallocated on retry. */
|
||||
|
||||
@@ -5219,7 +5392,8 @@ clusterNode *getNodeByQuery(client *c, struct redisCommand *cmd, robj **argv, in
|
||||
* node is a slave and the request is about an hash slot our master
|
||||
* is serving, we can reply without redirection. */
|
||||
if (c->flags & CLIENT_READONLY &&
|
||||
cmd->flags & CMD_READONLY &&
|
||||
(cmd->flags & CMD_READONLY || cmd->proc == evalCommand ||
|
||||
cmd->proc == evalShaCommand) &&
|
||||
nodeIsSlave(myself) &&
|
||||
myself->slaveof == n)
|
||||
{
|
||||
@@ -5285,8 +5459,9 @@ int clusterRedirectBlockedClientIfNeeded(client *c) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* All keys must belong to the same slot, so check first key only. */
|
||||
di = dictGetIterator(c->bpop.keys);
|
||||
while((de = dictNext(di)) != NULL) {
|
||||
if ((de = dictNext(di)) != NULL) {
|
||||
robj *key = dictGetKey(de);
|
||||
int slot = keyHashSlot((char*)key->ptr, sdslen(key->ptr));
|
||||
clusterNode *node = server.cluster->slots[slot];
|
||||
@@ -5304,6 +5479,7 @@ int clusterRedirectBlockedClientIfNeeded(client *c) {
|
||||
clusterRedirectClient(c,node,slot,
|
||||
CLUSTER_REDIR_MOVED);
|
||||
}
|
||||
dictReleaseIterator(di);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
+31
-24
@@ -73,6 +73,29 @@ typedef struct clusterLink {
|
||||
#define CLUSTER_CANT_FAILOVER_WAITING_VOTES 4
|
||||
#define CLUSTER_CANT_FAILOVER_RELOG_PERIOD (60*5) /* seconds. */
|
||||
|
||||
/* clusterState todo_before_sleep flags. */
|
||||
#define CLUSTER_TODO_HANDLE_FAILOVER (1<<0)
|
||||
#define CLUSTER_TODO_UPDATE_STATE (1<<1)
|
||||
#define CLUSTER_TODO_SAVE_CONFIG (1<<2)
|
||||
#define CLUSTER_TODO_FSYNC_CONFIG (1<<3)
|
||||
|
||||
/* Message types.
|
||||
*
|
||||
* Note that the PING, PONG and MEET messages are actually the same exact
|
||||
* kind of packet. PONG is the reply to ping, in the exact format as a PING,
|
||||
* while MEET is a special PING that forces the receiver to add the sender
|
||||
* as a node (if it is not already in the list). */
|
||||
#define CLUSTERMSG_TYPE_PING 0 /* Ping */
|
||||
#define CLUSTERMSG_TYPE_PONG 1 /* Pong (reply to Ping) */
|
||||
#define CLUSTERMSG_TYPE_MEET 2 /* Meet "let's join" message */
|
||||
#define CLUSTERMSG_TYPE_FAIL 3 /* Mark node xxx as failing */
|
||||
#define CLUSTERMSG_TYPE_PUBLISH 4 /* Pub/Sub Publish propagation */
|
||||
#define CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 5 /* May I failover? */
|
||||
#define CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 6 /* Yes, you have my vote */
|
||||
#define CLUSTERMSG_TYPE_UPDATE 7 /* Another node slots configuration */
|
||||
#define CLUSTERMSG_TYPE_MFSTART 8 /* Pause clients for manual failover */
|
||||
#define CLUSTERMSG_TYPE_COUNT 9 /* Total number of message types. */
|
||||
|
||||
/* This structure represent elements of node->fail_reports. */
|
||||
typedef struct clusterNodeFailReport {
|
||||
struct clusterNode *node; /* Node reporting the failure condition. */
|
||||
@@ -116,7 +139,8 @@ typedef struct clusterState {
|
||||
clusterNode *migrating_slots_to[CLUSTER_SLOTS];
|
||||
clusterNode *importing_slots_from[CLUSTER_SLOTS];
|
||||
clusterNode *slots[CLUSTER_SLOTS];
|
||||
zskiplist *slots_to_keys;
|
||||
uint64_t slots_keys_count[CLUSTER_SLOTS];
|
||||
rax *slots_to_keys;
|
||||
/* The following fields are used to take the slave state on elections. */
|
||||
mstime_t failover_auth_time; /* Time of previous or next election. */
|
||||
int failover_auth_count; /* Number of votes received so far. */
|
||||
@@ -138,32 +162,15 @@ typedef struct clusterState {
|
||||
/* The followign fields are used by masters to take state on elections. */
|
||||
uint64_t lastVoteEpoch; /* Epoch of the last vote granted. */
|
||||
int todo_before_sleep; /* Things to do in clusterBeforeSleep(). */
|
||||
long long stats_bus_messages_sent; /* Num of msg sent via cluster bus. */
|
||||
long long stats_bus_messages_received; /* Num of msg rcvd via cluster bus.*/
|
||||
/* Messages received and sent by type. */
|
||||
long long stats_bus_messages_sent[CLUSTERMSG_TYPE_COUNT];
|
||||
long long stats_bus_messages_received[CLUSTERMSG_TYPE_COUNT];
|
||||
long long stats_pfail_nodes; /* Number of nodes in PFAIL status,
|
||||
excluding nodes without address. */
|
||||
} clusterState;
|
||||
|
||||
/* clusterState todo_before_sleep flags. */
|
||||
#define CLUSTER_TODO_HANDLE_FAILOVER (1<<0)
|
||||
#define CLUSTER_TODO_UPDATE_STATE (1<<1)
|
||||
#define CLUSTER_TODO_SAVE_CONFIG (1<<2)
|
||||
#define CLUSTER_TODO_FSYNC_CONFIG (1<<3)
|
||||
|
||||
/* Redis cluster messages header */
|
||||
|
||||
/* Note that the PING, PONG and MEET messages are actually the same exact
|
||||
* kind of packet. PONG is the reply to ping, in the exact format as a PING,
|
||||
* while MEET is a special PING that forces the receiver to add the sender
|
||||
* as a node (if it is not already in the list). */
|
||||
#define CLUSTERMSG_TYPE_PING 0 /* Ping */
|
||||
#define CLUSTERMSG_TYPE_PONG 1 /* Pong (reply to Ping) */
|
||||
#define CLUSTERMSG_TYPE_MEET 2 /* Meet "let's join" message */
|
||||
#define CLUSTERMSG_TYPE_FAIL 3 /* Mark node xxx as failing */
|
||||
#define CLUSTERMSG_TYPE_PUBLISH 4 /* Pub/Sub Publish propagation */
|
||||
#define CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 5 /* May I failover? */
|
||||
#define CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 6 /* Yes, you have my vote */
|
||||
#define CLUSTERMSG_TYPE_UPDATE 7 /* Another node slots configuration */
|
||||
#define CLUSTERMSG_TYPE_MFSTART 8 /* Pause clients for manual failover */
|
||||
|
||||
/* Initially we don't know our "name", but we'll find it once we connect
|
||||
* to the first node, using the getsockname() function. Then we'll use this
|
||||
* address for all the next messages. */
|
||||
@@ -225,7 +232,7 @@ union clusterMsgData {
|
||||
typedef struct {
|
||||
char sig[4]; /* Siganture "RCmb" (Redis Cluster message bus). */
|
||||
uint32_t totlen; /* Total length of this message */
|
||||
uint16_t ver; /* Protocol version, currently set to 0. */
|
||||
uint16_t ver; /* Protocol version, currently set to 1. */
|
||||
uint16_t port; /* TCP base port number. */
|
||||
uint16_t type; /* Message type */
|
||||
uint16_t count; /* Only used for some kind of messages. */
|
||||
|
||||
+97
-20
@@ -283,6 +283,10 @@ void loadServerConfigFromString(char *config) {
|
||||
}
|
||||
fclose(logfp);
|
||||
}
|
||||
} else if (!strcasecmp(argv[0],"always-show-logo") && argc == 2) {
|
||||
if ((server.always_show_logo = yesnotoi(argv[1])) == -1) {
|
||||
err = "argument must be 'yes' or 'no'"; goto loaderr;
|
||||
}
|
||||
} else if (!strcasecmp(argv[0],"syslog-enabled") && argc == 2) {
|
||||
if ((server.syslog_enabled = yesnotoi(argv[1])) == -1) {
|
||||
err = "argument must be 'yes' or 'no'"; goto loaderr;
|
||||
@@ -326,13 +330,13 @@ void loadServerConfigFromString(char *config) {
|
||||
}
|
||||
} else if (!strcasecmp(argv[0],"lfu-log-factor") && argc == 2) {
|
||||
server.lfu_log_factor = atoi(argv[1]);
|
||||
if (server.maxmemory_samples < 0) {
|
||||
if (server.lfu_log_factor < 0) {
|
||||
err = "lfu-log-factor must be 0 or greater";
|
||||
goto loaderr;
|
||||
}
|
||||
} else if (!strcasecmp(argv[0],"lfu-decay-time") && argc == 2) {
|
||||
server.lfu_decay_time = atoi(argv[1]);
|
||||
if (server.maxmemory_samples < 1) {
|
||||
if (server.lfu_decay_time < 0) {
|
||||
err = "lfu-decay-time must be 0 or greater";
|
||||
goto loaderr;
|
||||
}
|
||||
@@ -419,6 +423,10 @@ void loadServerConfigFromString(char *config) {
|
||||
if ((server.repl_slave_lazy_flush = yesnotoi(argv[1])) == -1) {
|
||||
err = "argument must be 'yes' or 'no'"; goto loaderr;
|
||||
}
|
||||
} else if (!strcasecmp(argv[0],"activedefrag") && argc == 2) {
|
||||
if ((server.active_defrag_enabled = yesnotoi(argv[1])) == -1) {
|
||||
err = "argument must be 'yes' or 'no'"; goto loaderr;
|
||||
}
|
||||
} else if (!strcasecmp(argv[0],"daemonize") && argc == 2) {
|
||||
if ((server.daemonize = yesnotoi(argv[1])) == -1) {
|
||||
err = "argument must be 'yes' or 'no'"; goto loaderr;
|
||||
@@ -495,6 +503,36 @@ void loadServerConfigFromString(char *config) {
|
||||
}
|
||||
zfree(server.rdb_filename);
|
||||
server.rdb_filename = zstrdup(argv[1]);
|
||||
} else if (!strcasecmp(argv[0],"active-defrag-threshold-lower") && argc == 2) {
|
||||
server.active_defrag_threshold_lower = atoi(argv[1]);
|
||||
if (server.active_defrag_threshold_lower < 0) {
|
||||
err = "active-defrag-threshold-lower must be 0 or greater";
|
||||
goto loaderr;
|
||||
}
|
||||
} else if (!strcasecmp(argv[0],"active-defrag-threshold-upper") && argc == 2) {
|
||||
server.active_defrag_threshold_upper = atoi(argv[1]);
|
||||
if (server.active_defrag_threshold_upper < 0) {
|
||||
err = "active-defrag-threshold-upper must be 0 or greater";
|
||||
goto loaderr;
|
||||
}
|
||||
} else if (!strcasecmp(argv[0],"active-defrag-ignore-bytes") && argc == 2) {
|
||||
server.active_defrag_ignore_bytes = memtoll(argv[1], NULL);
|
||||
if (server.active_defrag_ignore_bytes <= 0) {
|
||||
err = "active-defrag-ignore-bytes must above 0";
|
||||
goto loaderr;
|
||||
}
|
||||
} else if (!strcasecmp(argv[0],"active-defrag-cycle-min") && argc == 2) {
|
||||
server.active_defrag_cycle_min = atoi(argv[1]);
|
||||
if (server.active_defrag_cycle_min < 1 || server.active_defrag_cycle_min > 99) {
|
||||
err = "active-defrag-cycle-min must be between 1 and 99";
|
||||
goto loaderr;
|
||||
}
|
||||
} else if (!strcasecmp(argv[0],"active-defrag-cycle-max") && argc == 2) {
|
||||
server.active_defrag_cycle_max = atoi(argv[1]);
|
||||
if (server.active_defrag_cycle_max < 1 || server.active_defrag_cycle_max > 99) {
|
||||
err = "active-defrag-cycle-max must be between 1 and 99";
|
||||
goto loaderr;
|
||||
}
|
||||
} else if (!strcasecmp(argv[0],"hash-max-ziplist-entries") && argc == 2) {
|
||||
server.hash_max_ziplist_entries = memtoll(argv[1], NULL);
|
||||
} else if (!strcasecmp(argv[0],"hash-max-ziplist-value") && argc == 2) {
|
||||
@@ -967,6 +1005,18 @@ void configSetCommand(client *c) {
|
||||
"slave-read-only",server.repl_slave_ro) {
|
||||
} config_set_bool_field(
|
||||
"activerehashing",server.activerehashing) {
|
||||
} config_set_bool_field(
|
||||
"activedefrag",server.active_defrag_enabled) {
|
||||
#ifndef HAVE_DEFRAG
|
||||
if (server.active_defrag_enabled) {
|
||||
server.active_defrag_enabled = 0;
|
||||
addReplyError(c,
|
||||
"Active defragmentation cannot be enabled: it requires a "
|
||||
"Redis server compiled with a modified Jemalloc like the "
|
||||
"one shipped by default with the Redis source distribution");
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
} config_set_bool_field(
|
||||
"protected-mode",server.protected_mode) {
|
||||
} config_set_bool_field(
|
||||
@@ -995,9 +1045,17 @@ void configSetCommand(client *c) {
|
||||
} config_set_numerical_field(
|
||||
"timeout",server.maxidletime,0,LONG_MAX) {
|
||||
} config_set_numerical_field(
|
||||
"auto-aof-rewrite-percentage",server.aof_rewrite_perc,0,LLONG_MAX){
|
||||
"active-defrag-threshold-lower",server.active_defrag_threshold_lower,0,1000) {
|
||||
} config_set_numerical_field(
|
||||
"auto-aof-rewrite-min-size",server.aof_rewrite_min_size,0,LLONG_MAX) {
|
||||
"active-defrag-threshold-upper",server.active_defrag_threshold_upper,0,1000) {
|
||||
} config_set_memory_field(
|
||||
"active-defrag-ignore-bytes",server.active_defrag_ignore_bytes) {
|
||||
} config_set_numerical_field(
|
||||
"active-defrag-cycle-min",server.active_defrag_cycle_min,1,99) {
|
||||
} config_set_numerical_field(
|
||||
"active-defrag-cycle-max",server.active_defrag_cycle_max,1,99) {
|
||||
} config_set_numerical_field(
|
||||
"auto-aof-rewrite-percentage",server.aof_rewrite_perc,0,LLONG_MAX){
|
||||
} config_set_numerical_field(
|
||||
"hash-max-ziplist-entries",server.hash_max_ziplist_entries,0,LLONG_MAX) {
|
||||
} config_set_numerical_field(
|
||||
@@ -1076,6 +1134,8 @@ void configSetCommand(client *c) {
|
||||
}
|
||||
} config_set_memory_field("repl-backlog-size",ll) {
|
||||
resizeReplicationBacklog(ll);
|
||||
} config_set_memory_field("auto-aof-rewrite-min-size",ll) {
|
||||
server.aof_rewrite_min_size = ll;
|
||||
|
||||
/* Enumeration fields.
|
||||
* config_set_enum_field(name,var,enum_var) */
|
||||
@@ -1161,7 +1221,14 @@ void configGetCommand(client *c) {
|
||||
/* Numerical values */
|
||||
config_get_numerical_field("maxmemory",server.maxmemory);
|
||||
config_get_numerical_field("maxmemory-samples",server.maxmemory_samples);
|
||||
config_get_numerical_field("lfu-log-factor",server.lfu_log_factor);
|
||||
config_get_numerical_field("lfu-decay-time",server.lfu_decay_time);
|
||||
config_get_numerical_field("timeout",server.maxidletime);
|
||||
config_get_numerical_field("active-defrag-threshold-lower",server.active_defrag_threshold_lower);
|
||||
config_get_numerical_field("active-defrag-threshold-upper",server.active_defrag_threshold_upper);
|
||||
config_get_numerical_field("active-defrag-ignore-bytes",server.active_defrag_ignore_bytes);
|
||||
config_get_numerical_field("active-defrag-cycle-min",server.active_defrag_cycle_min);
|
||||
config_get_numerical_field("active-defrag-cycle-max",server.active_defrag_cycle_max);
|
||||
config_get_numerical_field("auto-aof-rewrite-percentage",
|
||||
server.aof_rewrite_perc);
|
||||
config_get_numerical_field("auto-aof-rewrite-min-size",
|
||||
@@ -1226,6 +1293,7 @@ void configGetCommand(client *c) {
|
||||
config_get_bool_field("rdbcompression", server.rdb_compression);
|
||||
config_get_bool_field("rdbchecksum", server.rdb_checksum);
|
||||
config_get_bool_field("activerehashing", server.activerehashing);
|
||||
config_get_bool_field("activedefrag", server.active_defrag_enabled);
|
||||
config_get_bool_field("protected-mode", server.protected_mode);
|
||||
config_get_bool_field("repl-disable-tcp-nodelay",
|
||||
server.repl_disable_tcp_nodelay);
|
||||
@@ -1357,7 +1425,7 @@ void configGetCommand(client *c) {
|
||||
/* We use the following dictionary type to store where a configuration
|
||||
* option is mentioned in the old configuration file, so it's
|
||||
* like "maxmemory" -> list of line numbers (first line is zero). */
|
||||
unsigned int dictSdsCaseHash(const void *key);
|
||||
uint64_t dictSdsCaseHash(const void *key);
|
||||
int dictSdsKeyCaseCompare(void *privdata, const void *key1, const void *key2);
|
||||
void dictSdsDestructor(void *privdata, void *val);
|
||||
void dictListDestructor(void *privdata, void *val);
|
||||
@@ -1926,6 +1994,13 @@ int rewriteConfig(char *path) {
|
||||
rewriteConfigBytesOption(state,"maxmemory",server.maxmemory,CONFIG_DEFAULT_MAXMEMORY);
|
||||
rewriteConfigEnumOption(state,"maxmemory-policy",server.maxmemory_policy,maxmemory_policy_enum,CONFIG_DEFAULT_MAXMEMORY_POLICY);
|
||||
rewriteConfigNumericalOption(state,"maxmemory-samples",server.maxmemory_samples,CONFIG_DEFAULT_MAXMEMORY_SAMPLES);
|
||||
rewriteConfigNumericalOption(state,"lfu-log-factor",server.lfu_log_factor,CONFIG_DEFAULT_LFU_LOG_FACTOR);
|
||||
rewriteConfigNumericalOption(state,"lfu-decay-time",server.lfu_decay_time,CONFIG_DEFAULT_LFU_DECAY_TIME);
|
||||
rewriteConfigNumericalOption(state,"active-defrag-threshold-lower",server.active_defrag_threshold_lower,CONFIG_DEFAULT_DEFRAG_THRESHOLD_LOWER);
|
||||
rewriteConfigNumericalOption(state,"active-defrag-threshold-upper",server.active_defrag_threshold_upper,CONFIG_DEFAULT_DEFRAG_THRESHOLD_UPPER);
|
||||
rewriteConfigBytesOption(state,"active-defrag-ignore-bytes",server.active_defrag_ignore_bytes,CONFIG_DEFAULT_DEFRAG_IGNORE_BYTES);
|
||||
rewriteConfigNumericalOption(state,"active-defrag-cycle-min",server.active_defrag_cycle_min,CONFIG_DEFAULT_DEFRAG_CYCLE_MIN);
|
||||
rewriteConfigNumericalOption(state,"active-defrag-cycle-max",server.active_defrag_cycle_max,CONFIG_DEFAULT_DEFRAG_CYCLE_MAX);
|
||||
rewriteConfigYesNoOption(state,"appendonly",server.aof_state != AOF_OFF,0);
|
||||
rewriteConfigStringOption(state,"appendfilename",server.aof_filename,CONFIG_DEFAULT_AOF_FILENAME);
|
||||
rewriteConfigEnumOption(state,"appendfsync",server.aof_fsync,aof_fsync_enum,CONFIG_DEFAULT_AOF_FSYNC);
|
||||
@@ -1952,6 +2027,7 @@ int rewriteConfig(char *path) {
|
||||
rewriteConfigNumericalOption(state,"zset-max-ziplist-value",server.zset_max_ziplist_value,OBJ_ZSET_MAX_ZIPLIST_VALUE);
|
||||
rewriteConfigNumericalOption(state,"hll-sparse-max-bytes",server.hll_sparse_max_bytes,CONFIG_DEFAULT_HLL_SPARSE_MAX_BYTES);
|
||||
rewriteConfigYesNoOption(state,"activerehashing",server.activerehashing,CONFIG_DEFAULT_ACTIVE_REHASHING);
|
||||
rewriteConfigYesNoOption(state,"activedefrag",server.active_defrag_enabled,CONFIG_DEFAULT_ACTIVE_DEFRAG);
|
||||
rewriteConfigYesNoOption(state,"protected-mode",server.protected_mode,CONFIG_DEFAULT_PROTECTED_MODE);
|
||||
rewriteConfigClientoutputbufferlimitOption(state);
|
||||
rewriteConfigNumericalOption(state,"hz",server.hz,CONFIG_DEFAULT_HZ);
|
||||
@@ -1993,19 +2069,24 @@ void configCommand(client *c) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!strcasecmp(c->argv[1]->ptr,"set")) {
|
||||
if (c->argc != 4) goto badarity;
|
||||
if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"help")) {
|
||||
const char *help[] = {
|
||||
"get <pattern> -- Return parameters matching the glob-like <pattern> and their values.",
|
||||
"set <parameter> <value> -- Set parameter to value.",
|
||||
"resetstat -- Reset statistics reported by INFO.",
|
||||
"rewrite -- Rewrite the configuration file.",
|
||||
NULL
|
||||
};
|
||||
addReplyHelp(c, help);
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"set") && c->argc == 4) {
|
||||
configSetCommand(c);
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"get")) {
|
||||
if (c->argc != 3) goto badarity;
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"get") && c->argc == 3) {
|
||||
configGetCommand(c);
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"resetstat")) {
|
||||
if (c->argc != 2) goto badarity;
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"resetstat") && c->argc == 2) {
|
||||
resetServerStats();
|
||||
resetCommandTableStats();
|
||||
addReply(c,shared.ok);
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"rewrite")) {
|
||||
if (c->argc != 2) goto badarity;
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"rewrite") && c->argc == 2) {
|
||||
if (server.configfile == NULL) {
|
||||
addReplyError(c,"The server is running without a config file");
|
||||
return;
|
||||
@@ -2018,12 +2099,8 @@ void configCommand(client *c) {
|
||||
addReply(c,shared.ok);
|
||||
}
|
||||
} else {
|
||||
addReplyError(c,
|
||||
"CONFIG subcommand must be one of GET, SET, RESETSTAT, REWRITE");
|
||||
addReplyErrorFormat(c, "Unknown subcommand or wrong number of arguments for '%s'. Try CONFIG HELP",
|
||||
(char*)c->argv[1]->ptr);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
|
||||
badarity:
|
||||
addReplyErrorFormat(c,"Wrong number of arguments for CONFIG %s",
|
||||
(char*) c->argv[1]->ptr);
|
||||
}
|
||||
|
||||
@@ -206,4 +206,22 @@ void setproctitle(const char *fmt, ...);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/* Make sure we can test for ARM just checking for __arm__, since sometimes
|
||||
* __arm is defined but __arm__ is not. */
|
||||
#if defined(__arm) && !defined(__arm__)
|
||||
#define __arm__
|
||||
#endif
|
||||
#if defined (__aarch64__) && !defined(__arm64__)
|
||||
#define __arm64__
|
||||
#endif
|
||||
|
||||
/* Make sure we can test for SPARC just checking for __sparc__. */
|
||||
#if defined(__sparc) && !defined(__sparc__)
|
||||
#define __sparc__
|
||||
#endif
|
||||
|
||||
#if defined(__sparc__) || defined(__arm__)
|
||||
#define USE_ALIGNED_ACCESS
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
@@ -38,6 +38,15 @@
|
||||
* C-level DB API
|
||||
*----------------------------------------------------------------------------*/
|
||||
|
||||
/* 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. */
|
||||
void updateLFU(robj *val) {
|
||||
unsigned long counter = LFUDecrAndReturn(val);
|
||||
counter = LFULogIncr(counter);
|
||||
val->lru = (LFUGetTimeInMinutes()<<8) | counter;
|
||||
}
|
||||
|
||||
/* Low level key lookup API, not actually called directly from commands
|
||||
* implementations that should instead rely on lookupKeyRead(),
|
||||
* lookupKeyWrite() and lookupKeyReadWithFlags(). */
|
||||
@@ -54,9 +63,7 @@ robj *lookupKey(redisDb *db, robj *key, int flags) {
|
||||
!(flags & LOOKUP_NOTOUCH))
|
||||
{
|
||||
if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
|
||||
unsigned long ldt = val->lru >> 8;
|
||||
unsigned long counter = LFULogIncr(val->lru & 255);
|
||||
val->lru = (ldt << 8) | counter;
|
||||
updateLFU(val);
|
||||
} else {
|
||||
val->lru = LRU_CLOCK();
|
||||
}
|
||||
@@ -93,7 +100,7 @@ robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) {
|
||||
|
||||
if (expireIfNeeded(db,key) == 1) {
|
||||
/* 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 save
|
||||
* returns 0 only when the key does not exist at all, so it's safe
|
||||
* to return NULL ASAP. */
|
||||
if (server.masterhost == NULL) return NULL;
|
||||
|
||||
@@ -162,9 +169,9 @@ void dbAdd(redisDb *db, robj *key, robj *val) {
|
||||
int retval = dictAdd(db->dict, copy, val);
|
||||
|
||||
serverAssertWithInfo(NULL,key,retval == DICT_OK);
|
||||
if (val->type == OBJ_LIST) signalListAsReady(db, key);
|
||||
if (val->type == OBJ_LIST) signalKeyAsReady(db, key);
|
||||
if (server.cluster_enabled) slotToKeyAdd(key);
|
||||
}
|
||||
}
|
||||
|
||||
/* Overwrite an existing key with a new value. Incrementing the reference
|
||||
* count of the new value is up to the caller.
|
||||
@@ -180,6 +187,9 @@ void dbOverwrite(redisDb *db, robj *key, robj *val) {
|
||||
int saved_lru = old->lru;
|
||||
dictReplace(db->dict, key->ptr, val);
|
||||
val->lru = saved_lru;
|
||||
/* LFU should be not only copied but also updated
|
||||
* when a key is overwritten. */
|
||||
updateLFU(val);
|
||||
} else {
|
||||
dictReplace(db->dict, key->ptr, val);
|
||||
}
|
||||
@@ -190,7 +200,9 @@ void dbOverwrite(redisDb *db, robj *key, robj *val) {
|
||||
*
|
||||
* 1) The ref count of the value object is incremented.
|
||||
* 2) clients WATCHing for the destination key notified.
|
||||
* 3) The expire time of the key is reset (the key is made persistent). */
|
||||
* 3) The expire time of the key is reset (the key is made persistent).
|
||||
*
|
||||
* 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);
|
||||
@@ -330,6 +342,7 @@ long long emptyDb(int dbnum, int flags, void(callback)(void*)) {
|
||||
slotToKeyFlush();
|
||||
}
|
||||
}
|
||||
if (dbnum == -1) flushSlaveKeysWithExpireList();
|
||||
return removed;
|
||||
}
|
||||
|
||||
@@ -413,7 +426,9 @@ void flushallCommand(client *c) {
|
||||
/* Normally rdbSave() will reset dirty, but we don't want this here
|
||||
* as otherwise FLUSHALL will not be replicated nor put into the AOF. */
|
||||
int saved_dirty = server.dirty;
|
||||
rdbSave(server.rdb_filename);
|
||||
rdbSaveInfo rsi, *rsiptr;
|
||||
rsiptr = rdbPopulateSaveInfo(&rsi);
|
||||
rdbSave(server.rdb_filename,rsiptr);
|
||||
server.dirty = saved_dirty;
|
||||
}
|
||||
server.dirty++;
|
||||
@@ -471,7 +486,7 @@ void selectCommand(client *c) {
|
||||
return;
|
||||
}
|
||||
if (selectDb(c,id) == C_ERR) {
|
||||
addReplyError(c,"invalid DB index");
|
||||
addReplyError(c,"DB index is out of range");
|
||||
} else {
|
||||
addReply(c,shared.ok);
|
||||
}
|
||||
@@ -662,7 +677,7 @@ void scanGenericCommand(client *c, robj *o, unsigned long cursor) {
|
||||
privdata[0] = keys;
|
||||
privdata[1] = o;
|
||||
do {
|
||||
cursor = dictScan(ht, cursor, scanCallback, privdata);
|
||||
cursor = dictScan(ht, cursor, scanCallback, NULL, privdata);
|
||||
} while (cursor &&
|
||||
maxiterations-- &&
|
||||
listLength(keys) < (unsigned long)count);
|
||||
@@ -783,6 +798,7 @@ void typeCommand(client *c) {
|
||||
case OBJ_SET: type = "set"; break;
|
||||
case OBJ_ZSET: type = "zset"; break;
|
||||
case OBJ_HASH: type = "hash"; break;
|
||||
case OBJ_STREAM: type = "stream"; break;
|
||||
case OBJ_MODULE: {
|
||||
moduleValue *mv = o->ptr;
|
||||
type = mv->type->name;
|
||||
@@ -851,7 +867,7 @@ void renameGenericCommand(client *c, int nx) {
|
||||
dbDelete(c->db,c->argv[2]);
|
||||
}
|
||||
dbAdd(c->db,c->argv[2],o);
|
||||
if (expire != -1) setExpire(c->db,c->argv[2],expire);
|
||||
if (expire != -1) setExpire(c,c->db,c->argv[2],expire);
|
||||
dbDelete(c->db,c->argv[1]);
|
||||
signalModifiedKey(c->db,c->argv[1]);
|
||||
signalModifiedKey(c->db,c->argv[2]);
|
||||
@@ -917,7 +933,7 @@ void moveCommand(client *c) {
|
||||
return;
|
||||
}
|
||||
dbAdd(dst,c->argv[1],o);
|
||||
if (expire != -1) setExpire(dst,c->argv[1],expire);
|
||||
if (expire != -1) setExpire(c,dst,c->argv[1],expire);
|
||||
incrRefCount(o);
|
||||
|
||||
/* OK! key moved, free the entry in the source DB */
|
||||
@@ -926,6 +942,91 @@ void moveCommand(client *c) {
|
||||
addReply(c,shared.cone);
|
||||
}
|
||||
|
||||
/* Helper function for dbSwapDatabases(): scans the list of keys that have
|
||||
* one or more blocked clients for B[LR]POP or other list blocking commands
|
||||
* and signal the keys are ready if they are lists. See the comment where
|
||||
* the function is used for more info. */
|
||||
void scanDatabaseForReadyLists(redisDb *db) {
|
||||
dictEntry *de;
|
||||
dictIterator *di = dictGetSafeIterator(db->blocking_keys);
|
||||
while((de = dictNext(di)) != NULL) {
|
||||
robj *key = dictGetKey(de);
|
||||
robj *value = lookupKey(db,key,LOOKUP_NOTOUCH);
|
||||
if (value && (value->type == OBJ_LIST || value->type == OBJ_STREAM))
|
||||
signalKeyAsReady(db, key);
|
||||
}
|
||||
dictReleaseIterator(di);
|
||||
}
|
||||
|
||||
/* Swap two databases at runtime so that all clients will magically see
|
||||
* the new database even if already connected. Note that the client
|
||||
* structure c->db points to a given DB, so we need to be smarter and
|
||||
* swap the underlying referenced structures, otherwise we would need
|
||||
* to fix all the references to the Redis DB structure.
|
||||
*
|
||||
* Returns C_ERR if at least one of the DB ids are out of range, otherwise
|
||||
* C_OK is returned. */
|
||||
int dbSwapDatabases(int id1, int id2) {
|
||||
if (id1 < 0 || id1 >= server.dbnum ||
|
||||
id2 < 0 || id2 >= server.dbnum) return C_ERR;
|
||||
if (id1 == id2) return C_OK;
|
||||
redisDb aux = server.db[id1];
|
||||
redisDb *db1 = &server.db[id1], *db2 = &server.db[id2];
|
||||
|
||||
/* Swap hash tables. Note that we don't swap blocking_keys,
|
||||
* ready_keys and watched_keys, since we want clients to
|
||||
* remain in the same DB they were. */
|
||||
db1->dict = db2->dict;
|
||||
db1->expires = db2->expires;
|
||||
db1->avg_ttl = db2->avg_ttl;
|
||||
|
||||
db2->dict = aux.dict;
|
||||
db2->expires = aux.expires;
|
||||
db2->avg_ttl = aux.avg_ttl;
|
||||
|
||||
/* Now we need to handle clients blocked on lists: as an effect
|
||||
* of swapping the two DBs, a client that was waiting for list
|
||||
* X in a given DB, may now actually be unblocked if X happens
|
||||
* to exist in the new version of the DB, after the swap.
|
||||
*
|
||||
* However normally we only do this check for efficiency reasons
|
||||
* in dbAdd() when a list is created. So here we need to rescan
|
||||
* the list of clients blocked on lists and signal lists as ready
|
||||
* if needed. */
|
||||
scanDatabaseForReadyLists(db1);
|
||||
scanDatabaseForReadyLists(db2);
|
||||
return C_OK;
|
||||
}
|
||||
|
||||
/* SWAPDB db1 db2 */
|
||||
void swapdbCommand(client *c) {
|
||||
long id1, id2;
|
||||
|
||||
/* Not allowed in cluster mode: we have just DB 0 there. */
|
||||
if (server.cluster_enabled) {
|
||||
addReplyError(c,"SWAPDB is not allowed in cluster mode");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Get the two DBs indexes. */
|
||||
if (getLongFromObjectOrReply(c, c->argv[1], &id1,
|
||||
"invalid first DB index") != C_OK)
|
||||
return;
|
||||
|
||||
if (getLongFromObjectOrReply(c, c->argv[2], &id2,
|
||||
"invalid second DB index") != C_OK)
|
||||
return;
|
||||
|
||||
/* Swap... */
|
||||
if (dbSwapDatabases(id1,id2) == C_ERR) {
|
||||
addReplyError(c,"DB index is out of range");
|
||||
return;
|
||||
} else {
|
||||
server.dirty++;
|
||||
addReply(c,shared.ok);
|
||||
}
|
||||
}
|
||||
|
||||
/*-----------------------------------------------------------------------------
|
||||
* Expires API
|
||||
*----------------------------------------------------------------------------*/
|
||||
@@ -937,7 +1038,11 @@ int removeExpire(redisDb *db, robj *key) {
|
||||
return dictDelete(db->expires,key->ptr) == DICT_OK;
|
||||
}
|
||||
|
||||
void setExpire(redisDb *db, robj *key, long long when) {
|
||||
/* Set an expire to the specified key. If the expire is set in the context
|
||||
* of an user calling a command 'c' is the client, otherwise 'c' is set
|
||||
* to NULL. The 'when' parameter is the absolute unix time in milliseconds
|
||||
* after which the key will no longer be considered valid. */
|
||||
void setExpire(client *c, redisDb *db, robj *key, long long when) {
|
||||
dictEntry *kde, *de;
|
||||
|
||||
/* Reuse the sds from the main dict in the expire dict */
|
||||
@@ -945,6 +1050,10 @@ void setExpire(redisDb *db, robj *key, long long when) {
|
||||
serverAssertWithInfo(NULL,key,kde != NULL);
|
||||
de = dictAddOrFind(db->expires,dictGetKey(kde));
|
||||
dictSetSignedIntegerVal(de,when);
|
||||
|
||||
int writable_slave = server.masterhost && server.repl_slave_ro == 0;
|
||||
if (c && writable_slave && !(c->flags & CLIENT_MASTER))
|
||||
rememberSlaveKeyWithExpire(db,key);
|
||||
}
|
||||
|
||||
/* Return the expire time of the specified key, or -1 if no expire
|
||||
@@ -1037,11 +1146,24 @@ int *getKeysUsingCommandTable(struct redisCommand *cmd,robj **argv, int argc, in
|
||||
*numkeys = 0;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
last = cmd->lastkey;
|
||||
if (last < 0) last = argc+last;
|
||||
keys = zmalloc(sizeof(int)*((last - cmd->firstkey)+1));
|
||||
for (j = cmd->firstkey; j <= last; j += cmd->keystep) {
|
||||
serverAssert(j < argc);
|
||||
if (j >= argc) {
|
||||
/* Modules command do not have dispatch time arity checks, so
|
||||
* we need to handle the case where the user passed an invalid
|
||||
* number of arguments here. In this case we return no keys
|
||||
* and expect the module command to report an arity error. */
|
||||
if (cmd->flags & CMD_MODULE) {
|
||||
zfree(keys);
|
||||
*numkeys = 0;
|
||||
return NULL;
|
||||
} else {
|
||||
serverPanic("Redis built-in command declared keys positions not matching the arity requirements.");
|
||||
}
|
||||
}
|
||||
keys[i++] = j;
|
||||
}
|
||||
*numkeys = i;
|
||||
@@ -1203,92 +1325,155 @@ int *migrateGetKeys(struct redisCommand *cmd, robj **argv, int argc, int *numkey
|
||||
return keys;
|
||||
}
|
||||
|
||||
/* Helper function to extract keys from following commands:
|
||||
* GEORADIUS key x y radius unit [WITHDIST] [WITHHASH] [WITHCOORD] [ASC|DESC]
|
||||
* [COUNT count] [STORE key] [STOREDIST key]
|
||||
* GEORADIUSBYMEMBER key member radius unit ... options ... */
|
||||
int *georadiusGetKeys(struct redisCommand *cmd, robj **argv, int argc, int *numkeys) {
|
||||
int i, num, *keys;
|
||||
UNUSED(cmd);
|
||||
|
||||
/* Check for the presence of the stored key in the command */
|
||||
int stored_key = -1;
|
||||
for (i = 5; i < argc; i++) {
|
||||
char *arg = argv[i]->ptr;
|
||||
/* For the case when user specifies both "store" and "storedist" options, the
|
||||
* second key specified would override the first key. This behavior is kept
|
||||
* the same as in georadiusCommand method.
|
||||
*/
|
||||
if ((!strcasecmp(arg, "store") || !strcasecmp(arg, "storedist")) && ((i+1) < argc)) {
|
||||
stored_key = i+1;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
num = 1 + (stored_key == -1 ? 0 : 1);
|
||||
|
||||
/* Keys in the command come from two places:
|
||||
* argv[1] = key,
|
||||
* argv[5...n] = stored key if present
|
||||
*/
|
||||
keys = zmalloc(sizeof(int) * num);
|
||||
|
||||
/* Add all key positions to keys[] */
|
||||
keys[0] = 1;
|
||||
if(num > 1) {
|
||||
keys[1] = stored_key;
|
||||
}
|
||||
*numkeys = num;
|
||||
return keys;
|
||||
}
|
||||
|
||||
/* XREAD [BLOCK <milliseconds>] [COUNT <count>] [GROUP <groupname> <ttl>]
|
||||
* [RETRY <milliseconds> <ttl>] STREAMS key_1 key_2 ... key_N
|
||||
* ID_1 ID_2 ... ID_N */
|
||||
int *xreadGetKeys(struct redisCommand *cmd, robj **argv, int argc, int *numkeys) {
|
||||
int i, num, *keys;
|
||||
UNUSED(cmd);
|
||||
|
||||
/* We need to seek the last argument that contains "STREAMS", because other
|
||||
* arguments before may contain it (for example the group name). */
|
||||
int streams_pos = -1;
|
||||
for (i = 1; i < argc; i++) {
|
||||
char *arg = argv[i]->ptr;
|
||||
if (!strcasecmp(arg, "streams")) streams_pos = i;
|
||||
}
|
||||
if (streams_pos != -1) num = argc - streams_pos - 1;
|
||||
|
||||
/* Syntax error. */
|
||||
if (streams_pos == -1 || num % 2 != 0) {
|
||||
*numkeys = 0;
|
||||
return NULL;
|
||||
}
|
||||
num /= 2; /* We have half the keys as there are arguments because
|
||||
there are also the IDs, one per key. */
|
||||
|
||||
keys = zmalloc(sizeof(int) * num);
|
||||
for (i = streams_pos+1; i < argc; i++) keys[i-streams_pos-1] = i;
|
||||
*numkeys = num;
|
||||
return keys;
|
||||
}
|
||||
|
||||
/* Slot to Key API. This is used by Redis Cluster in order to obtain in
|
||||
* a fast way a key that belongs to a specified hash slot. This is useful
|
||||
* while rehashing the cluster. */
|
||||
void slotToKeyAdd(robj *key) {
|
||||
* while rehashing the cluster and in other conditions when we need to
|
||||
* understand if we have keys for a given hash slot. */
|
||||
void slotToKeyUpdateKey(robj *key, int add) {
|
||||
unsigned int hashslot = keyHashSlot(key->ptr,sdslen(key->ptr));
|
||||
unsigned char buf[64];
|
||||
unsigned char *indexed = buf;
|
||||
size_t keylen = sdslen(key->ptr);
|
||||
|
||||
sds sdskey = sdsdup(key->ptr);
|
||||
zslInsert(server.cluster->slots_to_keys,hashslot,sdskey);
|
||||
server.cluster->slots_keys_count[hashslot] += add ? 1 : -1;
|
||||
if (keylen+2 > 64) indexed = zmalloc(keylen+2);
|
||||
indexed[0] = (hashslot >> 8) & 0xff;
|
||||
indexed[1] = hashslot & 0xff;
|
||||
memcpy(indexed+2,key->ptr,keylen);
|
||||
if (add) {
|
||||
raxInsert(server.cluster->slots_to_keys,indexed,keylen+2,NULL,NULL);
|
||||
} else {
|
||||
raxRemove(server.cluster->slots_to_keys,indexed,keylen+2,NULL);
|
||||
}
|
||||
if (indexed != buf) zfree(indexed);
|
||||
}
|
||||
|
||||
void slotToKeyAdd(robj *key) {
|
||||
slotToKeyUpdateKey(key,1);
|
||||
}
|
||||
|
||||
void slotToKeyDel(robj *key) {
|
||||
unsigned int hashslot = keyHashSlot(key->ptr,sdslen(key->ptr));
|
||||
zslDelete(server.cluster->slots_to_keys,hashslot,key->ptr,NULL);
|
||||
slotToKeyUpdateKey(key,0);
|
||||
}
|
||||
|
||||
void slotToKeyFlush(void) {
|
||||
zslFree(server.cluster->slots_to_keys);
|
||||
server.cluster->slots_to_keys = zslCreate();
|
||||
raxFree(server.cluster->slots_to_keys);
|
||||
server.cluster->slots_to_keys = raxNew();
|
||||
memset(server.cluster->slots_keys_count,0,
|
||||
sizeof(server.cluster->slots_keys_count));
|
||||
}
|
||||
|
||||
/* Pupulate the specified array of objects with keys in the specified slot.
|
||||
* New objects are returned to represent keys, it's up to the caller to
|
||||
* decrement the reference count to release the keys names. */
|
||||
unsigned int getKeysInSlot(unsigned int hashslot, robj **keys, unsigned int count) {
|
||||
zskiplistNode *n;
|
||||
zrangespec range;
|
||||
raxIterator iter;
|
||||
int j = 0;
|
||||
unsigned char indexed[2];
|
||||
|
||||
range.min = range.max = hashslot;
|
||||
range.minex = range.maxex = 0;
|
||||
|
||||
n = zslFirstInRange(server.cluster->slots_to_keys, &range);
|
||||
while(n && n->score == hashslot && count--) {
|
||||
keys[j++] = createStringObject(n->ele,sdslen(n->ele));
|
||||
n = n->level[0].forward;
|
||||
indexed[0] = (hashslot >> 8) & 0xff;
|
||||
indexed[1] = hashslot & 0xff;
|
||||
raxStart(&iter,server.cluster->slots_to_keys);
|
||||
raxSeek(&iter,">=",indexed,2);
|
||||
while(count-- && raxNext(&iter)) {
|
||||
if (iter.key[0] != indexed[0] || iter.key[1] != indexed[1]) break;
|
||||
keys[j++] = createStringObject((char*)iter.key+2,iter.key_len-2);
|
||||
}
|
||||
raxStop(&iter);
|
||||
return j;
|
||||
}
|
||||
|
||||
/* Remove all the keys in the specified hash slot.
|
||||
* The number of removed items is returned. */
|
||||
unsigned int delKeysInSlot(unsigned int hashslot) {
|
||||
zskiplistNode *n;
|
||||
zrangespec range;
|
||||
raxIterator iter;
|
||||
int j = 0;
|
||||
unsigned char indexed[2];
|
||||
|
||||
range.min = range.max = hashslot;
|
||||
range.minex = range.maxex = 0;
|
||||
indexed[0] = (hashslot >> 8) & 0xff;
|
||||
indexed[1] = hashslot & 0xff;
|
||||
raxStart(&iter,server.cluster->slots_to_keys);
|
||||
while(server.cluster->slots_keys_count[hashslot]) {
|
||||
raxSeek(&iter,">=",indexed,2);
|
||||
raxNext(&iter);
|
||||
|
||||
n = zslFirstInRange(server.cluster->slots_to_keys, &range);
|
||||
while(n && n->score == hashslot) {
|
||||
sds sdskey = n->ele;
|
||||
robj *key = createStringObject(sdskey,sdslen(sdskey));
|
||||
n = n->level[0].forward; /* Go to the next item before freeing it. */
|
||||
robj *key = createStringObject((char*)iter.key+2,iter.key_len-2);
|
||||
dbDelete(&server.db[0],key);
|
||||
decrRefCount(key);
|
||||
j++;
|
||||
}
|
||||
raxStop(&iter);
|
||||
return j;
|
||||
}
|
||||
|
||||
unsigned int countKeysInSlot(unsigned int hashslot) {
|
||||
zskiplist *zsl = server.cluster->slots_to_keys;
|
||||
zskiplistNode *zn;
|
||||
zrangespec range;
|
||||
int rank, count = 0;
|
||||
|
||||
range.min = range.max = hashslot;
|
||||
range.minex = range.maxex = 0;
|
||||
|
||||
/* Find first element in range */
|
||||
zn = zslFirstInRange(zsl, &range);
|
||||
|
||||
/* Use rank of first element, if any, to determine preliminary count */
|
||||
if (zn != NULL) {
|
||||
rank = zslGetRank(zsl, zn->score, zn->ele);
|
||||
count = (zsl->length - (rank - 1));
|
||||
|
||||
/* Find last element in range */
|
||||
zn = zslLastInRange(zsl, &range);
|
||||
|
||||
/* Use rank of last element, if any, to determine the actual count */
|
||||
if (zn != NULL) {
|
||||
rank = zslGetRank(zsl, zn->score, zn->ele);
|
||||
count -= (zsl->length - rank);
|
||||
}
|
||||
}
|
||||
return count;
|
||||
return server.cluster->slots_keys_count[hashslot];
|
||||
}
|
||||
|
||||
+102
-55
@@ -126,7 +126,7 @@ void computeDatasetDigest(unsigned char *final) {
|
||||
redisDb *db = server.db+j;
|
||||
|
||||
if (dictSize(db->dict) == 0) continue;
|
||||
di = dictGetIterator(db->dict);
|
||||
di = dictGetSafeIterator(db->dict);
|
||||
|
||||
/* hash the DB id, so the same dataset moved in a different
|
||||
* DB will lead to a different digest */
|
||||
@@ -239,6 +239,36 @@ void computeDatasetDigest(unsigned char *final) {
|
||||
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");
|
||||
}
|
||||
@@ -253,51 +283,34 @@ void computeDatasetDigest(unsigned char *final) {
|
||||
}
|
||||
|
||||
void debugCommand(client *c) {
|
||||
if (c->argc == 1) {
|
||||
addReplyError(c,"You must specify a subcommand for DEBUG. Try DEBUG HELP for info.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!strcasecmp(c->argv[1]->ptr,"help")) {
|
||||
void *blenp = addDeferredMultiBulkLength(c);
|
||||
int blen = 0;
|
||||
blen++; addReplyStatus(c,
|
||||
"DEBUG <subcommand> arg arg ... arg. Subcommands:");
|
||||
blen++; addReplyStatus(c,
|
||||
"segfault -- Crash the server with sigsegv.");
|
||||
blen++; addReplyStatus(c,
|
||||
"restart -- Graceful restart: save config, db, restart.");
|
||||
blen++; addReplyStatus(c,
|
||||
"crash-and-recovery <milliseconds> -- Hard crash and restart after <milliseconds> delay.");
|
||||
blen++; addReplyStatus(c,
|
||||
"assert -- Crash by assertion failed.");
|
||||
blen++; addReplyStatus(c,
|
||||
"reload -- Save the RDB on disk and reload it back in memory.");
|
||||
blen++; addReplyStatus(c,
|
||||
"loadaof -- Flush the AOF buffers on disk and reload the AOF in memory.");
|
||||
blen++; addReplyStatus(c,
|
||||
"object <key> -- Show low level info about key and associated value.");
|
||||
blen++; addReplyStatus(c,
|
||||
"sdslen <key> -- Show low level SDS string info representing key and value.");
|
||||
blen++; addReplyStatus(c,
|
||||
"populate <count> [prefix] -- Create <count> string keys named key:<num>. If a prefix is specified is used instead of the 'key' prefix.");
|
||||
blen++; addReplyStatus(c,
|
||||
"digest -- Outputs an hex signature representing the current DB content.");
|
||||
blen++; addReplyStatus(c,
|
||||
"sleep <seconds> -- Stop the server for <seconds>. Decimals allowed.");
|
||||
blen++; addReplyStatus(c,
|
||||
"set-active-expire (0|1) -- Setting it to 0 disables expiring keys in background when they are not accessed (otherwise the Redis behavior). Setting it to 1 reenables back the default.");
|
||||
blen++; addReplyStatus(c,
|
||||
"lua-always-replicate-commands (0|1) -- Setting it to 1 makes Lua replication defaulting to replicating single commands, without the script having to enable effects replication.");
|
||||
blen++; addReplyStatus(c,
|
||||
"error <string> -- Return a Redis protocol error with <string> as message. Useful for clients unit tests to simulate Redis errors.");
|
||||
blen++; addReplyStatus(c,
|
||||
"structsize -- Return the size of different Redis core C structures.");
|
||||
blen++; addReplyStatus(c,
|
||||
"htstats <dbid> -- Return hash table statistics of the specified Redis database.");
|
||||
setDeferredMultiBulkLength(c,blenp,blen);
|
||||
if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"help")) {
|
||||
const char *help[] = {
|
||||
"assert -- Crash by assertion failed.",
|
||||
"change-repl-id -- Change the replication IDs of the instance. Dangerous, should be used only for testing the replication subsystem.",
|
||||
"crash-and-recovery <milliseconds> -- Hard crash and restart after <milliseconds> delay.",
|
||||
"digest -- Outputs an hex signature representing the current DB content.",
|
||||
"htstats <dbid> -- Return hash table statistics of the specified Redis database.",
|
||||
"loadaof -- Flush the AOF buffers on disk and reload the AOF in memory.",
|
||||
"lua-always-replicate-commands (0|1) -- Setting it to 1 makes Lua replication defaulting to replicating single commands, without the script having to enable effects replication.",
|
||||
"object <key> -- Show low level info about key and associated value.",
|
||||
"panic -- Crash the server simulating a panic.",
|
||||
"populate <count> [prefix] [size] -- Create <count> string keys named key:<num>. If a prefix is specified is used instead of the 'key' prefix.",
|
||||
"reload -- Save the RDB on disk and reload it back in memory.",
|
||||
"restart -- Graceful restart: save config, db, restart.",
|
||||
"sdslen <key> -- Show low level SDS string info representing key and value.",
|
||||
"segfault -- Crash the server with sigsegv.",
|
||||
"set-active-expire (0|1) -- Setting it to 0 disables expiring keys in background when they are not accessed (otherwise the Redis behavior). Setting it to 1 reenables back the default.",
|
||||
"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.",
|
||||
"error <string> -- Return a Redis protocol error with <string> as message. Useful for clients unit tests to simulate Redis errors.",
|
||||
NULL
|
||||
};
|
||||
addReplyHelp(c, help);
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"segfault")) {
|
||||
*((char*)-1) = 'x';
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"panic")) {
|
||||
serverPanic("DEBUG PANIC called at Unix time %ld", time(NULL));
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"restart") ||
|
||||
!strcasecmp(c->argv[1]->ptr,"crash-and-recover"))
|
||||
{
|
||||
@@ -320,12 +333,14 @@ void debugCommand(client *c) {
|
||||
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,"reload")) {
|
||||
if (rdbSave(server.rdb_filename) != C_OK) {
|
||||
rdbSaveInfo rsi, *rsiptr;
|
||||
rsiptr = rdbPopulateSaveInfo(&rsi);
|
||||
if (rdbSave(server.rdb_filename,rsiptr) != C_OK) {
|
||||
addReply(c,shared.err);
|
||||
return;
|
||||
}
|
||||
emptyDb(-1,EMPTYDB_NO_FLAGS,NULL);
|
||||
if (rdbLoad(server.rdb_filename) != C_OK) {
|
||||
if (rdbLoad(server.rdb_filename,NULL) != C_OK) {
|
||||
addReplyError(c,"Error trying to load the RDB dump");
|
||||
return;
|
||||
}
|
||||
@@ -353,13 +368,13 @@ void debugCommand(client *c) {
|
||||
val = dictGetVal(de);
|
||||
strenc = strEncoding(val->encoding);
|
||||
|
||||
char extra[128] = {0};
|
||||
char extra[138] = {0};
|
||||
if (val->encoding == OBJ_ENCODING_QUICKLIST) {
|
||||
char *nextra = extra;
|
||||
int remaining = sizeof(extra);
|
||||
quicklist *ql = val->ptr;
|
||||
/* Add number of quicklist nodes */
|
||||
int used = snprintf(nextra, remaining, " ql_nodes:%u", ql->len);
|
||||
int used = snprintf(nextra, remaining, " ql_nodes:%lu", ql->len);
|
||||
nextra += used;
|
||||
remaining -= used;
|
||||
/* Add average quicklist fill factor */
|
||||
@@ -418,8 +433,20 @@ void debugCommand(client *c) {
|
||||
(long long) sdsavail(val->ptr),
|
||||
(long long) getStringObjectSdsUsedMemory(val));
|
||||
}
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"ziplist") && c->argc == 3) {
|
||||
robj *o;
|
||||
|
||||
if ((o = objectCommandLookupOrReply(c,c->argv[2],shared.nokeyerr))
|
||||
== NULL) return;
|
||||
|
||||
if (o->encoding != OBJ_ENCODING_ZIPLIST) {
|
||||
addReplyError(c,"Not an sds encoded string.");
|
||||
} else {
|
||||
ziplistRepr(o->ptr);
|
||||
addReplyStatus(c,"Ziplist structure printed on stdout");
|
||||
}
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"populate") &&
|
||||
(c->argc == 3 || c->argc == 4)) {
|
||||
c->argc >= 3 && c->argc <= 5) {
|
||||
long keys, j;
|
||||
robj *key, *val;
|
||||
char buf[128];
|
||||
@@ -428,15 +455,25 @@ void debugCommand(client *c) {
|
||||
return;
|
||||
dictExpand(c->db->dict,keys);
|
||||
for (j = 0; j < keys; j++) {
|
||||
long valsize = 0;
|
||||
snprintf(buf,sizeof(buf),"%s:%lu",
|
||||
(c->argc == 3) ? "key" : (char*)c->argv[3]->ptr, j);
|
||||
key = createStringObject(buf,strlen(buf));
|
||||
if (c->argc == 5)
|
||||
if (getLongFromObjectOrReply(c, c->argv[4], &valsize, NULL) != C_OK)
|
||||
return;
|
||||
if (lookupKeyWrite(c->db,key) != NULL) {
|
||||
decrRefCount(key);
|
||||
continue;
|
||||
}
|
||||
snprintf(buf,sizeof(buf),"value:%lu",j);
|
||||
val = createStringObject(buf,strlen(buf));
|
||||
if (valsize==0)
|
||||
val = createStringObject(buf,strlen(buf));
|
||||
else {
|
||||
int buflen = strlen(buf);
|
||||
val = createStringObject(NULL,valsize);
|
||||
memcpy(val->ptr, buf, valsize<=buflen? valsize: buflen);
|
||||
}
|
||||
dbAdd(c->db,key,val);
|
||||
signalModifiedKey(c->db,key);
|
||||
decrRefCount(key);
|
||||
@@ -510,9 +547,15 @@ void debugCommand(client *c) {
|
||||
stats = sdscat(stats,buf);
|
||||
|
||||
addReplyBulkSds(c,stats);
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"change-repl-id") && c->argc == 2) {
|
||||
serverLog(LL_WARNING,"Changing replication IDs after receiving DEBUG change-repl-id");
|
||||
changeReplicationId();
|
||||
clearReplicationId2();
|
||||
addReply(c,shared.ok);
|
||||
} else {
|
||||
addReplyErrorFormat(c, "Unknown DEBUG subcommand or wrong number of arguments for '%s'",
|
||||
addReplyErrorFormat(c, "Unknown subcommand or wrong number of arguments for '%s'. Try DEBUG HELP",
|
||||
(char*)c->argv[1]->ptr);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -591,11 +634,17 @@ void _serverAssertWithInfo(const client *c, const robj *o, const char *estr, con
|
||||
_serverAssert(estr,file,line);
|
||||
}
|
||||
|
||||
void _serverPanic(const char *msg, const char *file, int line) {
|
||||
void _serverPanic(const char *file, int line, const char *msg, ...) {
|
||||
va_list ap;
|
||||
va_start(ap,msg);
|
||||
char fmtmsg[256];
|
||||
vsnprintf(fmtmsg,sizeof(fmtmsg),msg,ap);
|
||||
va_end(ap);
|
||||
|
||||
bugReportStart();
|
||||
serverLog(LL_WARNING,"------------------------------------------------");
|
||||
serverLog(LL_WARNING,"!!! Software Failure. Press left mouse button to continue");
|
||||
serverLog(LL_WARNING,"Guru Meditation: %s #%s:%d",msg,file,line);
|
||||
serverLog(LL_WARNING,"Guru Meditation: %s #%s:%d",fmtmsg,file,line);
|
||||
#ifdef HAVE_BACKTRACE
|
||||
serverLog(LL_WARNING,"(forcing SIGSEGV in order to print the stack trace)");
|
||||
#endif
|
||||
@@ -995,8 +1044,6 @@ void sigsegvHandler(int sig, siginfo_t *info, void *secret) {
|
||||
/* Log INFO and CLIENT LIST */
|
||||
serverLogRaw(LL_WARNING|LL_RAW, "\n------ INFO OUTPUT ------\n");
|
||||
infostring = genRedisInfoString("all");
|
||||
infostring = sdscatprintf(infostring, "hash_init_value: %u\n",
|
||||
dictGetHashFunctionSeed());
|
||||
serverLogRaw(LL_WARNING|LL_RAW, infostring);
|
||||
serverLogRaw(LL_WARNING|LL_RAW, "\n------ CLIENT LIST OUTPUT ------\n");
|
||||
clients = getAllClientsInfoString();
|
||||
|
||||
+579
@@ -0,0 +1,579 @@
|
||||
/*
|
||||
* Active memory defragmentation
|
||||
* Try to find key / value allocations that need to be re-allocated in order
|
||||
* to reduce external fragmentation.
|
||||
* We do that by scanning the keyspace and for each pointer we have, we can try to
|
||||
* ask the allocator if moving it to a new address will help reduce fragmentation.
|
||||
*
|
||||
* Copyright (c) 2017, Oran Agra
|
||||
* Copyright (c) 2017, Redis Labs, Inc
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "server.h"
|
||||
#include <time.h>
|
||||
#include <assert.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef HAVE_DEFRAG
|
||||
|
||||
/* this method was added to jemalloc in order to help us understand which
|
||||
* pointers are worthwhile moving and which aren't */
|
||||
int je_get_defrag_hint(void* ptr, int *bin_util, int *run_util);
|
||||
|
||||
/* Defrag helper for generic allocations.
|
||||
*
|
||||
* returns NULL in case the allocatoin wasn't moved.
|
||||
* when it returns a non-null value, the old pointer was already released
|
||||
* and should NOT be accessed. */
|
||||
void* activeDefragAlloc(void *ptr) {
|
||||
int bin_util, run_util;
|
||||
size_t size;
|
||||
void *newptr;
|
||||
if(!je_get_defrag_hint(ptr, &bin_util, &run_util)) {
|
||||
server.stat_active_defrag_misses++;
|
||||
return NULL;
|
||||
}
|
||||
/* if this run is more utilized than the average utilization in this bin
|
||||
* (or it is full), skip it. This will eventually move all the allocations
|
||||
* from relatively empty runs into relatively full runs. */
|
||||
if (run_util > bin_util || run_util == 1<<16) {
|
||||
server.stat_active_defrag_misses++;
|
||||
return NULL;
|
||||
}
|
||||
/* move this allocation to a new allocation.
|
||||
* make sure not to use the thread cache. so that we don't get back the same
|
||||
* pointers we try to free */
|
||||
size = zmalloc_size(ptr);
|
||||
newptr = zmalloc_no_tcache(size);
|
||||
memcpy(newptr, ptr, size);
|
||||
zfree_no_tcache(ptr);
|
||||
return newptr;
|
||||
}
|
||||
|
||||
/*Defrag helper for sds strings
|
||||
*
|
||||
* returns NULL in case the allocatoin wasn't moved.
|
||||
* when it returns a non-null value, the old pointer was already released
|
||||
* and should NOT be accessed. */
|
||||
sds activeDefragSds(sds sdsptr) {
|
||||
void* ptr = sdsAllocPtr(sdsptr);
|
||||
void* newptr = activeDefragAlloc(ptr);
|
||||
if (newptr) {
|
||||
size_t offset = sdsptr - (char*)ptr;
|
||||
sdsptr = (char*)newptr + offset;
|
||||
return sdsptr;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Defrag helper for robj and/or string objects
|
||||
*
|
||||
* returns NULL in case the allocatoin wasn't moved.
|
||||
* when it returns a non-null value, the old pointer was already released
|
||||
* and should NOT be accessed. */
|
||||
robj *activeDefragStringOb(robj* ob, int *defragged) {
|
||||
robj *ret = NULL;
|
||||
if (ob->refcount!=1)
|
||||
return NULL;
|
||||
|
||||
/* try to defrag robj (only if not an EMBSTR type (handled below). */
|
||||
if (ob->type!=OBJ_STRING || ob->encoding!=OBJ_ENCODING_EMBSTR) {
|
||||
if ((ret = activeDefragAlloc(ob))) {
|
||||
ob = ret;
|
||||
(*defragged)++;
|
||||
}
|
||||
}
|
||||
|
||||
/* try to defrag string object */
|
||||
if (ob->type == OBJ_STRING) {
|
||||
if(ob->encoding==OBJ_ENCODING_RAW) {
|
||||
sds newsds = activeDefragSds((sds)ob->ptr);
|
||||
if (newsds) {
|
||||
ob->ptr = newsds;
|
||||
(*defragged)++;
|
||||
}
|
||||
} else if (ob->encoding==OBJ_ENCODING_EMBSTR) {
|
||||
/* The sds is embedded in the object allocation, calculate the
|
||||
* offset and update the pointer in the new allocation. */
|
||||
long ofs = (intptr_t)ob->ptr - (intptr_t)ob;
|
||||
if ((ret = activeDefragAlloc(ob))) {
|
||||
ret->ptr = (void*)((intptr_t)ret + ofs);
|
||||
(*defragged)++;
|
||||
}
|
||||
} else if (ob->encoding!=OBJ_ENCODING_INT) {
|
||||
serverPanic("Unknown string encoding");
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* Defrag helper for dictEntries to be used during dict iteration (called on
|
||||
* each step). Teturns a stat of how many pointers were moved. */
|
||||
int dictIterDefragEntry(dictIterator *iter) {
|
||||
/* This function is a little bit dirty since it messes with the internals
|
||||
* of the dict and it's iterator, but the benefit is that it is very easy
|
||||
* to use, and require no other chagnes in the dict. */
|
||||
int defragged = 0;
|
||||
dictht *ht;
|
||||
/* Handle the next entry (if there is one), and update the pointer in the
|
||||
* current entry. */
|
||||
if (iter->nextEntry) {
|
||||
dictEntry *newde = activeDefragAlloc(iter->nextEntry);
|
||||
if (newde) {
|
||||
defragged++;
|
||||
iter->nextEntry = newde;
|
||||
iter->entry->next = newde;
|
||||
}
|
||||
}
|
||||
/* handle the case of the first entry in the hash bucket. */
|
||||
ht = &iter->d->ht[iter->table];
|
||||
if (ht->table[iter->index] == iter->entry) {
|
||||
dictEntry *newde = activeDefragAlloc(iter->entry);
|
||||
if (newde) {
|
||||
iter->entry = newde;
|
||||
ht->table[iter->index] = newde;
|
||||
defragged++;
|
||||
}
|
||||
}
|
||||
return defragged;
|
||||
}
|
||||
|
||||
/* Defrag helper for dict main allocations (dict struct, and hash tables).
|
||||
* receives a pointer to the dict* and implicitly updates it when the dict
|
||||
* struct itself was moved. Returns a stat of how many pointers were moved. */
|
||||
int dictDefragTables(dict** dictRef) {
|
||||
dict *d = *dictRef;
|
||||
dictEntry **newtable;
|
||||
int defragged = 0;
|
||||
/* handle the dict struct */
|
||||
dict *newd = activeDefragAlloc(d);
|
||||
if (newd)
|
||||
defragged++, *dictRef = d = newd;
|
||||
/* handle the first hash table */
|
||||
newtable = activeDefragAlloc(d->ht[0].table);
|
||||
if (newtable)
|
||||
defragged++, d->ht[0].table = newtable;
|
||||
/* handle the second hash table */
|
||||
if (d->ht[1].table) {
|
||||
newtable = activeDefragAlloc(d->ht[1].table);
|
||||
if (newtable)
|
||||
defragged++, d->ht[1].table = newtable;
|
||||
}
|
||||
return defragged;
|
||||
}
|
||||
|
||||
/* Internal function used by zslDefrag */
|
||||
void zslUpdateNode(zskiplist *zsl, zskiplistNode *oldnode, zskiplistNode *newnode, zskiplistNode **update) {
|
||||
int i;
|
||||
for (i = 0; i < zsl->level; i++) {
|
||||
if (update[i]->level[i].forward == oldnode)
|
||||
update[i]->level[i].forward = newnode;
|
||||
}
|
||||
serverAssert(zsl->header!=oldnode);
|
||||
if (newnode->level[0].forward) {
|
||||
serverAssert(newnode->level[0].forward->backward==oldnode);
|
||||
newnode->level[0].forward->backward = newnode;
|
||||
} else {
|
||||
serverAssert(zsl->tail==oldnode);
|
||||
zsl->tail = newnode;
|
||||
}
|
||||
}
|
||||
|
||||
/* Defrag helper for sorted set.
|
||||
* Update the robj pointer, defrag the skiplist struct and return the new score
|
||||
* reference. We may not access oldele pointer (not even the pointer stored in
|
||||
* the skiplist), as it was already freed. Newele may be null, in which case we
|
||||
* only need to defrag the skiplist, but not update the obj pointer.
|
||||
* When return value is non-NULL, it is the score reference that must be updated
|
||||
* in the dict record. */
|
||||
double *zslDefrag(zskiplist *zsl, double score, sds oldele, sds newele) {
|
||||
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x, *newx;
|
||||
int i;
|
||||
sds ele = newele? newele: oldele;
|
||||
|
||||
/* find the skiplist node referring to the object that was moved,
|
||||
* and all pointers that need to be updated if we'll end up moving the skiplist node. */
|
||||
x = zsl->header;
|
||||
for (i = zsl->level-1; i >= 0; i--) {
|
||||
while (x->level[i].forward &&
|
||||
x->level[i].forward->ele != oldele && /* make sure not to access the
|
||||
->obj pointer if it matches
|
||||
oldele */
|
||||
(x->level[i].forward->score < score ||
|
||||
(x->level[i].forward->score == score &&
|
||||
sdscmp(x->level[i].forward->ele,ele) < 0)))
|
||||
x = x->level[i].forward;
|
||||
update[i] = x;
|
||||
}
|
||||
|
||||
/* update the robj pointer inside the skip list record. */
|
||||
x = x->level[0].forward;
|
||||
serverAssert(x && score == x->score && x->ele==oldele);
|
||||
if (newele)
|
||||
x->ele = newele;
|
||||
|
||||
/* try to defrag the skiplist record itself */
|
||||
newx = activeDefragAlloc(x);
|
||||
if (newx) {
|
||||
zslUpdateNode(zsl, x, newx, update);
|
||||
return &newx->score;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Utility function that replaces an old key pointer in the dictionary with a
|
||||
* new pointer. Additionally, we try to defrag the dictEntry in that dict.
|
||||
* Oldkey mey be a dead pointer and should not be accessed (we get a
|
||||
* pre-calculated hash value). Newkey may be null if the key pointer wasn't
|
||||
* moved. Return value is the the dictEntry if found, or NULL if not found.
|
||||
* NOTE: this is very ugly code, but it let's us avoid the complication of
|
||||
* doing a scan on another dict. */
|
||||
dictEntry* replaceSateliteDictKeyPtrAndOrDefragDictEntry(dict *d, sds oldkey, sds newkey, unsigned int hash, int *defragged) {
|
||||
dictEntry **deref = dictFindEntryRefByPtrAndHash(d, oldkey, hash);
|
||||
if (deref) {
|
||||
dictEntry *de = *deref;
|
||||
dictEntry *newde = activeDefragAlloc(de);
|
||||
if (newde) {
|
||||
de = *deref = newde;
|
||||
(*defragged)++;
|
||||
}
|
||||
if (newkey)
|
||||
de->key = newkey;
|
||||
return de;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* for each key we scan in the main dict, this function will attempt to defrag
|
||||
* all the various pointers it has. Returns a stat of how many pointers were
|
||||
* moved. */
|
||||
int defragKey(redisDb *db, dictEntry *de) {
|
||||
sds keysds = dictGetKey(de);
|
||||
robj *newob, *ob;
|
||||
unsigned char *newzl;
|
||||
dict *d;
|
||||
dictIterator *di;
|
||||
int defragged = 0;
|
||||
sds newsds;
|
||||
|
||||
/* Try to defrag the key name. */
|
||||
newsds = activeDefragSds(keysds);
|
||||
if (newsds)
|
||||
defragged++, de->key = newsds;
|
||||
if (dictSize(db->expires)) {
|
||||
/* Dirty code:
|
||||
* I can't search in db->expires for that key after i already released
|
||||
* the pointer it holds it won't be able to do the string compare */
|
||||
uint64_t hash = dictGetHash(db->dict, de->key);
|
||||
replaceSateliteDictKeyPtrAndOrDefragDictEntry(db->expires, keysds, newsds, hash, &defragged);
|
||||
}
|
||||
|
||||
/* Try to defrag robj and / or string value. */
|
||||
ob = dictGetVal(de);
|
||||
if ((newob = activeDefragStringOb(ob, &defragged))) {
|
||||
de->v.val = newob;
|
||||
ob = newob;
|
||||
}
|
||||
|
||||
if (ob->type == OBJ_STRING) {
|
||||
/* Already handled in activeDefragStringOb. */
|
||||
} else if (ob->type == OBJ_LIST) {
|
||||
if (ob->encoding == OBJ_ENCODING_QUICKLIST) {
|
||||
quicklist *ql = ob->ptr, *newql;
|
||||
quicklistNode *node = ql->head, *newnode;
|
||||
if ((newql = activeDefragAlloc(ql)))
|
||||
defragged++, ob->ptr = ql = newql;
|
||||
while (node) {
|
||||
if ((newnode = activeDefragAlloc(node))) {
|
||||
if (newnode->prev)
|
||||
newnode->prev->next = newnode;
|
||||
else
|
||||
ql->head = newnode;
|
||||
if (newnode->next)
|
||||
newnode->next->prev = newnode;
|
||||
else
|
||||
ql->tail = newnode;
|
||||
node = newnode;
|
||||
defragged++;
|
||||
}
|
||||
if ((newzl = activeDefragAlloc(node->zl)))
|
||||
defragged++, node->zl = newzl;
|
||||
node = node->next;
|
||||
}
|
||||
} else if (ob->encoding == OBJ_ENCODING_ZIPLIST) {
|
||||
if ((newzl = activeDefragAlloc(ob->ptr)))
|
||||
defragged++, ob->ptr = newzl;
|
||||
} else {
|
||||
serverPanic("Unknown list encoding");
|
||||
}
|
||||
} else if (ob->type == OBJ_SET) {
|
||||
if (ob->encoding == OBJ_ENCODING_HT) {
|
||||
d = ob->ptr;
|
||||
di = dictGetIterator(d);
|
||||
while((de = dictNext(di)) != NULL) {
|
||||
sds sdsele = dictGetKey(de);
|
||||
if ((newsds = activeDefragSds(sdsele)))
|
||||
defragged++, de->key = newsds;
|
||||
defragged += dictIterDefragEntry(di);
|
||||
}
|
||||
dictReleaseIterator(di);
|
||||
dictDefragTables((dict**)&ob->ptr);
|
||||
} else if (ob->encoding == OBJ_ENCODING_INTSET) {
|
||||
intset *is = ob->ptr;
|
||||
intset *newis = activeDefragAlloc(is);
|
||||
if (newis)
|
||||
defragged++, ob->ptr = newis;
|
||||
} else {
|
||||
serverPanic("Unknown set encoding");
|
||||
}
|
||||
} else if (ob->type == OBJ_ZSET) {
|
||||
if (ob->encoding == OBJ_ENCODING_ZIPLIST) {
|
||||
if ((newzl = activeDefragAlloc(ob->ptr)))
|
||||
defragged++, ob->ptr = newzl;
|
||||
} else if (ob->encoding == OBJ_ENCODING_SKIPLIST) {
|
||||
zset *zs = (zset*)ob->ptr;
|
||||
zset *newzs;
|
||||
zskiplist *newzsl;
|
||||
struct zskiplistNode *newheader;
|
||||
if ((newzs = activeDefragAlloc(zs)))
|
||||
defragged++, ob->ptr = zs = newzs;
|
||||
if ((newzsl = activeDefragAlloc(zs->zsl)))
|
||||
defragged++, zs->zsl = newzsl;
|
||||
if ((newheader = activeDefragAlloc(zs->zsl->header)))
|
||||
defragged++, zs->zsl->header = newheader;
|
||||
d = zs->dict;
|
||||
di = dictGetIterator(d);
|
||||
while((de = dictNext(di)) != NULL) {
|
||||
double* newscore;
|
||||
sds sdsele = dictGetKey(de);
|
||||
if ((newsds = activeDefragSds(sdsele)))
|
||||
defragged++, de->key = newsds;
|
||||
newscore = zslDefrag(zs->zsl, *(double*)dictGetVal(de), sdsele, newsds);
|
||||
if (newscore) {
|
||||
dictSetVal(d, de, newscore);
|
||||
defragged++;
|
||||
}
|
||||
defragged += dictIterDefragEntry(di);
|
||||
}
|
||||
dictReleaseIterator(di);
|
||||
dictDefragTables(&zs->dict);
|
||||
} else {
|
||||
serverPanic("Unknown sorted set encoding");
|
||||
}
|
||||
} else if (ob->type == OBJ_HASH) {
|
||||
if (ob->encoding == OBJ_ENCODING_ZIPLIST) {
|
||||
if ((newzl = activeDefragAlloc(ob->ptr)))
|
||||
defragged++, ob->ptr = newzl;
|
||||
} else if (ob->encoding == OBJ_ENCODING_HT) {
|
||||
d = ob->ptr;
|
||||
di = dictGetIterator(d);
|
||||
while((de = dictNext(di)) != NULL) {
|
||||
sds sdsele = dictGetKey(de);
|
||||
if ((newsds = activeDefragSds(sdsele)))
|
||||
defragged++, de->key = newsds;
|
||||
sdsele = dictGetVal(de);
|
||||
if ((newsds = activeDefragSds(sdsele)))
|
||||
defragged++, de->v.val = newsds;
|
||||
defragged += dictIterDefragEntry(di);
|
||||
}
|
||||
dictReleaseIterator(di);
|
||||
dictDefragTables((dict**)&ob->ptr);
|
||||
} else {
|
||||
serverPanic("Unknown hash encoding");
|
||||
}
|
||||
} else if (ob->type == OBJ_MODULE) {
|
||||
/* Currently defragmenting modules private data types
|
||||
* is not supported. */
|
||||
} else {
|
||||
serverPanic("Unknown object type");
|
||||
}
|
||||
return defragged;
|
||||
}
|
||||
|
||||
/* Defrag scan callback for the main db dictionary. */
|
||||
void defragScanCallback(void *privdata, const dictEntry *de) {
|
||||
int defragged = defragKey((redisDb*)privdata, (dictEntry*)de);
|
||||
server.stat_active_defrag_hits += defragged;
|
||||
if(defragged)
|
||||
server.stat_active_defrag_key_hits++;
|
||||
else
|
||||
server.stat_active_defrag_key_misses++;
|
||||
}
|
||||
|
||||
/* Defrag scan callback for for each hash table bicket,
|
||||
* used in order to defrag the dictEntry allocations. */
|
||||
void defragDictBucketCallback(void *privdata, dictEntry **bucketref) {
|
||||
UNUSED(privdata);
|
||||
while(*bucketref) {
|
||||
dictEntry *de = *bucketref, *newde;
|
||||
if ((newde = activeDefragAlloc(de))) {
|
||||
*bucketref = newde;
|
||||
}
|
||||
bucketref = &(*bucketref)->next;
|
||||
}
|
||||
}
|
||||
|
||||
/* Utility function to get the fragmentation ratio from jemalloc.
|
||||
* It is critical to do that by comparing only heap maps that belown to
|
||||
* jemalloc, and skip ones the jemalloc keeps as spare. Since we use this
|
||||
* fragmentation ratio in order to decide if a defrag action should be taken
|
||||
* or not, a false detection can cause the defragmenter to waste a lot of CPU
|
||||
* without the possibility of getting any results. */
|
||||
float getAllocatorFragmentation(size_t *out_frag_bytes) {
|
||||
size_t epoch = 1, allocated = 0, resident = 0, active = 0, sz = sizeof(size_t);
|
||||
/* Update the statistics cached by mallctl. */
|
||||
je_mallctl("epoch", &epoch, &sz, &epoch, sz);
|
||||
/* Unlike RSS, this does not include RSS from shared libraries and other non
|
||||
* heap mappings. */
|
||||
je_mallctl("stats.resident", &resident, &sz, NULL, 0);
|
||||
/* Unlike resident, this doesn't not include the pages jemalloc reserves
|
||||
* for re-use (purge will clean that). */
|
||||
je_mallctl("stats.active", &active, &sz, NULL, 0);
|
||||
/* Unlike zmalloc_used_memory, this matches the stats.resident by taking
|
||||
* into account all allocations done by this process (not only zmalloc). */
|
||||
je_mallctl("stats.allocated", &allocated, &sz, NULL, 0);
|
||||
float frag_pct = ((float)active / allocated)*100 - 100;
|
||||
size_t frag_bytes = active - allocated;
|
||||
float rss_pct = ((float)resident / allocated)*100 - 100;
|
||||
size_t rss_bytes = resident - allocated;
|
||||
if(out_frag_bytes)
|
||||
*out_frag_bytes = frag_bytes;
|
||||
serverLog(LL_DEBUG,
|
||||
"allocated=%zu, active=%zu, resident=%zu, frag=%.0f%% (%.0f%% rss), frag_bytes=%zu (%zu%% rss)",
|
||||
allocated, active, resident, frag_pct, rss_pct, frag_bytes, rss_bytes);
|
||||
return frag_pct;
|
||||
}
|
||||
|
||||
#define INTERPOLATE(x, x1, x2, y1, y2) ( (y1) + ((x)-(x1)) * ((y2)-(y1)) / ((x2)-(x1)) )
|
||||
#define LIMIT(y, min, max) ((y)<(min)? min: ((y)>(max)? max: (y)))
|
||||
|
||||
/* Perform incremental defragmentation work from the serverCron.
|
||||
* This works in a similar way to activeExpireCycle, in the sense that
|
||||
* we do incremental work across calls. */
|
||||
void activeDefragCycle(void) {
|
||||
static int current_db = -1;
|
||||
static unsigned long cursor = 0;
|
||||
static redisDb *db = NULL;
|
||||
static long long start_scan, start_stat;
|
||||
unsigned int iterations = 0;
|
||||
unsigned long long defragged = server.stat_active_defrag_hits;
|
||||
long long start, timelimit;
|
||||
|
||||
if (server.aof_child_pid!=-1 || server.rdb_child_pid!=-1)
|
||||
return; /* Defragging memory while there's a fork will just do damage. */
|
||||
|
||||
/* Once a second, check if we the fragmentation justfies starting a scan
|
||||
* or making it more aggressive. */
|
||||
run_with_period(1000) {
|
||||
size_t frag_bytes;
|
||||
float frag_pct = getAllocatorFragmentation(&frag_bytes);
|
||||
/* If we're not already running, and below the threshold, exit. */
|
||||
if (!server.active_defrag_running) {
|
||||
if(frag_pct < server.active_defrag_threshold_lower || frag_bytes < server.active_defrag_ignore_bytes)
|
||||
return;
|
||||
}
|
||||
|
||||
/* Calculate the adaptive aggressiveness of the defrag */
|
||||
int cpu_pct = INTERPOLATE(frag_pct,
|
||||
server.active_defrag_threshold_lower,
|
||||
server.active_defrag_threshold_upper,
|
||||
server.active_defrag_cycle_min,
|
||||
server.active_defrag_cycle_max);
|
||||
cpu_pct = LIMIT(cpu_pct,
|
||||
server.active_defrag_cycle_min,
|
||||
server.active_defrag_cycle_max);
|
||||
/* We allow increasing the aggressiveness during a scan, but don't
|
||||
* reduce it. */
|
||||
if (!server.active_defrag_running ||
|
||||
cpu_pct > server.active_defrag_running)
|
||||
{
|
||||
server.active_defrag_running = cpu_pct;
|
||||
serverLog(LL_VERBOSE,
|
||||
"Starting active defrag, frag=%.0f%%, frag_bytes=%zu, cpu=%d%%",
|
||||
frag_pct, frag_bytes, cpu_pct);
|
||||
}
|
||||
}
|
||||
if (!server.active_defrag_running)
|
||||
return;
|
||||
|
||||
/* See activeExpireCycle for how timelimit is handled. */
|
||||
start = ustime();
|
||||
timelimit = 1000000*server.active_defrag_running/server.hz/100;
|
||||
if (timelimit <= 0) timelimit = 1;
|
||||
|
||||
do {
|
||||
if (!cursor) {
|
||||
/* Move on to next database, and stop if we reached the last one. */
|
||||
if (++current_db >= server.dbnum) {
|
||||
long long now = ustime();
|
||||
size_t frag_bytes;
|
||||
float frag_pct = getAllocatorFragmentation(&frag_bytes);
|
||||
serverLog(LL_VERBOSE,
|
||||
"Active defrag done in %dms, reallocated=%d, frag=%.0f%%, frag_bytes=%zu",
|
||||
(int)((now - start_scan)/1000), (int)(server.stat_active_defrag_hits - start_stat), frag_pct, frag_bytes);
|
||||
|
||||
start_scan = now;
|
||||
current_db = -1;
|
||||
cursor = 0;
|
||||
db = NULL;
|
||||
server.active_defrag_running = 0;
|
||||
return;
|
||||
}
|
||||
else if (current_db==0) {
|
||||
/* Start a scan from the first database. */
|
||||
start_scan = ustime();
|
||||
start_stat = server.stat_active_defrag_hits;
|
||||
}
|
||||
|
||||
db = &server.db[current_db];
|
||||
cursor = 0;
|
||||
}
|
||||
|
||||
do {
|
||||
cursor = dictScan(db->dict, cursor, defragScanCallback, defragDictBucketCallback, db);
|
||||
/* Once in 16 scan iterations, or 1000 pointer reallocations
|
||||
* (if we have a lot of pointers in one hash bucket), check if we
|
||||
* reached the tiem limit. */
|
||||
if (cursor && (++iterations > 16 || server.stat_active_defrag_hits - defragged > 1000)) {
|
||||
if ((ustime() - start) > timelimit) {
|
||||
return;
|
||||
}
|
||||
iterations = 0;
|
||||
defragged = server.stat_active_defrag_hits;
|
||||
}
|
||||
} while(cursor);
|
||||
} while(1);
|
||||
}
|
||||
|
||||
#else /* HAVE_DEFRAG */
|
||||
|
||||
void activeDefragCycle(void) {
|
||||
/* Not implemented yet. */
|
||||
}
|
||||
|
||||
#endif
|
||||
+56
-72
@@ -37,11 +37,11 @@
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
#include <limits.h>
|
||||
#include <sys/time.h>
|
||||
#include <ctype.h>
|
||||
|
||||
#include "dict.h"
|
||||
#include "zmalloc.h"
|
||||
@@ -66,82 +66,33 @@ static unsigned int dict_force_resize_ratio = 5;
|
||||
|
||||
static int _dictExpandIfNeeded(dict *ht);
|
||||
static unsigned long _dictNextPower(unsigned long size);
|
||||
static int _dictKeyIndex(dict *ht, const void *key, unsigned int hash, dictEntry **existing);
|
||||
static long _dictKeyIndex(dict *ht, const void *key, uint64_t hash, dictEntry **existing);
|
||||
static int _dictInit(dict *ht, dictType *type, void *privDataPtr);
|
||||
|
||||
/* -------------------------- hash functions -------------------------------- */
|
||||
|
||||
static uint32_t dict_hash_function_seed = 5381;
|
||||
static uint8_t dict_hash_function_seed[16];
|
||||
|
||||
void dictSetHashFunctionSeed(uint32_t seed) {
|
||||
dict_hash_function_seed = seed;
|
||||
void dictSetHashFunctionSeed(uint8_t *seed) {
|
||||
memcpy(dict_hash_function_seed,seed,sizeof(dict_hash_function_seed));
|
||||
}
|
||||
|
||||
uint32_t dictGetHashFunctionSeed(void) {
|
||||
uint8_t *dictGetHashFunctionSeed(void) {
|
||||
return dict_hash_function_seed;
|
||||
}
|
||||
|
||||
/* MurmurHash2, by Austin Appleby
|
||||
* Note - This code makes a few assumptions about how your machine behaves -
|
||||
* 1. We can read a 4-byte value from any address without crashing
|
||||
* 2. sizeof(int) == 4
|
||||
*
|
||||
* And it has a few limitations -
|
||||
*
|
||||
* 1. It will not work incrementally.
|
||||
* 2. It will not produce the same results on little-endian and big-endian
|
||||
* machines.
|
||||
*/
|
||||
unsigned int dictGenHashFunction(const void *key, int len) {
|
||||
/* 'm' and 'r' are mixing constants generated offline.
|
||||
They're not really 'magic', they just happen to work well. */
|
||||
uint32_t seed = dict_hash_function_seed;
|
||||
const uint32_t m = 0x5bd1e995;
|
||||
const int r = 24;
|
||||
/* The default hashing function uses SipHash implementation
|
||||
* in siphash.c. */
|
||||
|
||||
/* Initialize the hash to a 'random' value */
|
||||
uint32_t h = seed ^ len;
|
||||
uint64_t siphash(const uint8_t *in, const size_t inlen, const uint8_t *k);
|
||||
uint64_t siphash_nocase(const uint8_t *in, const size_t inlen, const uint8_t *k);
|
||||
|
||||
/* Mix 4 bytes at a time into the hash */
|
||||
const unsigned char *data = (const unsigned char *)key;
|
||||
|
||||
while(len >= 4) {
|
||||
uint32_t k = *(uint32_t*)data;
|
||||
|
||||
k *= m;
|
||||
k ^= k >> r;
|
||||
k *= m;
|
||||
|
||||
h *= m;
|
||||
h ^= k;
|
||||
|
||||
data += 4;
|
||||
len -= 4;
|
||||
}
|
||||
|
||||
/* Handle the last few bytes of the input array */
|
||||
switch(len) {
|
||||
case 3: h ^= data[2] << 16;
|
||||
case 2: h ^= data[1] << 8;
|
||||
case 1: h ^= data[0]; h *= m;
|
||||
};
|
||||
|
||||
/* Do a few final mixes of the hash to ensure the last few
|
||||
* bytes are well-incorporated. */
|
||||
h ^= h >> 13;
|
||||
h *= m;
|
||||
h ^= h >> 15;
|
||||
|
||||
return (unsigned int)h;
|
||||
uint64_t dictGenHashFunction(const void *key, int len) {
|
||||
return siphash(key,len,dict_hash_function_seed);
|
||||
}
|
||||
|
||||
/* And a case insensitive hash function (based on djb hash) */
|
||||
unsigned int dictGenCaseHashFunction(const unsigned char *buf, int len) {
|
||||
unsigned int hash = (unsigned int)dict_hash_function_seed;
|
||||
|
||||
while (len--)
|
||||
hash = ((hash << 5) + hash) + (tolower(*buf++)); /* hash * 33 + c */
|
||||
return hash;
|
||||
uint64_t dictGenCaseHashFunction(const unsigned char *buf, int len) {
|
||||
return siphash_nocase(buf,len,dict_hash_function_seed);
|
||||
}
|
||||
|
||||
/* ----------------------------- API implementation ------------------------- */
|
||||
@@ -251,7 +202,7 @@ int dictRehash(dict *d, int n) {
|
||||
de = d->ht[0].table[d->rehashidx];
|
||||
/* Move all the keys in this bucket from the old to the new hash HT */
|
||||
while(de) {
|
||||
unsigned int h;
|
||||
uint64_t h;
|
||||
|
||||
nextde = de->next;
|
||||
/* Get the index in the new hash table */
|
||||
@@ -340,7 +291,7 @@ int dictAdd(dict *d, void *key, void *val)
|
||||
*/
|
||||
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
|
||||
{
|
||||
int index;
|
||||
long index;
|
||||
dictEntry *entry;
|
||||
dictht *ht;
|
||||
|
||||
@@ -411,7 +362,7 @@ dictEntry *dictAddOrFind(dict *d, void *key) {
|
||||
* dictDelete() and dictUnlink(), please check the top comment
|
||||
* of those functions. */
|
||||
static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {
|
||||
unsigned int h, idx;
|
||||
uint64_t h, idx;
|
||||
dictEntry *he, *prevHe;
|
||||
int table;
|
||||
|
||||
@@ -525,7 +476,7 @@ void dictRelease(dict *d)
|
||||
dictEntry *dictFind(dict *d, const void *key)
|
||||
{
|
||||
dictEntry *he;
|
||||
unsigned int h, idx, table;
|
||||
uint64_t h, idx, table;
|
||||
|
||||
if (d->ht[0].used + d->ht[1].used == 0) return NULL; /* dict is empty */
|
||||
if (dictIsRehashing(d)) _dictRehashStep(d);
|
||||
@@ -659,7 +610,7 @@ void dictReleaseIterator(dictIterator *iter)
|
||||
dictEntry *dictGetRandomKey(dict *d)
|
||||
{
|
||||
dictEntry *he, *orighe;
|
||||
unsigned int h;
|
||||
unsigned long h;
|
||||
int listlen, listele;
|
||||
|
||||
if (dictSize(d) == 0) return NULL;
|
||||
@@ -885,6 +836,7 @@ static unsigned long rev(unsigned long v) {
|
||||
unsigned long dictScan(dict *d,
|
||||
unsigned long v,
|
||||
dictScanFunction *fn,
|
||||
dictScanBucketFunction* bucketfn,
|
||||
void *privdata)
|
||||
{
|
||||
dictht *t0, *t1;
|
||||
@@ -898,6 +850,7 @@ unsigned long dictScan(dict *d,
|
||||
m0 = t0->sizemask;
|
||||
|
||||
/* Emit entries at cursor */
|
||||
if (bucketfn) bucketfn(privdata, &t0->table[v & m0]);
|
||||
de = t0->table[v & m0];
|
||||
while (de) {
|
||||
next = de->next;
|
||||
@@ -919,6 +872,7 @@ unsigned long dictScan(dict *d,
|
||||
m1 = t1->sizemask;
|
||||
|
||||
/* Emit entries at cursor */
|
||||
if (bucketfn) bucketfn(privdata, &t0->table[v & m0]);
|
||||
de = t0->table[v & m0];
|
||||
while (de) {
|
||||
next = de->next;
|
||||
@@ -930,6 +884,7 @@ unsigned long dictScan(dict *d,
|
||||
* of the index pointed to by the cursor in the smaller table */
|
||||
do {
|
||||
/* Emit entries at cursor */
|
||||
if (bucketfn) bucketfn(privdata, &t1->table[v & m1]);
|
||||
de = t1->table[v & m1];
|
||||
while (de) {
|
||||
next = de->next;
|
||||
@@ -985,7 +940,7 @@ static unsigned long _dictNextPower(unsigned long size)
|
||||
{
|
||||
unsigned long i = DICT_HT_INITIAL_SIZE;
|
||||
|
||||
if (size >= LONG_MAX) return LONG_MAX;
|
||||
if (size >= LONG_MAX) return LONG_MAX + 1LU;
|
||||
while(1) {
|
||||
if (i >= size)
|
||||
return i;
|
||||
@@ -1000,9 +955,9 @@ static unsigned long _dictNextPower(unsigned long size)
|
||||
*
|
||||
* Note that if we are in the process of rehashing the hash table, the
|
||||
* index is always returned in the context of the second (new) hash table. */
|
||||
static int _dictKeyIndex(dict *d, const void *key, unsigned int hash, dictEntry **existing)
|
||||
static long _dictKeyIndex(dict *d, const void *key, uint64_t hash, dictEntry **existing)
|
||||
{
|
||||
unsigned int idx, table;
|
||||
unsigned long idx, table;
|
||||
dictEntry *he;
|
||||
if (existing) *existing = NULL;
|
||||
|
||||
@@ -1040,6 +995,35 @@ void dictDisableResize(void) {
|
||||
dict_can_resize = 0;
|
||||
}
|
||||
|
||||
uint64_t dictGetHash(dict *d, const void *key) {
|
||||
return dictHashKey(d, key);
|
||||
}
|
||||
|
||||
/* Finds the dictEntry reference by using pointer and pre-calculated hash.
|
||||
* oldkey is a dead pointer and should not be accessed.
|
||||
* the hash value should be provided using dictGetHash.
|
||||
* no string / key comparison is performed.
|
||||
* return value is the reference to the dictEntry if found, or NULL if not found. */
|
||||
dictEntry **dictFindEntryRefByPtrAndHash(dict *d, const void *oldptr, uint64_t hash) {
|
||||
dictEntry *he, **heref;
|
||||
unsigned long idx, table;
|
||||
|
||||
if (d->ht[0].used + d->ht[1].used == 0) return NULL; /* dict is empty */
|
||||
for (table = 0; table <= 1; table++) {
|
||||
idx = hash & d->ht[table].sizemask;
|
||||
heref = &d->ht[table].table[idx];
|
||||
he = *heref;
|
||||
while(he) {
|
||||
if (oldptr==he->key)
|
||||
return heref;
|
||||
heref = &he->next;
|
||||
he = *heref;
|
||||
}
|
||||
if (!dictIsRehashing(d)) return NULL;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ------------------------------- Debugging ---------------------------------*/
|
||||
|
||||
#define DICT_STATS_VECTLEN 50
|
||||
@@ -1125,7 +1109,7 @@ void dictGetStats(char *buf, size_t bufsize, dict *d) {
|
||||
|
||||
#include "sds.h"
|
||||
|
||||
unsigned int hashCallback(const void *key) {
|
||||
uint64_t hashCallback(const void *key) {
|
||||
return dictGenHashFunction((unsigned char*)key, sdslen((char*)key));
|
||||
}
|
||||
|
||||
|
||||
+9
-6
@@ -56,7 +56,7 @@ typedef struct dictEntry {
|
||||
} dictEntry;
|
||||
|
||||
typedef struct dictType {
|
||||
unsigned int (*hashFunction)(const void *key);
|
||||
uint64_t (*hashFunction)(const void *key);
|
||||
void *(*keyDup)(void *privdata, const void *key);
|
||||
void *(*valDup)(void *privdata, const void *obj);
|
||||
int (*keyCompare)(void *privdata, const void *key1, const void *key2);
|
||||
@@ -95,6 +95,7 @@ typedef struct dictIterator {
|
||||
} dictIterator;
|
||||
|
||||
typedef void (dictScanFunction)(void *privdata, const dictEntry *de);
|
||||
typedef void (dictScanBucketFunction)(void *privdata, dictEntry **bucketref);
|
||||
|
||||
/* This is the initial size of every hash table */
|
||||
#define DICT_HT_INITIAL_SIZE 4
|
||||
@@ -167,16 +168,18 @@ void dictReleaseIterator(dictIterator *iter);
|
||||
dictEntry *dictGetRandomKey(dict *d);
|
||||
unsigned int dictGetSomeKeys(dict *d, dictEntry **des, unsigned int count);
|
||||
void dictGetStats(char *buf, size_t bufsize, dict *d);
|
||||
unsigned int dictGenHashFunction(const void *key, int len);
|
||||
unsigned int dictGenCaseHashFunction(const unsigned char *buf, int len);
|
||||
uint64_t dictGenHashFunction(const void *key, int len);
|
||||
uint64_t dictGenCaseHashFunction(const unsigned char *buf, int len);
|
||||
void dictEmpty(dict *d, void(callback)(void*));
|
||||
void dictEnableResize(void);
|
||||
void dictDisableResize(void);
|
||||
int dictRehash(dict *d, int n);
|
||||
int dictRehashMilliseconds(dict *d, int ms);
|
||||
void dictSetHashFunctionSeed(unsigned int initval);
|
||||
unsigned int dictGetHashFunctionSeed(void);
|
||||
unsigned long dictScan(dict *d, unsigned long v, dictScanFunction *fn, void *privdata);
|
||||
void dictSetHashFunctionSeed(uint8_t *seed);
|
||||
uint8_t *dictGetHashFunctionSeed(void);
|
||||
unsigned long dictScan(dict *d, unsigned long v, dictScanFunction *fn, dictScanBucketFunction *bucketfn, void *privdata);
|
||||
uint64_t dictGetHash(dict *d, const void *key);
|
||||
dictEntry **dictFindEntryRefByPtrAndHash(dict *d, const void *oldptr, uint64_t hash);
|
||||
|
||||
/* Hash table types */
|
||||
extern dictType dictTypeHeapStringCopyKey;
|
||||
|
||||
+73
-35
@@ -32,6 +32,7 @@
|
||||
|
||||
#include "server.h"
|
||||
#include "bio.h"
|
||||
#include "atomicvar.h"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Data structures
|
||||
@@ -59,8 +60,6 @@ struct evictionPoolEntry {
|
||||
|
||||
static struct evictionPoolEntry *EvictionPoolLRU;
|
||||
|
||||
unsigned long LFUDecrAndReturn(robj *o);
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Implementation of eviction, aging and LRU
|
||||
* --------------------------------------------------------------------------*/
|
||||
@@ -72,6 +71,20 @@ unsigned int getLRUClock(void) {
|
||||
return (mstime()/LRU_CLOCK_RESOLUTION) & LRU_CLOCK_MAX;
|
||||
}
|
||||
|
||||
/* This function is used to obtain the current LRU clock.
|
||||
* If the current resolution is lower than the frequency we refresh the
|
||||
* LRU clock (as it should be in production servers) we return the
|
||||
* precomputed value, otherwise we need to resort to a system call. */
|
||||
unsigned int LRU_CLOCK(void) {
|
||||
unsigned int lruclock;
|
||||
if (1000/server.hz <= LRU_CLOCK_RESOLUTION) {
|
||||
atomicGet(server.lruclock,lruclock);
|
||||
} else {
|
||||
lruclock = getLRUClock();
|
||||
}
|
||||
return lruclock;
|
||||
}
|
||||
|
||||
/* Given an object returns the min number of milliseconds the object was never
|
||||
* requested, using an approximated LRU algorithm. */
|
||||
unsigned long long estimateObjectIdleTime(robj *o) {
|
||||
@@ -287,8 +300,8 @@ unsigned long LFUGetTimeInMinutes(void) {
|
||||
return (server.unixtime/60) & 65535;
|
||||
}
|
||||
|
||||
/* Given an object last decrement time, compute the minimum number of minutes
|
||||
* that elapsed since the last decrement. Handle overflow (ldt greater than
|
||||
/* Given an object last access time, compute the minimum number of minutes
|
||||
* that elapsed since the last access. Handle overflow (ldt greater than
|
||||
* the current 16 bits minutes time) considering the time as wrapping
|
||||
* exactly once. */
|
||||
unsigned long LFUTimeElapsed(unsigned long ldt) {
|
||||
@@ -309,25 +322,22 @@ uint8_t LFULogIncr(uint8_t counter) {
|
||||
return counter;
|
||||
}
|
||||
|
||||
/* If the object decrement time is reached, decrement the LFU counter and
|
||||
* update the decrement time field. Return the object frequency counter.
|
||||
/* If the object decrement time is reached decrement the LFU counter but
|
||||
* do not update LFU fields of the object, we update the access time
|
||||
* and counter in an explicit way when the object is really accessed.
|
||||
* And we will times halve the counter according to the times of
|
||||
* elapsed time than server.lfu_decay_time.
|
||||
* Return the object frequency counter.
|
||||
*
|
||||
* This function is used in order to scan the dataset for the best object
|
||||
* to fit: as we check for the candidate, we incrementally decrement the
|
||||
* counter of the scanned objects if needed. */
|
||||
#define LFU_DECR_INTERVAL 1
|
||||
unsigned long LFUDecrAndReturn(robj *o) {
|
||||
unsigned long ldt = o->lru >> 8;
|
||||
unsigned long counter = o->lru & 255;
|
||||
if (LFUTimeElapsed(ldt) >= server.lfu_decay_time && counter) {
|
||||
if (counter > LFU_INIT_VAL*2) {
|
||||
counter /= 2;
|
||||
if (counter < LFU_INIT_VAL*2) counter = LFU_INIT_VAL*2;
|
||||
} else {
|
||||
counter--;
|
||||
}
|
||||
o->lru = (LFUGetTimeInMinutes()<<8) | counter;
|
||||
}
|
||||
unsigned long num_periods = server.lfu_decay_time ? LFUTimeElapsed(ldt) / server.lfu_decay_time : 0;
|
||||
if (num_periods)
|
||||
counter = (num_periods > counter) ? 0 : counter - num_periods;
|
||||
return counter;
|
||||
}
|
||||
|
||||
@@ -336,11 +346,39 @@ unsigned long LFUDecrAndReturn(robj *o) {
|
||||
* server when there is data to add in order to make space if needed.
|
||||
* --------------------------------------------------------------------------*/
|
||||
|
||||
/* We don't want to count AOF buffers and slaves output buffers as
|
||||
* used memory: the eviction should use mostly data size. This function
|
||||
* returns the sum of AOF and slaves buffer. */
|
||||
size_t freeMemoryGetNotCountedMemory(void) {
|
||||
size_t overhead = 0;
|
||||
int slaves = listLength(server.slaves);
|
||||
|
||||
if (slaves) {
|
||||
listIter li;
|
||||
listNode *ln;
|
||||
|
||||
listRewind(server.slaves,&li);
|
||||
while((ln = listNext(&li))) {
|
||||
client *slave = listNodeValue(ln);
|
||||
overhead += getClientOutputBufferMemoryUsage(slave);
|
||||
}
|
||||
}
|
||||
if (server.aof_state != AOF_OFF) {
|
||||
overhead += sdslen(server.aof_buf)+aofRewriteBufferSize();
|
||||
}
|
||||
return overhead;
|
||||
}
|
||||
|
||||
int freeMemoryIfNeeded(void) {
|
||||
size_t mem_reported, mem_used, mem_tofree, mem_freed;
|
||||
int slaves = listLength(server.slaves);
|
||||
mstime_t latency, eviction_latency;
|
||||
long long delta;
|
||||
int slaves = listLength(server.slaves);
|
||||
|
||||
/* When clients are paused the dataset should be static not just from the
|
||||
* POV of clients not being able to write, but also from the POV of
|
||||
* expires and evictions of keys not being performed. */
|
||||
if (clientsArePaused()) return C_OK;
|
||||
|
||||
/* Check if we are over the memory usage limit. If we are not, no need
|
||||
* to subtract the slaves output buffers. We can just return ASAP. */
|
||||
@@ -350,24 +388,8 @@ int freeMemoryIfNeeded(void) {
|
||||
/* Remove the size of slaves output buffers and AOF buffer from the
|
||||
* count of used memory. */
|
||||
mem_used = mem_reported;
|
||||
if (slaves) {
|
||||
listIter li;
|
||||
listNode *ln;
|
||||
|
||||
listRewind(server.slaves,&li);
|
||||
while((ln = listNext(&li))) {
|
||||
client *slave = listNodeValue(ln);
|
||||
unsigned long obuf_bytes = getClientOutputBufferMemoryUsage(slave);
|
||||
if (obuf_bytes > mem_used)
|
||||
mem_used = 0;
|
||||
else
|
||||
mem_used -= obuf_bytes;
|
||||
}
|
||||
}
|
||||
if (server.aof_state != AOF_OFF) {
|
||||
mem_used -= sdslen(server.aof_buf);
|
||||
mem_used -= aofRewriteBufferSize();
|
||||
}
|
||||
size_t overhead = freeMemoryGetNotCountedMemory();
|
||||
mem_used = (mem_used > overhead) ? mem_used-overhead : 0;
|
||||
|
||||
/* Check if we are still over the memory limit. */
|
||||
if (mem_used <= server.maxmemory) return C_OK;
|
||||
@@ -498,6 +520,22 @@ int freeMemoryIfNeeded(void) {
|
||||
* deliver data to the slaves fast enough, so we force the
|
||||
* transmission here inside the loop. */
|
||||
if (slaves) flushSlavesOutputBuffers();
|
||||
|
||||
/* Normally our stop condition is the ability to release
|
||||
* a fixed, pre-computed amount of memory. However when we
|
||||
* are deleting objects in another thread, it's better to
|
||||
* check, from time to time, if we already reached our target
|
||||
* memory, since the "mem_freed" amount is computed only
|
||||
* across the dbAsyncDelete() call, while the thread can
|
||||
* release the memory all the time. */
|
||||
if (server.lazyfree_lazy_eviction && !(keys_freed % 16)) {
|
||||
overhead = freeMemoryGetNotCountedMemory();
|
||||
mem_used = zmalloc_used_memory();
|
||||
mem_used = (mem_used > overhead) ? mem_used-overhead : 0;
|
||||
if (mem_used <= server.maxmemory) {
|
||||
mem_freed = mem_tofree;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!keys_freed) {
|
||||
|
||||
+169
-16
@@ -92,7 +92,7 @@ int activeExpireCycleTryExpire(redisDb *db, dictEntry *de, long long now) {
|
||||
*
|
||||
* If type is ACTIVE_EXPIRE_CYCLE_SLOW, that normal expire cycle is
|
||||
* executed, where the time limit is a percentage of the REDIS_HZ period
|
||||
* as specified by the REDIS_EXPIRELOOKUPS_TIME_PERC define. */
|
||||
* as specified by the ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC define. */
|
||||
|
||||
void activeExpireCycle(int type) {
|
||||
/* This function has some global state in order to continue the work
|
||||
@@ -103,7 +103,12 @@ void activeExpireCycle(int type) {
|
||||
|
||||
int j, iteration = 0;
|
||||
int dbs_per_call = CRON_DBS_PER_CALL;
|
||||
long long start = ustime(), timelimit;
|
||||
long long start = ustime(), timelimit, elapsed;
|
||||
|
||||
/* When clients are paused the dataset should be static not just from the
|
||||
* POV of clients not being able to write, but also from the POV of
|
||||
* expires and evictions of keys not being performed. */
|
||||
if (clientsArePaused()) return;
|
||||
|
||||
if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
|
||||
/* Don't start a fast cycle if the previous cycle did not exited
|
||||
@@ -135,7 +140,7 @@ void activeExpireCycle(int type) {
|
||||
if (type == ACTIVE_EXPIRE_CYCLE_FAST)
|
||||
timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; /* in microseconds. */
|
||||
|
||||
for (j = 0; j < dbs_per_call; j++) {
|
||||
for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) {
|
||||
int expired;
|
||||
redisDb *db = server.db+(current_db % server.dbnum);
|
||||
|
||||
@@ -150,6 +155,7 @@ void activeExpireCycle(int type) {
|
||||
unsigned long num, slots;
|
||||
long long now, ttl_sum;
|
||||
int ttl_samples;
|
||||
iteration++;
|
||||
|
||||
/* If there is nothing to expire try next DB ASAP. */
|
||||
if ((num = dictSize(db->expires)) == 0) {
|
||||
@@ -202,18 +208,168 @@ void activeExpireCycle(int type) {
|
||||
/* We can't block forever here even if there are many keys to
|
||||
* expire. So after a given amount of milliseconds return to the
|
||||
* caller waiting for the other active expire cycle. */
|
||||
iteration++;
|
||||
if ((iteration & 0xf) == 0) { /* check once every 16 iterations. */
|
||||
long long elapsed = ustime()-start;
|
||||
|
||||
latencyAddSampleIfNeeded("expire-cycle",elapsed/1000);
|
||||
if (elapsed > timelimit) timelimit_exit = 1;
|
||||
elapsed = ustime()-start;
|
||||
if (elapsed > timelimit) {
|
||||
timelimit_exit = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (timelimit_exit) return;
|
||||
/* We don't repeat the cycle if there are less than 25% of keys
|
||||
* found expired in the current DB. */
|
||||
} while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
|
||||
}
|
||||
|
||||
elapsed = ustime()-start;
|
||||
latencyAddSampleIfNeeded("expire-cycle",elapsed/1000);
|
||||
}
|
||||
|
||||
/*-----------------------------------------------------------------------------
|
||||
* Expires of keys created in writable slaves
|
||||
*
|
||||
* Normally slaves do not process expires: they wait the masters to synthesize
|
||||
* DEL operations in order to retain consistency. However writable slaves are
|
||||
* an exception: if a key is created in the slave and an expire is assigned
|
||||
* to it, we need a way to expire such a key, since the master does not know
|
||||
* anything about such a key.
|
||||
*
|
||||
* In order to do so, we track keys created in the slave side with an expire
|
||||
* set, and call the expireSlaveKeys() function from time to time in order to
|
||||
* reclaim the keys if they already expired.
|
||||
*
|
||||
* Note that the use case we are trying to cover here, is a popular one where
|
||||
* slaves are put in writable mode in order to compute slow operations in
|
||||
* the slave side that are mostly useful to actually read data in a more
|
||||
* processed way. Think at sets intersections in a tmp key, with an expire so
|
||||
* that it is also used as a cache to avoid intersecting every time.
|
||||
*
|
||||
* This implementation is currently not perfect but a lot better than leaking
|
||||
* the keys as implemented in 3.2.
|
||||
*----------------------------------------------------------------------------*/
|
||||
|
||||
/* The dictionary where we remember key names and database ID of keys we may
|
||||
* want to expire from the slave. Since this function is not often used we
|
||||
* don't even care to initialize the database at startup. We'll do it once
|
||||
* the feature is used the first time, that is, when rememberSlaveKeyWithExpire()
|
||||
* is called.
|
||||
*
|
||||
* The dictionary has an SDS string representing the key as the hash table
|
||||
* key, while the value is a 64 bit unsigned integer with the bits corresponding
|
||||
* to the DB where the keys may exist set to 1. Currently the keys created
|
||||
* with a DB id > 63 are not expired, but a trivial fix is to set the bitmap
|
||||
* to the max 64 bit unsigned value when we know there is a key with a DB
|
||||
* ID greater than 63, and check all the configured DBs in such a case. */
|
||||
dict *slaveKeysWithExpire = NULL;
|
||||
|
||||
/* Check the set of keys created by the master with an expire set in order to
|
||||
* check if they should be evicted. */
|
||||
void expireSlaveKeys(void) {
|
||||
if (slaveKeysWithExpire == NULL ||
|
||||
dictSize(slaveKeysWithExpire) == 0) return;
|
||||
|
||||
int cycles = 0, noexpire = 0;
|
||||
mstime_t start = mstime();
|
||||
while(1) {
|
||||
dictEntry *de = dictGetRandomKey(slaveKeysWithExpire);
|
||||
sds keyname = dictGetKey(de);
|
||||
uint64_t dbids = dictGetUnsignedIntegerVal(de);
|
||||
uint64_t new_dbids = 0;
|
||||
|
||||
/* Check the key against every database corresponding to the
|
||||
* bits set in the value bitmap. */
|
||||
int dbid = 0;
|
||||
while(dbids && dbid < server.dbnum) {
|
||||
if ((dbids & 1) != 0) {
|
||||
redisDb *db = server.db+dbid;
|
||||
dictEntry *expire = dictFind(db->expires,keyname);
|
||||
int expired = 0;
|
||||
|
||||
if (expire &&
|
||||
activeExpireCycleTryExpire(server.db+dbid,expire,start))
|
||||
{
|
||||
expired = 1;
|
||||
}
|
||||
|
||||
/* If the key was not expired in this DB, we need to set the
|
||||
* corresponding bit in the new bitmap we set as value.
|
||||
* At the end of the loop if the bitmap is zero, it means we
|
||||
* no longer need to keep track of this key. */
|
||||
if (expire && !expired) {
|
||||
noexpire++;
|
||||
new_dbids |= (uint64_t)1 << dbid;
|
||||
}
|
||||
}
|
||||
dbid++;
|
||||
dbids >>= 1;
|
||||
}
|
||||
|
||||
/* Set the new bitmap as value of the key, in the dictionary
|
||||
* of keys with an expire set directly in the writable slave. Otherwise
|
||||
* if the bitmap is zero, we no longer need to keep track of it. */
|
||||
if (new_dbids)
|
||||
dictSetUnsignedIntegerVal(de,new_dbids);
|
||||
else
|
||||
dictDelete(slaveKeysWithExpire,keyname);
|
||||
|
||||
/* Stop conditions: found 3 keys we cna't expire in a row or
|
||||
* time limit was reached. */
|
||||
cycles++;
|
||||
if (noexpire > 3) break;
|
||||
if ((cycles % 64) == 0 && mstime()-start > 1) break;
|
||||
if (dictSize(slaveKeysWithExpire) == 0) break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Track keys that received an EXPIRE or similar command in the context
|
||||
* of a writable slave. */
|
||||
void rememberSlaveKeyWithExpire(redisDb *db, robj *key) {
|
||||
if (slaveKeysWithExpire == NULL) {
|
||||
static dictType dt = {
|
||||
dictSdsHash, /* hash function */
|
||||
NULL, /* key dup */
|
||||
NULL, /* val dup */
|
||||
dictSdsKeyCompare, /* key compare */
|
||||
dictSdsDestructor, /* key destructor */
|
||||
NULL /* val destructor */
|
||||
};
|
||||
slaveKeysWithExpire = dictCreate(&dt,NULL);
|
||||
}
|
||||
if (db->id > 63) return;
|
||||
|
||||
dictEntry *de = dictAddOrFind(slaveKeysWithExpire,key->ptr);
|
||||
/* If the entry was just created, set it to a copy of the SDS string
|
||||
* representing the key: we don't want to need to take those keys
|
||||
* in sync with the main DB. The keys will be removed by expireSlaveKeys()
|
||||
* as it scans to find keys to remove. */
|
||||
if (de->key == key->ptr) {
|
||||
de->key = sdsdup(key->ptr);
|
||||
dictSetUnsignedIntegerVal(de,0);
|
||||
}
|
||||
|
||||
uint64_t dbids = dictGetUnsignedIntegerVal(de);
|
||||
dbids |= (uint64_t)1 << db->id;
|
||||
dictSetUnsignedIntegerVal(de,dbids);
|
||||
}
|
||||
|
||||
/* Return the number of keys we are tracking. */
|
||||
size_t getSlaveKeyWithExpireCount(void) {
|
||||
if (slaveKeysWithExpire == NULL) return 0;
|
||||
return dictSize(slaveKeysWithExpire);
|
||||
}
|
||||
|
||||
/* Remove the keys in the hash table. We need to do that when data is
|
||||
* flushed from the server. We may receive new keys from the master with
|
||||
* the same name/db and it is no longer a good idea to expire them.
|
||||
*
|
||||
* Note: technically we should handle the case of a single DB being flushed
|
||||
* but it is not worth it since anyway race conditions using the same set
|
||||
* of key names in a wriatable slave and in its master will lead to
|
||||
* inconsistencies. This is just a best-effort thing we do. */
|
||||
void flushSlaveKeysWithExpireList(void) {
|
||||
if (slaveKeysWithExpire) {
|
||||
dictRelease(slaveKeysWithExpire);
|
||||
slaveKeysWithExpire = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/*-----------------------------------------------------------------------------
|
||||
@@ -265,7 +421,7 @@ void expireGenericCommand(client *c, long long basetime, int unit) {
|
||||
addReply(c, shared.cone);
|
||||
return;
|
||||
} else {
|
||||
setExpire(c->db,key,when);
|
||||
setExpire(c,c->db,key,when);
|
||||
addReply(c,shared.cone);
|
||||
signalModifiedKey(c->db,key);
|
||||
notifyKeyspaceEvent(NOTIFY_GENERIC,"expire",key,c->db->id);
|
||||
@@ -329,18 +485,15 @@ void pttlCommand(client *c) {
|
||||
|
||||
/* PERSIST key */
|
||||
void persistCommand(client *c) {
|
||||
dictEntry *de;
|
||||
|
||||
de = dictFind(c->db->dict,c->argv[1]->ptr);
|
||||
if (de == NULL) {
|
||||
addReply(c,shared.czero);
|
||||
} else {
|
||||
if (lookupKeyWrite(c->db,c->argv[1])) {
|
||||
if (removeExpire(c->db,c->argv[1])) {
|
||||
addReply(c,shared.cone);
|
||||
server.dirty++;
|
||||
} else {
|
||||
addReply(c,shared.czero);
|
||||
}
|
||||
} else {
|
||||
addReply(c,shared.czero);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ double extractDistanceOrReply(client *c, robj **argv,
|
||||
addReplyError(c,"radius cannot be negative");
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
double to_meters = extractUnitOrReply(c,argv[1]);
|
||||
if (to_meters < 0) {
|
||||
return -1;
|
||||
@@ -326,6 +326,7 @@ int membersOfGeoHashBox(robj *zobj, GeoHashBits hash, geoArray *ga, double lon,
|
||||
int membersOfAllNeighbors(robj *zobj, GeoHashRadius n, double lon, double lat, double radius, geoArray *ga) {
|
||||
GeoHashBits neighbors[9];
|
||||
unsigned int i, count = 0, last_processed = 0;
|
||||
int debugmsg = 0;
|
||||
|
||||
neighbors[0] = n.hash;
|
||||
neighbors[1] = n.neighbors.north;
|
||||
@@ -340,8 +341,26 @@ int membersOfAllNeighbors(robj *zobj, GeoHashRadius n, double lon, double lat, d
|
||||
/* For each neighbor (*and* our own hashbox), get all the matching
|
||||
* members and add them to the potential result list. */
|
||||
for (i = 0; i < sizeof(neighbors) / sizeof(*neighbors); i++) {
|
||||
if (HASHISZERO(neighbors[i]))
|
||||
if (HASHISZERO(neighbors[i])) {
|
||||
if (debugmsg) D("neighbors[%d] is zero",i);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Debugging info. */
|
||||
if (debugmsg) {
|
||||
GeoHashRange long_range, lat_range;
|
||||
geohashGetCoordRange(&long_range,&lat_range);
|
||||
GeoHashArea myarea = {{0}};
|
||||
geohashDecode(long_range, lat_range, neighbors[i], &myarea);
|
||||
|
||||
/* Dump center square. */
|
||||
D("neighbors[%d]:\n",i);
|
||||
D("area.longitude.min: %f\n", myarea.longitude.min);
|
||||
D("area.longitude.max: %f\n", myarea.longitude.max);
|
||||
D("area.latitude.min: %f\n", myarea.latitude.min);
|
||||
D("area.latitude.max: %f\n", myarea.latitude.max);
|
||||
D("\n");
|
||||
}
|
||||
|
||||
/* When a huge Radius (in the 5000 km range or more) is used,
|
||||
* adjacent neighbors can be the same, leading to duplicated
|
||||
@@ -350,7 +369,11 @@ int membersOfAllNeighbors(robj *zobj, GeoHashRadius n, double lon, double lat, d
|
||||
if (last_processed &&
|
||||
neighbors[i].bits == neighbors[last_processed].bits &&
|
||||
neighbors[i].step == neighbors[last_processed].step)
|
||||
{
|
||||
if (debugmsg)
|
||||
D("Skipping processing of %d, same as previous\n",i);
|
||||
continue;
|
||||
}
|
||||
count += membersOfGeoHashBox(zobj, neighbors[i], ga, lon, lat, radius);
|
||||
last_processed = i;
|
||||
}
|
||||
@@ -429,13 +452,14 @@ void geoaddCommand(client *c) {
|
||||
#define SORT_ASC 1
|
||||
#define SORT_DESC 2
|
||||
|
||||
#define RADIUS_COORDS 1
|
||||
#define RADIUS_MEMBER 2
|
||||
#define RADIUS_COORDS (1<<0) /* Search around coordinates. */
|
||||
#define RADIUS_MEMBER (1<<1) /* Search around member. */
|
||||
#define RADIUS_NOSTORE (1<<2) /* Do not acceot STORE/STOREDIST option. */
|
||||
|
||||
/* GEORADIUS key x y radius unit [WITHDIST] [WITHHASH] [WITHCOORD] [ASC|DESC]
|
||||
* [COUNT count] [STORE key] [STOREDIST key]
|
||||
* GEORADIUSBYMEMBER key member radius unit ... options ... */
|
||||
void georadiusGeneric(client *c, int type) {
|
||||
void georadiusGeneric(client *c, int flags) {
|
||||
robj *key = c->argv[1];
|
||||
robj *storekey = NULL;
|
||||
int storedist = 0; /* 0 for STORE, 1 for STOREDIST. */
|
||||
@@ -450,11 +474,11 @@ void georadiusGeneric(client *c, int type) {
|
||||
/* Find long/lat to use for radius search based on inquiry type */
|
||||
int base_args;
|
||||
double xy[2] = { 0 };
|
||||
if (type == RADIUS_COORDS) {
|
||||
if (flags & RADIUS_COORDS) {
|
||||
base_args = 6;
|
||||
if (extractLongLatOrReply(c, c->argv + 2, xy) == C_ERR)
|
||||
return;
|
||||
} else if (type == RADIUS_MEMBER) {
|
||||
} else if (flags & RADIUS_MEMBER) {
|
||||
base_args = 5;
|
||||
robj *member = c->argv[2];
|
||||
if (longLatFromMember(zobj, member, xy) == C_ERR) {
|
||||
@@ -462,7 +486,7 @@ void georadiusGeneric(client *c, int type) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
addReplyError(c, "unknown georadius search type");
|
||||
addReplyError(c, "Unknown georadius search type");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -499,11 +523,17 @@ void georadiusGeneric(client *c, int type) {
|
||||
return;
|
||||
}
|
||||
i++;
|
||||
} else if (!strcasecmp(arg, "store") && (i+1) < remaining) {
|
||||
} else if (!strcasecmp(arg, "store") &&
|
||||
(i+1) < remaining &&
|
||||
!(flags & RADIUS_NOSTORE))
|
||||
{
|
||||
storekey = c->argv[base_args+i+1];
|
||||
storedist = 0;
|
||||
i++;
|
||||
} else if (!strcasecmp(arg, "storedist") && (i+1) < remaining) {
|
||||
} else if (!strcasecmp(arg, "storedist") &&
|
||||
(i+1) < remaining &&
|
||||
!(flags & RADIUS_NOSTORE))
|
||||
{
|
||||
storekey = c->argv[base_args+i+1];
|
||||
storedist = 1;
|
||||
i++;
|
||||
@@ -648,10 +678,20 @@ void georadiusCommand(client *c) {
|
||||
}
|
||||
|
||||
/* GEORADIUSBYMEMBER wrapper function. */
|
||||
void georadiusByMemberCommand(client *c) {
|
||||
void georadiusbymemberCommand(client *c) {
|
||||
georadiusGeneric(c, RADIUS_MEMBER);
|
||||
}
|
||||
|
||||
/* GEORADIUS_RO wrapper function. */
|
||||
void georadiusroCommand(client *c) {
|
||||
georadiusGeneric(c, RADIUS_COORDS|RADIUS_NOSTORE);
|
||||
}
|
||||
|
||||
/* GEORADIUSBYMEMBER_RO wrapper function. */
|
||||
void georadiusbymemberroCommand(client *c) {
|
||||
georadiusGeneric(c, RADIUS_MEMBER|RADIUS_NOSTORE);
|
||||
}
|
||||
|
||||
/* GEOHASH key ele1 ele2 ... eleN
|
||||
*
|
||||
* Returns an array with an 11 characters geohash representation of the
|
||||
@@ -661,16 +701,15 @@ void geohashCommand(client *c) {
|
||||
int j;
|
||||
|
||||
/* Look up the requested zset */
|
||||
robj *zobj = NULL;
|
||||
if ((zobj = lookupKeyReadOrReply(c, c->argv[1], shared.emptymultibulk))
|
||||
== NULL || checkType(c, zobj, OBJ_ZSET)) return;
|
||||
robj *zobj = lookupKeyRead(c->db, c->argv[1]);
|
||||
if (zobj && checkType(c, zobj, OBJ_ZSET)) return;
|
||||
|
||||
/* Geohash elements one after the other, using a null bulk reply for
|
||||
* missing elements. */
|
||||
addReplyMultiBulkLen(c,c->argc-2);
|
||||
for (j = 2; j < c->argc; j++) {
|
||||
double score;
|
||||
if (zsetScore(zobj, c->argv[j]->ptr, &score) == C_ERR) {
|
||||
if (!zobj || zsetScore(zobj, c->argv[j]->ptr, &score) == C_ERR) {
|
||||
addReply(c,shared.nullbulk);
|
||||
} else {
|
||||
/* The internal format we use for geocoding is a bit different
|
||||
@@ -715,16 +754,15 @@ void geoposCommand(client *c) {
|
||||
int j;
|
||||
|
||||
/* Look up the requested zset */
|
||||
robj *zobj = NULL;
|
||||
if ((zobj = lookupKeyReadOrReply(c, c->argv[1], shared.emptymultibulk))
|
||||
== NULL || checkType(c, zobj, OBJ_ZSET)) return;
|
||||
robj *zobj = lookupKeyRead(c->db, c->argv[1]);
|
||||
if (zobj && checkType(c, zobj, OBJ_ZSET)) return;
|
||||
|
||||
/* Report elements one after the other, using a null bulk reply for
|
||||
* missing elements. */
|
||||
addReplyMultiBulkLen(c,c->argc-2);
|
||||
for (j = 2; j < c->argc; j++) {
|
||||
double score;
|
||||
if (zsetScore(zobj, c->argv[j]->ptr, &score) == C_ERR) {
|
||||
if (!zobj || zsetScore(zobj, c->argv[j]->ptr, &score) == C_ERR) {
|
||||
addReply(c,shared.nullmultibulk);
|
||||
} else {
|
||||
/* Decode... */
|
||||
@@ -759,7 +797,7 @@ void geodistCommand(client *c) {
|
||||
|
||||
/* Look up the requested zset */
|
||||
robj *zobj = NULL;
|
||||
if ((zobj = lookupKeyReadOrReply(c, c->argv[1], shared.emptybulk))
|
||||
if ((zobj = lookupKeyReadOrReply(c, c->argv[1], shared.nullbulk))
|
||||
== NULL || checkType(c, zobj, OBJ_ZSET)) return;
|
||||
|
||||
/* Get the scores. We need both otherwise NULL is returned. */
|
||||
|
||||
+44
-62
@@ -82,30 +82,32 @@ uint8_t geohashEstimateStepsByRadius(double range_meters, double lat) {
|
||||
return step;
|
||||
}
|
||||
|
||||
/* Return the bounding box of the search area centered at latitude,longitude
|
||||
* having a radius of radius_meter. bounds[0] - bounds[2] is the minimum
|
||||
* and maxium longitude, while bounds[1] - bounds[3] is the minimum and
|
||||
* maximum latitude.
|
||||
*
|
||||
* This function does not behave correctly with very large radius values, for
|
||||
* instance for the coordinates 81.634948934258375 30.561509253718668 and a
|
||||
* radius of 7083 kilometers, it reports as bounding boxes:
|
||||
*
|
||||
* min_lon 7.680495, min_lat -33.119473, max_lon 155.589402, max_lat 94.242491
|
||||
*
|
||||
* However, for instance, a min_lon of 7.680495 is not correct, because the
|
||||
* point -1.27579540014266968 61.33421815228281559 is at less than 7000
|
||||
* kilometers away.
|
||||
*
|
||||
* Since this function is currently only used as an optimization, the
|
||||
* optimization is not used for very big radiuses, however the function
|
||||
* should be fixed. */
|
||||
int geohashBoundingBox(double longitude, double latitude, double radius_meters,
|
||||
double *bounds) {
|
||||
if (!bounds) return 0;
|
||||
|
||||
double lonr, latr;
|
||||
lonr = deg_rad(longitude);
|
||||
latr = deg_rad(latitude);
|
||||
|
||||
if (radius_meters > EARTH_RADIUS_IN_METERS)
|
||||
radius_meters = EARTH_RADIUS_IN_METERS;
|
||||
double distance = radius_meters / EARTH_RADIUS_IN_METERS;
|
||||
double min_latitude = latr - distance;
|
||||
double max_latitude = latr + distance;
|
||||
|
||||
/* Note: we're being lazy and not accounting for coordinates near poles */
|
||||
double min_longitude, max_longitude;
|
||||
double difference_longitude = asin(sin(distance) / cos(latr));
|
||||
min_longitude = lonr - difference_longitude;
|
||||
max_longitude = lonr + difference_longitude;
|
||||
|
||||
bounds[0] = rad_deg(min_longitude);
|
||||
bounds[1] = rad_deg(min_latitude);
|
||||
bounds[2] = rad_deg(max_longitude);
|
||||
bounds[3] = rad_deg(max_latitude);
|
||||
bounds[0] = longitude - rad_deg(radius_meters/EARTH_RADIUS_IN_METERS/cos(deg_rad(latitude)));
|
||||
bounds[2] = longitude + rad_deg(radius_meters/EARTH_RADIUS_IN_METERS/cos(deg_rad(latitude)));
|
||||
bounds[1] = latitude - rad_deg(radius_meters/EARTH_RADIUS_IN_METERS);
|
||||
bounds[3] = latitude + rad_deg(radius_meters/EARTH_RADIUS_IN_METERS);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -158,55 +160,35 @@ GeoHashRadius geohashGetAreasByRadius(double longitude, double latitude, double
|
||||
< radius_meters) decrease_step = 1;
|
||||
}
|
||||
|
||||
if (decrease_step) {
|
||||
if (steps > 1 && decrease_step) {
|
||||
steps--;
|
||||
geohashEncode(&long_range,&lat_range,longitude,latitude,steps,&hash);
|
||||
geohashNeighbors(&hash,&neighbors);
|
||||
geohashDecode(long_range,lat_range,hash,&area);
|
||||
}
|
||||
|
||||
/* Example debug info. This turns to be very useful every time there is
|
||||
* to investigate radius search potential bugs. So better to leave it
|
||||
* here. */
|
||||
if (0) {
|
||||
GeoHashArea myarea = {{0}};
|
||||
geohashDecode(long_range, lat_range, neighbors.west, &myarea);
|
||||
|
||||
/* Dump West. */
|
||||
D("Neighbors");
|
||||
D("area.longitude.min: %f\n", myarea.longitude.min);
|
||||
D("area.longitude.max: %f\n", myarea.longitude.max);
|
||||
D("area.latitude.min: %f\n", myarea.latitude.min);
|
||||
D("area.latitude.max: %f\n", myarea.latitude.max);
|
||||
|
||||
/* Dump center square. */
|
||||
D("Area");
|
||||
D("area.longitude.min: %f\n", area.longitude.min);
|
||||
D("area.longitude.max: %f\n", area.longitude.max);
|
||||
D("area.latitude.min: %f\n", area.latitude.min);
|
||||
D("area.latitude.max: %f\n", area.latitude.max);
|
||||
}
|
||||
|
||||
/* Exclude the search areas that are useless. */
|
||||
if (area.latitude.min < min_lat) {
|
||||
GZERO(neighbors.south);
|
||||
GZERO(neighbors.south_west);
|
||||
GZERO(neighbors.south_east);
|
||||
}
|
||||
if (area.latitude.max > max_lat) {
|
||||
GZERO(neighbors.north);
|
||||
GZERO(neighbors.north_east);
|
||||
GZERO(neighbors.north_west);
|
||||
}
|
||||
if (area.longitude.min < min_lon) {
|
||||
GZERO(neighbors.west);
|
||||
GZERO(neighbors.south_west);
|
||||
GZERO(neighbors.north_west);
|
||||
}
|
||||
if (area.longitude.max > max_lon) {
|
||||
GZERO(neighbors.east);
|
||||
GZERO(neighbors.south_east);
|
||||
GZERO(neighbors.north_east);
|
||||
if (steps >= 2) {
|
||||
if (area.latitude.min < min_lat) {
|
||||
GZERO(neighbors.south);
|
||||
GZERO(neighbors.south_west);
|
||||
GZERO(neighbors.south_east);
|
||||
}
|
||||
if (area.latitude.max > max_lat) {
|
||||
GZERO(neighbors.north);
|
||||
GZERO(neighbors.north_east);
|
||||
GZERO(neighbors.north_west);
|
||||
}
|
||||
if (area.longitude.min < min_lon) {
|
||||
GZERO(neighbors.west);
|
||||
GZERO(neighbors.south_west);
|
||||
GZERO(neighbors.north_west);
|
||||
}
|
||||
if (area.longitude.max > max_lon) {
|
||||
GZERO(neighbors.east);
|
||||
GZERO(neighbors.south_east);
|
||||
GZERO(neighbors.north_east);
|
||||
}
|
||||
}
|
||||
radius.hash = hash;
|
||||
radius.neighbors = neighbors;
|
||||
|
||||
+19
-25
@@ -401,7 +401,11 @@ uint64_t MurmurHash64A (const void * key, int len, unsigned int seed) {
|
||||
uint64_t k;
|
||||
|
||||
#if (BYTE_ORDER == LITTLE_ENDIAN)
|
||||
#ifdef USE_ALIGNED_ACCESS
|
||||
memcpy(&k,data,sizeof(uint64_t));
|
||||
#else
|
||||
k = *((uint64_t*)data);
|
||||
#endif
|
||||
#else
|
||||
k = (uint64_t) data[0];
|
||||
k |= (uint64_t) data[1] << 8;
|
||||
@@ -994,32 +998,21 @@ uint64_t hllCount(struct hllhdr *hdr, int *invalid) {
|
||||
serverPanic("Unknown HyperLogLog encoding in hllCount()");
|
||||
}
|
||||
|
||||
/* Muliply the inverse of E for alpha_m * m^2 to have the raw estimate. */
|
||||
E = (1/E)*alpha*m*m;
|
||||
/* Apply loglog-beta to the raw estimate. See:
|
||||
* "LogLog-Beta and More: A New Algorithm for Cardinality Estimation
|
||||
* Based on LogLog Counting" Jason Qin, Denys Kim, Yumei Tung
|
||||
* arXiv:1612.02284 */
|
||||
double zl = log(ez + 1);
|
||||
double beta = -0.370393911*ez +
|
||||
0.070471823*zl +
|
||||
0.17393686*pow(zl,2) +
|
||||
0.16339839*pow(zl,3) +
|
||||
-0.09237745*pow(zl,4) +
|
||||
0.03738027*pow(zl,5) +
|
||||
-0.005384159*pow(zl,6) +
|
||||
0.00042419*pow(zl,7);
|
||||
|
||||
/* Use the LINEARCOUNTING algorithm for small cardinalities.
|
||||
* For larger values but up to 72000 HyperLogLog raw approximation is
|
||||
* used since linear counting error starts to increase. However HyperLogLog
|
||||
* shows a strong bias in the range 2.5*16384 - 72000, so we try to
|
||||
* compensate for it. */
|
||||
if (E < m*2.5 && ez != 0) {
|
||||
E = m*log(m/ez); /* LINEARCOUNTING() */
|
||||
} else if (m == 16384 && E < 72000) {
|
||||
/* We did polynomial regression of the bias for this range, this
|
||||
* way we can compute the bias for a given cardinality and correct
|
||||
* according to it. Only apply the correction for P=14 that's what
|
||||
* we use and the value the correction was verified with. */
|
||||
double bias = 5.9119*1.0e-18*(E*E*E*E)
|
||||
-1.4253*1.0e-12*(E*E*E)+
|
||||
1.2940*1.0e-7*(E*E)
|
||||
-5.2921*1.0e-3*E+
|
||||
83.3216;
|
||||
E -= E*(bias/100);
|
||||
}
|
||||
/* We don't apply the correction for E > 1/30 of 2^32 since we use
|
||||
* a 64 bit function and 6 bit counters. To apply the correction for
|
||||
* 1/30 of 2^64 is not needed since it would require a huge set
|
||||
* to approach such a value. */
|
||||
E = llroundl(alpha*m*(m-ez)*(1/(E+beta)));
|
||||
return (uint64_t) E;
|
||||
}
|
||||
|
||||
@@ -1128,6 +1121,7 @@ int isHLLObjectOrReply(client *c, robj *o) {
|
||||
if (checkType(c,o,OBJ_STRING))
|
||||
return C_ERR; /* Error already sent. */
|
||||
|
||||
if (!sdsEncodedObject(o)) goto invalid;
|
||||
if (stringObjectLen(o) < sizeof(*hdr)) goto invalid;
|
||||
hdr = o->ptr;
|
||||
|
||||
|
||||
+1
-1
@@ -261,7 +261,7 @@ int64_t intsetRandom(intset *is) {
|
||||
return _intsetGet(is,rand()%intrev32ifbe(is->length));
|
||||
}
|
||||
|
||||
/* Sets the value to the value at the given position. When this position is
|
||||
/* Get the value at the given position. When this position is
|
||||
* out of range the function returns 0, when in range it returns 1. */
|
||||
uint8_t intsetGet(intset *is, uint32_t pos, int64_t *value) {
|
||||
if (pos < intrev32ifbe(is->length)) {
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ int dictStringKeyCompare(void *privdata, const void *key1, const void *key2) {
|
||||
return strcmp(key1,key2) == 0;
|
||||
}
|
||||
|
||||
unsigned int dictStringHash(const void *key) {
|
||||
uint64_t dictStringHash(const void *key) {
|
||||
return dictGenHashFunction(key, strlen(key));
|
||||
}
|
||||
|
||||
|
||||
+18
-15
@@ -8,7 +8,9 @@ pthread_mutex_t lazyfree_objects_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
/* Return the number of currently pending objects to free. */
|
||||
size_t lazyfreeGetPendingObjectsCount(void) {
|
||||
return lazyfree_objects;
|
||||
size_t aux;
|
||||
atomicGet(lazyfree_objects,aux);
|
||||
return aux;
|
||||
}
|
||||
|
||||
/* Return the amount of work needed in order to free an object.
|
||||
@@ -65,7 +67,7 @@ int dbAsyncDelete(redisDb *db, robj *key) {
|
||||
/* If releasing the object is too much work, let's put it into the
|
||||
* lazy free list. */
|
||||
if (free_effort > LAZYFREE_THRESHOLD) {
|
||||
atomicIncr(lazyfree_objects,1,lazyfree_objects_mutex);
|
||||
atomicIncr(lazyfree_objects,1);
|
||||
bioCreateBackgroundJob(BIO_LAZY_FREE,val,NULL,NULL);
|
||||
dictSetVal(db->dict,de,NULL);
|
||||
}
|
||||
@@ -89,26 +91,27 @@ void emptyDbAsync(redisDb *db) {
|
||||
dict *oldht1 = db->dict, *oldht2 = db->expires;
|
||||
db->dict = dictCreate(&dbDictType,NULL);
|
||||
db->expires = dictCreate(&keyptrDictType,NULL);
|
||||
atomicIncr(lazyfree_objects,dictSize(oldht1),
|
||||
lazyfree_objects_mutex);
|
||||
atomicIncr(lazyfree_objects,dictSize(oldht1));
|
||||
bioCreateBackgroundJob(BIO_LAZY_FREE,NULL,oldht1,oldht2);
|
||||
}
|
||||
|
||||
/* Empty the slots-keys map of Redis CLuster by creating a new empty one
|
||||
* and scheduiling the old for lazy freeing. */
|
||||
void slotToKeyFlushAsync(void) {
|
||||
zskiplist *oldsl = server.cluster->slots_to_keys;
|
||||
server.cluster->slots_to_keys = zslCreate();
|
||||
atomicIncr(lazyfree_objects,oldsl->length,
|
||||
lazyfree_objects_mutex);
|
||||
bioCreateBackgroundJob(BIO_LAZY_FREE,NULL,NULL,oldsl);
|
||||
rax *old = server.cluster->slots_to_keys;
|
||||
|
||||
server.cluster->slots_to_keys = raxNew();
|
||||
memset(server.cluster->slots_keys_count,0,
|
||||
sizeof(server.cluster->slots_keys_count));
|
||||
atomicIncr(lazyfree_objects,old->numele);
|
||||
bioCreateBackgroundJob(BIO_LAZY_FREE,NULL,NULL,old);
|
||||
}
|
||||
|
||||
/* Release objects from the lazyfree thread. It's just decrRefCount()
|
||||
* updating the count of objects to release. */
|
||||
void lazyfreeFreeObjectFromBioThread(robj *o) {
|
||||
decrRefCount(o);
|
||||
atomicDecr(lazyfree_objects,1,lazyfree_objects_mutex);
|
||||
atomicDecr(lazyfree_objects,1);
|
||||
}
|
||||
|
||||
/* Release a database from the lazyfree thread. The 'db' pointer is the
|
||||
@@ -120,13 +123,13 @@ void lazyfreeFreeDatabaseFromBioThread(dict *ht1, dict *ht2) {
|
||||
size_t numkeys = dictSize(ht1);
|
||||
dictRelease(ht1);
|
||||
dictRelease(ht2);
|
||||
atomicDecr(lazyfree_objects,numkeys,lazyfree_objects_mutex);
|
||||
atomicDecr(lazyfree_objects,numkeys);
|
||||
}
|
||||
|
||||
/* Release the skiplist mapping Redis Cluster keys to slots in the
|
||||
* lazyfree thread. */
|
||||
void lazyfreeFreeSlotsMapFromBioThread(zskiplist *sl) {
|
||||
size_t len = sl->length;
|
||||
zslFree(sl);
|
||||
atomicDecr(lazyfree_objects,len,lazyfree_objects_mutex);
|
||||
void lazyfreeFreeSlotsMapFromBioThread(rax *rt) {
|
||||
size_t len = rt->numele;
|
||||
raxFree(rt);
|
||||
atomicDecr(lazyfree_objects,len);
|
||||
}
|
||||
|
||||
+783
@@ -0,0 +1,783 @@
|
||||
/* Listpack -- A lists of strings serialization format
|
||||
*
|
||||
* This file implements the specification you can find at:
|
||||
*
|
||||
* https://github.com/antirez/listpack
|
||||
*
|
||||
* Copyright (c) 2017, 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.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <limits.h>
|
||||
#include <sys/types.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "listpack.h"
|
||||
#include "listpack_malloc.h"
|
||||
|
||||
#define LP_HDR_SIZE 6 /* 32 bit total len + 16 bit number of elements. */
|
||||
#define LP_HDR_NUMELE_UNKNOWN UINT16_MAX
|
||||
#define LP_MAX_INT_ENCODING_LEN 9
|
||||
#define LP_MAX_BACKLEN_SIZE 5
|
||||
#define LP_MAX_ENTRY_BACKLEN 34359738367ULL
|
||||
#define LP_ENCODING_INT 0
|
||||
#define LP_ENCODING_STRING 1
|
||||
|
||||
#define LP_ENCODING_7BIT_UINT 0
|
||||
#define LP_ENCODING_7BIT_UINT_MASK 0x80
|
||||
#define LP_ENCODING_IS_7BIT_UINT(byte) (((byte)&LP_ENCODING_7BIT_UINT_MASK)==LP_ENCODING_7BIT_UINT)
|
||||
|
||||
#define LP_ENCODING_6BIT_STR 0x80
|
||||
#define LP_ENCODING_6BIT_STR_MASK 0xC0
|
||||
#define LP_ENCODING_IS_6BIT_STR(byte) (((byte)&LP_ENCODING_6BIT_STR_MASK)==LP_ENCODING_6BIT_STR)
|
||||
|
||||
#define LP_ENCODING_13BIT_INT 0xC0
|
||||
#define LP_ENCODING_13BIT_INT_MASK 0xE0
|
||||
#define LP_ENCODING_IS_13BIT_INT(byte) (((byte)&LP_ENCODING_13BIT_INT_MASK)==LP_ENCODING_13BIT_INT)
|
||||
|
||||
#define LP_ENCODING_12BIT_STR 0xE0
|
||||
#define LP_ENCODING_12BIT_STR_MASK 0xF0
|
||||
#define LP_ENCODING_IS_12BIT_STR(byte) (((byte)&LP_ENCODING_12BIT_STR_MASK)==LP_ENCODING_12BIT_STR)
|
||||
|
||||
#define LP_ENCODING_16BIT_INT 0xF1
|
||||
#define LP_ENCODING_16BIT_INT_MASK 0xFF
|
||||
#define LP_ENCODING_IS_16BIT_INT(byte) (((byte)&LP_ENCODING_16BIT_INT_MASK)==LP_ENCODING_16BIT_INT)
|
||||
|
||||
#define LP_ENCODING_24BIT_INT 0xF2
|
||||
#define LP_ENCODING_24BIT_INT_MASK 0xFF
|
||||
#define LP_ENCODING_IS_24BIT_INT(byte) (((byte)&LP_ENCODING_24BIT_INT_MASK)==LP_ENCODING_24BIT_INT)
|
||||
|
||||
#define LP_ENCODING_32BIT_INT 0xF3
|
||||
#define LP_ENCODING_32BIT_INT_MASK 0xFF
|
||||
#define LP_ENCODING_IS_32BIT_INT(byte) (((byte)&LP_ENCODING_32BIT_INT_MASK)==LP_ENCODING_32BIT_INT)
|
||||
|
||||
#define LP_ENCODING_64BIT_INT 0xF4
|
||||
#define LP_ENCODING_64BIT_INT_MASK 0xFF
|
||||
#define LP_ENCODING_IS_64BIT_INT(byte) (((byte)&LP_ENCODING_64BIT_INT_MASK)==LP_ENCODING_64BIT_INT)
|
||||
|
||||
#define LP_ENCODING_32BIT_STR 0xF0
|
||||
#define LP_ENCODING_32BIT_STR_MASK 0xFF
|
||||
#define LP_ENCODING_IS_32BIT_STR(byte) (((byte)&LP_ENCODING_32BIT_STR_MASK)==LP_ENCODING_32BIT_STR)
|
||||
|
||||
#define LP_EOF 0xFF
|
||||
|
||||
#define LP_ENCODING_6BIT_STR_LEN(p) ((p)[0] & 0x3F)
|
||||
#define LP_ENCODING_12BIT_STR_LEN(p) ((((p)[0] & 0xF) << 8) | (p)[1])
|
||||
#define LP_ENCODING_32BIT_STR_LEN(p) (((uint32_t)(p)[1]<<0) | \
|
||||
((uint32_t)(p)[2]<<8) | \
|
||||
((uint32_t)(p)[3]<<16) | \
|
||||
((uint32_t)(p)[4]<<24))
|
||||
|
||||
#define lpGetTotalBytes(p) (((uint32_t)(p)[0]<<0) | \
|
||||
((uint32_t)(p)[1]<<8) | \
|
||||
((uint32_t)(p)[2]<<16) | \
|
||||
((uint32_t)(p)[3]<<24))
|
||||
|
||||
#define lpGetNumElements(p) (((uint32_t)(p)[4]<<0) | \
|
||||
((uint32_t)(p)[5]<<8))
|
||||
#define lpSetTotalBytes(p,v) do { \
|
||||
(p)[0] = (v)&0xff; \
|
||||
(p)[1] = ((v)>>8)&0xff; \
|
||||
(p)[2] = ((v)>>16)&0xff; \
|
||||
(p)[3] = ((v)>>24)&0xff; \
|
||||
} while(0)
|
||||
|
||||
#define lpSetNumElements(p,v) do { \
|
||||
(p)[4] = (v)&0xff; \
|
||||
(p)[5] = ((v)>>8)&0xff; \
|
||||
} while(0)
|
||||
|
||||
/* Convert a string into a signed 64 bit integer.
|
||||
* The function returns 1 if the string could be parsed into a (non-overflowing)
|
||||
* signed 64 bit int, 0 otherwise. The 'value' will be set to the parsed value
|
||||
* when the function returns success.
|
||||
*
|
||||
* Note that this function demands that the string strictly represents
|
||||
* a int64 value: no spaces or other characters before or after the string
|
||||
* representing the number are accepted, nor zeroes at the start if not
|
||||
* for the string "0" representing the zero number.
|
||||
*
|
||||
* Because of its strictness, it is safe to use this function to check if
|
||||
* you can convert a string into a long long, and obtain back the string
|
||||
* from the number without any loss in the string representation. *
|
||||
*
|
||||
* -----------------------------------------------------------------------------
|
||||
*
|
||||
* Credits: this function was adapted from the Redis source code, file
|
||||
* "utils.c", function string2ll(), and is copyright:
|
||||
*
|
||||
* Copyright(C) 2011, Pieter Noordhuis
|
||||
* Copyright(C) 2011, Salvatore Sanfilippo
|
||||
*
|
||||
* The function is released under the BSD 3-clause license.
|
||||
*/
|
||||
int lpStringToInt64(const char *s, unsigned long slen, int64_t *value) {
|
||||
const char *p = s;
|
||||
unsigned long plen = 0;
|
||||
int negative = 0;
|
||||
uint64_t v;
|
||||
|
||||
if (plen == slen)
|
||||
return 0;
|
||||
|
||||
/* Special case: first and only digit is 0. */
|
||||
if (slen == 1 && p[0] == '0') {
|
||||
if (value != NULL) *value = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (p[0] == '-') {
|
||||
negative = 1;
|
||||
p++; plen++;
|
||||
|
||||
/* Abort on only a negative sign. */
|
||||
if (plen == slen)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* First digit should be 1-9, otherwise the string should just be 0. */
|
||||
if (p[0] >= '1' && p[0] <= '9') {
|
||||
v = p[0]-'0';
|
||||
p++; plen++;
|
||||
} else if (p[0] == '0' && slen == 1) {
|
||||
*value = 0;
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
|
||||
while (plen < slen && p[0] >= '0' && p[0] <= '9') {
|
||||
if (v > (UINT64_MAX / 10)) /* Overflow. */
|
||||
return 0;
|
||||
v *= 10;
|
||||
|
||||
if (v > (UINT64_MAX - (p[0]-'0'))) /* Overflow. */
|
||||
return 0;
|
||||
v += p[0]-'0';
|
||||
|
||||
p++; plen++;
|
||||
}
|
||||
|
||||
/* Return if not all bytes were used. */
|
||||
if (plen < slen)
|
||||
return 0;
|
||||
|
||||
if (negative) {
|
||||
if (v > ((uint64_t)(-(INT64_MIN+1))+1)) /* Overflow. */
|
||||
return 0;
|
||||
if (value != NULL) *value = -v;
|
||||
} else {
|
||||
if (v > INT64_MAX) /* Overflow. */
|
||||
return 0;
|
||||
if (value != NULL) *value = v;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Create a new, empty listpack.
|
||||
* On success the new listpack is returned, otherwise an error is returned. */
|
||||
unsigned char *lpNew(void) {
|
||||
unsigned char *lp = lp_malloc(LP_HDR_SIZE+1);
|
||||
if (lp == NULL) return NULL;
|
||||
lpSetTotalBytes(lp,LP_HDR_SIZE+1);
|
||||
lpSetNumElements(lp,0);
|
||||
lp[LP_HDR_SIZE] = LP_EOF;
|
||||
return lp;
|
||||
}
|
||||
|
||||
/* Free the specified listpack. */
|
||||
void lpFree(unsigned char *lp) {
|
||||
lp_free(lp);
|
||||
}
|
||||
|
||||
/* Given an element 'ele' of size 'size', determine if the element can be
|
||||
* represented inside the listpack encoded as integer, and returns
|
||||
* LP_ENCODING_INT if so. Otherwise returns LP_ENCODING_STR if no integer
|
||||
* encoding is possible.
|
||||
*
|
||||
* If the LP_ENCODING_INT is returned, the function stores the integer encoded
|
||||
* representation of the element in the 'intenc' buffer.
|
||||
*
|
||||
* Regardless of the returned encoding, 'enclen' is populated by reference to
|
||||
* the number of bytes that the string or integer encoded element will require
|
||||
* in order to be represented. */
|
||||
int lpEncodeGetType(unsigned char *ele, uint32_t size, unsigned char *intenc, uint64_t *enclen) {
|
||||
int64_t v;
|
||||
if (lpStringToInt64((const char*)ele, size, &v)) {
|
||||
if (v >= 0 && v <= 127) {
|
||||
/* Single byte 0-127 integer. */
|
||||
intenc[0] = v;
|
||||
*enclen = 1;
|
||||
} else if (v >= -4096 && v <= 4095) {
|
||||
/* 13 bit integer. */
|
||||
if (v < 0) v = ((int64_t)1<<13)+v;
|
||||
intenc[0] = (v>>8)|LP_ENCODING_13BIT_INT;
|
||||
intenc[1] = v&0xff;
|
||||
*enclen = 2;
|
||||
} else if (v >= -32768 && v <= 32767) {
|
||||
/* 16 bit integer. */
|
||||
if (v < 0) v = ((int64_t)1<<16)+v;
|
||||
intenc[0] = LP_ENCODING_16BIT_INT;
|
||||
intenc[1] = v&0xff;
|
||||
intenc[2] = v>>8;
|
||||
*enclen = 3;
|
||||
} else if (v >= -8388608 && v <= 8388607) {
|
||||
/* 24 bit integer. */
|
||||
if (v < 0) v = ((int64_t)1<<24)+v;
|
||||
intenc[0] = LP_ENCODING_24BIT_INT;
|
||||
intenc[1] = v&0xff;
|
||||
intenc[2] = (v>>8)&0xff;
|
||||
intenc[3] = v>>16;
|
||||
*enclen = 4;
|
||||
} else if (v >= -2147483648 && v <= 2147483647) {
|
||||
/* 32 bit integer. */
|
||||
if (v < 0) v = ((int64_t)1<<32)+v;
|
||||
intenc[0] = LP_ENCODING_32BIT_INT;
|
||||
intenc[1] = v&0xff;
|
||||
intenc[2] = (v>>8)&0xff;
|
||||
intenc[3] = (v>>16)&0xff;
|
||||
intenc[4] = v>>24;
|
||||
*enclen = 5;
|
||||
} else {
|
||||
/* 64 bit integer. */
|
||||
uint64_t uv = v;
|
||||
intenc[0] = LP_ENCODING_64BIT_INT;
|
||||
intenc[1] = uv&0xff;
|
||||
intenc[2] = (uv>>8)&0xff;
|
||||
intenc[3] = (uv>>16)&0xff;
|
||||
intenc[4] = (uv>>24)&0xff;
|
||||
intenc[5] = (uv>>32)&0xff;
|
||||
intenc[6] = (uv>>40)&0xff;
|
||||
intenc[7] = (uv>>48)&0xff;
|
||||
intenc[8] = uv>>56;
|
||||
*enclen = 9;
|
||||
}
|
||||
return LP_ENCODING_INT;
|
||||
} else {
|
||||
if (size < 64) *enclen = 1+size;
|
||||
else if (size < 4096) *enclen = 2+size;
|
||||
else *enclen = 5+size;
|
||||
return LP_ENCODING_STRING;
|
||||
}
|
||||
}
|
||||
|
||||
/* Store a reverse-encoded variable length field, representing the length
|
||||
* of the previous element of size 'l', in the target buffer 'buf'.
|
||||
* The function returns the number of bytes used to encode it, from
|
||||
* 1 to 5. If 'buf' is NULL the funciton just returns the number of bytes
|
||||
* needed in order to encode the backlen. */
|
||||
unsigned long lpEncodeBacklen(unsigned char *buf, uint64_t l) {
|
||||
if (l <= 127) {
|
||||
if (buf) buf[0] = l;
|
||||
return 1;
|
||||
} else if (l < 16383) {
|
||||
if (buf) {
|
||||
buf[0] = l>>7;
|
||||
buf[1] = (l&127)|128;
|
||||
}
|
||||
return 2;
|
||||
} else if (l < 2097151) {
|
||||
if (buf) {
|
||||
buf[0] = l>>14;
|
||||
buf[1] = ((l>>7)&127)|128;
|
||||
buf[2] = (l&127)|128;
|
||||
}
|
||||
return 3;
|
||||
} else if (l < 268435455) {
|
||||
if (buf) {
|
||||
buf[0] = l>>21;
|
||||
buf[1] = ((l>>14)&127)|128;
|
||||
buf[2] = ((l>>7)&127)|128;
|
||||
buf[3] = (l&127)|128;
|
||||
}
|
||||
return 4;
|
||||
} else {
|
||||
if (buf) {
|
||||
buf[0] = l>>28;
|
||||
buf[1] = ((l>>21)&127)|128;
|
||||
buf[2] = ((l>>14)&127)|128;
|
||||
buf[3] = ((l>>7)&127)|128;
|
||||
buf[4] = (l&127)|128;
|
||||
}
|
||||
return 5;
|
||||
}
|
||||
}
|
||||
|
||||
/* Decode the backlen and returns it. If the encoding looks invalid (more than
|
||||
* 5 bytes are used), UINT64_MAX is returned to report the problem. */
|
||||
uint64_t lpDecodeBacklen(unsigned char *p) {
|
||||
uint64_t val = 0;
|
||||
uint64_t shift = 0;
|
||||
do {
|
||||
val |= (uint64_t)(p[0] & 127) << shift;
|
||||
if (!(p[0] & 128)) break;
|
||||
shift += 7;
|
||||
p--;
|
||||
if (shift > 28) return UINT64_MAX;
|
||||
} while(1);
|
||||
return val;
|
||||
}
|
||||
|
||||
/* Encode the string element pointed by 's' of size 'len' in the target
|
||||
* buffer 's'. The function should be called with 'buf' having always enough
|
||||
* space for encoding the string. This is done by calling lpEncodeGetType()
|
||||
* before calling this function. */
|
||||
void lpEncodeString(unsigned char *buf, unsigned char *s, uint32_t len) {
|
||||
if (len < 64) {
|
||||
buf[0] = len | LP_ENCODING_6BIT_STR;
|
||||
memcpy(buf+1,s,len);
|
||||
} else if (len < 4096) {
|
||||
buf[0] = (len >> 8) | LP_ENCODING_12BIT_STR;
|
||||
buf[1] = len & 0xff;
|
||||
memcpy(buf+2,s,len);
|
||||
} else {
|
||||
buf[0] = LP_ENCODING_32BIT_STR;
|
||||
buf[1] = len & 0xff;
|
||||
buf[2] = (len >> 8) & 0xff;
|
||||
buf[3] = (len >> 16) & 0xff;
|
||||
buf[4] = (len >> 24) & 0xff;
|
||||
memcpy(buf+5,s,len);
|
||||
}
|
||||
}
|
||||
|
||||
/* Return the encoded length of the listpack element pointed by 'p'. If the
|
||||
* element encoding is wrong then 0 is returned. */
|
||||
uint32_t lpCurrentEncodedSize(unsigned char *p) {
|
||||
if (LP_ENCODING_IS_7BIT_UINT(p[0])) return 1;
|
||||
if (LP_ENCODING_IS_6BIT_STR(p[0])) return 1+LP_ENCODING_6BIT_STR_LEN(p);
|
||||
if (LP_ENCODING_IS_13BIT_INT(p[0])) return 2;
|
||||
if (LP_ENCODING_IS_16BIT_INT(p[0])) return 3;
|
||||
if (LP_ENCODING_IS_24BIT_INT(p[0])) return 4;
|
||||
if (LP_ENCODING_IS_32BIT_INT(p[0])) return 5;
|
||||
if (LP_ENCODING_IS_64BIT_INT(p[0])) return 9;
|
||||
if (LP_ENCODING_IS_12BIT_STR(p[0])) return 2+LP_ENCODING_12BIT_STR_LEN(p);
|
||||
if (LP_ENCODING_IS_32BIT_STR(p[0])) return 5+LP_ENCODING_32BIT_STR_LEN(p);
|
||||
if (p[0] == LP_EOF) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Skip the current entry returning the next. It is invalid to call this
|
||||
* function if the current element is the EOF element at the end of the
|
||||
* listpack, however, while this function is used to implement lpNext(),
|
||||
* it does not return NULL when the EOF element is encountered. */
|
||||
unsigned char *lpSkip(unsigned char *p) {
|
||||
unsigned long entrylen = lpCurrentEncodedSize(p);
|
||||
entrylen += lpEncodeBacklen(NULL,entrylen);
|
||||
p += entrylen;
|
||||
return p;
|
||||
}
|
||||
|
||||
/* If 'p' points to an element of the listpack, calling lpNext() will return
|
||||
* the pointer to the next element (the one on the right), or NULL if 'p'
|
||||
* already pointed to the last element of the listpack. */
|
||||
unsigned char *lpNext(unsigned char *lp, unsigned char *p) {
|
||||
((void) lp); /* lp is not used for now. However lpPrev() uses it. */
|
||||
p = lpSkip(p);
|
||||
if (p[0] == LP_EOF) return NULL;
|
||||
return p;
|
||||
}
|
||||
|
||||
/* If 'p' points to an element of the listpack, calling lpPrev() will return
|
||||
* the pointer to the preivous element (the one on the left), or NULL if 'p'
|
||||
* already pointed to the first element of the listpack. */
|
||||
unsigned char *lpPrev(unsigned char *lp, unsigned char *p) {
|
||||
if (p-lp == LP_HDR_SIZE) return NULL;
|
||||
p--; /* Seek the first backlen byte of the last element. */
|
||||
uint64_t prevlen = lpDecodeBacklen(p);
|
||||
prevlen += lpEncodeBacklen(NULL,prevlen);
|
||||
return p-prevlen+1; /* Seek the first byte of the previous entry. */
|
||||
}
|
||||
|
||||
/* Return a pointer to the first element of the listpack, or NULL if the
|
||||
* listpack has no elements. */
|
||||
unsigned char *lpFirst(unsigned char *lp) {
|
||||
lp += LP_HDR_SIZE; /* Skip the header. */
|
||||
if (lp[0] == LP_EOF) return NULL;
|
||||
return lp;
|
||||
}
|
||||
|
||||
/* Return a pointer to the last element of the listpack, or NULL if the
|
||||
* listpack has no elements. */
|
||||
unsigned char *lpLast(unsigned char *lp) {
|
||||
unsigned char *p = lp+lpGetTotalBytes(lp)-1; /* Seek EOF element. */
|
||||
return lpPrev(lp,p); /* Will return NULL if EOF is the only element. */
|
||||
}
|
||||
|
||||
/* Return the number of elements inside the listpack. This function attempts
|
||||
* to use the cached value when within range, otherwise a full scan is
|
||||
* needed. As a side effect of calling this function, the listpack header
|
||||
* could be modified, because if the count is found to be already within
|
||||
* the 'numele' header field range, the new value is set. */
|
||||
uint32_t lpLength(unsigned char *lp) {
|
||||
uint32_t numele = lpGetNumElements(lp);
|
||||
if (numele != LP_HDR_NUMELE_UNKNOWN) return numele;
|
||||
|
||||
/* Too many elements inside the listpack. We need to scan in order
|
||||
* to get the total number. */
|
||||
uint32_t count = 0;
|
||||
unsigned char *p = lpFirst(lp);
|
||||
while(p) {
|
||||
count++;
|
||||
p = lpNext(lp,p);
|
||||
}
|
||||
|
||||
/* If the count is again within range of the header numele field,
|
||||
* set it. */
|
||||
if (count < LP_HDR_NUMELE_UNKNOWN) lpSetNumElements(lp,count);
|
||||
return count;
|
||||
}
|
||||
|
||||
/* Return the listpack element pointed by 'p'.
|
||||
*
|
||||
* The function changes behavior depending on the passed 'intbuf' value.
|
||||
* Specifically, if 'intbuf' is NULL:
|
||||
*
|
||||
* If the element is internally encoded as an integer, the function returns
|
||||
* NULL and populates the integer value by reference in 'count'. Otherwise if
|
||||
* the element is encoded as a string a pointer to the string (pointing inside
|
||||
* the listpack itself) is returned, and 'count' is set to the length of the
|
||||
* string.
|
||||
*
|
||||
* If instead 'intbuf' points to a buffer passed by the caller, that must be
|
||||
* at least LP_INTBUF_SIZE bytes, the function always returns the element as
|
||||
* it was a string (returning the pointer to the string and setting the
|
||||
* 'count' argument to the string length by reference). However if the element
|
||||
* is encoded as an integer, the 'intbuf' buffer is used in order to store
|
||||
* the string representation.
|
||||
*
|
||||
* The user should use one or the other form depending on what the value will
|
||||
* be used for. If there is immediate usage for an integer value returned
|
||||
* by the function, than to pass a buffer (and convert it back to a number)
|
||||
* is of course useless.
|
||||
*
|
||||
* If the function is called against a badly encoded ziplist, so that there
|
||||
* is no valid way to parse it, the function returns like if there was an
|
||||
* integer encoded with value 12345678900000000 + <unrecognized byte>, this may
|
||||
* be an hint to understand that something is wrong. To crash in this case is
|
||||
* not sensible because of the different requirements of the application using
|
||||
* this lib.
|
||||
*
|
||||
* Similarly, there is no error returned since the listpack normally can be
|
||||
* assumed to be valid, so that would be a very high API cost. However a function
|
||||
* in order to check the integrity of the listpack at load time is provided,
|
||||
* check lpIsValid(). */
|
||||
unsigned char *lpGet(unsigned char *p, int64_t *count, unsigned char *intbuf) {
|
||||
int64_t val;
|
||||
uint64_t uval, negstart, negmax;
|
||||
|
||||
if (LP_ENCODING_IS_7BIT_UINT(p[0])) {
|
||||
negstart = UINT64_MAX; /* 7 bit ints are always positive. */
|
||||
negmax = 0;
|
||||
uval = p[0] & 0x7f;
|
||||
} else if (LP_ENCODING_IS_6BIT_STR(p[0])) {
|
||||
*count = LP_ENCODING_6BIT_STR_LEN(p);
|
||||
return p+1;
|
||||
} else if (LP_ENCODING_IS_13BIT_INT(p[0])) {
|
||||
uval = ((p[0]&0x1f)<<8) | p[1];
|
||||
negstart = (uint64_t)1<<12;
|
||||
negmax = 8191;
|
||||
} else if (LP_ENCODING_IS_16BIT_INT(p[0])) {
|
||||
uval = (uint64_t)p[1] |
|
||||
(uint64_t)p[2]<<8;
|
||||
negstart = (uint64_t)1<<15;
|
||||
negmax = UINT16_MAX;
|
||||
} else if (LP_ENCODING_IS_24BIT_INT(p[0])) {
|
||||
uval = (uint64_t)p[1] |
|
||||
(uint64_t)p[2]<<8 |
|
||||
(uint64_t)p[3]<<16;
|
||||
negstart = (uint64_t)1<<23;
|
||||
negmax = UINT32_MAX>>8;
|
||||
} else if (LP_ENCODING_IS_32BIT_INT(p[0])) {
|
||||
uval = (uint64_t)p[1] |
|
||||
(uint64_t)p[2]<<8 |
|
||||
(uint64_t)p[3]<<16 |
|
||||
(uint64_t)p[4]<<24;
|
||||
negstart = (uint64_t)1<<31;
|
||||
negmax = UINT32_MAX;
|
||||
} else if (LP_ENCODING_IS_64BIT_INT(p[0])) {
|
||||
uval = (uint64_t)p[1] |
|
||||
(uint64_t)p[2]<<8 |
|
||||
(uint64_t)p[3]<<16 |
|
||||
(uint64_t)p[4]<<24 |
|
||||
(uint64_t)p[5]<<32 |
|
||||
(uint64_t)p[6]<<40 |
|
||||
(uint64_t)p[7]<<48 |
|
||||
(uint64_t)p[8]<<56;
|
||||
negstart = (uint64_t)1<<63;
|
||||
negmax = UINT64_MAX;
|
||||
} else if (LP_ENCODING_IS_12BIT_STR(p[0])) {
|
||||
*count = LP_ENCODING_12BIT_STR_LEN(p);
|
||||
return p+2;
|
||||
} else if (LP_ENCODING_IS_32BIT_STR(p[0])) {
|
||||
*count = LP_ENCODING_32BIT_STR_LEN(p);
|
||||
return p+5;
|
||||
} else {
|
||||
uval = 12345678900000000ULL + p[0];
|
||||
negstart = UINT64_MAX;
|
||||
negmax = 0;
|
||||
}
|
||||
|
||||
/* We reach this code path only for integer encodings.
|
||||
* Convert the unsigned value to the signed one using two's complement
|
||||
* rule. */
|
||||
if (uval >= negstart) {
|
||||
/* This three steps conversion should avoid undefined behaviors
|
||||
* in the unsigned -> signed conversion. */
|
||||
uval = negmax-uval;
|
||||
val = uval;
|
||||
val = -val-1;
|
||||
} else {
|
||||
val = uval;
|
||||
}
|
||||
|
||||
/* Return the string representation of the integer or the value itself
|
||||
* depending on intbuf being NULL or not. */
|
||||
if (intbuf) {
|
||||
*count = snprintf((char*)intbuf,LP_INTBUF_SIZE,"%lld",val);
|
||||
return intbuf;
|
||||
} else {
|
||||
*count = val;
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/* Insert, delete or replace the specified element 'ele' of lenght 'len' at
|
||||
* the specified position 'p', with 'p' being a listpack element pointer
|
||||
* obtained with lpFirst(), lpLast(), lpIndex(), lpNext(), lpPrev() or
|
||||
* lpSeek().
|
||||
*
|
||||
* The element is inserted before, after, or replaces the element pointed
|
||||
* by 'p' depending on the 'where' argument, that can be LP_BEFORE, LP_AFTER
|
||||
* or LP_REPLACE.
|
||||
*
|
||||
* If 'ele' is set to NULL, the function removes the element pointed by 'p'
|
||||
* instead of inserting one.
|
||||
*
|
||||
* Returns NULL on out of memory or when the listpack total length would exceed
|
||||
* the max allowed size of 2^32-1, otherwise the new pointer to the listpack
|
||||
* holding the new element is returned (and the old pointer passed is no longer
|
||||
* considered valid)
|
||||
*
|
||||
* If 'newp' is not NULL, at the end of a successful call '*newp' will be set
|
||||
* to the address of the element just added, so that it will be possible to
|
||||
* continue an interation with lpNext() and lpPrev().
|
||||
*
|
||||
* For deletion operations ('ele' set to NULL) 'newp' is set to the next
|
||||
* element, on the right of the deleted one, or to NULL if the deleted element
|
||||
* was the last one. */
|
||||
unsigned char *lpInsert(unsigned char *lp, unsigned char *ele, uint32_t size, unsigned char *p, int where, unsigned char **newp) {
|
||||
unsigned char intenc[LP_MAX_INT_ENCODING_LEN];
|
||||
unsigned char backlen[LP_MAX_BACKLEN_SIZE];
|
||||
|
||||
uint64_t enclen; /* The length of the encoded element. */
|
||||
|
||||
/* An element pointer set to NULL means deletion, which is conceptually
|
||||
* replacing the element with a zero-length element. So whatever we
|
||||
* get passed as 'where', set it to LP_REPLACE. */
|
||||
if (ele == NULL) where = LP_REPLACE;
|
||||
|
||||
/* If we need to insert after the current element, we just jump to the
|
||||
* next element (that could be the EOF one) and handle the case of
|
||||
* inserting before. So the function will actually deal with just two
|
||||
* cases: LP_BEFORE and LP_REPLACE. */
|
||||
if (where == LP_AFTER) {
|
||||
p = lpSkip(p);
|
||||
where = LP_BEFORE;
|
||||
}
|
||||
|
||||
/* Store the offset of the element 'p', so that we can obtain its
|
||||
* address again after a reallocation. */
|
||||
unsigned long poff = p-lp;
|
||||
|
||||
/* Calling lpEncodeGetType() results into the encoded version of the
|
||||
* element to be stored into 'intenc' in case it is representable as
|
||||
* an integer: in that case, the function returns LP_ENCODING_INT.
|
||||
* Otherwise if LP_ENCODING_STR is returned, we'll have to call
|
||||
* lpEncodeString() to actually write the encoded string on place later.
|
||||
*
|
||||
* Whatever the returned encoding is, 'enclen' is populated with the
|
||||
* length of the encoded element. */
|
||||
int enctype;
|
||||
if (ele) {
|
||||
enctype = lpEncodeGetType(ele,size,intenc,&enclen);
|
||||
} else {
|
||||
enctype = -1;
|
||||
enclen = 0;
|
||||
}
|
||||
|
||||
/* We need to also encode the backward-parsable length of the element
|
||||
* and append it to the end: this allows to traverse the listpack from
|
||||
* the end to the start. */
|
||||
unsigned long backlen_size = ele ? lpEncodeBacklen(backlen,enclen) : 0;
|
||||
uint64_t old_listpack_bytes = lpGetTotalBytes(lp);
|
||||
uint32_t replaced_len = 0;
|
||||
if (where == LP_REPLACE) {
|
||||
replaced_len = lpCurrentEncodedSize(p);
|
||||
replaced_len += lpEncodeBacklen(NULL,replaced_len);
|
||||
}
|
||||
|
||||
uint64_t new_listpack_bytes = old_listpack_bytes + enclen + backlen_size
|
||||
- replaced_len;
|
||||
if (new_listpack_bytes > UINT32_MAX) return NULL;
|
||||
|
||||
/* We now need to reallocate in order to make space or shrink the
|
||||
* allocation (in case 'when' value is LP_REPLACE and the new element is
|
||||
* smaller). However we do that before memmoving the memory to
|
||||
* make room for the new element if the final allocation will get
|
||||
* larger, or we do it after if the final allocation will get smaller. */
|
||||
|
||||
unsigned char *dst = lp + poff; /* May be updated after reallocation. */
|
||||
|
||||
/* Realloc before: we need more room. */
|
||||
if (new_listpack_bytes > old_listpack_bytes) {
|
||||
if ((lp = lp_realloc(lp,new_listpack_bytes)) == NULL) return NULL;
|
||||
dst = lp + poff;
|
||||
}
|
||||
|
||||
/* Setup the listpack relocating the elements to make the exact room
|
||||
* we need to store the new one. */
|
||||
if (where == LP_BEFORE) {
|
||||
memmove(dst+enclen+backlen_size,dst,old_listpack_bytes-poff);
|
||||
} else { /* LP_REPLACE. */
|
||||
long lendiff = (enclen+backlen_size)-replaced_len;
|
||||
memmove(dst+replaced_len+lendiff,
|
||||
dst+replaced_len,
|
||||
old_listpack_bytes-poff-replaced_len);
|
||||
}
|
||||
|
||||
/* Realloc after: we need to free space. */
|
||||
if (new_listpack_bytes < old_listpack_bytes) {
|
||||
if ((lp = lp_realloc(lp,new_listpack_bytes)) == NULL) return NULL;
|
||||
dst = lp + poff;
|
||||
}
|
||||
|
||||
/* Store the entry. */
|
||||
if (newp) {
|
||||
*newp = dst;
|
||||
/* In case of deletion, set 'newp' to NULL if the next element is
|
||||
* the EOF element. */
|
||||
if (!ele && dst[0] == LP_EOF) *newp = NULL;
|
||||
}
|
||||
if (ele) {
|
||||
if (enctype == LP_ENCODING_INT) {
|
||||
memcpy(dst,intenc,enclen);
|
||||
} else {
|
||||
lpEncodeString(dst,ele,size);
|
||||
}
|
||||
dst += enclen;
|
||||
memcpy(dst,backlen,backlen_size);
|
||||
dst += backlen_size;
|
||||
}
|
||||
|
||||
/* Update header. */
|
||||
if (where != LP_REPLACE || ele == NULL) {
|
||||
uint32_t num_elements = lpGetNumElements(lp);
|
||||
if (num_elements != LP_HDR_NUMELE_UNKNOWN) {
|
||||
if (ele)
|
||||
lpSetNumElements(lp,num_elements+1);
|
||||
else
|
||||
lpSetNumElements(lp,num_elements-1);
|
||||
}
|
||||
}
|
||||
lpSetTotalBytes(lp,new_listpack_bytes);
|
||||
return lp;
|
||||
}
|
||||
|
||||
/* Append the specified element 'ele' of lenght 'len' at the end of the
|
||||
* listpack. It is implemented in terms of lpInsert(), so the return value is
|
||||
* the same as lpInsert(). */
|
||||
unsigned char *lpAppend(unsigned char *lp, unsigned char *ele, uint32_t size) {
|
||||
uint64_t listpack_bytes = lpGetTotalBytes(lp);
|
||||
unsigned char *eofptr = lp + listpack_bytes - 1;
|
||||
return lpInsert(lp,ele,size,eofptr,LP_BEFORE,NULL);
|
||||
}
|
||||
|
||||
/* Remove the element pointed by 'p', and return the resulting listpack.
|
||||
* If 'newp' is not NULL, the next element pointer (to the right of the
|
||||
* deleted one) is returned by reference. If the deleted element was the
|
||||
* last one, '*newp' is set to NULL. */
|
||||
unsigned char *lpDelete(unsigned char *lp, unsigned char *p, unsigned char **newp) {
|
||||
return lpInsert(lp,NULL,0,p,LP_REPLACE,newp);
|
||||
}
|
||||
|
||||
/* Return the total number of bytes the listpack is composed of. */
|
||||
uint32_t lpBytes(unsigned char *lp) {
|
||||
return lpGetTotalBytes(lp);
|
||||
}
|
||||
|
||||
/* Seek the specified element and returns the pointer to the seeked element.
|
||||
* Positive indexes specify the zero-based element to seek from the head to
|
||||
* the tail, negative indexes specify elements starting from the tail, where
|
||||
* -1 means the last element, -2 the penultimate and so forth. If the index
|
||||
* is out of range, NULL is returned. */
|
||||
unsigned char *lpSeek(unsigned char *lp, long index) {
|
||||
int forward = 1; /* Seek forward by default. */
|
||||
|
||||
/* We want to seek from left to right or the other way around
|
||||
* depending on the listpack length and the element position.
|
||||
* However if the listpack length cannot be obtained in constant time,
|
||||
* we always seek from left to right. */
|
||||
uint32_t numele = lpGetNumElements(lp);
|
||||
if (numele != LP_HDR_NUMELE_UNKNOWN) {
|
||||
if (index < 0) index = (long)numele+index;
|
||||
if (index < 0) return NULL; /* Index still < 0 means out of range. */
|
||||
if (index >= numele) return NULL; /* Out of range the other side. */
|
||||
/* We want to scan right-to-left if the element we are looking for
|
||||
* is past the half of the listpack. */
|
||||
if (index > numele/2) {
|
||||
forward = 0;
|
||||
/* Left to right scanning always expects a negative index. Convert
|
||||
* our index to negative form. */
|
||||
index -= numele;
|
||||
}
|
||||
} else {
|
||||
/* If the listpack length is unspecified, for negative indexes we
|
||||
* want to always scan left-to-right. */
|
||||
if (index < 0) forward = 0;
|
||||
}
|
||||
|
||||
/* Forward and backward scanning is trivially based on lpNext()/lpPrev(). */
|
||||
if (forward) {
|
||||
unsigned char *ele = lpFirst(lp);
|
||||
while (index > 0 && ele) {
|
||||
ele = lpNext(lp,ele);
|
||||
index--;
|
||||
}
|
||||
return ele;
|
||||
} else {
|
||||
unsigned char *ele = lpLast(lp);
|
||||
while (index < -1 && ele) {
|
||||
ele = lpPrev(lp,ele);
|
||||
index++;
|
||||
}
|
||||
return ele;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/* Listpack -- A lists of strings serialization format
|
||||
*
|
||||
* This file implements the specification you can find at:
|
||||
*
|
||||
* https://github.com/antirez/listpack
|
||||
*
|
||||
* Copyright (c) 2017, 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 __LISTPACK_H
|
||||
#define __LISTPACK_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define LP_INTBUF_SIZE 21 /* 20 digits of -2^63 + 1 null term = 21. */
|
||||
|
||||
/* lpInsert() where argument possible values: */
|
||||
#define LP_BEFORE 0
|
||||
#define LP_AFTER 1
|
||||
#define LP_REPLACE 2
|
||||
|
||||
unsigned char *lpNew(void);
|
||||
void lpFree(unsigned char *lp);
|
||||
unsigned char *lpInsert(unsigned char *lp, unsigned char *ele, uint32_t size, unsigned char *p, int where, unsigned char **newp);
|
||||
unsigned char *lpAppend(unsigned char *lp, unsigned char *ele, uint32_t size);
|
||||
unsigned char *lpDelete(unsigned char *lp, unsigned char *p, unsigned char **newp);
|
||||
uint32_t lpLength(unsigned char *lp);
|
||||
unsigned char *lpGet(unsigned char *p, int64_t *count, unsigned char *intbuf);
|
||||
unsigned char *lpFirst(unsigned char *lp);
|
||||
unsigned char *lpLast(unsigned char *lp);
|
||||
unsigned char *lpNext(unsigned char *lp, unsigned char *p);
|
||||
unsigned char *lpPrev(unsigned char *lp, unsigned char *p);
|
||||
uint32_t lpBytes(unsigned char *lp);
|
||||
unsigned char *lpSeek(unsigned char *lp, long index);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,45 @@
|
||||
/* Listpack -- A lists of strings serialization format
|
||||
* https://github.com/antirez/listpack
|
||||
*
|
||||
* Copyright (c) 2017, 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.
|
||||
*/
|
||||
|
||||
/* Allocator selection.
|
||||
*
|
||||
* This file is used in order to change the Rax allocator at compile time.
|
||||
* Just define the following defines to what you want to use. Also add
|
||||
* the include of your alternate allocator if needed (not needed in order
|
||||
* to use the default libc allocator). */
|
||||
|
||||
#ifndef LISTPACK_ALLOC_H
|
||||
#define LISTPACK_ALLOC_H
|
||||
#include "zmalloc.h"
|
||||
#define lp_malloc zmalloc
|
||||
#define lp_realloc zrealloc
|
||||
#define lp_free zfree
|
||||
#endif
|
||||
+5
-1
@@ -79,7 +79,11 @@
|
||||
* Unconditionally aligning does not cost very much, so do it if unsure
|
||||
*/
|
||||
#ifndef STRICT_ALIGN
|
||||
# define STRICT_ALIGN !(defined(__i386) || defined (__amd64))
|
||||
# if !(defined(__i386) || defined (__amd64))
|
||||
# define STRICT_ALIGN 1
|
||||
# else
|
||||
# define STRICT_ALIGN 0
|
||||
# endif
|
||||
#endif
|
||||
|
||||
/*
|
||||
|
||||
+827
-162
File diff suppressed because it is too large
Load Diff
-1145
File diff suppressed because it is too large
Load Diff
@@ -1,856 +0,0 @@
|
||||
Redis Modules: an introduction to the API
|
||||
===
|
||||
|
||||
The modules documentation is composed of the following files:
|
||||
|
||||
* `INTRO.md` (this file). An overview about Redis Modules system and API. It's a good idea to start your reading here.
|
||||
* `API.md` is generated from module.c top comments of RedisMoule functions. It is a good reference in order to understand how each function works.
|
||||
* `TYPES.md` covers the implementation of native data types into modules.
|
||||
|
||||
Redis modules make possible to extend Redis functionality using external
|
||||
modules, implementing new Redis commands at a speed and with features
|
||||
similar to what can be done inside the core itself.
|
||||
|
||||
Redis modules are dynamic libraries, that can be loaded into Redis at
|
||||
startup or using the `MODULE LOAD` command. Redis exports a C API, in the
|
||||
form of a single C header file called `redismodule.h`. Modules are meant
|
||||
to be written in C, however it will be possible to use C++ or other languages
|
||||
that have C binding functionalities.
|
||||
|
||||
Modules are designed in order to be loaded into different versions of Redis,
|
||||
so a given module does not need to be designed, or recompiled, in order to
|
||||
run with a specific version of Redis. For this reason, the module will
|
||||
register to the Redis core using a specific API version. The current API
|
||||
version is "1".
|
||||
|
||||
This document is about an alpha version of Redis modules. API, functionalities
|
||||
and other details may change in the future.
|
||||
|
||||
# Loading modules
|
||||
|
||||
In order to test the module you are developing, you can load the module
|
||||
using the following `redis.conf` configuration directive:
|
||||
|
||||
loadmodule /path/to/mymodule.so
|
||||
|
||||
It is also possible to load a module at runtime using the following command:
|
||||
|
||||
MODULE LOAD /path/to/mymodule.so
|
||||
|
||||
In order to list all loaded modules, use:
|
||||
|
||||
MODULE LIST
|
||||
|
||||
Finally, you can unload (and later reload if you wish) a module using the
|
||||
following command:
|
||||
|
||||
MODULE UNLOAD mymodule
|
||||
|
||||
Note that `mymodule` above is not the filename without the `.so` suffix, but
|
||||
instead, the name the module used to register itself into the Redis core.
|
||||
The name can be obtained using `MODULE LIST`. However it is good practice
|
||||
that the filename of the dynamic library is the same as the name the module
|
||||
uses to register itself into the Redis core.
|
||||
|
||||
# The simplest module you can write
|
||||
|
||||
In order to show the different parts of a module, here we'll show a very
|
||||
simple module that implements a command that outputs a random number.
|
||||
|
||||
#include "redismodule.h"
|
||||
#include <stdlib.h>
|
||||
|
||||
int HelloworldRand_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
RedisModule_ReplyWithLongLong(ctx,rand());
|
||||
return REDISMODULE_OK;
|
||||
}
|
||||
|
||||
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
if (RedisModule_Init(ctx,"helloworld",1,REDISMODULE_APIVER_1)
|
||||
== REDISMODULE_ERR) return REDISMODULE_ERR;
|
||||
|
||||
if (RedisModule_CreateCommand(ctx,"helloworld.rand",
|
||||
HelloworldRand_RedisCommand) == REDISMODULE_ERR)
|
||||
return REDISMODULE_ERR;
|
||||
|
||||
return REDISMODULE_OK;
|
||||
}
|
||||
|
||||
The example module has two functions. One implements a command called
|
||||
HELLOWORLD.RAND. This function is specific of that module. However the
|
||||
other function called `RedisModule_OnLoad()` must be present in each
|
||||
Redis module. It is the entry point for the module to be initialized,
|
||||
register its commands, and potentially other private data structures
|
||||
it uses.
|
||||
|
||||
Note that it is a good idea for modules to call commands with the
|
||||
name of the module followed by a dot, and finally the command name,
|
||||
like in the case of `HELLOWORLD.RAND`. This way it is less likely to
|
||||
have collisions.
|
||||
|
||||
Note that if different modules have colliding commands, they'll not be
|
||||
able to work in Redis at the same time, since the function
|
||||
`RedisModule_CreateCommand` will fail in one of the modules, so the module
|
||||
loading will abort returning an error condition.
|
||||
|
||||
# Module initialization
|
||||
|
||||
The above example shows the usage of the function `RedisModule_Init()`.
|
||||
It should be the first function called by the module `OnLoad` function.
|
||||
The following is the function prototype:
|
||||
|
||||
int RedisModule_Init(RedisModuleCtx *ctx, const char *modulename,
|
||||
int module_version, int api_version);
|
||||
|
||||
The `Init` function announces the Redis core that the module has a given
|
||||
name, its version (that is reported by `MODULE LIST`), and that is willing
|
||||
to use a specific version of the API.
|
||||
|
||||
If the API version is wrong, the name is already taken, or there are other
|
||||
similar errors, the function will return `REDISMODULE_ERR`, and the module
|
||||
`OnLoad` function should return ASAP with an error.
|
||||
|
||||
Before the `Init` function is called, no other API function can be called,
|
||||
otherwise the module will segfault and the Redis instance will crash.
|
||||
|
||||
The second function called, `RedisModule_CreateCommand`, is used in order
|
||||
to register commands into the Redis core. The following is the prototype:
|
||||
|
||||
int RedisModule_CreateCommand(RedisModuleCtx *ctx, const char *cmdname,
|
||||
RedisModuleCmdFunc cmdfunc);
|
||||
|
||||
As you can see, most Redis modules API calls all take as first argument
|
||||
the `context` of the module, so that they have a reference to the module
|
||||
calling it, to the command and client executing a given command, and so forth.
|
||||
|
||||
To create a new command, the above function needs the context, the command
|
||||
name, and the function pointer of the function implementing the command,
|
||||
which must have the following prototype:
|
||||
|
||||
|
||||
int mycommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc);
|
||||
|
||||
The command function arguments are just the context, that will be passed
|
||||
to all the other API calls, the command argument vector, and total number
|
||||
of arguments, as passed by the user.
|
||||
|
||||
As you can see, the arguments are provided as pointers to a specific data
|
||||
type, the `RedisModuleString`. This is an opaque data type you have API
|
||||
functions to access and use, direct access to its fields is never needed.
|
||||
|
||||
Zooming into the example command implementation, we can find another call:
|
||||
|
||||
int RedisModule_ReplyWithLongLong(RedisModuleCtx *ctx, long long integer);
|
||||
|
||||
This function returns an integer to the client that invoked the command,
|
||||
exactly like other Redis commands do, like for example `INCR` or `SCARD`.
|
||||
|
||||
# Setup and dependencies of a Redis module
|
||||
|
||||
Redis modules don't depend on Redis or some other library, nor they
|
||||
need to be compiled with a specific `redismodule.h` file. In order
|
||||
to create a new module, just copy a recent version of `redismodule.h`
|
||||
in your source tree, link all the libraries you want, and create
|
||||
a dynamic library having the `RedisModule_OnLoad()` function symbol
|
||||
exported.
|
||||
|
||||
The module will be able to load into different versions of Redis.
|
||||
|
||||
# Passing configuration parameters to Redis modules
|
||||
|
||||
When the module is loaded with the `MODULE LOAD` command, or using the
|
||||
`loadmodule` directive in the `redis.conf` file, the user is able to pass
|
||||
configuration parameters to the module by adding arguments after the module
|
||||
file name:
|
||||
|
||||
loadmodule mymodule.so foo bar 1234
|
||||
|
||||
In the above example the strings `foo`, `bar` and `123` will be passed
|
||||
to the module `OnLoad()` function in the `argv` argument as an array
|
||||
of RedisModuleString pointers. The number of arguments passed is into `argc`.
|
||||
|
||||
The way you can access those strings will be explained in the rest of this
|
||||
document. Normally the module will store the module configuration parameters
|
||||
in some `static` global variable that can be accessed module wide, so that
|
||||
the configuration can change the behavior of different commands.
|
||||
|
||||
# Working with RedisModuleString objects
|
||||
|
||||
The command argument vector `argv` passed to module commands, and the
|
||||
return value of other module APIs functions, are of type `RedisModuleString`.
|
||||
|
||||
Usually you directly pass module strings to other API calls, however sometimes
|
||||
you may need to directly access the string object.
|
||||
|
||||
There are a few functions in order to work with string objects:
|
||||
|
||||
const char *RedisModule_StringPtrLen(RedisModuleString *string, size_t *len);
|
||||
|
||||
The above function accesses a string by returning its pointer and setting its
|
||||
length in `len`.
|
||||
You should never write to a string object pointer, as you can see from the
|
||||
`const` pointer qualifier.
|
||||
|
||||
However, if you want, you can create new string objects using the following
|
||||
API:
|
||||
|
||||
RedisModuleString *RedisModule_CreateString(RedisModuleCtx *ctx, const char *ptr, size_t len);
|
||||
|
||||
The string returned by the above command must be freed using a corresponding
|
||||
call to `RedisModule_FreeString()`:
|
||||
|
||||
void RedisModule_FreeString(RedisModuleString *str);
|
||||
|
||||
However if you want to avoid having to free strings, the automatic memory
|
||||
management, covered later in this document, can be a good alternative, by
|
||||
doing it for you.
|
||||
|
||||
Note that the strings provided via the argument vector `argv` never need
|
||||
to be freed. You only need to free new strings you create, or new strings
|
||||
returned by other APIs, where it is specified that the returned string must
|
||||
be freed.
|
||||
|
||||
## Creating strings from numbers or parsing strings as numbers
|
||||
|
||||
Creating a new string from an integer is a very common operation, so there
|
||||
is a function to do this:
|
||||
|
||||
RedisModuleString *mystr = RedisModule_CreateStringFromLongLong(ctx,10);
|
||||
|
||||
Similarly in order to parse a string as a number:
|
||||
|
||||
long long myval;
|
||||
if (RedisModule_StringToLongLong(ctx,argv[1],&myval) == REDISMODULE_OK) {
|
||||
/* Do something with 'myval' */
|
||||
}
|
||||
|
||||
## Accessing Redis keys from modules
|
||||
|
||||
Most Redis modules, in order to be useful, have to interact with the Redis
|
||||
data space (this is not always true, for example an ID generator may
|
||||
never touch Redis keys). Redis modules have two different APIs in order to
|
||||
access the Redis data space, one is a low level API that provides very
|
||||
fast access and a set of functions to manipulate Redis data structures.
|
||||
The other API is more high level, and allows to call Redis commands and
|
||||
fetch the result, similarly to how Lua scripts access Redis.
|
||||
|
||||
The high level API is also useful in order to access Redis functionalities
|
||||
that are not available as APIs.
|
||||
|
||||
In general modules developers should prefer the low level API, because commands
|
||||
implemented using the low level API run at a speed comparable to the speed
|
||||
of native Redis commands. However there are definitely use cases for the
|
||||
higher level API. For example often the bottleneck could be processing the
|
||||
data and not accessing it.
|
||||
|
||||
Also note that sometimes using the low level API is not harder compared to
|
||||
the higher level one.
|
||||
|
||||
# Calling Redis commands
|
||||
|
||||
The high level API to access Redis is the sum of the `RedisModule_Call()`
|
||||
function, together with the functions needed in order to access the
|
||||
reply object returned by `Call()`.
|
||||
|
||||
`RedisModule_Call` uses a special calling convention, with a format specifier
|
||||
that is used to specify what kind of objects you are passing as arguments
|
||||
to the function.
|
||||
|
||||
Redis commands are invoked just using a command name and a list of arguments.
|
||||
However when calling commands, the arguments may originate from different
|
||||
kind of strings: null-terminated C strings, RedisModuleString objects as
|
||||
received from the `argv` parameter in the command implementation, binary
|
||||
safe C buffers with a pointer and a length, and so forth.
|
||||
|
||||
For example if I want to call `INCRBY` using a first argument (the key)
|
||||
a string received in the argument vector `argv`, which is an array
|
||||
of RedisModuleString object pointers, and a C string representing the
|
||||
number "10" as second argument (the increment), I'll use the following
|
||||
function call:
|
||||
|
||||
RedisModuleCallReply *reply;
|
||||
reply = RedisModule_Call(ctx,"INCR","sc",argv[1],"10");
|
||||
|
||||
The first argument is the context, and the second is always a null terminated
|
||||
C string with the command name. The third argument is the format specifier
|
||||
where each character corresponds to the type of the arguments that will follow.
|
||||
In the above case `"sc"` means a RedisModuleString object, and a null
|
||||
terminated C string. The other arguments are just the two arguments as
|
||||
specified. In fact `argv[1]` is a RedisModuleString and `"10"` is a null
|
||||
terminated C string.
|
||||
|
||||
This is the full list of format specifiers:
|
||||
|
||||
* **c** -- Null terminated C string pointer.
|
||||
* **b** -- C buffer, two arguments needed: C string pointer and `size_t` length.
|
||||
* **s** -- RedisModuleString as received in `argv` or by other Redis module APIs returning a RedisModuleString object.
|
||||
* **l** -- Long long integer.
|
||||
* **v** -- Array of RedisModuleString objects.
|
||||
* **!** -- This modifier just tells the function to replicate the command to slaves and AOF. It is ignored from the point of view of arguments parsing.
|
||||
|
||||
The function returns a `RedisModuleCallReply` object on success, on
|
||||
error NULL is returned.
|
||||
|
||||
NULL is returned when the command name is invalid, the format specifier uses
|
||||
characters that are not recognized, or when the command is called with the
|
||||
wrong number of arguments. In the above cases the `errno` var is set to `EINVAL`. NULL is also returned when, in an instance with Cluster enabled, the target
|
||||
keys are about non local hash slots. In this case `errno` is set to `EPERM`.
|
||||
|
||||
## Working with RedisModuleCallReply objects.
|
||||
|
||||
`RedisModuleCall` returns reply objects that can be accessed using the
|
||||
`RedisModule_CallReply*` family of functions.
|
||||
|
||||
In order to obtain the type or reply (corresponding to one of the data types
|
||||
supported by the Redis protocol), the function `RedisModule_CallReplyType()`
|
||||
is used:
|
||||
|
||||
reply = RedisModule_Call(ctx,"INCR","sc",argv[1],"10");
|
||||
if (RedisModule_CallReplyType(reply) == REDISMODULE_REPLY_INTEGER) {
|
||||
long long myval = RedisModule_CallReplyInteger(reply);
|
||||
/* Do something with myval. */
|
||||
}
|
||||
|
||||
Valid reply types are:
|
||||
|
||||
* `REDISMODULE_REPLY_STRING` Bulk string or status replies.
|
||||
* `REDISMODULE_REPLY_ERROR` Errors.
|
||||
* `REDISMODULE_REPLY_INTEGER` Signed 64 bit integers.
|
||||
* `REDISMODULE_REPLY_ARRAY` Array of replies.
|
||||
* `REDISMODULE_REPLY_NULL` NULL reply.
|
||||
|
||||
Strings, errors and arrays have an associated length. For strings and errors
|
||||
the length corresponds to the length of the string. For arrays the length
|
||||
is the number of elements. To obtain the reply length the following function
|
||||
is used:
|
||||
|
||||
size_t reply_len = RedisModule_CallReplyLength(reply);
|
||||
|
||||
In order to obtain the value of an integer reply, the following function is used, as already shown in the example above:
|
||||
|
||||
long long reply_integer_val = RedisModule_CallReplyInteger(reply);
|
||||
|
||||
Called with a reply object of the wrong type, the above function always
|
||||
returns `LLONG_MIN`.
|
||||
|
||||
Sub elements of array replies are accessed this way:
|
||||
|
||||
RedisModuleCallReply *subreply;
|
||||
subreply = RedisModule_CallReplyArrayElement(reply,idx);
|
||||
|
||||
The above function returns NULL if you try to access out of range elements.
|
||||
|
||||
Strings and errors (which are like strings but with a different type) can
|
||||
be accessed using in the following way, making sure to never write to
|
||||
the resulting pointer (that is returned as as `const` pointer so that
|
||||
misusing must be pretty explicit):
|
||||
|
||||
size_t len;
|
||||
char *ptr = RedisModule_CallReplyStringPtr(reply,&len);
|
||||
|
||||
If the reply type is not a string or an error, NULL is returned.
|
||||
|
||||
RedisCallReply objects are not the same as module string objects
|
||||
(RedisModuleString types). However sometimes you may need to pass replies
|
||||
of type string or integer, to API functions expecting a module string.
|
||||
|
||||
When this is the case, you may want to evaluate if using the low level
|
||||
API could be a simpler way to implement your command, or you can use
|
||||
the following function in order to create a new string object from a
|
||||
call reply of type string, error or integer:
|
||||
|
||||
RedisModuleString *mystr = RedisModule_CreateStringFromCallReply(myreply);
|
||||
|
||||
If the reply is not of the right type, NULL is returned.
|
||||
The returned string object should be released with `RedisModule_FreeString()`
|
||||
as usually, or by enabling automatic memory management (see corresponding
|
||||
section).
|
||||
|
||||
# Releasing call reply objects
|
||||
|
||||
Reply objects must be freed using `RedisModule_FreeCallReply`. For arrays,
|
||||
you need to free only the top level reply, not the nested replies.
|
||||
Currently the module implementation provides a protection in order to avoid
|
||||
crashing if you free a nested reply object for error, however this feature
|
||||
is not guaranteed to be here forever, so should not be considered part
|
||||
of the API.
|
||||
|
||||
If you use automatic memory management (explained later in this document)
|
||||
you don't need to free replies (but you still could if you wish to release
|
||||
memory ASAP).
|
||||
|
||||
## Returning values from Redis commands
|
||||
|
||||
Like normal Redis commands, new commands implemented via modules must be
|
||||
able to return values to the caller. The API exports a set of functions for
|
||||
this goal, in order to return the usual types of the Redis protocol, and
|
||||
arrays of such types as elemented. Also errors can be returned with any
|
||||
error string and code (the error code is the initial uppercase letters in
|
||||
the error message, like the "BUSY" string in the "BUSY the sever is busy" error
|
||||
message).
|
||||
|
||||
All the functions to send a reply to the client are called
|
||||
`RedisModule_ReplyWith<something>`.
|
||||
|
||||
To return an error, use:
|
||||
|
||||
RedisModule_ReplyWithError(RedisModuleCtx *ctx, const char *err);
|
||||
|
||||
There is a predefined error string for key of wrong type errors:
|
||||
|
||||
REDISMODULE_ERRORMSG_WRONGTYPE
|
||||
|
||||
Example usage:
|
||||
|
||||
RedisModule_ReplyWithError(ctx,"ERR invalid arguments");
|
||||
|
||||
We already saw how to reply with a long long in the examples above:
|
||||
|
||||
RedisModule_ReplyWithLongLong(ctx,12345);
|
||||
|
||||
To reply with a simple string, that can't contain binary values or newlines,
|
||||
(so it's suitable to send small words, like "OK") we use:
|
||||
|
||||
RedisModule_ReplyWithSimpleString(ctx,"OK");
|
||||
|
||||
It's possible to reply with "bulk strings" that are binary safe, using
|
||||
two different functions:
|
||||
|
||||
int RedisModule_ReplyWithStringBuffer(RedisModuleCtx *ctx, const char *buf, size_t len);
|
||||
|
||||
int RedisModule_ReplyWithString(RedisModuleCtx *ctx, RedisModuleString *str);
|
||||
|
||||
The first function gets a C pointer and length. The second a RedisMoudleString
|
||||
object. Use one or the other depending on the source type you have at hand.
|
||||
|
||||
In order to reply with an array, you just need to use a function to emit the
|
||||
array length, followed by as many calls to the above functions as the number
|
||||
of elements of the array are:
|
||||
|
||||
RedisModule_ReplyWithArray(ctx,2);
|
||||
RedisModule_ReplyWithStringBuffer(ctx,"age",3);
|
||||
RedisModule_ReplyWithLongLong(ctx,22);
|
||||
|
||||
To return nested arrays is easy, your nested array element just uses another
|
||||
call to `RedisModule_ReplyWithArray()` followed by the calls to emit the
|
||||
sub array elements.
|
||||
|
||||
## Returning arrays with dynamic length
|
||||
|
||||
Sometimes it is not possible to know beforehand the number of items of
|
||||
an array. As an example, think of a Redis module implementing a FACTOR
|
||||
command that given a number outputs the prime factors. Instead of
|
||||
factorializing the number, storing the prime factors into an array, and
|
||||
later produce the command reply, a better solution is to start an array
|
||||
reply where the length is not known, and set it later. This is accomplished
|
||||
with a special argument to `RedisModule_ReplyWithArray()`:
|
||||
|
||||
RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_ARRAY_LEN);
|
||||
|
||||
The above call starts an array reply so we can use other `ReplyWith` calls
|
||||
in order to produce the array items. Finally in order to set the length
|
||||
se use the following call:
|
||||
|
||||
RedisModule_ReplySetArrayLength(ctx, number_of_items);
|
||||
|
||||
In the case of the FACTOR command, this translates to some code similar
|
||||
to this:
|
||||
|
||||
RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_ARRAY_LEN);
|
||||
number_of_factors = 0;
|
||||
while(still_factors) {
|
||||
RedisModule_ReplyWithLongLong(ctx, some_factor);
|
||||
number_of_factors++;
|
||||
}
|
||||
RedisModule_ReplySetArrayLength(ctx, number_of_factors);
|
||||
|
||||
Another common use case for this feature is iterating over the arrays of
|
||||
some collection and only returning the ones passing some kind of filtering.
|
||||
|
||||
It is possible to have multiple nested arrays with postponed reply.
|
||||
Each call to `SetArray()` will set the length of the latest corresponding
|
||||
call to `ReplyWithArray()`:
|
||||
|
||||
RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_ARRAY_LEN);
|
||||
... generate 100 elements ...
|
||||
RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_ARRAY_LEN);
|
||||
... generate 10 elements ...
|
||||
RedisModule_ReplySetArrayLength(ctx, 10);
|
||||
RedisModule_ReplySetArrayLength(ctx, 100);
|
||||
|
||||
This creates a 100 items array having as last element a 10 items array.
|
||||
|
||||
# Arity and type checks
|
||||
|
||||
Often commands need to check that the number of arguments and type of the key
|
||||
is correct. In order to report a wrong arity, there is a specific function
|
||||
called `RedisModule_WrongArity()`. The usage is trivial:
|
||||
|
||||
if (argc != 2) return RedisModule_WrongArity(ctx);
|
||||
|
||||
Checking for the wrong type involves opening the key and checking the type:
|
||||
|
||||
RedisModuleKey *key = RedisModule_OpenKey(ctx,argv[1],
|
||||
REDISMODULE_READ|REDISMODULE_WRITE);
|
||||
|
||||
int keytype = RedisModule_KeyType(key);
|
||||
if (keytype != REDISMODULE_KEYTYPE_STRING &&
|
||||
keytype != REDISMODULE_KEYTYPE_EMPTY)
|
||||
{
|
||||
RedisModule_CloseKey(key);
|
||||
return RedisModule_ReplyWithError(ctx,REDISMODULE_ERRORMSG_WRONGTYPE);
|
||||
}
|
||||
|
||||
Note that you often want to proceed with a command both if the key
|
||||
is of the expected type, or if it's empty.
|
||||
|
||||
## Low level access to keys
|
||||
|
||||
Low level access to keys allow to perform operations on value objects associated
|
||||
to keys directly, with a speed similar to what Redis uses internally to
|
||||
implement the built-in commands.
|
||||
|
||||
Once a key is opened, a key pointer is returned that will be used with all the
|
||||
other low level API calls in order to perform operations on the key or its
|
||||
associated value.
|
||||
|
||||
Because the API is meant to be very fast, it cannot do too many run-time
|
||||
checks, so the user must be aware of certain rules to follow:
|
||||
|
||||
* Opening the same key multiple times where at least one instance is opened for writing, is undefined and may lead to crashes.
|
||||
* While a key is open, it should only be accessed via the low level key API. For example opening a key, then calling DEL on the same key using the `RedisModule_Call()` API will result into a crash. However it is safe to open a key, perform some operation with the low level API, closing it, then using other APIs to manage the same key, and later opening it again to do some more work.
|
||||
|
||||
In order to open a key the `RedisModule_OpenKey` function is used. It returns
|
||||
a key pointer, that we'll use with all the next calls to access and modify
|
||||
the value:
|
||||
|
||||
RedisModuleKey *key;
|
||||
key = RedisModule_OpenKey(ctx,argv[1],REDISMODULE_READ);
|
||||
|
||||
The second argument is the key name, that must be a `RedisModuleString` object.
|
||||
The third argument is the mode: `REDISMODULE_READ` or `REDISMODULE_WRITE`.
|
||||
It is possible to use `|` to bitwise OR the two modes to open the key in
|
||||
both modes. Currently a key opened for writing can also be accessed for reading
|
||||
but this is to be considered an implementation detail. The right mode should
|
||||
be used in sane modules.
|
||||
|
||||
You can open non exisitng keys for writing, since the keys will be created
|
||||
when an attempt to write to the key is performed. However when opening keys
|
||||
just for reading, `RedisModule_OpenKey` will return NULL if the key does not
|
||||
exist.
|
||||
|
||||
Once you are done using a key, you can close it with:
|
||||
|
||||
RedisModule_CloseKey(key);
|
||||
|
||||
Note that if automatic memory management is enabled, you are not forced to
|
||||
close keys. When the module function returns, Redis will take care to close
|
||||
all the keys which are still open.
|
||||
|
||||
## Getting the key type
|
||||
|
||||
In order to obtain the value of a key, use the `RedisModule_KeyType()` function:
|
||||
|
||||
int keytype = RedisModule_KeyType(key);
|
||||
|
||||
It returns one of the following values:
|
||||
|
||||
REDISMODULE_KEYTYPE_EMPTY
|
||||
REDISMODULE_KEYTYPE_STRING
|
||||
REDISMODULE_KEYTYPE_LIST
|
||||
REDISMODULE_KEYTYPE_HASH
|
||||
REDISMODULE_KEYTYPE_SET
|
||||
REDISMODULE_KEYTYPE_ZSET
|
||||
|
||||
The above are just the usual Redis key types, with the addition of an empty
|
||||
type, that signals the key pointer is associated with an empty key that
|
||||
does not yet exists.
|
||||
|
||||
## Creating new keys
|
||||
|
||||
To create a new key, open it for writing and then write to it using one
|
||||
of the key writing functions. Example:
|
||||
|
||||
RedisModuleKey *key;
|
||||
key = RedisModule_OpenKey(ctx,argv[1],REDISMODULE_READ);
|
||||
if (RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_EMPTY) {
|
||||
RedisModule_StringSet(key,argv[2]);
|
||||
}
|
||||
|
||||
## Deleting keys
|
||||
|
||||
Just use:
|
||||
|
||||
RedisModule_DeleteKey(key);
|
||||
|
||||
The function returns `REDISMODULE_ERR` if the key is not open for writing.
|
||||
Note that after a key gets deleted, it is setup in order to be targeted
|
||||
by new key commands. For example `RedisModule_KeyType()` will return it is
|
||||
an empty key, and writing to it will create a new key, possibly of another
|
||||
type (depending on the API used).
|
||||
|
||||
## Managing key expires (TTLs)
|
||||
|
||||
To control key expires two functions are provided, that are able to set,
|
||||
modify, get, and unset the time to live associated with a key.
|
||||
|
||||
One function is used in order to query the current expire of an open key:
|
||||
|
||||
mstime_t RedisModule_GetExpire(RedisModuleKey *key);
|
||||
|
||||
The function returns the time to live of the key in milliseconds, or
|
||||
`REDISMODULE_NO_EXPIRE` as a special value to signal the key has no associated
|
||||
expire or does not exist at all (you can differentiate the two cases checking
|
||||
if the key type is `REDISMODULE_KEYTYPE_EMPTY`).
|
||||
|
||||
In order to change the expire of a key the following function is used instead:
|
||||
|
||||
int RedisModule_SetExpire(RedisModuleKey *key, mstime_t expire);
|
||||
|
||||
When called on a non existing key, `REDISMODULE_ERR` is returned, because
|
||||
the function can only associate expires to existing open keys (non existing
|
||||
open keys are only useful in order to create new values with data type
|
||||
specific write operations).
|
||||
|
||||
Again the `expire` time is specified in milliseconds. If the key has currently
|
||||
no expire, a new expire is set. If the key already have an expire, it is
|
||||
replaced with the new value.
|
||||
|
||||
If the key has an expire, and the special value `REDISMODULE_NO_EXPIRE` is
|
||||
used as a new expire, the expire is removed, similarly to the Redis
|
||||
`PERSIST` command. In case the key was already persistent, no operation is
|
||||
performed.
|
||||
|
||||
## Obtaining the length of values
|
||||
|
||||
There is a single function in order to retrieve the length of the value
|
||||
associated to an open key. The returned length is value-specific, and is
|
||||
the string length for strings, and the number of elements for the aggregated
|
||||
data types (how many elements there is in a list, set, sorted set, hash).
|
||||
|
||||
size_t len = RedisModule_ValueLength(key);
|
||||
|
||||
If the key does not exist, 0 is returned by the function:
|
||||
|
||||
## String type API
|
||||
|
||||
Setting a new string value, like the Redis `SET` command does, is performed
|
||||
using:
|
||||
|
||||
int RedisModule_StringSet(RedisModuleKey *key, RedisModuleString *str);
|
||||
|
||||
The function works exactly like the Redis `SET` command itself, that is, if
|
||||
there is a prior value (of any type) it will be deleted.
|
||||
|
||||
Accessing existing string values is performed using DMA (direct memory
|
||||
access) for speed. The API will return a pointer and a length, so that's
|
||||
possible to access and, if needed, modify the string directly.
|
||||
|
||||
size_t len, j;
|
||||
char *myptr = RedisModule_StringDMA(key,&len,REDISMODULE_WRITE);
|
||||
for (j = 0; j < len; j++) myptr[j] = 'A';
|
||||
|
||||
In the above example we write directly on the string. Note that if you want
|
||||
to write, you must be sure to ask for `WRITE` mode.
|
||||
|
||||
DMA pointers are only valid if no other operations are performed with the key
|
||||
before using the pointer, after the DMA call.
|
||||
|
||||
Sometimes when we want to manipulate strings directly, we need to change
|
||||
their size as well. For this scope, the `RedisModule_StringTruncate` function
|
||||
is used. Example:
|
||||
|
||||
RedisModule_StringTruncate(mykey,1024);
|
||||
|
||||
The function truncates, or enlarges the string as needed, padding it with
|
||||
zero bytes if the previos length is smaller than the new length we request.
|
||||
If the string does not exist since `key` is associated to an open empty key,
|
||||
a string value is created and associated to the key.
|
||||
|
||||
Note that every time `StringTruncate()` is called, we need to re-obtain
|
||||
the DMA pointer again, since the old may be invalid.
|
||||
|
||||
## List type API
|
||||
|
||||
It's possible to push and pop values from list values:
|
||||
|
||||
int RedisModule_ListPush(RedisModuleKey *key, int where, RedisModuleString *ele);
|
||||
RedisModuleString *RedisModule_ListPop(RedisModuleKey *key, int where);
|
||||
|
||||
In both the APIs the `where` argument specifies if to push or pop from tail
|
||||
or head, using the following macros:
|
||||
|
||||
REDISMODULE_LIST_HEAD
|
||||
REDISMODULE_LIST_TAIL
|
||||
|
||||
Elements returned by `RedisModule_ListPop()` are like strings craeted with
|
||||
`RedisModule_CreateString()`, they must be released with
|
||||
`RedisModule_FreeString()` or by enabling automatic memory management.
|
||||
|
||||
## Set type API
|
||||
|
||||
Work in progress.
|
||||
|
||||
## Sorted set type API
|
||||
|
||||
Documentation missing, please refer to the top comments inside `module.c`
|
||||
for the following functions:
|
||||
|
||||
* `RedisModule_ZsetAdd`
|
||||
* `RedisModule_ZsetIncrby`
|
||||
* `RedisModule_ZsetScore`
|
||||
* `RedisModule_ZsetRem`
|
||||
|
||||
And for the sorted set iterator:
|
||||
|
||||
* `RedisModule_ZsetRangeStop`
|
||||
* `RedisModule_ZsetFirstInScoreRange`
|
||||
* `RedisModule_ZsetLastInScoreRange`
|
||||
* `RedisModule_ZsetFirstInLexRange`
|
||||
* `RedisModule_ZsetLastInLexRange`
|
||||
* `RedisModule_ZsetRangeCurrentElement`
|
||||
* `RedisModule_ZsetRangeNext`
|
||||
* `RedisModule_ZsetRangePrev`
|
||||
* `RedisModule_ZsetRangeEndReached`
|
||||
|
||||
## Hash type API
|
||||
|
||||
Documentation missing, please refer to the top comments inside `module.c`
|
||||
for the following functions:
|
||||
|
||||
* `RedisModule_HashSet`
|
||||
* `RedisModule_HashGet`
|
||||
|
||||
## Iterating aggregated values
|
||||
|
||||
Work in progress.
|
||||
|
||||
# Replicating commands
|
||||
|
||||
If you want to use module commands exactly like normal Redis commands, in the
|
||||
context of replicated Redis instances, or using the AOF file for persistence,
|
||||
it is important for module commands to handle their replication in a consistent
|
||||
way.
|
||||
|
||||
When using the higher level APIs to invoke commands, replication happens
|
||||
automatically if you use the "!" modifier in the format string of
|
||||
`RedisModule_Call()` as in the following example:
|
||||
|
||||
reply = RedisModule_Call(ctx,"INCR","!sc",argv[1],"10");
|
||||
|
||||
As you can see the format specifier is `"!sc"`. The bang is not parsed as a
|
||||
format specifier, but it internally flags the command as "must replicate".
|
||||
|
||||
If you use the above programming style, there are no problems.
|
||||
However sometimes things are more complex than that, and you use the low level
|
||||
API. In this case, if there are no side effects in the command execution, and
|
||||
it consistently always performs the same work, what is possible to do is to
|
||||
replicate the command verbatim as the user executed it. To do that, you just
|
||||
need to call the following function:
|
||||
|
||||
RedisModule_ReplicateVerbatim(ctx);
|
||||
|
||||
When you use the above API, you should not use any other replication function
|
||||
since they are not guaranteed to mix well.
|
||||
|
||||
However this is not the only option. It's also possible to exactly tell
|
||||
Redis what commands to replicate as the effect of the command execution, using
|
||||
an API similar to `RedisModule_Call()` but that instead of calling the command
|
||||
sends it to the AOF / slaves stream. Example:
|
||||
|
||||
RedisModule_Replicate(ctx,"INCRBY","cl","foo",my_increment);
|
||||
|
||||
It's possible to call `RedisModule_Replicate` multiple times, and each
|
||||
will emit a command. All the sequence emitted is wrapped between a
|
||||
`MULTI/EXEC` transaction, so that the AOF and replication effects are the
|
||||
same as executing a single command.
|
||||
|
||||
Note that `Call()` replication and `Replicate()` replication have a rule,
|
||||
in case you want to mix both forms of replication (not necessarily a good
|
||||
idea if there are simpler approaches). Commands replicated with `Call()`
|
||||
are always the first emitted in the final `MULTI/EXEC` block, while all
|
||||
the commands emitted with `Replicate()` will follow.
|
||||
|
||||
# Automatic memory management
|
||||
|
||||
Normally when writing programs in the C language, programmers need to manage
|
||||
memory manually. This is why the Redis modules API has functions to release
|
||||
strings, close open keys, free replies, and so forth.
|
||||
|
||||
However given that commands are executed in a contained environment and
|
||||
with a set of strict APIs, Redis is able to provide automatic memory management
|
||||
to modules, at the cost of some performance (most of the time, a very low
|
||||
cost).
|
||||
|
||||
When automatic memory management is enabled:
|
||||
|
||||
1. You don't need to close open keys.
|
||||
2. You don't need to free replies.
|
||||
3. You don't need to free RedisModuleString objects.
|
||||
|
||||
However you can still do it, if you want. For example, automatic memory
|
||||
management may be active, but inside a loop allocating a lot of strings,
|
||||
you may still want to free strings no longer used.
|
||||
|
||||
In order to enable automatic memory management, just call the following
|
||||
function at the start of the command implementation:
|
||||
|
||||
RedisModule_AutoMemory(ctx);
|
||||
|
||||
Automatic memory management is usually the way to go, however experienced
|
||||
C programmers may not use it in order to gain some speed and memory usage
|
||||
benefit.
|
||||
|
||||
# Allocating memory into modules
|
||||
|
||||
Normal C programs use `malloc()` and `free()` in order to allocate and
|
||||
release memory dynamically. While in Redis modules the use of malloc is
|
||||
not technically forbidden, it is a lot better to use the Redis Modules
|
||||
specific functions, that are exact replacements for `malloc`, `free`,
|
||||
`realloc` and `strdup`. These functions are:
|
||||
|
||||
void *RedisModule_Alloc(size_t bytes);
|
||||
void* RedisModule_Realloc(void *ptr, size_t bytes);
|
||||
void RedisModule_Free(void *ptr);
|
||||
void RedisModule_Calloc(size_t nmemb, size_t size);
|
||||
char *RedisModule_Strdup(const char *str);
|
||||
|
||||
They work exactly like their `libc` equivalent calls, however they use
|
||||
the same allocator Redis uses, and the memory allocated using these
|
||||
functions is reported by the `INFO` command in the memory section, is
|
||||
accounted when enforcing the `maxmemory` policy, and in general is
|
||||
a first citizen of the Redis executable. On the contrar, the method
|
||||
allocated inside modules with libc `malloc()` is transparent to Redis.
|
||||
|
||||
Another reason to use the modules functions in order to allocate memory
|
||||
is that, when creating native data types inside modules, the RDB loading
|
||||
functions can return deserialized strings (from the RDB file) directly
|
||||
as `RedisModule_Alloc()` allocations, so they can be used directly to
|
||||
populate data structures after loading, instead of having to copy them
|
||||
to the data structure.
|
||||
|
||||
## Pool allocator
|
||||
|
||||
Sometimes in commands implementations, it is required to perform many
|
||||
small allocations that will be not retained at the end of the command
|
||||
execution, but are just functional to execute the command itself.
|
||||
|
||||
This work can be more easily accomplished using the Redis pool allocator:
|
||||
|
||||
void *RedisModule_PoolAlloc(RedisModuleCtx *ctx, size_t bytes);
|
||||
|
||||
It works similarly to `malloc()`, and returns memory aligned to the
|
||||
next power of two of greater or equal to `bytes` (for a maximum alignment
|
||||
of 8 bytes). However it allocates memory in blocks, so it the overhead
|
||||
of the allocations is small, and more important, the memory allocated
|
||||
is automatically released when the command returns.
|
||||
|
||||
So in general short living allocations are a good candidates for the pool
|
||||
allocator.
|
||||
|
||||
# Writing commands compatible with Redis Cluster
|
||||
|
||||
Documentation missing, please check the following functions inside `module.c`:
|
||||
|
||||
RedisModule_IsKeysPositionRequest(ctx);
|
||||
RedisModule_KeyAtPos(ctx,pos);
|
||||
@@ -4,16 +4,16 @@ uname_S := $(shell sh -c 'uname -s 2>/dev/null || echo not')
|
||||
|
||||
# Compile flags for linux / osx
|
||||
ifeq ($(uname_S),Linux)
|
||||
SHOBJ_CFLAGS ?= -fno-common -g -ggdb -std=c99 -O2
|
||||
SHOBJ_CFLAGS ?= -W -Wall -fno-common -g -ggdb -std=c99 -O2
|
||||
SHOBJ_LDFLAGS ?= -shared
|
||||
else
|
||||
SHOBJ_CFLAGS ?= -dynamic -fno-common -g -ggdb -std=c99 -O2
|
||||
SHOBJ_CFLAGS ?= -W -Wall -dynamic -fno-common -g -ggdb -std=c99 -O2
|
||||
SHOBJ_LDFLAGS ?= -bundle -undefined dynamic_lookup
|
||||
endif
|
||||
|
||||
.SUFFIXES: .c .so .xo .o
|
||||
|
||||
all: helloworld.so hellotype.so testmodule.so
|
||||
all: helloworld.so hellotype.so helloblock.so testmodule.so
|
||||
|
||||
.c.xo:
|
||||
$(CC) -I. $(CFLAGS) $(SHOBJ_CFLAGS) -fPIC -c $< -o $@
|
||||
@@ -28,6 +28,11 @@ hellotype.xo: ../redismodule.h
|
||||
hellotype.so: hellotype.xo
|
||||
$(LD) -o $@ $< $(SHOBJ_LDFLAGS) $(LIBS) -lc
|
||||
|
||||
helloblock.xo: ../redismodule.h
|
||||
|
||||
helloblock.so: helloblock.xo
|
||||
$(LD) -o $@ $< $(SHOBJ_LDFLAGS) $(LIBS) -lpthread -lc
|
||||
|
||||
testmodule.xo: ../redismodule.h
|
||||
|
||||
testmodule.so: testmodule.xo
|
||||
|
||||
@@ -1,371 +0,0 @@
|
||||
Native types in Redis modules
|
||||
===
|
||||
|
||||
Redis modules can access Redis built-in data structures both at high level,
|
||||
by calling Redis commands, and at low level, by manipulating the data structures
|
||||
directly.
|
||||
|
||||
By using these capabilities in order to build new abstractions on top of existing
|
||||
Redis data structures, or by using strings DMA in order to encode modules
|
||||
data structures into Redis strings, it is possible to create modules that
|
||||
*feel like* they are exporting new data types. However, for more complex
|
||||
problems, this is not enough, and the implementation of new data structures
|
||||
inside the module is needed.
|
||||
|
||||
We call the ability of Redis modules to implement new data structures that
|
||||
feel like native Redis ones **native types support**. This document describes
|
||||
the API exported by the Redis modules system in order to create new data
|
||||
structures and handle the serialization in RDB files, the rewriting process
|
||||
in AOF, the type reporting via the `TYPE` command, and so forth.
|
||||
|
||||
Overview of native types
|
||||
---
|
||||
|
||||
A module exporting a native type is composed of the following main parts:
|
||||
|
||||
* The implementation of some kind of new data structure and of commands operating on the new data structure.
|
||||
* A set of callbacks that handle: RDB saving, RDB loading, AOF rewriting, releasing of a value associated with a key, calculation of a value digest (hash) to be used with the `DEBUG DIGEST` command.
|
||||
* A 9 characters name that is unique to each module native data type.
|
||||
* An encoding version, used to persist into RDB files a module-specific data version, so that a module will be able to load older representations from RDB files.
|
||||
|
||||
While to handle RDB loading, saving and AOF rewriting may look complex as a first glance, the modules API provide very high level function for handling all this, without requiring the user to handle read/write errors, so in practical terms, writing a new data structure for Redis is a simple task.
|
||||
|
||||
A **very easy** to understand but complete example of native type implementation
|
||||
is available inside the Redis distribution in the `/modules/hellotype.c` file.
|
||||
The reader is encouraged to read the documentation by looking at this example
|
||||
implementation to see how things are applied in the practice.
|
||||
|
||||
Registering a new data type
|
||||
===
|
||||
|
||||
In order to register a new native type into the Redis core, the module needs
|
||||
to declare a global variable that will hold a reference to the data type.
|
||||
The API to register the data type will return a data type reference that will
|
||||
be stored in the global variable.
|
||||
|
||||
static RedisModuleType *MyType;
|
||||
#define MYTYPE_ENCODING_VERSION 0
|
||||
|
||||
int RedisModule_OnLoad(RedisModuleCtx *ctx) {
|
||||
MyType = RedisModule_CreateDataType("MyType-AZ", MYTYPE_ENCODING_VERSION,
|
||||
MyTypeRDBLoad, MyTypeRDBSave, MyTypeAOFRewrite, MyTypeDigest,
|
||||
MyTypeFree);
|
||||
if (MyType == NULL) return REDISMODULE_ERR;
|
||||
}
|
||||
|
||||
As you can see from the example above, a single API call is needed in order to
|
||||
register the new type. However a number of function pointers are passed as
|
||||
arguments. The prototype of `RedisModule_CreateDataType` is the following:
|
||||
|
||||
moduleType *RedisModule_CreateDataType(RedisModuleCtx *ctx,
|
||||
const char *name, int encver,
|
||||
moduleTypeLoadFunc rdb_load,
|
||||
moduleTypeSaveFunc rdb_save,
|
||||
moduleTypeRewriteFunc aof_rewrite,
|
||||
moduleTypeDigestFunc digest,
|
||||
moduleTypeFreeFunc free);
|
||||
|
||||
The `ctx` argument is the context that we receive in the `OnLoad` function.
|
||||
The type `name` is a 9 character name in the character set that includes
|
||||
from `A-Z`, `a-z`, `0-9`, plus the underscore `_` and minus `-` characters.
|
||||
|
||||
Note that **this name must be unique** for each data type in the Redis
|
||||
ecosystem, so be creative, use both lower-case and upper case if it makes
|
||||
sense, and try to use the convention of mixing the type name with the name
|
||||
of the author of the module, to create a 9 character unique name.
|
||||
|
||||
For example if I'm building a *b-tree* data structure and my name is *antirez*
|
||||
I'll call my type **btree1-az**. The name, converted to a 64 bit integer,
|
||||
is stored inside the RDB file when saving the type, and will be used when the
|
||||
RDB data is loaded in order to resolve what module can load the data. If Redis
|
||||
finds no matching module, the integer is converted back to a name in order to
|
||||
provide some clue to the user about what module is missing in order to load
|
||||
the data.
|
||||
|
||||
The type name is also used as a reply for the `TYPE` command when called
|
||||
with a key holding the registered type.
|
||||
|
||||
The `encver` argument is the encoding version used by the module to store data
|
||||
inside the RDB file. For example I can start with an encoding version of 0,
|
||||
but later when I release version 2.0 of my module, I can switch encoding to
|
||||
something better. The new module will register with an encoding version of 1,
|
||||
so when it saves new RDB files, the new version will be stored on disk. However
|
||||
when loading RDB files, the module `rdb_load` method will be called even if
|
||||
there is data found for a different encoding version (and the encoding version
|
||||
is passed as argument to `rdb_load`), so that the module can still load old
|
||||
RDB files.
|
||||
|
||||
The remaining arguments `rdb_load`, `rdb_save`, `aof_rewrite`, `digest` and
|
||||
`free` are all callbacks with the following prototypes and uses:
|
||||
|
||||
typedef void *(*RedisModuleTypeLoadFunc)(RedisModuleIO *rdb, int encver);
|
||||
typedef void (*RedisModuleTypeSaveFunc)(RedisModuleIO *rdb, void *value);
|
||||
typedef void (*RedisModuleTypeRewriteFunc)(RedisModuleIO *aof, RedisModuleString *key, void *value);
|
||||
typedef void (*RedisModuleTypeDigestFunc)(RedisModuleDigest *digest, void *value);
|
||||
typedef void (*RedisModuleTypeFreeFunc)(void *value);
|
||||
|
||||
* `rdb_load` is called when loading data from the RDB file. It loads data in the same format as `rdb_save` produces.
|
||||
* `rdb_save` is called when saving data to the RDB file.
|
||||
* `aof_rewrite` is called when the AOF is being rewritten, and the module needs to tell Redis what is the sequence of commands to recreate the content of a given key.
|
||||
* `digest` is called when `DEBUG DIGEST` is executed and a key holding this module type is found. Currently this is not yet implemented so the function ca be left empty.
|
||||
* `free` is called when a key with the module native type is deleted via `DEL` or in any other mean, in order to let the module reclaim the memory associated with such a value.
|
||||
|
||||
Ok, but *why* modules types require a 9 characters name?
|
||||
---
|
||||
|
||||
Oh, I understand you need to understand this, so here is a very specific
|
||||
explanation.
|
||||
|
||||
When Redis persists to RDB files, modules specific data types require to
|
||||
be persisted as well. Now RDB files are sequences of key-value pairs
|
||||
like the following:
|
||||
|
||||
[1 byte type] [key] [a type specific value]
|
||||
|
||||
The 1 byte type identifies strings, lists, sets, and so forth. In the case
|
||||
of modules data, it is set to a special value of `module data`, but of
|
||||
course this is not enough, we need the information needed to link a specific
|
||||
value with a specific module type that is able to load and handle it.
|
||||
|
||||
So when we save a `type specific value` about a module, we prefix it with
|
||||
a 64 bit integer. 64 bits is large enough to store the informations needed
|
||||
in order to lookup the module that can handle that specific type, but is
|
||||
short enough that we can prefix each module value we store inside the RDB
|
||||
without making the final RDB file too big. At the same time, this solution
|
||||
of prefixing the value with a 64 bit *signature* does not require to do
|
||||
strange things like defining in the RDB header a list of modules specific
|
||||
types. Everything is pretty simple.
|
||||
|
||||
So, what you can store in 64 bits in order to identify a given module in
|
||||
a reliable way? Well if you build a character set of 64 symbols, you can
|
||||
easily store 9 characters of 6 bits, and you are left with 10 bits, that
|
||||
are used in order to store the *encoding version* of the type, so that
|
||||
the same type can evolve in the future and provide a different and more
|
||||
efficient or updated serialization format for RDB files.
|
||||
|
||||
So the 64 bit prefix stored before each module value is like the following:
|
||||
|
||||
6|6|6|6|6|6|6|6|6|10
|
||||
|
||||
The first 9 elements are 6-bits characters, the final 10 bits is the
|
||||
encoding version.
|
||||
|
||||
When the RDB file is loaded back, it reads the 64 bit value, masks the final
|
||||
10 bits, and searches for a matching module in the modules types cache.
|
||||
When a matching one is found, the method to load the RDB file value is called
|
||||
with the 10 bits encoding version as argument, so that the module knows
|
||||
what version of the data layout to load, if it can support multiple versions.
|
||||
|
||||
Now the interesting thing about all this is that, if instead the module type
|
||||
cannot be resolved, since there is no loaded module having this signature,
|
||||
we can convert back the 64 bit value into a 9 characters name, and print
|
||||
an error to the user that includes the module type name! So that she or he
|
||||
immediately realizes what's wrong.
|
||||
|
||||
Setting and getting keys
|
||||
---
|
||||
|
||||
After registering our new data type in the `RedisModule_OnLoad()` function,
|
||||
we also need to be able to set Redis keys having as value our native type.
|
||||
|
||||
This normally happens in the context of commands that write data to a key.
|
||||
The native types API allow to set and get keys to module native data types,
|
||||
and to test if a given key is already associated to a value of a specific data
|
||||
type.
|
||||
|
||||
The API uses the normal modules `RedisModule_OpenKey()` low level key access
|
||||
interface in order to deal with this. This is an eaxmple of setting a
|
||||
native type private data structure to a Redis key:
|
||||
|
||||
RedisModuleKey *key = RedisModule_OpenKey(ctx,keyname,REDISMODULE_WRITE);
|
||||
struct some_private_struct *data = createMyDataStructure();
|
||||
RedisModule_ModuleTypeSetValue(key,MyType,data);
|
||||
|
||||
The function `RedisModule_ModuleTypeSetValue()` is used with a key handle open
|
||||
for writing, and gets three arguments: the key handle, the reference to the
|
||||
native type, as obtained during the type registration, and finally a `void*`
|
||||
pointer that contains the private data implementing the module native type.
|
||||
|
||||
Note that Redis has no clues at all about what your data contains. It will
|
||||
just call the callbacks you provided during the method registration in order
|
||||
to perform operations on the type.
|
||||
|
||||
Similarly we can retrieve the private data from a key using this function:
|
||||
|
||||
struct some_private_struct *data;
|
||||
data = RedisModule_ModuleTypeGetValue(key);
|
||||
|
||||
We can also test for a key to have our native type as value:
|
||||
|
||||
if (RedisModule_ModuleTypeGetType(key) == MyType) {
|
||||
/* ... do something ... */
|
||||
}
|
||||
|
||||
However for the calls to do the right thing, we need to check if the key
|
||||
is empty, if it contains a value of the right kind, and so forth. So
|
||||
the idiomatic code to implement a command writing to our native type
|
||||
is along these lines:
|
||||
|
||||
RedisModuleKey *key = RedisModule_OpenKey(ctx,argv[1],
|
||||
REDISMODULE_READ|REDISMODULE_WRITE);
|
||||
int type = RedisModule_KeyType(key);
|
||||
if (type != REDISMODULE_KEYTYPE_EMPTY &&
|
||||
RedisModule_ModuleTypeGetType(key) != MyType)
|
||||
{
|
||||
return RedisModule_ReplyWithError(ctx,REDISMODULE_ERRORMSG_WRONGTYPE);
|
||||
}
|
||||
|
||||
Then if we successfully verified the key is not of the wrong type, and
|
||||
we are going to write to it, we usually want to create a new data structure if
|
||||
the key is empty, or retrieve the reference to the value associated to the
|
||||
key if there is already one:
|
||||
|
||||
/* Create an empty value object if the key is currently empty. */
|
||||
struct some_private_struct *data;
|
||||
if (type == REDISMODULE_KEYTYPE_EMPTY) {
|
||||
data = createMyDataStructure();
|
||||
RedisModule_ModuleTypeSetValue(key,MyTyke,data);
|
||||
} else {
|
||||
data = RedisModule_ModuleTypeGetValue(key);
|
||||
}
|
||||
/* Do something with 'data'... */
|
||||
|
||||
Free method
|
||||
---
|
||||
|
||||
As already mentioned, when Redis needs to free a key holding a native type
|
||||
value, it needs help from the module in order to release the memory. This
|
||||
is the reason why we pass a `free` callback during the type registration:
|
||||
|
||||
typedef void (*RedisModuleTypeFreeFunc)(void *value);
|
||||
|
||||
A trivial implementation of the free method can be something like this,
|
||||
assuming our data structure is composed of a single allocation:
|
||||
|
||||
void MyTypeFreeCallback(void *value) {
|
||||
RedisModule_Free(value);
|
||||
}
|
||||
|
||||
However a more real world one will call some function that performs a more
|
||||
complex memory reclaiming, by casting the void pointer to some structure
|
||||
and freeing all the resources composing the value.
|
||||
|
||||
RDB load and save methods
|
||||
---
|
||||
|
||||
The RDB saving and loading callbacks need to create (and load back) a
|
||||
representation of the data type on disk. Redis offers an high level API
|
||||
that can automatically store inside the RDB file the following types:
|
||||
|
||||
* Unsigned 64 bit integers.
|
||||
* Signed 64 bit integers.
|
||||
* Doubles.
|
||||
* Strings.
|
||||
|
||||
It is up to the module to find a viable representation using the above base
|
||||
types. However note that while the integer and double values are stored
|
||||
and loaded in an architecture and *endianess* agnostic way, if you use
|
||||
the raw string saving API to, for example, save a structure on disk, you
|
||||
have to care those details yourself.
|
||||
|
||||
This is the list of functions performing RDB saving and loading:
|
||||
|
||||
void RedisModule_SaveUnsigned(RedisModuleIO *io, uint64_t value);
|
||||
uint64_t RedisModule_LoadUnsigned(RedisModuleIO *io);
|
||||
void RedisModule_SaveSigned(RedisModuleIO *io, int64_t value);
|
||||
int64_t RedisModule_LoadSigned(RedisModuleIO *io);
|
||||
void RedisModule_SaveString(RedisModuleIO *io, RedisModuleString *s);
|
||||
void RedisModule_SaveStringBuffer(RedisModuleIO *io, const char *str, size_t len);
|
||||
RedisModuleString *RedisModule_LoadString(RedisModuleIO *io);
|
||||
char *RedisModule_LoadStringBuffer(RedisModuleIO *io, size_t *lenptr);
|
||||
void RedisModule_SaveDouble(RedisModuleIO *io, double value);
|
||||
double RedisModule_LoadDouble(RedisModuleIO *io);
|
||||
|
||||
The functions don't require any error checking from the module, that can
|
||||
always assume calls succeed.
|
||||
|
||||
As an example, imagine I've a native type that implements an array of
|
||||
double values, with the following structure:
|
||||
|
||||
struct double_array {
|
||||
size_t count;
|
||||
double *values;
|
||||
};
|
||||
|
||||
My `rdb_save` method may look like the following:
|
||||
|
||||
void DoubleArrayRDBSave(RedisModuleIO *io, void *ptr) {
|
||||
struct dobule_array *da = ptr;
|
||||
RedisModule_SaveUnsigned(io,da->count);
|
||||
for (size_t j = 0; j < da->count; j++)
|
||||
RedisModule_SaveDouble(io,da->values[j]);
|
||||
}
|
||||
|
||||
What we did was to store the number of elements followed by each double
|
||||
value. So when later we'll have to load the structure in the `rdb_load`
|
||||
method we'll do something like this:
|
||||
|
||||
void *DoubleArrayRDBLoad(RedisModuleIO *io, int encver) {
|
||||
if (encver != DOUBLE_ARRAY_ENC_VER) {
|
||||
/* We should actually log an error here, or try to implement
|
||||
the ability to load older versions of our data structure. */
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct double_array *da;
|
||||
da = RedisModule_Alloc(sizeof(*da));
|
||||
da->count = RedisModule_LoadUnsigned(io);
|
||||
da->values = RedisModule_Alloc(da->count * sizeof(double));
|
||||
for (size_t j = 0; j < da->count; j++)
|
||||
da->values = RedisModule_LoadDouble(io);
|
||||
return da;
|
||||
}
|
||||
|
||||
The load callback just reconstruct back the data structure from the data
|
||||
we stored in the RDB file.
|
||||
|
||||
Note that while there is no error handling on the API that writes and reads
|
||||
from disk, still the load callback can return NULL on errors in case what
|
||||
it reads does not look correct. Redis will just panic in that case.
|
||||
|
||||
AOF rewriting
|
||||
---
|
||||
|
||||
void RedisModule_EmitAOF(RedisModuleIO *io, const char *cmdname, const char *fmt, ...);
|
||||
|
||||
Handling multiple encodings
|
||||
---
|
||||
|
||||
WORK IN PROGRESS
|
||||
|
||||
Allocating memory
|
||||
---
|
||||
|
||||
Modules data types should try to use `RedisModule_Alloc()` functions family
|
||||
in order to allocate, reallocate and release heap memory used to implement the native data structures (see the other Redis Modules documentation for detailed information).
|
||||
|
||||
This is not just useful in order for Redis to be able to account for the memory used by the module, but there are also more advantages:
|
||||
|
||||
* Redis uses the `jemalloc` allcator, that often prevents fragmentation problems that could be caused by using the libc allocator.
|
||||
* When loading strings from the RDB file, the native types API is able to return strings allocated directly with `RedisModule_Alloc()`, so that the module can directly link this memory into the data structure representation, avoiding an useless copy of the data.
|
||||
|
||||
Even if you are using external libraries implementing your data structures, the
|
||||
allocation functions provided by the module API is exactly compatible with
|
||||
`malloc()`, `realloc()`, `free()` and `strdup()`, so converting the libraries
|
||||
in order to use these functions should be trivial.
|
||||
|
||||
In case you have an external library that uses libc `malloc()`, and you want
|
||||
to avoid replacing manually all the calls with the Redis Modules API calls,
|
||||
an approach could be to use simple macros in order to replace the libc calls
|
||||
with the Redis API calls. Something like this could work:
|
||||
|
||||
#define malloc RedisModule_Alloc
|
||||
#define realloc RedisModule_Realloc
|
||||
#define free RedisModule_Free
|
||||
#define strdup RedisModule_Strdup
|
||||
|
||||
However take in mind that mixing libc calls with Redis API calls will result
|
||||
into troubles and crashes, so if you replace calls using macros, you need to
|
||||
make sure that all the calls are correctly replaced, and that the code with
|
||||
the substituted calls will never, for example, attempt to call
|
||||
`RedisModule_Free()` with a pointer allocated using libc `malloc()`.
|
||||
+15
-7
@@ -6,21 +6,29 @@ def markdown(s)
|
||||
s = s.gsub(/\*\/$/,"")
|
||||
s = s.gsub(/^ \* {0,1}/,"")
|
||||
s = s.gsub(/^\/\* /,"")
|
||||
if s[0] != ' '
|
||||
s = s.gsub(/RM_[A-z()]+/){|x| "`#{x}`"}
|
||||
s = s.gsub(/RedisModule_[A-z()]+/){|x| "`#{x}`"}
|
||||
s = s.gsub(/REDISMODULE_[A-z]+/){|x| "`#{x}`"}
|
||||
end
|
||||
s.chop! while s[-1] == "\n" || s[-1] == " "
|
||||
return s
|
||||
lines = s.split("\n")
|
||||
newlines = []
|
||||
lines.each{|l|
|
||||
if l[0] != ' '
|
||||
l = l.gsub(/RM_[A-z()]+/){|x| "`#{x}`"}
|
||||
l = l.gsub(/RedisModule_[A-z()]+/){|x| "`#{x}`"}
|
||||
l = l.gsub(/REDISMODULE_[A-z]+/){|x| "`#{x}`"}
|
||||
end
|
||||
newlines << l
|
||||
}
|
||||
return newlines.join("\n")
|
||||
end
|
||||
|
||||
# Given the source code array and the index at which an exported symbol was
|
||||
# detected, extracts and outputs the documentation.
|
||||
def docufy(src,i)
|
||||
m = /RM_[A-z0-9]+/.match(src[i])
|
||||
name = m[0]
|
||||
name = name.sub("RM_","RedisModule_")
|
||||
proto = src[i].sub("{","").strip+";\n"
|
||||
puts "## `#{m[0]}`\n\n"
|
||||
proto = proto.sub("RM_","RedisModule_")
|
||||
puts "## `#{name}`\n\n"
|
||||
puts " #{proto}\n"
|
||||
comment = ""
|
||||
while true
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
/* Helloblock module -- An example of blocking command implementation
|
||||
* with threads.
|
||||
*
|
||||
* -----------------------------------------------------------------------------
|
||||
*
|
||||
* Copyright (c) 2016, 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.
|
||||
*/
|
||||
|
||||
#define REDISMODULE_EXPERIMENTAL_API
|
||||
#include "../redismodule.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <pthread.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* Reply callback for blocking command HELLO.BLOCK */
|
||||
int HelloBlock_Reply(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
REDISMODULE_NOT_USED(argv);
|
||||
REDISMODULE_NOT_USED(argc);
|
||||
int *myint = RedisModule_GetBlockedClientPrivateData(ctx);
|
||||
return RedisModule_ReplyWithLongLong(ctx,*myint);
|
||||
}
|
||||
|
||||
/* Timeout callback for blocking command HELLO.BLOCK */
|
||||
int HelloBlock_Timeout(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
REDISMODULE_NOT_USED(argv);
|
||||
REDISMODULE_NOT_USED(argc);
|
||||
return RedisModule_ReplyWithSimpleString(ctx,"Request timedout");
|
||||
}
|
||||
|
||||
/* Private data freeing callback for HELLO.BLOCK command. */
|
||||
void HelloBlock_FreeData(void *privdata) {
|
||||
RedisModule_Free(privdata);
|
||||
}
|
||||
|
||||
/* The thread entry point that actually executes the blocking part
|
||||
* of the command HELLO.BLOCK. */
|
||||
void *HelloBlock_ThreadMain(void *arg) {
|
||||
void **targ = arg;
|
||||
RedisModuleBlockedClient *bc = targ[0];
|
||||
long long delay = (unsigned long)targ[1];
|
||||
RedisModule_Free(targ);
|
||||
|
||||
sleep(delay);
|
||||
int *r = RedisModule_Alloc(sizeof(int));
|
||||
*r = rand();
|
||||
RedisModule_UnblockClient(bc,r);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* HELLO.BLOCK <delay> <timeout> -- Block for <count> seconds, then reply with
|
||||
* a random number. Timeout is the command timeout, so that you can test
|
||||
* what happens when the delay is greater than the timeout. */
|
||||
int HelloBlock_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
if (argc != 3) return RedisModule_WrongArity(ctx);
|
||||
long long delay;
|
||||
long long timeout;
|
||||
|
||||
if (RedisModule_StringToLongLong(argv[1],&delay) != REDISMODULE_OK) {
|
||||
return RedisModule_ReplyWithError(ctx,"ERR invalid count");
|
||||
}
|
||||
|
||||
if (RedisModule_StringToLongLong(argv[2],&timeout) != REDISMODULE_OK) {
|
||||
return RedisModule_ReplyWithError(ctx,"ERR invalid count");
|
||||
}
|
||||
|
||||
pthread_t tid;
|
||||
RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx,HelloBlock_Reply,HelloBlock_Timeout,HelloBlock_FreeData,timeout);
|
||||
|
||||
/* Now that we setup a blocking client, we need to pass the control
|
||||
* to the thread. However we need to pass arguments to the thread:
|
||||
* the delay and a reference to the blocked client handle. */
|
||||
void **targ = RedisModule_Alloc(sizeof(void*)*2);
|
||||
targ[0] = bc;
|
||||
targ[1] = (void*)(unsigned long) delay;
|
||||
|
||||
if (pthread_create(&tid,NULL,HelloBlock_ThreadMain,targ) != 0) {
|
||||
RedisModule_AbortBlock(bc);
|
||||
return RedisModule_ReplyWithError(ctx,"-ERR Can't start thread");
|
||||
}
|
||||
return REDISMODULE_OK;
|
||||
}
|
||||
|
||||
/* The thread entry point that actually executes the blocking part
|
||||
* of the command HELLO.KEYS.
|
||||
*
|
||||
* Note: this implementation is very simple on purpose, so no duplicated
|
||||
* keys (returned by SCAN) are filtered. However adding such a functionality
|
||||
* would be trivial just using any data structure implementing a dictionary
|
||||
* in order to filter the duplicated items. */
|
||||
void *HelloKeys_ThreadMain(void *arg) {
|
||||
RedisModuleBlockedClient *bc = arg;
|
||||
RedisModuleCtx *ctx = RedisModule_GetThreadSafeContext(bc);
|
||||
long long cursor = 0;
|
||||
size_t replylen = 0;
|
||||
|
||||
RedisModule_ReplyWithArray(ctx,REDISMODULE_POSTPONED_ARRAY_LEN);
|
||||
do {
|
||||
RedisModule_ThreadSafeContextLock(ctx);
|
||||
RedisModuleCallReply *reply = RedisModule_Call(ctx,
|
||||
"SCAN","l",(long long)cursor);
|
||||
RedisModule_ThreadSafeContextUnlock(ctx);
|
||||
|
||||
RedisModuleCallReply *cr_cursor =
|
||||
RedisModule_CallReplyArrayElement(reply,0);
|
||||
RedisModuleCallReply *cr_keys =
|
||||
RedisModule_CallReplyArrayElement(reply,1);
|
||||
|
||||
RedisModuleString *s = RedisModule_CreateStringFromCallReply(cr_cursor);
|
||||
RedisModule_StringToLongLong(s,&cursor);
|
||||
RedisModule_FreeString(ctx,s);
|
||||
|
||||
size_t items = RedisModule_CallReplyLength(cr_keys);
|
||||
for (size_t j = 0; j < items; j++) {
|
||||
RedisModuleCallReply *ele =
|
||||
RedisModule_CallReplyArrayElement(cr_keys,j);
|
||||
RedisModule_ReplyWithCallReply(ctx,ele);
|
||||
replylen++;
|
||||
}
|
||||
RedisModule_FreeCallReply(reply);
|
||||
} while (cursor != 0);
|
||||
RedisModule_ReplySetArrayLength(ctx,replylen);
|
||||
|
||||
RedisModule_FreeThreadSafeContext(ctx);
|
||||
RedisModule_UnblockClient(bc,NULL);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* HELLO.KEYS -- Return all the keys in the current database without blocking
|
||||
* the server. The keys do not represent a point-in-time state so only the keys
|
||||
* that were in the database from the start to the end are guaranteed to be
|
||||
* there. */
|
||||
int HelloKeys_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
REDISMODULE_NOT_USED(argv);
|
||||
if (argc != 1) return RedisModule_WrongArity(ctx);
|
||||
|
||||
pthread_t tid;
|
||||
|
||||
/* Note that when blocking the client we do not set any callback: no
|
||||
* timeout is possible since we passed '0', nor we need a reply callback
|
||||
* because we'll use the thread safe context to accumulate a reply. */
|
||||
RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx,NULL,NULL,NULL,0);
|
||||
|
||||
/* Now that we setup a blocking client, we need to pass the control
|
||||
* to the thread. However we need to pass arguments to the thread:
|
||||
* the reference to the blocked client handle. */
|
||||
if (pthread_create(&tid,NULL,HelloKeys_ThreadMain,bc) != 0) {
|
||||
RedisModule_AbortBlock(bc);
|
||||
return RedisModule_ReplyWithError(ctx,"-ERR Can't start thread");
|
||||
}
|
||||
return REDISMODULE_OK;
|
||||
}
|
||||
|
||||
/* This function must be present on each Redis module. It is used in order to
|
||||
* register the commands into the Redis server. */
|
||||
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
REDISMODULE_NOT_USED(argv);
|
||||
REDISMODULE_NOT_USED(argc);
|
||||
|
||||
if (RedisModule_Init(ctx,"helloblock",1,REDISMODULE_APIVER_1)
|
||||
== REDISMODULE_ERR) return REDISMODULE_ERR;
|
||||
|
||||
if (RedisModule_CreateCommand(ctx,"hello.block",
|
||||
HelloBlock_RedisCommand,"",0,0,0) == REDISMODULE_ERR)
|
||||
return REDISMODULE_ERR;
|
||||
if (RedisModule_CreateCommand(ctx,"hello.keys",
|
||||
HelloKeys_RedisCommand,"",0,0,0) == REDISMODULE_ERR)
|
||||
return REDISMODULE_ERR;
|
||||
|
||||
return REDISMODULE_OK;
|
||||
}
|
||||
+30
-3
@@ -226,21 +226,48 @@ void HelloTypeAofRewrite(RedisModuleIO *aof, RedisModuleString *key, void *value
|
||||
}
|
||||
}
|
||||
|
||||
void HelloTypeDigest(RedisModuleDigest *digest, void *value) {
|
||||
/* TODO: The DIGEST module interface is yet not implemented. */
|
||||
/* The goal of this function is to return the amount of memory used by
|
||||
* the HelloType value. */
|
||||
size_t HelloTypeMemUsage(const void *value) {
|
||||
const struct HelloTypeObject *hto = value;
|
||||
struct HelloTypeNode *node = hto->head;
|
||||
return sizeof(*hto) + sizeof(*node)*hto->len;
|
||||
}
|
||||
|
||||
void HelloTypeFree(void *value) {
|
||||
HelloTypeReleaseObject(value);
|
||||
}
|
||||
|
||||
void HelloTypeDigest(RedisModuleDigest *md, void *value) {
|
||||
struct HelloTypeObject *hto = value;
|
||||
struct HelloTypeNode *node = hto->head;
|
||||
while(node) {
|
||||
RedisModule_DigestAddLongLong(md,node->value);
|
||||
node = node->next;
|
||||
}
|
||||
RedisModule_DigestEndSequence(md);
|
||||
}
|
||||
|
||||
/* This function must be present on each Redis module. It is used in order to
|
||||
* register the commands into the Redis server. */
|
||||
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
REDISMODULE_NOT_USED(argv);
|
||||
REDISMODULE_NOT_USED(argc);
|
||||
|
||||
if (RedisModule_Init(ctx,"hellotype",1,REDISMODULE_APIVER_1)
|
||||
== REDISMODULE_ERR) return REDISMODULE_ERR;
|
||||
|
||||
HelloType = RedisModule_CreateDataType(ctx,"hellotype",0,HelloTypeRdbLoad,HelloTypeRdbSave,HelloTypeAofRewrite,HelloTypeDigest,HelloTypeFree);
|
||||
RedisModuleTypeMethods tm = {
|
||||
.version = REDISMODULE_TYPE_METHOD_VERSION,
|
||||
.rdb_load = HelloTypeRdbLoad,
|
||||
.rdb_save = HelloTypeRdbSave,
|
||||
.aof_rewrite = HelloTypeAofRewrite,
|
||||
.mem_usage = HelloTypeMemUsage,
|
||||
.free = HelloTypeFree,
|
||||
.digest = HelloTypeDigest
|
||||
};
|
||||
|
||||
HelloType = RedisModule_CreateDataType(ctx,"hellotype",0,&tm);
|
||||
if (HelloType == NULL) return REDISMODULE_ERR;
|
||||
|
||||
if (RedisModule_CreateCommand(ctx,"hellotype.insert",
|
||||
|
||||
@@ -46,6 +46,8 @@
|
||||
* fetch the currently selected DB, the other in order to send the client
|
||||
* an integer reply as response. */
|
||||
int HelloSimple_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
REDISMODULE_NOT_USED(argv);
|
||||
REDISMODULE_NOT_USED(argc);
|
||||
RedisModule_ReplyWithLongLong(ctx,RedisModule_GetSelectedDb(ctx));
|
||||
return REDISMODULE_OK;
|
||||
}
|
||||
@@ -237,7 +239,8 @@ int HelloRandArray_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, i
|
||||
* comments the function implementation). */
|
||||
int HelloRepl1_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
|
||||
{
|
||||
RedisModuleCallReply *reply;
|
||||
REDISMODULE_NOT_USED(argv);
|
||||
REDISMODULE_NOT_USED(argc);
|
||||
RedisModule_AutoMemory(ctx);
|
||||
|
||||
/* This will be replicated *after* the two INCR statements, since
|
||||
@@ -254,8 +257,8 @@ int HelloRepl1_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int a
|
||||
|
||||
/* Using the "!" modifier we replicate the command if it
|
||||
* modified the dataset in some way. */
|
||||
reply = RedisModule_Call(ctx,"INCR","c!","foo");
|
||||
reply = RedisModule_Call(ctx,"INCR","c!","bar");
|
||||
RedisModule_Call(ctx,"INCR","c!","foo");
|
||||
RedisModule_Call(ctx,"INCR","c!","bar");
|
||||
|
||||
RedisModule_ReplyWithLongLong(ctx,0);
|
||||
|
||||
@@ -519,7 +522,7 @@ int HelloLeftPad_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int
|
||||
|
||||
/* If the string is already larger than the target len, just return
|
||||
* the string itself. */
|
||||
if (strlen >= padlen)
|
||||
if (strlen >= (size_t)padlen)
|
||||
return RedisModule_ReplyWithString(ctx,argv[1]);
|
||||
|
||||
/* Padding must be a single character in this simple implementation. */
|
||||
@@ -530,7 +533,7 @@ int HelloLeftPad_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int
|
||||
/* Here we use our pool allocator, for our throw-away allocation. */
|
||||
padlen -= strlen;
|
||||
char *buf = RedisModule_PoolAlloc(ctx,padlen+strlen);
|
||||
for (size_t j = 0; j < padlen; j++) buf[j] = *ch;
|
||||
for (long long j = 0; j < padlen; j++) buf[j] = *ch;
|
||||
memcpy(buf+padlen,str,strlen);
|
||||
|
||||
RedisModule_ReplyWithStringBuffer(ctx,buf,padlen+strlen);
|
||||
|
||||
@@ -48,6 +48,9 @@ int TestMatchReply(RedisModuleCallReply *reply, char *str) {
|
||||
|
||||
/* TEST.CALL -- Test Call() API. */
|
||||
int TestCall(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
REDISMODULE_NOT_USED(argv);
|
||||
REDISMODULE_NOT_USED(argc);
|
||||
|
||||
RedisModule_AutoMemory(ctx);
|
||||
RedisModuleCallReply *reply;
|
||||
|
||||
@@ -75,6 +78,9 @@ fail:
|
||||
|
||||
/* TEST.STRING.APPEND -- Test appending to an existing string object. */
|
||||
int TestStringAppend(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
REDISMODULE_NOT_USED(argv);
|
||||
REDISMODULE_NOT_USED(argc);
|
||||
|
||||
RedisModuleString *s = RedisModule_CreateString(ctx,"foo",3);
|
||||
RedisModule_StringAppendBuffer(ctx,s,"bar",3);
|
||||
RedisModule_ReplyWithString(ctx,s);
|
||||
@@ -84,6 +90,9 @@ int TestStringAppend(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
|
||||
/* TEST.STRING.APPEND.AM -- Test append with retain when auto memory is on. */
|
||||
int TestStringAppendAM(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
REDISMODULE_NOT_USED(argv);
|
||||
REDISMODULE_NOT_USED(argc);
|
||||
|
||||
RedisModule_AutoMemory(ctx);
|
||||
RedisModuleString *s = RedisModule_CreateString(ctx,"foo",3);
|
||||
RedisModule_RetainString(ctx,s);
|
||||
@@ -112,6 +121,81 @@ int TestStringPrintf(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
}
|
||||
|
||||
|
||||
/* TEST.CTXFLAGS -- Test GetContextFlags. */
|
||||
int TestCtxFlags(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
REDISMODULE_NOT_USED(argc);
|
||||
REDISMODULE_NOT_USED(argv);
|
||||
|
||||
RedisModule_AutoMemory(ctx);
|
||||
|
||||
int ok = 1;
|
||||
const char *errString = NULL;
|
||||
|
||||
#define FAIL(msg) \
|
||||
{ \
|
||||
ok = 0; \
|
||||
errString = msg; \
|
||||
goto end; \
|
||||
}
|
||||
|
||||
int flags = RedisModule_GetContextFlags(ctx);
|
||||
if (flags == 0) {
|
||||
FAIL("Got no flags");
|
||||
}
|
||||
|
||||
if (flags & REDISMODULE_CTX_FLAGS_LUA) FAIL("Lua flag was set");
|
||||
if (flags & REDISMODULE_CTX_FLAGS_MULTI) FAIL("Multi flag was set");
|
||||
|
||||
if (flags & REDISMODULE_CTX_FLAGS_AOF) FAIL("AOF Flag was set")
|
||||
/* Enable AOF to test AOF flags */
|
||||
RedisModule_Call(ctx, "config", "ccc", "set", "appendonly", "yes");
|
||||
flags = RedisModule_GetContextFlags(ctx);
|
||||
if (!(flags & REDISMODULE_CTX_FLAGS_AOF))
|
||||
FAIL("AOF Flag not set after config set");
|
||||
|
||||
if (flags & REDISMODULE_CTX_FLAGS_RDB) FAIL("RDB Flag was set");
|
||||
/* Enable RDB to test RDB flags */
|
||||
RedisModule_Call(ctx, "config", "ccc", "set", "save", "900 1");
|
||||
flags = RedisModule_GetContextFlags(ctx);
|
||||
if (!(flags & REDISMODULE_CTX_FLAGS_RDB))
|
||||
FAIL("RDB Flag was not set after config set");
|
||||
|
||||
if (!(flags & REDISMODULE_CTX_FLAGS_MASTER)) FAIL("Master flag was not set");
|
||||
if (flags & REDISMODULE_CTX_FLAGS_SLAVE) FAIL("Slave flag was set");
|
||||
if (flags & REDISMODULE_CTX_FLAGS_READONLY) FAIL("Read-only flag was set");
|
||||
if (flags & REDISMODULE_CTX_FLAGS_CLUSTER) FAIL("Cluster flag was set");
|
||||
|
||||
if (flags & REDISMODULE_CTX_FLAGS_MAXMEMORY) FAIL("Maxmemory flag was set");
|
||||
;
|
||||
RedisModule_Call(ctx, "config", "ccc", "set", "maxmemory", "100000000");
|
||||
flags = RedisModule_GetContextFlags(ctx);
|
||||
if (!(flags & REDISMODULE_CTX_FLAGS_MAXMEMORY))
|
||||
FAIL("Maxmemory flag was not set after config set");
|
||||
|
||||
if (flags & REDISMODULE_CTX_FLAGS_EVICT) FAIL("Eviction flag was set");
|
||||
RedisModule_Call(ctx, "config", "ccc", "set", "maxmemory-policy",
|
||||
"allkeys-lru");
|
||||
flags = RedisModule_GetContextFlags(ctx);
|
||||
if (!(flags & REDISMODULE_CTX_FLAGS_EVICT))
|
||||
FAIL("Eviction flag was not set after config set");
|
||||
|
||||
end:
|
||||
/* Revert config changes */
|
||||
RedisModule_Call(ctx, "config", "ccc", "set", "appendonly", "no");
|
||||
RedisModule_Call(ctx, "config", "ccc", "set", "save", "");
|
||||
RedisModule_Call(ctx, "config", "ccc", "set", "maxmemory", "0");
|
||||
RedisModule_Call(ctx, "config", "ccc", "set", "maxmemory-policy", "noeviction");
|
||||
|
||||
if (!ok) {
|
||||
RedisModule_Log(ctx, "warning", "Failed CTXFLAGS Test. Reason: %s",
|
||||
errString);
|
||||
return RedisModule_ReplyWithSimpleString(ctx, "ERR");
|
||||
}
|
||||
|
||||
return RedisModule_ReplyWithSimpleString(ctx, "OK");
|
||||
}
|
||||
|
||||
|
||||
/* ----------------------------- Test framework ----------------------------- */
|
||||
|
||||
/* Return 1 if the reply matches the specified string, otherwise log errors
|
||||
@@ -163,6 +247,9 @@ int TestAssertIntegerReply(RedisModuleCtx *ctx, RedisModuleCallReply *reply, lon
|
||||
|
||||
/* TEST.IT -- Run all the tests. */
|
||||
int TestIt(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
REDISMODULE_NOT_USED(argv);
|
||||
REDISMODULE_NOT_USED(argc);
|
||||
|
||||
RedisModule_AutoMemory(ctx);
|
||||
RedisModuleCallReply *reply;
|
||||
|
||||
@@ -176,6 +263,9 @@ int TestIt(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
T("test.call","");
|
||||
if (!TestAssertStringReply(ctx,reply,"OK",2)) goto fail;
|
||||
|
||||
T("test.ctxflags","");
|
||||
if (!TestAssertStringReply(ctx,reply,"OK",2)) goto fail;
|
||||
|
||||
T("test.string.append","");
|
||||
if (!TestAssertStringReply(ctx,reply,"foobar",6)) goto fail;
|
||||
|
||||
@@ -195,6 +285,9 @@ fail:
|
||||
}
|
||||
|
||||
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
REDISMODULE_NOT_USED(argv);
|
||||
REDISMODULE_NOT_USED(argc);
|
||||
|
||||
if (RedisModule_Init(ctx,"test",1,REDISMODULE_APIVER_1)
|
||||
== REDISMODULE_ERR) return REDISMODULE_ERR;
|
||||
|
||||
@@ -214,6 +307,10 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
|
||||
TestStringPrintf,"write deny-oom",1,1,1) == REDISMODULE_ERR)
|
||||
return REDISMODULE_ERR;
|
||||
|
||||
if (RedisModule_CreateCommand(ctx,"test.ctxflags",
|
||||
TestCtxFlags,"readonly",1,1,1) == REDISMODULE_ERR)
|
||||
return REDISMODULE_ERR;
|
||||
|
||||
if (RedisModule_CreateCommand(ctx,"test.it",
|
||||
TestIt,"readonly",1,1,1) == REDISMODULE_ERR)
|
||||
return REDISMODULE_ERR;
|
||||
|
||||
+18
-3
@@ -117,6 +117,7 @@ void execCommand(client *c) {
|
||||
int orig_argc;
|
||||
struct redisCommand *orig_cmd;
|
||||
int must_propagate = 0; /* Need to propagate MULTI/EXEC to AOF / slaves? */
|
||||
int was_master = server.masterhost == NULL;
|
||||
|
||||
if (!(c->flags & CLIENT_MULTI)) {
|
||||
addReplyError(c,"EXEC without MULTI");
|
||||
@@ -147,11 +148,12 @@ void execCommand(client *c) {
|
||||
c->argv = c->mstate.commands[j].argv;
|
||||
c->cmd = c->mstate.commands[j].cmd;
|
||||
|
||||
/* Propagate a MULTI request once we encounter the first write op.
|
||||
/* Propagate a MULTI request once we encounter the first command which
|
||||
* is not readonly nor an administrative one.
|
||||
* This way we'll deliver the MULTI/..../EXEC block as a whole and
|
||||
* both the AOF and the replication link will have the same consistency
|
||||
* and atomicity guarantees. */
|
||||
if (!must_propagate && !(c->cmd->flags & CMD_READONLY)) {
|
||||
if (!must_propagate && !(c->cmd->flags & (CMD_READONLY|CMD_ADMIN))) {
|
||||
execCommandPropagateMulti(c);
|
||||
must_propagate = 1;
|
||||
}
|
||||
@@ -167,9 +169,22 @@ void execCommand(client *c) {
|
||||
c->argc = orig_argc;
|
||||
c->cmd = orig_cmd;
|
||||
discardTransaction(c);
|
||||
|
||||
/* Make sure the EXEC command will be propagated as well if MULTI
|
||||
* was already propagated. */
|
||||
if (must_propagate) server.dirty++;
|
||||
if (must_propagate) {
|
||||
int is_master = server.masterhost == NULL;
|
||||
server.dirty++;
|
||||
/* If inside the MULTI/EXEC block this instance was suddenly
|
||||
* switched from master to slave (using the SLAVEOF command), the
|
||||
* initial MULTI was propagated into the replication backlog, but the
|
||||
* rest was not. We need to make sure to at least terminate the
|
||||
* backlog with the final EXEC. */
|
||||
if (server.repl_backlog && was_master && !is_master) {
|
||||
char *execcmd = "*1\r\n$4\r\nEXEC\r\n";
|
||||
feedReplicationBacklog(execcmd,strlen(execcmd));
|
||||
}
|
||||
}
|
||||
|
||||
handle_monitor:
|
||||
/* Send EXEC to clients waiting data from MONITOR. We do it here
|
||||
|
||||
+171
-29
@@ -28,10 +28,12 @@
|
||||
*/
|
||||
|
||||
#include "server.h"
|
||||
#include "atomicvar.h"
|
||||
#include <sys/uio.h>
|
||||
#include <math.h>
|
||||
#include <ctype.h>
|
||||
|
||||
static void setProtocolError(client *c, int pos);
|
||||
static void setProtocolError(const char *errstr, client *c, int pos);
|
||||
|
||||
/* Return the size consumed from the allocator, for the specified SDS string,
|
||||
* including internal fragmentation. This function is used in order to compute
|
||||
@@ -65,6 +67,16 @@ int listMatchObjects(void *a, void *b) {
|
||||
return equalStringObjects(a,b);
|
||||
}
|
||||
|
||||
/* This function links the client to the global linked list of clients.
|
||||
* unlinkClient() does the opposite, among other things. */
|
||||
void linkClient(client *c) {
|
||||
listAddNodeTail(server.clients,c);
|
||||
/* Note that we remember the linked list node where the client is stored,
|
||||
* this way removing the client in unlinkClient() will not require
|
||||
* a linear scan, but just a constant time operation. */
|
||||
c->client_list_node = listLast(server.clients);
|
||||
}
|
||||
|
||||
client *createClient(int fd) {
|
||||
client *c = zmalloc(sizeof(client));
|
||||
|
||||
@@ -87,11 +99,14 @@ client *createClient(int fd) {
|
||||
}
|
||||
|
||||
selectDb(c,0);
|
||||
c->id = server.next_client_id++;
|
||||
uint64_t client_id;
|
||||
atomicGetIncr(server.next_client_id,client_id,1);
|
||||
c->id = client_id;
|
||||
c->fd = fd;
|
||||
c->name = NULL;
|
||||
c->bufpos = 0;
|
||||
c->querybuf = sdsempty();
|
||||
c->pending_querybuf = sdsempty();
|
||||
c->querybuf_peak = 0;
|
||||
c->reqtype = 0;
|
||||
c->argc = 0;
|
||||
@@ -106,6 +121,7 @@ client *createClient(int fd) {
|
||||
c->replstate = REPL_STATE_NONE;
|
||||
c->repl_put_online_on_ack = 0;
|
||||
c->reploff = 0;
|
||||
c->read_reploff = 0;
|
||||
c->repl_ack_off = 0;
|
||||
c->repl_ack_time = 0;
|
||||
c->slave_listening_port = 0;
|
||||
@@ -118,8 +134,9 @@ client *createClient(int fd) {
|
||||
listSetDupMethod(c->reply,dupClientReplyValue);
|
||||
c->btype = BLOCKED_NONE;
|
||||
c->bpop.timeout = 0;
|
||||
c->bpop.keys = dictCreate(&objectKeyPointerValueDictType,NULL);
|
||||
c->bpop.keys = dictCreate(&objectKeyHeapPointerValueDictType,NULL);
|
||||
c->bpop.target = NULL;
|
||||
c->bpop.xread_group = NULL;
|
||||
c->bpop.numreplicas = 0;
|
||||
c->bpop.reploffset = 0;
|
||||
c->woff = 0;
|
||||
@@ -127,9 +144,10 @@ client *createClient(int fd) {
|
||||
c->pubsub_channels = dictCreate(&objectKeyPointerValueDictType,NULL);
|
||||
c->pubsub_patterns = listCreate();
|
||||
c->peerid = NULL;
|
||||
c->client_list_node = NULL;
|
||||
listSetFreeMethod(c->pubsub_patterns,decrRefCountVoid);
|
||||
listSetMatchMethod(c->pubsub_patterns,listMatchObjects);
|
||||
if (fd != -1) listAddNodeTail(server.clients,c);
|
||||
if (fd != -1) linkClient(c);
|
||||
initClientMultiState(c);
|
||||
return c;
|
||||
}
|
||||
@@ -352,6 +370,14 @@ void addReplySds(client *c, sds s) {
|
||||
}
|
||||
}
|
||||
|
||||
/* This low level function just adds whatever protocol you send it to the
|
||||
* client buffer, trying the static buffer initially, and using the string
|
||||
* of objects if not possible.
|
||||
*
|
||||
* It is efficient because does not create an SDS object nor an Redis object
|
||||
* if not needed. The object will only be created by calling
|
||||
* _addReplyStringToList() if we fail to extend the existing tail object
|
||||
* in the list of objects. */
|
||||
void addReplyString(client *c, const char *s, size_t len) {
|
||||
if (prepareClientToWrite(c) != C_OK) return;
|
||||
if (_addReplyToBuffer(c,s,len) != C_OK)
|
||||
@@ -547,13 +573,12 @@ void addReplyBulkCBuffer(client *c, const void *p, size_t len) {
|
||||
|
||||
/* Add sds to reply (takes ownership of sds and frees it) */
|
||||
void addReplyBulkSds(client *c, sds s) {
|
||||
addReplySds(c,sdscatfmt(sdsempty(),"$%u\r\n",
|
||||
(unsigned long)sdslen(s)));
|
||||
addReplyLongLongWithPrefix(c,sdslen(s),'$');
|
||||
addReplySds(c,s);
|
||||
addReply(c,shared.crlf);
|
||||
}
|
||||
|
||||
/* Add a C nul term string as bulk reply */
|
||||
/* Add a C null term string as bulk reply */
|
||||
void addReplyBulkCString(client *c, const char *s) {
|
||||
if (s == NULL) {
|
||||
addReply(c,shared.nullbulk);
|
||||
@@ -571,6 +596,26 @@ void addReplyBulkLongLong(client *c, long long ll) {
|
||||
addReplyBulkCBuffer(c,buf,len);
|
||||
}
|
||||
|
||||
/* Add an array of C strings as status replies with a heading.
|
||||
* This function is typically invoked by from commands that support
|
||||
* subcommands in response to the 'help' subcommand. The help array
|
||||
* is terminated by NULL sentinel. */
|
||||
void addReplyHelp(client *c, const char **help) {
|
||||
sds cmd = sdsnew((char*) c->argv[0]->ptr);
|
||||
void *blenp = addDeferredMultiBulkLength(c);
|
||||
int blen = 0;
|
||||
|
||||
sdstoupper(cmd);
|
||||
addReplyStatusFormat(c,
|
||||
"%s <subcommand> arg arg ... arg. Subcommands are:",cmd);
|
||||
sdsfree(cmd);
|
||||
|
||||
while (help[blen]) addReplyStatus(c,help[blen++]);
|
||||
|
||||
blen++; /* Account for the header line(s). */
|
||||
setDeferredMultiBulkLength(c,blenp,blen);
|
||||
}
|
||||
|
||||
/* Copy 'src' client output buffers into 'dst' client output buffers.
|
||||
* The function takes care of freeing the old output buffers of the
|
||||
* destination client. */
|
||||
@@ -730,9 +775,10 @@ void unlinkClient(client *c) {
|
||||
* fd is already set to -1. */
|
||||
if (c->fd != -1) {
|
||||
/* Remove from the list of active clients. */
|
||||
ln = listSearchKey(server.clients,c);
|
||||
serverAssert(ln != NULL);
|
||||
listDelNode(server.clients,ln);
|
||||
if (c->client_list_node) {
|
||||
listDelNode(server.clients,c->client_list_node);
|
||||
c->client_list_node = NULL;
|
||||
}
|
||||
|
||||
/* Unregister async I/O handlers and close the socket. */
|
||||
aeDeleteFileEvent(server.el,c->fd,AE_READABLE);
|
||||
@@ -787,6 +833,7 @@ void freeClient(client *c) {
|
||||
|
||||
/* Free the query buffer */
|
||||
sdsfree(c->querybuf);
|
||||
sdsfree(c->pending_querybuf);
|
||||
c->querybuf = NULL;
|
||||
|
||||
/* Deallocate structures used to block on blocking ops. */
|
||||
@@ -912,6 +959,10 @@ int writeToClient(int fd, client *c, int handler_installed) {
|
||||
listDelNode(c->reply,listFirst(c->reply));
|
||||
c->sentlen = 0;
|
||||
c->reply_bytes -= objlen;
|
||||
/* If there are no longer objects in the list, we expect
|
||||
* the count of reply bytes to be exactly zero. */
|
||||
if (listLength(c->reply) == 0)
|
||||
serverAssert(c->reply_bytes == 0);
|
||||
}
|
||||
}
|
||||
/* Note that we avoid to send more than NET_MAX_WRITES_PER_EVENT
|
||||
@@ -922,11 +973,11 @@ int writeToClient(int fd, client *c, int handler_installed) {
|
||||
*
|
||||
* However if we are over the maxmemory limit we ignore that and
|
||||
* just deliver as much data as it is possible to deliver. */
|
||||
server.stat_net_output_bytes += totwritten;
|
||||
if (totwritten > NET_MAX_WRITES_PER_EVENT &&
|
||||
(server.maxmemory == 0 ||
|
||||
zmalloc_used_memory() < server.maxmemory)) break;
|
||||
}
|
||||
server.stat_net_output_bytes += totwritten;
|
||||
if (nwritten == -1) {
|
||||
if (errno == EAGAIN) {
|
||||
nwritten = 0;
|
||||
@@ -1018,6 +1069,13 @@ void resetClient(client *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
|
||||
* is ready to be executed, or C_ERR if there is still protocol to read to
|
||||
* have a well formed command. The function also returns C_ERR when there is
|
||||
* a protocol error: in such a case the client structure is setup to reply
|
||||
* with the error and close the connection. */
|
||||
int processInlineBuffer(client *c) {
|
||||
char *newline;
|
||||
int argc, j;
|
||||
@@ -1031,7 +1089,7 @@ int processInlineBuffer(client *c) {
|
||||
if (newline == NULL) {
|
||||
if (sdslen(c->querybuf) > PROTO_INLINE_MAX_SIZE) {
|
||||
addReplyError(c,"Protocol error: too big inline request");
|
||||
setProtocolError(c,0);
|
||||
setProtocolError("too big inline request",c,0);
|
||||
}
|
||||
return C_ERR;
|
||||
}
|
||||
@@ -1047,7 +1105,7 @@ int processInlineBuffer(client *c) {
|
||||
sdsfree(aux);
|
||||
if (argv == NULL) {
|
||||
addReplyError(c,"Protocol error: unbalanced quotes in request");
|
||||
setProtocolError(c,0);
|
||||
setProtocolError("unbalanced quotes in inline request",c,0);
|
||||
return C_ERR;
|
||||
}
|
||||
|
||||
@@ -1081,17 +1139,46 @@ int processInlineBuffer(client *c) {
|
||||
|
||||
/* Helper function. Trims query buffer to make the function that processes
|
||||
* multi bulk requests idempotent. */
|
||||
static void setProtocolError(client *c, int pos) {
|
||||
#define PROTO_DUMP_LEN 128
|
||||
static void setProtocolError(const char *errstr, client *c, int pos) {
|
||||
if (server.verbosity <= LL_VERBOSE) {
|
||||
sds client = catClientInfoString(sdsempty(),c);
|
||||
|
||||
/* Sample some protocol to given an idea about what was inside. */
|
||||
char buf[256];
|
||||
if (sdslen(c->querybuf) < PROTO_DUMP_LEN) {
|
||||
snprintf(buf,sizeof(buf),"Query buffer during protocol error: '%s'", c->querybuf);
|
||||
} else {
|
||||
snprintf(buf,sizeof(buf),"Query buffer during protocol error: '%.*s' (... more %zu bytes ...) '%.*s'", PROTO_DUMP_LEN/2, c->querybuf, sdslen(c->querybuf)-PROTO_DUMP_LEN, PROTO_DUMP_LEN/2, c->querybuf+sdslen(c->querybuf)-PROTO_DUMP_LEN/2);
|
||||
}
|
||||
|
||||
/* Remove non printable chars. */
|
||||
char *p = buf;
|
||||
while (*p != '\0') {
|
||||
if (!isprint(*p)) *p = '.';
|
||||
p++;
|
||||
}
|
||||
|
||||
/* Log all the client and protocol info. */
|
||||
serverLog(LL_VERBOSE,
|
||||
"Protocol error from client: %s", client);
|
||||
"Protocol error (%s) from client: %s. %s", errstr, client, buf);
|
||||
sdsfree(client);
|
||||
}
|
||||
c->flags |= CLIENT_CLOSE_AFTER_REPLY;
|
||||
sdsrange(c->querybuf,pos,-1);
|
||||
}
|
||||
|
||||
/* Process the query buffer for client 'c', setting up the client argument
|
||||
* vector for command execution. Returns C_OK if after running the function
|
||||
* the client has a well-formed ready to be processed command, otherwise
|
||||
* C_ERR if there is still to read more buffer to get the full command.
|
||||
* The function also returns C_ERR when there is a protocol error: in such a
|
||||
* case the client structure is setup to reply with the error and close
|
||||
* the connection.
|
||||
*
|
||||
* This function is called if processInputBuffer() detects that the next
|
||||
* command is in RESP format, so the first byte in the command is found
|
||||
* to be '*'. Otherwise for inline commands processInlineBuffer() is called. */
|
||||
int processMultibulkBuffer(client *c) {
|
||||
char *newline = NULL;
|
||||
int pos = 0, ok;
|
||||
@@ -1106,7 +1193,7 @@ int processMultibulkBuffer(client *c) {
|
||||
if (newline == NULL) {
|
||||
if (sdslen(c->querybuf) > PROTO_INLINE_MAX_SIZE) {
|
||||
addReplyError(c,"Protocol error: too big mbulk count string");
|
||||
setProtocolError(c,0);
|
||||
setProtocolError("too big mbulk count string",c,0);
|
||||
}
|
||||
return C_ERR;
|
||||
}
|
||||
@@ -1121,7 +1208,7 @@ int processMultibulkBuffer(client *c) {
|
||||
ok = string2ll(c->querybuf+1,newline-(c->querybuf+1),&ll);
|
||||
if (!ok || ll > 1024*1024) {
|
||||
addReplyError(c,"Protocol error: invalid multibulk length");
|
||||
setProtocolError(c,pos);
|
||||
setProtocolError("invalid mbulk count",c,pos);
|
||||
return C_ERR;
|
||||
}
|
||||
|
||||
@@ -1147,7 +1234,7 @@ int processMultibulkBuffer(client *c) {
|
||||
if (sdslen(c->querybuf) > PROTO_INLINE_MAX_SIZE) {
|
||||
addReplyError(c,
|
||||
"Protocol error: too big bulk count string");
|
||||
setProtocolError(c,0);
|
||||
setProtocolError("too big bulk count string",c,0);
|
||||
return C_ERR;
|
||||
}
|
||||
break;
|
||||
@@ -1161,14 +1248,14 @@ int processMultibulkBuffer(client *c) {
|
||||
addReplyErrorFormat(c,
|
||||
"Protocol error: expected '$', got '%c'",
|
||||
c->querybuf[pos]);
|
||||
setProtocolError(c,pos);
|
||||
setProtocolError("expected $ but got something else",c,pos);
|
||||
return C_ERR;
|
||||
}
|
||||
|
||||
ok = string2ll(c->querybuf+pos+1,newline-(c->querybuf+pos+1),&ll);
|
||||
if (!ok || ll < 0 || ll > 512*1024*1024) {
|
||||
addReplyError(c,"Protocol error: invalid bulk length");
|
||||
setProtocolError(c,pos);
|
||||
setProtocolError("invalid bulk length",c,pos);
|
||||
return C_ERR;
|
||||
}
|
||||
|
||||
@@ -1226,10 +1313,14 @@ int processMultibulkBuffer(client *c) {
|
||||
/* We're done when c->multibulk == 0 */
|
||||
if (c->multibulklen == 0) return C_OK;
|
||||
|
||||
/* Still not read to process the command */
|
||||
/* Still not ready to process the command */
|
||||
return C_ERR;
|
||||
}
|
||||
|
||||
/* This function is called every time, in the client structure 'c', there is
|
||||
* more query buffer to process, because we read more data from the socket
|
||||
* or because a client was blocked and later reactivated, so there could be
|
||||
* pending query buffer, already representing a full command, to process. */
|
||||
void processInputBuffer(client *c) {
|
||||
server.current_client = c;
|
||||
/* Keep processing while there is something in the input buffer */
|
||||
@@ -1269,10 +1360,22 @@ void processInputBuffer(client *c) {
|
||||
resetClient(c);
|
||||
} else {
|
||||
/* Only reset the client when the command was executed. */
|
||||
if (processCommand(c) == C_OK)
|
||||
resetClient(c);
|
||||
/* freeMemoryIfNeeded may flush slave output buffers. This may result
|
||||
* into a slave, that may be the active client, to be freed. */
|
||||
if (processCommand(c) == C_OK) {
|
||||
if (c->flags & CLIENT_MASTER && !(c->flags & CLIENT_MULTI)) {
|
||||
/* Update the applied replication offset of our master. */
|
||||
c->reploff = c->read_reploff - sdslen(c->querybuf);
|
||||
}
|
||||
|
||||
/* Don't reset the client structure for clients blocked in a
|
||||
* module blocking command, so that the reply callback will
|
||||
* still be able to access the client argv and argc field.
|
||||
* The client will be reset in unblockClientFromModule(). */
|
||||
if (!(c->flags & CLIENT_BLOCKED) || c->btype != BLOCKED_MODULE)
|
||||
resetClient(c);
|
||||
}
|
||||
/* 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) break;
|
||||
}
|
||||
}
|
||||
@@ -1317,11 +1420,17 @@ void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
|
||||
serverLog(LL_VERBOSE, "Client closed connection");
|
||||
freeClient(c);
|
||||
return;
|
||||
} else if (c->flags & CLIENT_MASTER) {
|
||||
/* Append the query buffer to the pending (not applied) buffer
|
||||
* of the master. We'll use this buffer later in order to have a
|
||||
* copy of the string applied by the last command executed. */
|
||||
c->pending_querybuf = sdscatlen(c->pending_querybuf,
|
||||
c->querybuf+qblen,nread);
|
||||
}
|
||||
|
||||
sdsIncrLen(c->querybuf,nread);
|
||||
c->lastinteraction = server.unixtime;
|
||||
if (c->flags & CLIENT_MASTER) c->reploff += nread;
|
||||
if (c->flags & CLIENT_MASTER) c->read_reploff += nread;
|
||||
server.stat_net_input_bytes += nread;
|
||||
if (sdslen(c->querybuf) > server.client_max_querybuf_len) {
|
||||
sds ci = catClientInfoString(sdsempty(),c), bytes = sdsempty();
|
||||
@@ -1333,7 +1442,25 @@ void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
|
||||
freeClient(c);
|
||||
return;
|
||||
}
|
||||
processInputBuffer(c);
|
||||
|
||||
/* Time to process the buffer. If the client is a master we need to
|
||||
* compute the difference between the applied offset before and after
|
||||
* processing the buffer, to understand how much of the replication stream
|
||||
* was actually applied to the master state: this quantity, and its
|
||||
* corresponding part of the replication stream, will be propagated to
|
||||
* the sub-slaves and to the replication backlog. */
|
||||
if (!(c->flags & CLIENT_MASTER)) {
|
||||
processInputBuffer(c);
|
||||
} else {
|
||||
size_t prev_offset = c->reploff;
|
||||
processInputBuffer(c);
|
||||
size_t applied = c->reploff - prev_offset;
|
||||
if (applied) {
|
||||
replicationFeedSlavesFromMasterStream(server.slaves,
|
||||
c->pending_querybuf, applied);
|
||||
sdsrange(c->pending_querybuf,applied,-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void getClientsMaxBuffers(unsigned long *longest_output_list,
|
||||
@@ -1462,7 +1589,22 @@ void clientCommand(client *c) {
|
||||
listIter li;
|
||||
client *client;
|
||||
|
||||
if (!strcasecmp(c->argv[1]->ptr,"list") && c->argc == 2) {
|
||||
if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"help")) {
|
||||
const char *help[] = {
|
||||
"getname -- Return the name of the current connection.",
|
||||
"kill <ip:port> -- Kill connection made from <ip:port>.",
|
||||
"kill <option> <value> [option value ...] -- Kill connections. Options are:",
|
||||
" addr <ip:port> -- Kill connection made from <ip:port>.",
|
||||
" type (normal|master|slave|pubsub) -- Kill connections by type.",
|
||||
" skipme (yes|no) -- Skip killing current connection (default: yes).",
|
||||
"list -- Return information about client connections.",
|
||||
"pause <timeout> -- Suspend all Redis clients for <timout> milliseconds.",
|
||||
"reply (on|off|skip) -- Control the replies sent to the current connection.",
|
||||
"setname <name> -- Assign the name <name> to the current connection.",
|
||||
NULL
|
||||
};
|
||||
addReplyHelp(c, help);
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"list") && c->argc == 2) {
|
||||
/* CLIENT LIST */
|
||||
sds o = getAllClientsInfoString();
|
||||
addReplyBulkCBuffer(c,o,sdslen(o));
|
||||
@@ -1608,7 +1750,7 @@ void clientCommand(client *c) {
|
||||
pauseClients(duration);
|
||||
addReply(c,shared.ok);
|
||||
} else {
|
||||
addReplyError(c, "Syntax error, try CLIENT (LIST | KILL | GETNAME | SETNAME | PAUSE | REPLY)");
|
||||
addReplyErrorFormat(c, "Unknown subcommand or wrong number of arguments for '%s'. Try CLIENT HELP", (char*)c->argv[1]->ptr);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ int keyspaceEventsStringToFlags(char *classes) {
|
||||
case 'e': flags |= NOTIFY_EVICTED; break;
|
||||
case 'K': flags |= NOTIFY_KEYSPACE; break;
|
||||
case 'E': flags |= NOTIFY_KEYEVENT; break;
|
||||
case 't': flags |= NOTIFY_STREAM; break;
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
@@ -79,6 +80,7 @@ sds keyspaceEventsFlagsToString(int flags) {
|
||||
if (flags & NOTIFY_ZSET) res = sdscatlen(res,"z",1);
|
||||
if (flags & NOTIFY_EXPIRED) res = sdscatlen(res,"x",1);
|
||||
if (flags & NOTIFY_EVICTED) res = sdscatlen(res,"e",1);
|
||||
if (flags & NOTIFY_STREAM) res = sdscatlen(res,"t",1);
|
||||
}
|
||||
if (flags & NOTIFY_KEYSPACE) res = sdscatlen(res,"K",1);
|
||||
if (flags & NOTIFY_KEYEVENT) res = sdscatlen(res,"E",1);
|
||||
|
||||
+107
-27
@@ -232,6 +232,13 @@ robj *createZsetZiplistObject(void) {
|
||||
return o;
|
||||
}
|
||||
|
||||
robj *createStreamObject(void) {
|
||||
stream *s = streamNew();
|
||||
robj *o = createObject(OBJ_STREAM,s);
|
||||
o->encoding = OBJ_ENCODING_STREAM;
|
||||
return o;
|
||||
}
|
||||
|
||||
robj *createModuleObject(moduleType *mt, void *value) {
|
||||
moduleValue *mv = zmalloc(sizeof(*mv));
|
||||
mv->type = mt;
|
||||
@@ -246,11 +253,9 @@ void freeStringObject(robj *o) {
|
||||
}
|
||||
|
||||
void freeListObject(robj *o) {
|
||||
switch (o->encoding) {
|
||||
case OBJ_ENCODING_QUICKLIST:
|
||||
if (o->encoding == OBJ_ENCODING_QUICKLIST) {
|
||||
quicklistRelease(o->ptr);
|
||||
break;
|
||||
default:
|
||||
} else {
|
||||
serverPanic("Unknown list encoding type");
|
||||
}
|
||||
}
|
||||
@@ -305,6 +310,10 @@ void freeModuleObject(robj *o) {
|
||||
zfree(mv);
|
||||
}
|
||||
|
||||
void freeStreamObject(robj *o) {
|
||||
freeStream(o->ptr);
|
||||
}
|
||||
|
||||
void incrRefCount(robj *o) {
|
||||
if (o->refcount != OBJ_SHARED_REFCOUNT) o->refcount++;
|
||||
}
|
||||
@@ -318,6 +327,7 @@ void decrRefCount(robj *o) {
|
||||
case OBJ_ZSET: freeZsetObject(o); break;
|
||||
case OBJ_HASH: freeHashObject(o); break;
|
||||
case OBJ_MODULE: freeModuleObject(o); break;
|
||||
case OBJ_STREAM: freeStreamObject(o); break;
|
||||
default: serverPanic("Unknown object type"); break;
|
||||
}
|
||||
zfree(o);
|
||||
@@ -560,11 +570,11 @@ int getDoubleFromObject(const robj *o, double *target) {
|
||||
if (sdsEncodedObject(o)) {
|
||||
errno = 0;
|
||||
value = strtod(o->ptr, &eptr);
|
||||
if (isspace(((const char*)o->ptr)[0]) ||
|
||||
eptr[0] != '\0' ||
|
||||
if (sdslen(o->ptr) == 0 ||
|
||||
isspace(((const char*)o->ptr)[0]) ||
|
||||
(size_t)(eptr-(char*)o->ptr) != sdslen(o->ptr) ||
|
||||
(errno == ERANGE &&
|
||||
(value == HUGE_VAL || value == -HUGE_VAL || value == 0)) ||
|
||||
errno == EINVAL ||
|
||||
isnan(value))
|
||||
return C_ERR;
|
||||
} else if (o->encoding == OBJ_ENCODING_INT) {
|
||||
@@ -602,8 +612,12 @@ int getLongDoubleFromObject(robj *o, long double *target) {
|
||||
if (sdsEncodedObject(o)) {
|
||||
errno = 0;
|
||||
value = strtold(o->ptr, &eptr);
|
||||
if (isspace(((char*)o->ptr)[0]) || eptr[0] != '\0' ||
|
||||
errno == ERANGE || isnan(value))
|
||||
if (sdslen(o->ptr) == 0 ||
|
||||
isspace(((const char*)o->ptr)[0]) ||
|
||||
(size_t)(eptr-(char*)o->ptr) != sdslen(o->ptr) ||
|
||||
(errno == ERANGE &&
|
||||
(value == HUGE_VAL || value == -HUGE_VAL || value == 0)) ||
|
||||
isnan(value))
|
||||
return C_ERR;
|
||||
} else if (o->encoding == OBJ_ENCODING_INT) {
|
||||
value = (long)o->ptr;
|
||||
@@ -786,6 +800,57 @@ size_t objectComputeSize(robj *o, size_t sample_size) {
|
||||
} else {
|
||||
serverPanic("Unknown hash encoding");
|
||||
}
|
||||
} else if (o->type == OBJ_STREAM) {
|
||||
stream *s = o->ptr;
|
||||
/* Note: to guess the size of the radix tree is not trivial, so we
|
||||
* approximate it considering 64 bytes of data overhead for each
|
||||
* key (the ID), and then adding the number of bare nodes, plus some
|
||||
* overhead due by the data and child pointers. This secret recipe
|
||||
* was obtained by checking the average radix tree created by real
|
||||
* workloads, and then adjusting the constants to get numbers that
|
||||
* more or less match the real memory usage.
|
||||
*
|
||||
* Actually the number of nodes and keys may be different depending
|
||||
* on the insertion speed and thus the ability of the radix tree
|
||||
* to compress prefixes. */
|
||||
asize = sizeof(*o);
|
||||
asize += s->rax->numele * 64;
|
||||
asize += s->rax->numnodes * sizeof(raxNode);
|
||||
asize += s->rax->numnodes * 32*7; /* Add a few child pointers... */
|
||||
|
||||
/* Now we have to add the listpacks. The last listpack is often non
|
||||
* complete, so we estimate the size of the first N listpacks, and
|
||||
* use the average to compute the size of the first N-1 listpacks, and
|
||||
* finally add the real size of the last node. */
|
||||
raxIterator ri;
|
||||
raxStart(&ri,s->rax);
|
||||
raxSeek(&ri,"^",NULL,0);
|
||||
size_t lpsize = 0, samples = 0;
|
||||
while(samples < sample_size && raxNext(&ri)) {
|
||||
unsigned char *lp = ri.data;
|
||||
lpsize += lpBytes(lp);
|
||||
samples++;
|
||||
}
|
||||
if (s->rax->numele <= samples) {
|
||||
asize += lpsize;
|
||||
} else {
|
||||
if (samples) lpsize /= samples; /* Compute the average. */
|
||||
asize += lpsize * (s->rax->numele-1);
|
||||
/* No need to check if seek succeeded, we enter this branch only
|
||||
* if there are a few elements in the radix tree. */
|
||||
raxSeek(&ri,"$",NULL,0);
|
||||
raxNext(&ri);
|
||||
asize += lpBytes(ri.data);
|
||||
}
|
||||
raxStop(&ri);
|
||||
} else if (o->type == OBJ_MODULE) {
|
||||
moduleValue *mv = o->ptr;
|
||||
moduleType *mt = mv->type;
|
||||
if (mt->mem_usage != NULL) {
|
||||
asize = mt->mem_usage(mv->value);
|
||||
} else {
|
||||
asize = 0;
|
||||
}
|
||||
} else {
|
||||
serverPanic("Unknown object type");
|
||||
}
|
||||
@@ -828,9 +893,9 @@ struct redisMemOverhead *getMemoryOverheadData(void) {
|
||||
|
||||
listRewind(server.slaves,&li);
|
||||
while((ln = listNext(&li))) {
|
||||
client *client = listNodeValue(ln);
|
||||
mem += getClientOutputBufferMemoryUsage(client);
|
||||
mem += sdsAllocSize(client->querybuf);
|
||||
client *c = listNodeValue(ln);
|
||||
mem += getClientOutputBufferMemoryUsage(c);
|
||||
mem += sdsAllocSize(c->querybuf);
|
||||
mem += sizeof(client);
|
||||
}
|
||||
}
|
||||
@@ -844,11 +909,11 @@ struct redisMemOverhead *getMemoryOverheadData(void) {
|
||||
|
||||
listRewind(server.clients,&li);
|
||||
while((ln = listNext(&li))) {
|
||||
client *client = listNodeValue(ln);
|
||||
if (client->flags & CLIENT_SLAVE)
|
||||
client *c = listNodeValue(ln);
|
||||
if (c->flags & CLIENT_SLAVE)
|
||||
continue;
|
||||
mem += getClientOutputBufferMemoryUsage(client);
|
||||
mem += sdsAllocSize(client->querybuf);
|
||||
mem += getClientOutputBufferMemoryUsage(c);
|
||||
mem += sdsAllocSize(c->querybuf);
|
||||
mem += sizeof(client);
|
||||
}
|
||||
}
|
||||
@@ -945,7 +1010,7 @@ sds getMemoryDoctorReport(void) {
|
||||
}
|
||||
|
||||
/* Slaves using more than 10 MB each? */
|
||||
if (mh->clients_slaves / numslaves > (1024*1024*10)) {
|
||||
if (numslaves > 0 && mh->clients_slaves / numslaves > (1024*1024*10)) {
|
||||
big_slave_buf = 1;
|
||||
num_reports++;
|
||||
}
|
||||
@@ -955,14 +1020,14 @@ sds getMemoryDoctorReport(void) {
|
||||
if (num_reports == 0) {
|
||||
s = sdsnew(
|
||||
"Hi Sam, I can't find any memory issue in your instance. "
|
||||
"I can only account for what occurs on this base.");
|
||||
"I can only account for what occurs on this base.\n");
|
||||
} else if (empty == 1) {
|
||||
s = sdsnew(
|
||||
"Hi Sam, this instance is empty or is using very little memory, "
|
||||
"my issues detector can't be used in these conditions. "
|
||||
"Please, leave for your mission on Earth and fill it with some data. "
|
||||
"The new Sam and I will be back to our programming as soon as I "
|
||||
"finished rebooting.");
|
||||
"finished rebooting.\n");
|
||||
} else {
|
||||
s = sdsnew("Sam, I detected a few issues in this Redis instance memory implants:\n\n");
|
||||
if (big_peak) {
|
||||
@@ -1002,11 +1067,20 @@ robj *objectCommandLookupOrReply(client *c, robj *key, robj *reply) {
|
||||
}
|
||||
|
||||
/* Object command allows to inspect the internals of an Redis Object.
|
||||
* Usage: OBJECT <refcount|encoding|idletime> <key> */
|
||||
* Usage: OBJECT <refcount|encoding|idletime|freq> <key> */
|
||||
void objectCommand(client *c) {
|
||||
robj *o;
|
||||
|
||||
if (!strcasecmp(c->argv[1]->ptr,"refcount") && c->argc == 3) {
|
||||
if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"help")) {
|
||||
const char *help[] = {
|
||||
"encoding <key> -- Return the kind of internal representation used in order to store the value associated with a key.",
|
||||
"freq <key> -- Return the access frequency index of the key. The returned integer is proportional to the logarithm of the recent access frequency of the key.",
|
||||
"idletime <key> -- Return the idle time of the key, that is the approximated number of seconds elapsed since the last access to the key.",
|
||||
"refcount <key> -- Return the number of references of the value associated with the specified key.",
|
||||
NULL
|
||||
};
|
||||
addReplyHelp(c, help);
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"refcount") && c->argc == 3) {
|
||||
if ((o = objectCommandLookupOrReply(c,c->argv[2],shared.nullbulk))
|
||||
== NULL) return;
|
||||
addReplyLongLong(c,o->refcount);
|
||||
@@ -1025,13 +1099,17 @@ void objectCommand(client *c) {
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"freq") && c->argc == 3) {
|
||||
if ((o = objectCommandLookupOrReply(c,c->argv[2],shared.nullbulk))
|
||||
== NULL) return;
|
||||
if (server.maxmemory_policy & MAXMEMORY_FLAG_LRU) {
|
||||
addReplyError(c,"An LRU maxmemory policy is selected, access frequency not tracked. Please note that when switching between policies at runtime LRU and LFU data will take some time to adjust.");
|
||||
if (!(server.maxmemory_policy & MAXMEMORY_FLAG_LFU)) {
|
||||
addReplyError(c,"An LFU maxmemory policy is not selected, access frequency not tracked. Please note that when switching between policies at runtime LRU and LFU data will take some time to adjust.");
|
||||
return;
|
||||
}
|
||||
addReplyLongLong(c,o->lru&255);
|
||||
/* LFUDecrAndReturn should be called
|
||||
* in case of the key has not been accessed for a long time,
|
||||
* because we update the access time only
|
||||
* when the key is read or overwritten. */
|
||||
addReplyLongLong(c,LFUDecrAndReturn(o));
|
||||
} else {
|
||||
addReplyError(c,"Syntax error. Try OBJECT (refcount|encoding|idletime|freq)");
|
||||
addReplyErrorFormat(c, "Unknown subcommand or wrong number of arguments for '%s'. Try OBJECT help", (char *)c->argv[1]->ptr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1064,7 +1142,7 @@ void memoryCommand(client *c) {
|
||||
if ((o = objectCommandLookupOrReply(c,c->argv[2],shared.nullbulk))
|
||||
== NULL) return;
|
||||
size_t usage = objectComputeSize(o,samples);
|
||||
usage += sdsAllocSize(c->argv[1]->ptr);
|
||||
usage += sdsAllocSize(c->argv[2]->ptr);
|
||||
usage += sizeof(dictEntry);
|
||||
addReplyLongLong(c,usage);
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"stats") && c->argc == 2) {
|
||||
@@ -1157,7 +1235,9 @@ void memoryCommand(client *c) {
|
||||
/* Nothing to do for other allocators. */
|
||||
#endif
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"help") && c->argc == 2) {
|
||||
addReplyMultiBulkLen(c,4);
|
||||
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,
|
||||
|
||||
+11
-4
@@ -325,8 +325,16 @@ void publishCommand(client *c) {
|
||||
|
||||
/* PUBSUB command for Pub/Sub introspection. */
|
||||
void pubsubCommand(client *c) {
|
||||
if (!strcasecmp(c->argv[1]->ptr,"channels") &&
|
||||
(c->argc == 2 || c->argc ==3))
|
||||
if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"help")) {
|
||||
const char *help[] = {
|
||||
"channels [<pattern>] -- Return the currently active channels matching a pattern (default: all).",
|
||||
"numpat -- Return number of subscriptions to patterns.",
|
||||
"numsub [channel-1 .. channel-N] -- Returns the number of subscribers for the specified channels (excluding patterns, default: none).",
|
||||
NULL
|
||||
};
|
||||
addReplyHelp(c, help);
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"channels") &&
|
||||
(c->argc == 2 || c->argc == 3))
|
||||
{
|
||||
/* PUBSUB CHANNELS [<pattern>] */
|
||||
sds pat = (c->argc == 2) ? NULL : c->argv[2]->ptr;
|
||||
@@ -364,8 +372,7 @@ void pubsubCommand(client *c) {
|
||||
/* PUBSUB NUMPAT */
|
||||
addReplyLongLong(c,listLength(server.pubsub_patterns));
|
||||
} else {
|
||||
addReplyErrorFormat(c,
|
||||
"Unknown PUBSUB subcommand or wrong number of arguments for '%s'",
|
||||
addReplyErrorFormat(c, "Unknown subcommand or wrong number of arguments for '%s'. Try PUBSUB HELP",
|
||||
(char*)c->argv[1]->ptr);
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -149,7 +149,7 @@ REDIS_STATIC quicklistNode *quicklistCreateNode(void) {
|
||||
}
|
||||
|
||||
/* Return cached quicklist count */
|
||||
unsigned int quicklistCount(const quicklist *ql) { return ql->count; }
|
||||
unsigned long quicklistCount(const quicklist *ql) { return ql->count; }
|
||||
|
||||
/* Free entire quicklist. */
|
||||
void quicklistRelease(quicklist *quicklist) {
|
||||
@@ -1192,12 +1192,12 @@ quicklist *quicklistDup(quicklist *orig) {
|
||||
current = current->next) {
|
||||
quicklistNode *node = quicklistCreateNode();
|
||||
|
||||
if (node->encoding == QUICKLIST_NODE_ENCODING_LZF) {
|
||||
quicklistLZF *lzf = (quicklistLZF *)node->zl;
|
||||
if (current->encoding == QUICKLIST_NODE_ENCODING_LZF) {
|
||||
quicklistLZF *lzf = (quicklistLZF *)current->zl;
|
||||
size_t lzf_sz = sizeof(*lzf) + lzf->sz;
|
||||
node->zl = zmalloc(lzf_sz);
|
||||
memcpy(node->zl, current->zl, lzf_sz);
|
||||
} else if (node->encoding == QUICKLIST_NODE_ENCODING_RAW) {
|
||||
} else if (current->encoding == QUICKLIST_NODE_ENCODING_RAW) {
|
||||
node->zl = zmalloc(current->sz);
|
||||
memcpy(node->zl, current->zl, current->sz);
|
||||
}
|
||||
|
||||
+3
-3
@@ -64,7 +64,7 @@ typedef struct quicklistLZF {
|
||||
char compressed[];
|
||||
} quicklistLZF;
|
||||
|
||||
/* quicklist is a 32 byte struct (on 64-bit systems) describing a quicklist.
|
||||
/* quicklist is a 40 byte struct (on 64-bit systems) describing a quicklist.
|
||||
* 'count' is the number of total entries.
|
||||
* 'len' is the number of quicklist nodes.
|
||||
* 'compress' is: -1 if compression disabled, otherwise it's the number
|
||||
@@ -74,7 +74,7 @@ typedef struct quicklist {
|
||||
quicklistNode *head;
|
||||
quicklistNode *tail;
|
||||
unsigned long count; /* total count of all entries in all ziplists */
|
||||
unsigned int len; /* number of quicklistNodes */
|
||||
unsigned long len; /* number of quicklistNodes */
|
||||
int fill : 16; /* fill factor for individual nodes */
|
||||
unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
|
||||
} quicklist;
|
||||
@@ -154,7 +154,7 @@ int quicklistPopCustom(quicklist *quicklist, int where, unsigned char **data,
|
||||
void *(*saver)(unsigned char *data, unsigned int sz));
|
||||
int quicklistPop(quicklist *quicklist, int where, unsigned char **data,
|
||||
unsigned int *sz, long long *slong);
|
||||
unsigned int quicklistCount(const quicklist *ql);
|
||||
unsigned long quicklistCount(const quicklist *ql);
|
||||
int quicklistCompare(unsigned char *p1, unsigned char *p2, int p2_len);
|
||||
size_t quicklistGetLzf(const quicklistNode *node, void **data);
|
||||
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
#ifndef RAX_H
|
||||
#define RAX_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Representation of a radix tree as implemented in this file, that contains
|
||||
* the strings "foo", "foobar" and "footer" after the insertion of each
|
||||
* word. When the node represents a key inside the radix tree, we write it
|
||||
* between [], otherwise it is written between ().
|
||||
*
|
||||
* This is the vanilla representation:
|
||||
*
|
||||
* (f) ""
|
||||
* \
|
||||
* (o) "f"
|
||||
* \
|
||||
* (o) "fo"
|
||||
* \
|
||||
* [t b] "foo"
|
||||
* / \
|
||||
* "foot" (e) (a) "foob"
|
||||
* / \
|
||||
* "foote" (r) (r) "fooba"
|
||||
* / \
|
||||
* "footer" [] [] "foobar"
|
||||
*
|
||||
* However, this implementation implements a very common optimization where
|
||||
* successive nodes having a single child are "compressed" into the node
|
||||
* itself as a string of characters, each representing a next-level child,
|
||||
* and only the link to the node representing the last character node is
|
||||
* provided inside the representation. So the above representation is turend
|
||||
* into:
|
||||
*
|
||||
* ["foo"] ""
|
||||
* |
|
||||
* [t b] "foo"
|
||||
* / \
|
||||
* "foot" ("er") ("ar") "foob"
|
||||
* / \
|
||||
* "footer" [] [] "foobar"
|
||||
*
|
||||
* However this optimization makes the implementation a bit more complex.
|
||||
* For instance if a key "first" is added in the above radix tree, a
|
||||
* "node splitting" operation is needed, since the "foo" prefix is no longer
|
||||
* composed of nodes having a single child one after the other. This is the
|
||||
* above tree and the resulting node splitting after this event happens:
|
||||
*
|
||||
*
|
||||
* (f) ""
|
||||
* /
|
||||
* (i o) "f"
|
||||
* / \
|
||||
* "firs" ("rst") (o) "fo"
|
||||
* / \
|
||||
* "first" [] [t b] "foo"
|
||||
* / \
|
||||
* "foot" ("er") ("ar") "foob"
|
||||
* / \
|
||||
* "footer" [] [] "foobar"
|
||||
*
|
||||
* Similarly after deletion, if a new chain of nodes having a single child
|
||||
* is created (the chain must also not include nodes that represent keys),
|
||||
* it must be compressed back into a single node.
|
||||
*
|
||||
*/
|
||||
|
||||
#define RAX_NODE_MAX_SIZE ((1<<29)-1)
|
||||
typedef struct raxNode {
|
||||
uint32_t iskey:1; /* Does this node contain a key? */
|
||||
uint32_t isnull:1; /* Associated value is NULL (don't store it). */
|
||||
uint32_t iscompr:1; /* Node is compressed. */
|
||||
uint32_t size:29; /* Number of children, or compressed string len. */
|
||||
/* Data layout is as follows:
|
||||
*
|
||||
* If node is not compressed we have 'size' bytes, one for each children
|
||||
* character, and 'size' raxNode pointers, point to each child node.
|
||||
* Note how the character is not stored in the children but in the
|
||||
* edge of the parents:
|
||||
*
|
||||
* [header strlen=0][abc][a-ptr][b-ptr][c-ptr](value-ptr?)
|
||||
*
|
||||
* 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 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
|
||||
* nodes).
|
||||
*
|
||||
* If the node has an associated key (iskey=1) and is not NULL
|
||||
* (isnull=0), then after the raxNode pointers poiting to the
|
||||
* childen, an additional value pointer is present (as you can see
|
||||
* in the representation above as "value-ptr" field).
|
||||
*/
|
||||
unsigned char data[];
|
||||
} raxNode;
|
||||
|
||||
typedef struct rax {
|
||||
raxNode *head;
|
||||
uint64_t numele;
|
||||
uint64_t numnodes;
|
||||
} rax;
|
||||
|
||||
/* Stack data structure used by raxLowWalk() in order to, optionally, return
|
||||
* a list of parent nodes to the caller. The nodes do not have a "parent"
|
||||
* field for space concerns, so we use the auxiliary stack when needed. */
|
||||
#define RAX_STACK_STATIC_ITEMS 32
|
||||
typedef struct raxStack {
|
||||
void **stack; /* Points to static_items or an heap allocated array. */
|
||||
size_t items, maxitems; /* Number of items contained and total space. */
|
||||
/* Up to RAXSTACK_STACK_ITEMS items we avoid to allocate on the heap
|
||||
* and use this static array of pointers instead. */
|
||||
void *static_items[RAX_STACK_STATIC_ITEMS];
|
||||
int oom; /* True if pushing into this stack failed for OOM at some point. */
|
||||
} raxStack;
|
||||
|
||||
/* Radix tree iterator state is encapsulated into this data structure. */
|
||||
#define RAX_ITER_STATIC_LEN 128
|
||||
#define RAX_ITER_JUST_SEEKED (1<<0) /* Iterator was just seeked. Return current
|
||||
element for the first iteration and
|
||||
clear the flag. */
|
||||
#define RAX_ITER_EOF (1<<1) /* End of iteration reached. */
|
||||
#define RAX_ITER_SAFE (1<<2) /* Safe iterator, allows operations while
|
||||
iterating. But it is slower. */
|
||||
typedef struct raxIterator {
|
||||
int flags;
|
||||
rax *rt; /* Radix tree we are iterating. */
|
||||
unsigned char *key; /* The current string. */
|
||||
void *data; /* Data associated to this key. */
|
||||
size_t key_len; /* Current key length. */
|
||||
size_t key_max; /* Max key len the current key buffer can hold. */
|
||||
unsigned char key_static_string[RAX_ITER_STATIC_LEN];
|
||||
raxNode *node; /* Current node. Only for unsafe iteration. */
|
||||
raxStack stack; /* Stack used for unsafe iteration. */
|
||||
} raxIterator;
|
||||
|
||||
/* A special pointer returned for not found items. */
|
||||
extern void *raxNotFound;
|
||||
|
||||
/* Exported API. */
|
||||
rax *raxNew(void);
|
||||
int raxInsert(rax *rax, unsigned char *s, size_t len, void *data, void **old);
|
||||
int raxRemove(rax *rax, unsigned char *s, size_t len, void **old);
|
||||
void *raxFind(rax *rax, unsigned char *s, size_t len);
|
||||
void raxFree(rax *rax);
|
||||
void raxFreeWithCallback(rax *rax, void (*free_callback)(void*));
|
||||
void raxStart(raxIterator *it, rax *rt);
|
||||
int raxSeek(raxIterator *it, const char *op, unsigned char *ele, size_t len);
|
||||
int raxNext(raxIterator *it);
|
||||
int raxPrev(raxIterator *it);
|
||||
int raxRandomWalk(raxIterator *it, size_t steps);
|
||||
int raxCompare(raxIterator *iter, const char *op, unsigned char *key, size_t key_len);
|
||||
void raxStop(raxIterator *it);
|
||||
int raxEOF(raxIterator *it);
|
||||
void raxShow(rax *rax);
|
||||
uint64_t raxSize(rax *rax);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,44 @@
|
||||
/* Rax -- A radix tree implementation.
|
||||
*
|
||||
* Copyright (c) 2017, 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.
|
||||
*/
|
||||
|
||||
/* Allocator selection.
|
||||
*
|
||||
* This file is used in order to change the Rax allocator at compile time.
|
||||
* Just define the following defines to what you want to use. Also add
|
||||
* the include of your alternate allocator if needed (not needed in order
|
||||
* to use the default libc allocator). */
|
||||
|
||||
#ifndef RAX_ALLOC_H
|
||||
#define RAX_ALLOC_H
|
||||
#include "zmalloc.h"
|
||||
#define rax_malloc zmalloc
|
||||
#define rax_realloc zrealloc
|
||||
#define rax_free zfree
|
||||
#endif
|
||||
@@ -31,6 +31,7 @@
|
||||
#include "lzf.h" /* LZF compression library */
|
||||
#include "zipmap.h"
|
||||
#include "endianconv.h"
|
||||
#include "stream.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <sys/types.h>
|
||||
@@ -61,7 +62,7 @@ void rdbCheckThenExit(int linenum, char *reason, ...) {
|
||||
if (!rdbCheckMode) {
|
||||
serverLog(LL_WARNING, "%s", msg);
|
||||
char *argv[2] = {"",server.rdb_filename};
|
||||
redis_check_rdb_main(2,argv);
|
||||
redis_check_rdb_main(2,argv,NULL);
|
||||
} else {
|
||||
rdbCheckError("%s",msg);
|
||||
}
|
||||
@@ -622,8 +623,10 @@ int rdbSaveObjectType(rio *rdb, robj *o) {
|
||||
return rdbSaveType(rdb,RDB_TYPE_HASH);
|
||||
else
|
||||
serverPanic("Unknown hash encoding");
|
||||
case OBJ_STREAM:
|
||||
return rdbSaveType(rdb,RDB_TYPE_STREAM_LISTPACKS);
|
||||
case OBJ_MODULE:
|
||||
return rdbSaveType(rdb,RDB_TYPE_MODULE);
|
||||
return rdbSaveType(rdb,RDB_TYPE_MODULE_2);
|
||||
default:
|
||||
serverPanic("Unknown object type");
|
||||
}
|
||||
@@ -656,7 +659,7 @@ ssize_t rdbSaveObject(rio *rdb, robj *o) {
|
||||
if ((n = rdbSaveLen(rdb,ql->len)) == -1) return -1;
|
||||
nwritten += n;
|
||||
|
||||
do {
|
||||
while(node) {
|
||||
if (quicklistNodeIsCompressed(node)) {
|
||||
void *data;
|
||||
size_t compress_len = quicklistGetLzf(node, &data);
|
||||
@@ -666,7 +669,8 @@ ssize_t rdbSaveObject(rio *rdb, robj *o) {
|
||||
if ((n = rdbSaveRawString(rdb,node->zl,node->sz)) == -1) return -1;
|
||||
nwritten += n;
|
||||
}
|
||||
} while ((node = node->next));
|
||||
node = node->next;
|
||||
}
|
||||
} else {
|
||||
serverPanic("Unknown list encoding");
|
||||
}
|
||||
@@ -704,23 +708,30 @@ ssize_t rdbSaveObject(rio *rdb, robj *o) {
|
||||
nwritten += n;
|
||||
} else if (o->encoding == OBJ_ENCODING_SKIPLIST) {
|
||||
zset *zs = o->ptr;
|
||||
dictIterator *di = dictGetIterator(zs->dict);
|
||||
dictEntry *de;
|
||||
zskiplist *zsl = zs->zsl;
|
||||
|
||||
if ((n = rdbSaveLen(rdb,dictSize(zs->dict))) == -1) return -1;
|
||||
if ((n = rdbSaveLen(rdb,zsl->length)) == -1) return -1;
|
||||
nwritten += n;
|
||||
|
||||
while((de = dictNext(di)) != NULL) {
|
||||
sds ele = dictGetKey(de);
|
||||
double *score = dictGetVal(de);
|
||||
|
||||
if ((n = rdbSaveRawString(rdb,(unsigned char*)ele,sdslen(ele)))
|
||||
== -1) return -1;
|
||||
/* We save the skiplist elements from the greatest to the smallest
|
||||
* (that's trivial since the elements are already ordered in the
|
||||
* skiplist): this improves the load process, since the next loaded
|
||||
* element will always be the smaller, so adding to the skiplist
|
||||
* will always immediately stop at the head, making the insertion
|
||||
* O(1) instead of O(log(N)). */
|
||||
zskiplistNode *zn = zsl->tail;
|
||||
while (zn != NULL) {
|
||||
if ((n = rdbSaveRawString(rdb,
|
||||
(unsigned char*)zn->ele,sdslen(zn->ele))) == -1)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
nwritten += n;
|
||||
if ((n = rdbSaveBinaryDoubleValue(rdb,*score)) == -1) return -1;
|
||||
if ((n = rdbSaveBinaryDoubleValue(rdb,zn->score)) == -1)
|
||||
return -1;
|
||||
nwritten += n;
|
||||
zn = zn->backward;
|
||||
}
|
||||
dictReleaseIterator(di);
|
||||
} else {
|
||||
serverPanic("Unknown sorted set encoding");
|
||||
}
|
||||
@@ -754,7 +765,39 @@ ssize_t rdbSaveObject(rio *rdb, robj *o) {
|
||||
} else {
|
||||
serverPanic("Unknown hash encoding");
|
||||
}
|
||||
} else if (o->type == OBJ_STREAM) {
|
||||
/* Store how many listpacks we have inside the radix tree. */
|
||||
stream *s = o->ptr;
|
||||
rax *rax = s->rax;
|
||||
if ((n = rdbSaveLen(rdb,raxSize(rax))) == -1) return -1;
|
||||
nwritten += n;
|
||||
|
||||
/* Serialize all the listpacks inside the radix tree as they are,
|
||||
* when loading back, we'll use the first entry of each listpack
|
||||
* to insert it back into the radix tree. */
|
||||
raxIterator ri;
|
||||
raxStart(&ri,rax);
|
||||
raxSeek(&ri,"^",NULL,0);
|
||||
while (raxNext(&ri)) {
|
||||
unsigned char *lp = ri.data;
|
||||
size_t lp_bytes = lpBytes(lp);
|
||||
if ((n = rdbSaveRawString(rdb,ri.key,ri.key_len)) == -1) return -1;
|
||||
nwritten += n;
|
||||
if ((n = rdbSaveRawString(rdb,lp,lp_bytes)) == -1) return -1;
|
||||
nwritten += n;
|
||||
}
|
||||
raxStop(&ri);
|
||||
|
||||
/* Save the number of elements inside the stream. We cannot obtain
|
||||
* this easily later, since our macro nodes should be checked for
|
||||
* number of items: not a great CPU / space tradeoff. */
|
||||
if ((n = rdbSaveLen(rdb,s->length)) == -1) return -1;
|
||||
nwritten += n;
|
||||
/* Save the last entry ID. */
|
||||
if ((n = rdbSaveLen(rdb,s->last_id.ms)) == -1) return -1;
|
||||
nwritten += n;
|
||||
if ((n = rdbSaveLen(rdb,s->last_id.seq)) == -1) return -1;
|
||||
nwritten += n;
|
||||
} else if (o->type == OBJ_MODULE) {
|
||||
/* Save a module-specific value. */
|
||||
RedisModuleIO io;
|
||||
@@ -768,8 +811,12 @@ ssize_t rdbSaveObject(rio *rdb, robj *o) {
|
||||
if (retval == -1) return -1;
|
||||
io.bytes += retval;
|
||||
|
||||
/* Then write the module-specific representation. */
|
||||
/* Then write the module-specific representation + EOF marker. */
|
||||
mt->rdb_save(&io,mv->value);
|
||||
retval = rdbSaveLen(rdb,RDB_MODULE_OPCODE_EOF);
|
||||
if (retval == -1) return -1;
|
||||
io.bytes += retval;
|
||||
|
||||
if (io.ctx) {
|
||||
moduleFreeContext(io.ctx);
|
||||
zfree(io.ctx);
|
||||
@@ -835,7 +882,7 @@ int rdbSaveAuxFieldStrInt(rio *rdb, char *key, long long val) {
|
||||
}
|
||||
|
||||
/* Save a few default AUX fields with information about the RDB generated. */
|
||||
int rdbSaveInfoAuxFields(rio *rdb, int flags) {
|
||||
int rdbSaveInfoAuxFields(rio *rdb, int flags, rdbSaveInfo *rsi) {
|
||||
int redis_bits = (sizeof(void*) == 8) ? 64 : 32;
|
||||
int aof_preamble = (flags & RDB_SAVE_AOF_PREAMBLE) != 0;
|
||||
|
||||
@@ -844,6 +891,16 @@ int rdbSaveInfoAuxFields(rio *rdb, int flags) {
|
||||
if (rdbSaveAuxFieldStrInt(rdb,"redis-bits",redis_bits) == -1) return -1;
|
||||
if (rdbSaveAuxFieldStrInt(rdb,"ctime",time(NULL)) == -1) return -1;
|
||||
if (rdbSaveAuxFieldStrInt(rdb,"used-mem",zmalloc_used_memory()) == -1) return -1;
|
||||
|
||||
/* Handle saving options that generate aux fields. */
|
||||
if (rsi) {
|
||||
if (rdbSaveAuxFieldStrInt(rdb,"repl-stream-db",rsi->repl_stream_db)
|
||||
== -1) return -1;
|
||||
if (rdbSaveAuxFieldStrStr(rdb,"repl-id",server.replid)
|
||||
== -1) return -1;
|
||||
if (rdbSaveAuxFieldStrInt(rdb,"repl-offset",server.master_repl_offset)
|
||||
== -1) return -1;
|
||||
}
|
||||
if (rdbSaveAuxFieldStrInt(rdb,"aof-preamble",aof_preamble) == -1) return -1;
|
||||
return 1;
|
||||
}
|
||||
@@ -856,7 +913,7 @@ int rdbSaveInfoAuxFields(rio *rdb, int flags) {
|
||||
* When the function returns C_ERR and if 'error' is not NULL, the
|
||||
* integer pointed by 'error' is set to the value of errno just after the I/O
|
||||
* error. */
|
||||
int rdbSaveRio(rio *rdb, int *error, int flags) {
|
||||
int rdbSaveRio(rio *rdb, int *error, int flags, rdbSaveInfo *rsi) {
|
||||
dictIterator *di = NULL;
|
||||
dictEntry *de;
|
||||
char magic[10];
|
||||
@@ -869,7 +926,7 @@ int rdbSaveRio(rio *rdb, int *error, int flags) {
|
||||
rdb->update_cksum = rioGenericUpdateChecksum;
|
||||
snprintf(magic,sizeof(magic),"REDIS%04d",RDB_VERSION);
|
||||
if (rdbWriteRaw(rdb,magic,9) == -1) goto werr;
|
||||
if (rdbSaveInfoAuxFields(rdb,flags) == -1) goto werr;
|
||||
if (rdbSaveInfoAuxFields(rdb,flags,rsi) == -1) goto werr;
|
||||
|
||||
for (j = 0; j < server.dbnum; j++) {
|
||||
redisDb *db = server.db+j;
|
||||
@@ -921,6 +978,20 @@ int rdbSaveRio(rio *rdb, int *error, int flags) {
|
||||
}
|
||||
di = NULL; /* So that we don't release it again on error. */
|
||||
|
||||
/* If we are storing the replication information on disk, persist
|
||||
* the script cache as well: on successful PSYNC after a restart, we need
|
||||
* to be able to process any EVALSHA inside the replication backlog the
|
||||
* master will send us. */
|
||||
if (rsi && dictSize(server.lua_scripts)) {
|
||||
di = dictGetIterator(server.lua_scripts);
|
||||
while((de = dictNext(di)) != NULL) {
|
||||
robj *body = dictGetVal(de);
|
||||
if (rdbSaveAuxField(rdb,"lua",3,body->ptr,sdslen(body->ptr)) == -1)
|
||||
goto werr;
|
||||
}
|
||||
dictReleaseIterator(di);
|
||||
}
|
||||
|
||||
/* EOF opcode */
|
||||
if (rdbSaveType(rdb,RDB_OPCODE_EOF) == -1) goto werr;
|
||||
|
||||
@@ -945,7 +1016,7 @@ werr:
|
||||
* While the suffix is the 40 bytes hex string we announced in the prefix.
|
||||
* This way processes receiving the payload can understand when it ends
|
||||
* without doing any processing of the content. */
|
||||
int rdbSaveRioWithEOFMark(rio *rdb, int *error) {
|
||||
int rdbSaveRioWithEOFMark(rio *rdb, int *error, rdbSaveInfo *rsi) {
|
||||
char eofmark[RDB_EOF_MARK_SIZE];
|
||||
|
||||
getRandomHexChars(eofmark,RDB_EOF_MARK_SIZE);
|
||||
@@ -953,7 +1024,7 @@ int rdbSaveRioWithEOFMark(rio *rdb, int *error) {
|
||||
if (rioWrite(rdb,"$EOF:",5) == 0) goto werr;
|
||||
if (rioWrite(rdb,eofmark,RDB_EOF_MARK_SIZE) == 0) goto werr;
|
||||
if (rioWrite(rdb,"\r\n",2) == 0) goto werr;
|
||||
if (rdbSaveRio(rdb,error,RDB_SAVE_NONE) == C_ERR) goto werr;
|
||||
if (rdbSaveRio(rdb,error,RDB_SAVE_NONE,rsi) == C_ERR) goto werr;
|
||||
if (rioWrite(rdb,eofmark,RDB_EOF_MARK_SIZE) == 0) goto werr;
|
||||
return C_OK;
|
||||
|
||||
@@ -964,7 +1035,7 @@ werr: /* Write error. */
|
||||
}
|
||||
|
||||
/* Save the DB on disk. Return C_ERR on error, C_OK on success. */
|
||||
int rdbSave(char *filename) {
|
||||
int rdbSave(char *filename, rdbSaveInfo *rsi) {
|
||||
char tmpfile[256];
|
||||
char cwd[MAXPATHLEN]; /* Current working dir path for error messages. */
|
||||
FILE *fp;
|
||||
@@ -985,7 +1056,7 @@ int rdbSave(char *filename) {
|
||||
}
|
||||
|
||||
rioInitWithFile(&rdb,fp);
|
||||
if (rdbSaveRio(&rdb,&error,RDB_SAVE_NONE) == C_ERR) {
|
||||
if (rdbSaveRio(&rdb,&error,RDB_SAVE_NONE,rsi) == C_ERR) {
|
||||
errno = error;
|
||||
goto werr;
|
||||
}
|
||||
@@ -1023,7 +1094,7 @@ werr:
|
||||
return C_ERR;
|
||||
}
|
||||
|
||||
int rdbSaveBackground(char *filename) {
|
||||
int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
|
||||
pid_t childpid;
|
||||
long long start;
|
||||
|
||||
@@ -1040,7 +1111,7 @@ int rdbSaveBackground(char *filename) {
|
||||
/* Child */
|
||||
closeListeningSockets(0);
|
||||
redisSetProcTitle("redis-rdb-bgsave");
|
||||
retval = rdbSave(filename);
|
||||
retval = rdbSave(filename,rsi);
|
||||
if (retval == C_OK) {
|
||||
size_t private_dirty = zmalloc_get_private_dirty(-1);
|
||||
|
||||
@@ -1083,6 +1154,45 @@ void rdbRemoveTempFile(pid_t childpid) {
|
||||
unlink(tmpfile);
|
||||
}
|
||||
|
||||
/* This function is called by rdbLoadObject() when the code is in RDB-check
|
||||
* mode and we find a module value of type 2 that can be parsed without
|
||||
* the need of the actual module. The value is parsed for errors, finally
|
||||
* a dummy redis object is returned just to conform to the API. */
|
||||
robj *rdbLoadCheckModuleValue(rio *rdb, char *modulename) {
|
||||
uint64_t opcode;
|
||||
while((opcode = rdbLoadLen(rdb,NULL)) != RDB_MODULE_OPCODE_EOF) {
|
||||
if (opcode == RDB_MODULE_OPCODE_SINT ||
|
||||
opcode == RDB_MODULE_OPCODE_UINT)
|
||||
{
|
||||
uint64_t len;
|
||||
if (rdbLoadLenByRef(rdb,NULL,&len) == -1) {
|
||||
rdbExitReportCorruptRDB(
|
||||
"Error reading integer from module %s value", modulename);
|
||||
}
|
||||
} else if (opcode == RDB_MODULE_OPCODE_STRING) {
|
||||
robj *o = rdbGenericLoadStringObject(rdb,RDB_LOAD_NONE,NULL);
|
||||
if (o == NULL) {
|
||||
rdbExitReportCorruptRDB(
|
||||
"Error reading string from module %s value", modulename);
|
||||
}
|
||||
decrRefCount(o);
|
||||
} else if (opcode == RDB_MODULE_OPCODE_FLOAT) {
|
||||
float val;
|
||||
if (rdbLoadBinaryFloatValue(rdb,&val) == -1) {
|
||||
rdbExitReportCorruptRDB(
|
||||
"Error reading float from module %s value", modulename);
|
||||
}
|
||||
} else if (opcode == RDB_MODULE_OPCODE_DOUBLE) {
|
||||
double val;
|
||||
if (rdbLoadBinaryDoubleValue(rdb,&val) == -1) {
|
||||
rdbExitReportCorruptRDB(
|
||||
"Error reading double from module %s value", modulename);
|
||||
}
|
||||
}
|
||||
}
|
||||
return createStringObject("module-dummy-value",18);
|
||||
}
|
||||
|
||||
/* Load a Redis object of the specified type from the specified file.
|
||||
* On success a newly allocated object is returned, otherwise NULL. */
|
||||
robj *rdbLoadObject(int rdbtype, rio *rdb) {
|
||||
@@ -1334,11 +1444,53 @@ robj *rdbLoadObject(int rdbtype, rio *rdb) {
|
||||
rdbExitReportCorruptRDB("Unknown RDB encoding type %d",rdbtype);
|
||||
break;
|
||||
}
|
||||
} else if (rdbtype == RDB_TYPE_MODULE) {
|
||||
} else if (rdbtype == RDB_TYPE_STREAM_LISTPACKS) {
|
||||
o = createStreamObject();
|
||||
stream *s = o->ptr;
|
||||
uint64_t listpacks = rdbLoadLen(rdb,NULL);
|
||||
|
||||
while(listpacks--) {
|
||||
/* Get the master ID, the one we'll use as key of the radix tree
|
||||
* node: the entries inside the listpack itself are delta-encoded
|
||||
* relatively to this ID. */
|
||||
sds nodekey = rdbGenericLoadStringObject(rdb,RDB_LOAD_SDS,NULL);
|
||||
if (sdslen(nodekey) != sizeof(streamID)) {
|
||||
rdbExitReportCorruptRDB("Stream node key entry is not the "
|
||||
"size of a stream ID");
|
||||
}
|
||||
|
||||
/* Load the listpack. */
|
||||
unsigned char *lp =
|
||||
rdbGenericLoadStringObject(rdb,RDB_LOAD_PLAIN,NULL);
|
||||
if (lp == NULL) return NULL;
|
||||
unsigned char *first = lpFirst(lp);
|
||||
if (first == NULL) {
|
||||
/* Serialized listpacks should never be empty, since on
|
||||
* deletion we should remove the radix tree key if the
|
||||
* resulting listpack is emtpy. */
|
||||
rdbExitReportCorruptRDB("Empty listpack inside stream");
|
||||
}
|
||||
|
||||
/* Insert the key in the radix tree. */
|
||||
int retval = raxInsert(s->rax,
|
||||
(unsigned char*)nodekey,sizeof(streamID),lp,NULL);
|
||||
sdsfree(nodekey);
|
||||
if (!retval)
|
||||
rdbExitReportCorruptRDB("Listpack re-added with existing key");
|
||||
}
|
||||
/* Load total number of items inside the stream. */
|
||||
s->length = rdbLoadLen(rdb,NULL);
|
||||
/* Load the last entry ID. */
|
||||
s->last_id.ms = rdbLoadLen(rdb,NULL);
|
||||
s->last_id.seq = rdbLoadLen(rdb,NULL);
|
||||
} else if (rdbtype == RDB_TYPE_MODULE || rdbtype == RDB_TYPE_MODULE_2) {
|
||||
uint64_t moduleid = rdbLoadLen(rdb,NULL);
|
||||
moduleType *mt = moduleTypeLookupModuleByID(moduleid);
|
||||
char name[10];
|
||||
|
||||
if (rdbCheckMode && rdbtype == RDB_TYPE_MODULE_2)
|
||||
return rdbLoadCheckModuleValue(rdb,name);
|
||||
|
||||
if (mt == NULL) {
|
||||
moduleTypeNameByID(name,moduleid);
|
||||
serverLog(LL_WARNING,"The RDB file contains module data I can't load: no matching module '%s'", name);
|
||||
@@ -1346,9 +1498,24 @@ robj *rdbLoadObject(int rdbtype, rio *rdb) {
|
||||
}
|
||||
RedisModuleIO io;
|
||||
moduleInitIOContext(io,mt,rdb);
|
||||
io.ver = (rdbtype == RDB_TYPE_MODULE) ? 1 : 2;
|
||||
/* Call the rdb_load method of the module providing the 10 bit
|
||||
* encoding version in the lower 10 bits of the module ID. */
|
||||
void *ptr = mt->rdb_load(&io,moduleid&1023);
|
||||
if (io.ctx) {
|
||||
moduleFreeContext(io.ctx);
|
||||
zfree(io.ctx);
|
||||
}
|
||||
|
||||
/* Module v2 serialization has an EOF mark at the end. */
|
||||
if (io.ver == 2) {
|
||||
uint64_t eof = rdbLoadLen(rdb,NULL);
|
||||
if (eof != RDB_MODULE_OPCODE_EOF) {
|
||||
serverLog(LL_WARNING,"The RDB file contains module data for the module '%s' that is not terminated by the proper module value EOF marker", name);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (ptr == NULL) {
|
||||
moduleTypeNameByID(name,moduleid);
|
||||
serverLog(LL_WARNING,"The RDB file contains module data for the module type '%s', that the responsible module is not able to load. Check for modules log above for additional clues.", name);
|
||||
@@ -1410,7 +1577,7 @@ void rdbLoadProgressCallback(rio *r, const void *buf, size_t len) {
|
||||
|
||||
/* Load an RDB file from the rio stream 'rdb'. On success C_OK is returned,
|
||||
* otherwise C_ERR is returned and 'errno' is set accordingly. */
|
||||
int rdbLoadRio(rio *rdb) {
|
||||
int rdbLoadRio(rio *rdb, rdbSaveInfo *rsi) {
|
||||
uint64_t dbid;
|
||||
int type, rdbver;
|
||||
redisDb *db = server.db+0;
|
||||
@@ -1501,6 +1668,22 @@ int rdbLoadRio(rio *rdb) {
|
||||
serverLog(LL_NOTICE,"RDB '%s': %s",
|
||||
(char*)auxkey->ptr,
|
||||
(char*)auxval->ptr);
|
||||
} else if (!strcasecmp(auxkey->ptr,"repl-stream-db")) {
|
||||
if (rsi) rsi->repl_stream_db = atoi(auxval->ptr);
|
||||
} else if (!strcasecmp(auxkey->ptr,"repl-id")) {
|
||||
if (rsi && sdslen(auxval->ptr) == CONFIG_RUN_ID_SIZE) {
|
||||
memcpy(rsi->repl_id,auxval->ptr,CONFIG_RUN_ID_SIZE+1);
|
||||
rsi->repl_id_is_set = 1;
|
||||
}
|
||||
} else if (!strcasecmp(auxkey->ptr,"repl-offset")) {
|
||||
if (rsi) rsi->repl_offset = strtoll(auxval->ptr,NULL,10);
|
||||
} else if (!strcasecmp(auxkey->ptr,"lua")) {
|
||||
/* Load the script back in memory. */
|
||||
if (luaCreateFunction(NULL,server.lua,auxval) == NULL) {
|
||||
rdbExitReportCorruptRDB(
|
||||
"Can't load Lua script from RDB file! "
|
||||
"BODY: %s", auxval->ptr);
|
||||
}
|
||||
} else {
|
||||
/* We ignore fields we don't understand, as by AUX field
|
||||
* contract. */
|
||||
@@ -1531,7 +1714,7 @@ int rdbLoadRio(rio *rdb) {
|
||||
dbAdd(db,key,val);
|
||||
|
||||
/* Set the expire time if needed */
|
||||
if (expiretime != -1) setExpire(db,key,expiretime);
|
||||
if (expiretime != -1) setExpire(NULL,db,key,expiretime);
|
||||
|
||||
decrRefCount(key);
|
||||
}
|
||||
@@ -1559,8 +1742,11 @@ eoferr: /* unexpected end of file is handled here with a fatal exit */
|
||||
/* Like rdbLoadRio() but takes a filename instead of a rio stream. The
|
||||
* filename is open for reading and a rio stream object created in order
|
||||
* to do the actual loading. Moreover the ETA displayed in the INFO
|
||||
* output is initialized and finalized. */
|
||||
int rdbLoad(char *filename) {
|
||||
* output is initialized and finalized.
|
||||
*
|
||||
* If you pass an 'rsi' structure initialied with RDB_SAVE_OPTION_INIT, the
|
||||
* loading code will fiil the information fields in the structure. */
|
||||
int rdbLoad(char *filename, rdbSaveInfo *rsi) {
|
||||
FILE *fp;
|
||||
rio rdb;
|
||||
int retval;
|
||||
@@ -1568,7 +1754,7 @@ int rdbLoad(char *filename) {
|
||||
if ((fp = fopen(filename,"r")) == NULL) return C_ERR;
|
||||
startLoading(fp);
|
||||
rioInitWithFile(&rdb,fp);
|
||||
retval = rdbLoadRio(&rdb);
|
||||
retval = rdbLoadRio(&rdb,rsi);
|
||||
fclose(fp);
|
||||
stopLoading();
|
||||
return retval;
|
||||
@@ -1721,7 +1907,7 @@ void backgroundSaveDoneHandler(int exitcode, int bysignal) {
|
||||
|
||||
/* Spawn an RDB child that writes the RDB to the sockets of the slaves
|
||||
* that are currently in SLAVE_STATE_WAIT_BGSAVE_START state. */
|
||||
int rdbSaveToSlavesSockets(void) {
|
||||
int rdbSaveToSlavesSockets(rdbSaveInfo *rsi) {
|
||||
int *fds;
|
||||
uint64_t *clientids;
|
||||
int numfds;
|
||||
@@ -1779,7 +1965,7 @@ int rdbSaveToSlavesSockets(void) {
|
||||
closeListeningSockets(0);
|
||||
redisSetProcTitle("redis-rdb-to-slaves");
|
||||
|
||||
retval = rdbSaveRioWithEOFMark(&slave_sockets,NULL);
|
||||
retval = rdbSaveRioWithEOFMark(&slave_sockets,NULL,rsi);
|
||||
if (retval == C_OK && rioFlush(&slave_sockets) == 0)
|
||||
retval = C_ERR;
|
||||
|
||||
@@ -1839,9 +2025,6 @@ int rdbSaveToSlavesSockets(void) {
|
||||
exitFromChild((retval == C_OK) ? 0 : 1);
|
||||
} else {
|
||||
/* Parent */
|
||||
server.stat_fork_time = ustime()-start;
|
||||
server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024*1024*1024); /* GB per second. */
|
||||
latencyAddSampleIfNeeded("fork",server.stat_fork_time/1000);
|
||||
if (childpid == -1) {
|
||||
serverLog(LL_WARNING,"Can't save in background: fork: %s",
|
||||
strerror(errno));
|
||||
@@ -1865,6 +2048,10 @@ int rdbSaveToSlavesSockets(void) {
|
||||
close(pipefds[1]);
|
||||
closeChildInfoPipe();
|
||||
} else {
|
||||
server.stat_fork_time = ustime()-start;
|
||||
server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024*1024*1024); /* GB per second. */
|
||||
latencyAddSampleIfNeeded("fork",server.stat_fork_time/1000);
|
||||
|
||||
serverLog(LL_NOTICE,"Background RDB transfer started by pid %d",
|
||||
childpid);
|
||||
server.rdb_save_time_start = time(NULL);
|
||||
@@ -1884,7 +2071,9 @@ void saveCommand(client *c) {
|
||||
addReplyError(c,"Background save already in progress");
|
||||
return;
|
||||
}
|
||||
if (rdbSave(server.rdb_filename) == C_OK) {
|
||||
rdbSaveInfo rsi, *rsiptr;
|
||||
rsiptr = rdbPopulateSaveInfo(&rsi);
|
||||
if (rdbSave(server.rdb_filename,rsiptr) == C_OK) {
|
||||
addReply(c,shared.ok);
|
||||
} else {
|
||||
addReply(c,shared.err);
|
||||
@@ -1906,6 +2095,9 @@ void bgsaveCommand(client *c) {
|
||||
}
|
||||
}
|
||||
|
||||
rdbSaveInfo rsi, *rsiptr;
|
||||
rsiptr = rdbPopulateSaveInfo(&rsi);
|
||||
|
||||
if (server.rdb_child_pid != -1) {
|
||||
addReplyError(c,"Background save already in progress");
|
||||
} else if (server.aof_child_pid != -1) {
|
||||
@@ -1915,12 +2107,61 @@ void bgsaveCommand(client *c) {
|
||||
} else {
|
||||
addReplyError(c,
|
||||
"An AOF log rewriting in progress: can't BGSAVE right now. "
|
||||
"Use BGSAVE SCHEDULE in order to schedule a BGSAVE whenver "
|
||||
"Use BGSAVE SCHEDULE in order to schedule a BGSAVE whenever "
|
||||
"possible.");
|
||||
}
|
||||
} else if (rdbSaveBackground(server.rdb_filename) == C_OK) {
|
||||
} else if (rdbSaveBackground(server.rdb_filename,rsiptr) == C_OK) {
|
||||
addReplyStatus(c,"Background saving started");
|
||||
} else {
|
||||
addReply(c,shared.err);
|
||||
}
|
||||
}
|
||||
|
||||
/* Populate the rdbSaveInfo structure used to persist the replication
|
||||
* information inside the RDB file. Currently the structure explicitly
|
||||
* contains just the currently selected DB from the master stream, however
|
||||
* if the rdbSave*() family functions receive a NULL rsi structure also
|
||||
* the Replication ID/offset is not saved. The function popultes 'rsi'
|
||||
* that is normally stack-allocated in the caller, returns the populated
|
||||
* pointer if the instance has a valid master client, otherwise NULL
|
||||
* is returned, and the RDB saving will not persist any replication related
|
||||
* information. */
|
||||
rdbSaveInfo *rdbPopulateSaveInfo(rdbSaveInfo *rsi) {
|
||||
rdbSaveInfo rsi_init = RDB_SAVE_INFO_INIT;
|
||||
*rsi = rsi_init;
|
||||
|
||||
/* If the instance is a master, we can populate the replication info
|
||||
* only when repl_backlog is not NULL. If the repl_backlog is NULL,
|
||||
* it means that the instance isn't in any replication chains. In this
|
||||
* scenario the replication info is useless, because when a slave
|
||||
* connects to us, the NULL repl_backlog will trigger a full
|
||||
* synchronization, at the same time we will use a new replid and clear
|
||||
* replid2. */
|
||||
if (!server.masterhost && server.repl_backlog) {
|
||||
/* Note that when server.slaveseldb is -1, it means that this master
|
||||
* didn't apply any write commands after a full synchronization.
|
||||
* So we can let repl_stream_db be 0, this allows a restarted slave
|
||||
* to reload replication ID/offset, it's safe because the next write
|
||||
* command must generate a SELECT statement. */
|
||||
rsi->repl_stream_db = server.slaveseldb == -1 ? 0 : server.slaveseldb;
|
||||
return rsi;
|
||||
}
|
||||
|
||||
/* If the instance is a slave we need a connected master
|
||||
* in order to fetch the currently selected DB. */
|
||||
if (server.master) {
|
||||
rsi->repl_stream_db = server.master->db->id;
|
||||
return rsi;
|
||||
}
|
||||
|
||||
/* If we have a cached master we can use it in order to populate the
|
||||
* replication selected DB info inside the RDB file: the slave can
|
||||
* increment the master_repl_offset only from data arriving from the
|
||||
* master, so if we are disconnected the offset in the cached master
|
||||
* is valid. */
|
||||
if (server.cached_master) {
|
||||
rsi->repl_stream_db = server.cached_master->db->id;
|
||||
return rsi;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -69,8 +69,9 @@
|
||||
#define RDB_ENC_INT32 2 /* 32 bit signed integer */
|
||||
#define RDB_ENC_LZF 3 /* string compressed with FASTLZ */
|
||||
|
||||
/* Dup object types to RDB object types. Only reason is readability (are we
|
||||
* dealing with RDB types or with in-memory object types?). */
|
||||
/* Map object types to RDB object types. Macros starting with OBJ_ are for
|
||||
* memory storage and may change. Instead RDB types must be fixed because
|
||||
* we store them on disk. */
|
||||
#define RDB_TYPE_STRING 0
|
||||
#define RDB_TYPE_LIST 1
|
||||
#define RDB_TYPE_SET 2
|
||||
@@ -78,6 +79,8 @@
|
||||
#define RDB_TYPE_HASH 4
|
||||
#define RDB_TYPE_ZSET_2 5 /* ZSET version 2 with doubles stored in binary. */
|
||||
#define RDB_TYPE_MODULE 6
|
||||
#define RDB_TYPE_MODULE_2 7 /* Module value with annotations for parsing without
|
||||
the generating module being loaded. */
|
||||
/* NOTE: WHEN ADDING NEW RDB TYPE, UPDATE rdbIsObjectType() BELOW */
|
||||
|
||||
/* Object types for encoded objects. */
|
||||
@@ -87,10 +90,11 @@
|
||||
#define RDB_TYPE_ZSET_ZIPLIST 12
|
||||
#define RDB_TYPE_HASH_ZIPLIST 13
|
||||
#define RDB_TYPE_LIST_QUICKLIST 14
|
||||
#define RDB_TYPE_STREAM_LISTPACKS 15
|
||||
/* NOTE: WHEN ADDING NEW RDB TYPE, UPDATE rdbIsObjectType() BELOW */
|
||||
|
||||
/* Test if a type is an object type. */
|
||||
#define rdbIsObjectType(t) ((t >= 0 && t <= 6) || (t >= 9 && t <= 14))
|
||||
#define rdbIsObjectType(t) ((t >= 0 && t <= 7) || (t >= 9 && t <= 15))
|
||||
|
||||
/* Special RDB opcodes (saved/loaded with rdbSaveType/rdbLoadType). */
|
||||
#define RDB_OPCODE_AUX 250
|
||||
@@ -100,6 +104,14 @@
|
||||
#define RDB_OPCODE_SELECTDB 254
|
||||
#define RDB_OPCODE_EOF 255
|
||||
|
||||
/* Module serialized values sub opcodes */
|
||||
#define RDB_MODULE_OPCODE_EOF 0 /* End of module value. */
|
||||
#define RDB_MODULE_OPCODE_SINT 1 /* Signed integer. */
|
||||
#define RDB_MODULE_OPCODE_UINT 2 /* Unsigned integer. */
|
||||
#define RDB_MODULE_OPCODE_FLOAT 3 /* Float. */
|
||||
#define RDB_MODULE_OPCODE_DOUBLE 4 /* Double. */
|
||||
#define RDB_MODULE_OPCODE_STRING 5 /* String. */
|
||||
|
||||
/* rdbLoad...() functions flags. */
|
||||
#define RDB_LOAD_NONE 0
|
||||
#define RDB_LOAD_ENC (1<<0)
|
||||
@@ -118,11 +130,11 @@ uint64_t rdbLoadLen(rio *rdb, int *isencoded);
|
||||
int rdbLoadLenByRef(rio *rdb, int *isencoded, uint64_t *lenptr);
|
||||
int rdbSaveObjectType(rio *rdb, robj *o);
|
||||
int rdbLoadObjectType(rio *rdb);
|
||||
int rdbLoad(char *filename);
|
||||
int rdbSaveBackground(char *filename);
|
||||
int rdbSaveToSlavesSockets(void);
|
||||
int rdbLoad(char *filename, rdbSaveInfo *rsi);
|
||||
int rdbSaveBackground(char *filename, rdbSaveInfo *rsi);
|
||||
int rdbSaveToSlavesSockets(rdbSaveInfo *rsi);
|
||||
void rdbRemoveTempFile(pid_t childpid);
|
||||
int rdbSave(char *filename);
|
||||
int rdbSave(char *filename, rdbSaveInfo *rsi);
|
||||
ssize_t rdbSaveObject(rio *rdb, robj *o);
|
||||
size_t rdbSavedObjectLen(robj *o);
|
||||
robj *rdbLoadObject(int type, rio *rdb);
|
||||
@@ -136,6 +148,7 @@ int rdbSaveBinaryDoubleValue(rio *rdb, double val);
|
||||
int rdbLoadBinaryDoubleValue(rio *rdb, double *val);
|
||||
int rdbSaveBinaryFloatValue(rio *rdb, float val);
|
||||
int rdbLoadBinaryFloatValue(rio *rdb, float *val);
|
||||
int rdbLoadRio(rio *rdb);
|
||||
int rdbLoadRio(rio *rdb, rdbSaveInfo *rsi);
|
||||
rdbSaveInfo *rdbPopulateSaveInfo(rdbSaveInfo *rsi);
|
||||
|
||||
#endif
|
||||
|
||||
+10
-3
@@ -565,15 +565,15 @@ invalid:
|
||||
|
||||
usage:
|
||||
printf(
|
||||
"Usage: redis-benchmark [-h <host>] [-p <port>] [-c <clients>] [-n <requests]> [-k <boolean>]\n\n"
|
||||
"Usage: redis-benchmark [-h <host>] [-p <port>] [-c <clients>] [-n <requests>] [-k <boolean>]\n\n"
|
||||
" -h <hostname> Server hostname (default 127.0.0.1)\n"
|
||||
" -p <port> Server port (default 6379)\n"
|
||||
" -s <socket> Server socket (overrides host and port)\n"
|
||||
" -a <password> Password for Redis Auth\n"
|
||||
" -c <clients> Number of parallel connections (default 50)\n"
|
||||
" -n <requests> Total number of requests (default 100000)\n"
|
||||
" -d <size> Data size of SET/GET value in bytes (default 2)\n"
|
||||
" -dbnum <db> SELECT the specified db number (default 0)\n"
|
||||
" -d <size> Data size of SET/GET value in bytes (default 3)\n"
|
||||
" --dbnum <db> SELECT the specified db number (default 0)\n"
|
||||
" -k <boolean> 1=keep alive 0=reconnect (default 1)\n"
|
||||
" -r <keyspacelen> Use random keys for SET/GET/INCR, random values for SADD\n"
|
||||
" Using this option the benchmark will expand the string __rand_int__\n"
|
||||
@@ -779,6 +779,13 @@ int main(int argc, const char **argv) {
|
||||
free(cmd);
|
||||
}
|
||||
|
||||
if (test_is_selected("hset")) {
|
||||
len = redisFormatCommand(&cmd,
|
||||
"HSET myset:__rand_int__ element:__rand_int__ %s",data);
|
||||
benchmark("HSET",cmd,len);
|
||||
free(cmd);
|
||||
}
|
||||
|
||||
if (test_is_selected("spop")) {
|
||||
len = redisFormatCommand(&cmd,"SPOP myset");
|
||||
benchmark("SPOP",cmd,len);
|
||||
|
||||
+28
-13
@@ -28,13 +28,8 @@
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "fmacros.h"
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include "server.h"
|
||||
#include <sys/stat.h>
|
||||
#include "config.h"
|
||||
|
||||
#define ERROR(...) { \
|
||||
char __buf[1024]; \
|
||||
@@ -60,7 +55,7 @@ int readLong(FILE *fp, char prefix, long *target) {
|
||||
return 0;
|
||||
}
|
||||
if (buf[0] != prefix) {
|
||||
ERROR("Expected prefix '%c', got: '%c'",buf[0],prefix);
|
||||
ERROR("Expected prefix '%c', got: '%c'",prefix,buf[0]);
|
||||
return 0;
|
||||
}
|
||||
*target = strtol(buf+1,&eptr,10);
|
||||
@@ -87,7 +82,7 @@ int readString(FILE *fp, char** target) {
|
||||
|
||||
/* Increase length to also consume \r\n */
|
||||
len += 2;
|
||||
*target = (char*)malloc(len);
|
||||
*target = (char*)zmalloc(len);
|
||||
if (!readBytes(fp,*target,len)) {
|
||||
return 0;
|
||||
}
|
||||
@@ -127,12 +122,12 @@ off_t process(FILE *fp) {
|
||||
}
|
||||
}
|
||||
}
|
||||
free(str);
|
||||
zfree(str);
|
||||
}
|
||||
|
||||
/* Stop if the loop did not finish */
|
||||
if (i < argc) {
|
||||
if (str) free(str);
|
||||
if (str) zfree(str);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -146,7 +141,7 @@ off_t process(FILE *fp) {
|
||||
return pos;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int redis_check_aof_main(int argc, char **argv) {
|
||||
char *filename;
|
||||
int fix = 0;
|
||||
|
||||
@@ -185,6 +180,25 @@ int main(int argc, char **argv) {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/* This AOF file may have an RDB preamble. Check this to start, and if this
|
||||
* is the case, start processing the RDB part. */
|
||||
if (size >= 8) { /* There must be at least room for the RDB header. */
|
||||
char sig[5];
|
||||
int has_preamble = fread(sig,sizeof(sig),1,fp) == 1 &&
|
||||
memcmp(sig,"REDIS",sizeof(sig)) == 0;
|
||||
rewind(fp);
|
||||
if (has_preamble) {
|
||||
printf("The AOF appears to start with an RDB preamble.\n"
|
||||
"Checking the RDB preamble to start:\n");
|
||||
if (redis_check_rdb_main(argc,argv,fp) == C_ERR) {
|
||||
printf("RDB preamble of AOF file is not sane, aborting.\n");
|
||||
exit(1);
|
||||
} else {
|
||||
printf("RDB preamble is OK, proceeding with AOF tail...\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
off_t pos = process(fp);
|
||||
off_t diff = size-pos;
|
||||
printf("AOF analyzed: size=%lld, ok_up_to=%lld, diff=%lld\n",
|
||||
@@ -206,7 +220,8 @@ int main(int argc, char **argv) {
|
||||
printf("Successfully truncated AOF\n");
|
||||
}
|
||||
} else {
|
||||
printf("AOF is not valid\n");
|
||||
printf("AOF is not valid. "
|
||||
"Use the --fix option to try fixing it.\n");
|
||||
exit(1);
|
||||
}
|
||||
} else {
|
||||
@@ -214,5 +229,5 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
return 0;
|
||||
exit(0);
|
||||
}
|
||||
|
||||
+27
-14
@@ -173,16 +173,18 @@ void rdbCheckSetupSignals(void) {
|
||||
}
|
||||
|
||||
/* Check the specified RDB file. Return 0 if the RDB looks sane, otherwise
|
||||
* 1 is returned. */
|
||||
int redis_check_rdb(char *rdbfilename) {
|
||||
* 1 is returned.
|
||||
* The file is specified as a filename in 'rdbfilename' if 'fp' is not NULL,
|
||||
* otherwise the already open file 'fp' is checked. */
|
||||
int redis_check_rdb(char *rdbfilename, FILE *fp) {
|
||||
uint64_t dbid;
|
||||
int type, rdbver;
|
||||
char buf[1024];
|
||||
long long expiretime, now = mstime();
|
||||
FILE *fp;
|
||||
static rio rdb; /* Pointed by global struct riostate. */
|
||||
|
||||
if ((fp = fopen(rdbfilename,"r")) == NULL) return 1;
|
||||
int closefile = (fp == NULL);
|
||||
if (fp == NULL && (fp = fopen(rdbfilename,"r")) == NULL) return 1;
|
||||
|
||||
rioInitWithFile(&rdb,fp);
|
||||
rdbstate.rio = &rdb;
|
||||
@@ -191,12 +193,12 @@ int redis_check_rdb(char *rdbfilename) {
|
||||
buf[9] = '\0';
|
||||
if (memcmp(buf,"REDIS",5) != 0) {
|
||||
rdbCheckError("Wrong signature trying to load DB from file");
|
||||
return 1;
|
||||
goto err;
|
||||
}
|
||||
rdbver = atoi(buf+5);
|
||||
if (rdbver < 1 || rdbver > RDB_VERSION) {
|
||||
rdbCheckError("Can't handle RDB format version %d",rdbver);
|
||||
return 1;
|
||||
goto err;
|
||||
}
|
||||
|
||||
startLoading(fp);
|
||||
@@ -268,7 +270,7 @@ int redis_check_rdb(char *rdbfilename) {
|
||||
} else {
|
||||
if (!rdbIsObjectType(type)) {
|
||||
rdbCheckError("Invalid object type: %d", type);
|
||||
return 1;
|
||||
goto err;
|
||||
}
|
||||
rdbstate.key_type = type;
|
||||
}
|
||||
@@ -305,12 +307,13 @@ int redis_check_rdb(char *rdbfilename) {
|
||||
rdbCheckInfo("RDB file was saved with checksum disabled: no check performed.");
|
||||
} else if (cksum != expected) {
|
||||
rdbCheckError("RDB CRC error");
|
||||
goto err;
|
||||
} else {
|
||||
rdbCheckInfo("Checksum OK");
|
||||
}
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
if (closefile) fclose(fp);
|
||||
return 0;
|
||||
|
||||
eoferr: /* unexpected end of file is handled here with a fatal exit */
|
||||
@@ -319,16 +322,25 @@ eoferr: /* unexpected end of file is handled here with a fatal exit */
|
||||
} else {
|
||||
rdbCheckError("Unexpected EOF reading RDB file");
|
||||
}
|
||||
err:
|
||||
if (closefile) fclose(fp);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* RDB check main: called form redis.c when Redis is executed with the
|
||||
* redis-check-rdb alias.
|
||||
* redis-check-rdb alias, on during RDB loading errors.
|
||||
*
|
||||
* The function never returns, but exits with the status code according
|
||||
* to success (RDB is sane) or error (RDB is corrupted). */
|
||||
int redis_check_rdb_main(int argc, char **argv) {
|
||||
if (argc != 2) {
|
||||
* The function works in two ways: can be called with argc/argv as a
|
||||
* standalone executable, or called with a non NULL 'fp' argument if we
|
||||
* already have an open file to check. This happens when the function
|
||||
* is used to check an RDB preamble inside an AOF file.
|
||||
*
|
||||
* When called with fp = NULL, the function never returns, but exits with the
|
||||
* status code according to success (RDB is sane) or error (RDB is corrupted).
|
||||
* Otherwise if called with a non NULL fp, the function returns C_OK or
|
||||
* C_ERR depending on the success or failure. */
|
||||
int redis_check_rdb_main(int argc, char **argv, FILE *fp) {
|
||||
if (argc != 2 && fp == NULL) {
|
||||
fprintf(stderr, "Usage: %s <rdb-file-name>\n", argv[0]);
|
||||
exit(1);
|
||||
}
|
||||
@@ -341,10 +353,11 @@ int redis_check_rdb_main(int argc, char **argv) {
|
||||
rdbCheckMode = 1;
|
||||
rdbCheckInfo("Checking RDB file %s", argv[1]);
|
||||
rdbCheckSetupSignals();
|
||||
int retval = redis_check_rdb(argv[1]);
|
||||
int retval = redis_check_rdb(argv[1],fp);
|
||||
if (retval == 0) {
|
||||
rdbCheckInfo("\\o/ RDB looks OK! \\o/");
|
||||
rdbShowGenericInfo();
|
||||
}
|
||||
if (fp) return (retval == 0) ? C_OK : C_ERR;
|
||||
exit(retval);
|
||||
}
|
||||
|
||||
+298
-21
@@ -107,6 +107,7 @@ static struct config {
|
||||
char *pattern;
|
||||
char *rdb_filename;
|
||||
int bigkeys;
|
||||
int hotkeys;
|
||||
int stdinarg; /* get last arg from stdin. (-x option) */
|
||||
char *auth;
|
||||
int output; /* output mode, see OUTPUT_* defines */
|
||||
@@ -198,6 +199,92 @@ static sds getDotfilePath(char *envoverride, char *dotfilename) {
|
||||
return dotPath;
|
||||
}
|
||||
|
||||
/* URL-style percent decoding. */
|
||||
#define isHexChar(c) (isdigit(c) || (c >= 'a' && c <= 'f'))
|
||||
#define decodeHexChar(c) (isdigit(c) ? c - '0' : c - 'a' + 10)
|
||||
#define decodeHex(h, l) ((decodeHexChar(h) << 4) + decodeHexChar(l))
|
||||
|
||||
static sds percentDecode(const char *pe, size_t len) {
|
||||
const char *end = pe + len;
|
||||
sds ret = sdsempty();
|
||||
const char *curr = pe;
|
||||
|
||||
while (curr < end) {
|
||||
if (*curr == '%') {
|
||||
if ((end - curr) < 2) {
|
||||
fprintf(stderr, "Incomplete URI encoding\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
char h = tolower(*(++curr));
|
||||
char l = tolower(*(++curr));
|
||||
if (!isHexChar(h) || !isHexChar(l)) {
|
||||
fprintf(stderr, "Illegal character in URI encoding\n");
|
||||
exit(1);
|
||||
}
|
||||
char c = decodeHex(h, l);
|
||||
ret = sdscatlen(ret, &c, 1);
|
||||
curr++;
|
||||
} else {
|
||||
ret = sdscatlen(ret, curr++, 1);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* Parse a URI and extract the server connection information.
|
||||
* URI scheme is based on the the provisional specification[1] excluding support
|
||||
* for query parameters. Valid URIs are:
|
||||
* scheme: "redis://"
|
||||
* authority: [<username> ":"] <password> "@"] [<hostname> [":" <port>]]
|
||||
* path: ["/" [<db>]]
|
||||
*
|
||||
* [1]: https://www.iana.org/assignments/uri-schemes/prov/redis */
|
||||
static void parseRedisUri(const char *uri) {
|
||||
|
||||
const char *scheme = "redis://";
|
||||
const char *curr = uri;
|
||||
const char *end = uri + strlen(uri);
|
||||
const char *userinfo, *username, *port, *host, *path;
|
||||
|
||||
/* URI must start with a valid scheme. */
|
||||
if (strncasecmp(scheme, curr, strlen(scheme))) {
|
||||
fprintf(stderr,"Invalid URI scheme\n");
|
||||
exit(1);
|
||||
}
|
||||
curr += strlen(scheme);
|
||||
if (curr == end) return;
|
||||
|
||||
/* Extract user info. */
|
||||
if ((userinfo = strchr(curr,'@'))) {
|
||||
if ((username = strchr(curr, ':')) && username < userinfo) {
|
||||
/* If provided, username is ignored. */
|
||||
curr = username + 1;
|
||||
}
|
||||
|
||||
config.auth = percentDecode(curr, userinfo - curr);
|
||||
curr = userinfo + 1;
|
||||
}
|
||||
if (curr == end) return;
|
||||
|
||||
/* Extract host and port. */
|
||||
path = strchr(curr, '/');
|
||||
if (*curr != '/') {
|
||||
host = path ? path - 1 : end;
|
||||
if ((port = strchr(curr, ':'))) {
|
||||
config.hostport = atoi(port + 1);
|
||||
host = port - 1;
|
||||
}
|
||||
config.hostip = sdsnewlen(curr, host - curr + 1);
|
||||
}
|
||||
curr = path ? path + 1 : end;
|
||||
if (curr == end) return;
|
||||
|
||||
/* Extract database number. */
|
||||
config.dbnum = atoi(curr);
|
||||
}
|
||||
|
||||
/*------------------------------------------------------------------------------
|
||||
* Help functions
|
||||
*--------------------------------------------------------------------------- */
|
||||
@@ -275,6 +362,10 @@ static void cliIntegrateHelp(void) {
|
||||
* don't already match what we have. */
|
||||
for (size_t j = 0; j < reply->elements; j++) {
|
||||
redisReply *entry = reply->element[j];
|
||||
if (entry->type != REDIS_REPLY_ARRAY || entry->elements < 4 ||
|
||||
entry->element[0]->type != REDIS_REPLY_STRING ||
|
||||
entry->element[1]->type != REDIS_REPLY_INTEGER ||
|
||||
entry->element[3]->type != REDIS_REPLY_INTEGER) return;
|
||||
char *cmdname = entry->element[0]->str;
|
||||
int i;
|
||||
|
||||
@@ -336,7 +427,7 @@ static void cliOutputGenericHelp(void) {
|
||||
" \"help <tab>\" to get a list of possible help topics\n"
|
||||
" \"quit\" to exit\n"
|
||||
"\n"
|
||||
"To set redis-cli perferences:\n"
|
||||
"To set redis-cli preferences:\n"
|
||||
" \":set hints\" enable online hints\n"
|
||||
" \":set nohints\" disable online hints\n"
|
||||
"Set your preferences in ~/.redisclirc\n",
|
||||
@@ -620,7 +711,7 @@ int isColorTerm(void) {
|
||||
return t != NULL && strstr(t,"xterm") != NULL;
|
||||
}
|
||||
|
||||
/* Helpe function for sdsCatColorizedLdbReply() appending colorize strings
|
||||
/* Helper function for sdsCatColorizedLdbReply() appending colorize strings
|
||||
* to an SDS string. */
|
||||
sds sdscatcolor(sds o, char *s, size_t len, char *color) {
|
||||
if (!isColorTerm()) return sdscatlen(o,s,len);
|
||||
@@ -628,7 +719,6 @@ sds sdscatcolor(sds o, char *s, size_t len, char *color) {
|
||||
int bold = strstr(color,"bold") != NULL;
|
||||
int ccode = 37; /* Defaults to white. */
|
||||
if (strstr(color,"red")) ccode = 31;
|
||||
else if (strstr(color,"red")) ccode = 31;
|
||||
else if (strstr(color,"green")) ccode = 32;
|
||||
else if (strstr(color,"yellow")) ccode = 33;
|
||||
else if (strstr(color,"blue")) ccode = 34;
|
||||
@@ -999,6 +1089,8 @@ static int parseOptions(int argc, char **argv) {
|
||||
config.dbnum = atoi(argv[++i]);
|
||||
} else if (!strcmp(argv[i],"-a") && !lastarg) {
|
||||
config.auth = argv[++i];
|
||||
} else if (!strcmp(argv[i],"-u") && !lastarg) {
|
||||
parseRedisUri(argv[++i]);
|
||||
} else if (!strcmp(argv[i],"--raw")) {
|
||||
config.output = OUTPUT_RAW;
|
||||
} else if (!strcmp(argv[i],"--no-raw")) {
|
||||
@@ -1038,6 +1130,8 @@ static int parseOptions(int argc, char **argv) {
|
||||
config.pipe_timeout = atoi(argv[++i]);
|
||||
} else if (!strcmp(argv[i],"--bigkeys")) {
|
||||
config.bigkeys = 1;
|
||||
} else if (!strcmp(argv[i],"--hotkeys")) {
|
||||
config.hotkeys = 1;
|
||||
} else if (!strcmp(argv[i],"--eval") && !lastarg) {
|
||||
config.eval = argv[++i];
|
||||
} else if (!strcmp(argv[i],"--ldb")) {
|
||||
@@ -1106,6 +1200,7 @@ 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"
|
||||
" -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"
|
||||
" It is possible to specify sub-second times like -i 0.1.\n"
|
||||
@@ -1119,6 +1214,12 @@ static void usage(void) {
|
||||
" --csv Output in CSV format.\n"
|
||||
" --stat Print rolling stats about server: mem, clients, ...\n"
|
||||
" --latency Enter a special mode continuously sampling latency.\n"
|
||||
" If you use this mode in an interactive session it runs\n"
|
||||
" forever displaying real-time stats. Otherwise if --raw or\n"
|
||||
" --csv is specified, or if you redirect the output to a non\n"
|
||||
" TTY, it samples the latency for 1 second (you can use\n"
|
||||
" -i to change the interval), then produces a single output\n"
|
||||
" and exits.\n"
|
||||
" --latency-history Like --latency but tracking latency changes over time.\n"
|
||||
" Default time interval is 15 sec. Change it using -i.\n"
|
||||
" --latency-dist Shows latency as a spectrum, requires xterm 256 colors.\n"
|
||||
@@ -1131,6 +1232,8 @@ static void usage(void) {
|
||||
" no reply is received within <n> seconds.\n"
|
||||
" Default timeout: %d. Use 0 to wait forever.\n"
|
||||
" --bigkeys Sample Redis keys looking for big keys.\n"
|
||||
" --hotkeys Sample Redis keys looking for hot keys.\n"
|
||||
" only works when maxmemory-policy is *lfu.\n"
|
||||
" --scan List all keys using the SCAN command.\n"
|
||||
" --pattern <pat> Useful with --scan to specify a SCAN pattern.\n"
|
||||
" --intrinsic-latency <sec> Run a test to measure intrinsic system latency.\n"
|
||||
@@ -1222,7 +1325,7 @@ static sds *cliSplitArgs(char *line, int *argc) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Set the CLI perferences. This function is invoked when an interactive
|
||||
/* Set the CLI preferences. This function is invoked when an interactive
|
||||
* ":command" is called, or when reading ~/.redisclirc file, in order to
|
||||
* set user preferences. */
|
||||
void cliSetPreferences(char **argv, int argc, int interactive) {
|
||||
@@ -1257,6 +1360,7 @@ void cliLoadPreferences(void) {
|
||||
if (argc > 0) cliSetPreferences(argv,argc,0);
|
||||
sdsfreesplitres(argv,argc);
|
||||
}
|
||||
fclose(fp);
|
||||
}
|
||||
sdsfree(rcfile);
|
||||
}
|
||||
@@ -1268,6 +1372,11 @@ static void repl(void) {
|
||||
int argc;
|
||||
sds *argv;
|
||||
|
||||
/* Initialize the help and, if possible, use the COMMAND command in order
|
||||
* to retrieve missing entries. */
|
||||
cliInitHelp();
|
||||
cliIntegrateHelp();
|
||||
|
||||
config.interactive = 1;
|
||||
linenoiseSetMultiLine(1);
|
||||
linenoiseSetCompletionCallback(completionCallback);
|
||||
@@ -1277,8 +1386,9 @@ static void repl(void) {
|
||||
/* Only use history and load the rc file when stdin is a tty. */
|
||||
if (isatty(fileno(stdin))) {
|
||||
historyfile = getDotfilePath(REDIS_CLI_HISTFILE_ENV,REDIS_CLI_HISTFILE_DEFAULT);
|
||||
//keep in-memory history always regardless if history file can be determined
|
||||
history = 1;
|
||||
if (historyfile != NULL) {
|
||||
history = 1;
|
||||
linenoiseHistoryLoad(historyfile);
|
||||
}
|
||||
cliLoadPreferences();
|
||||
@@ -1322,9 +1432,10 @@ static void repl(void) {
|
||||
} else {
|
||||
long long start_time = mstime(), elapsed;
|
||||
int repeat, skipargs = 0;
|
||||
char *endptr;
|
||||
|
||||
repeat = atoi(argv[0]);
|
||||
if (argc > 1 && repeat) {
|
||||
repeat = strtol(argv[0], &endptr, 10);
|
||||
if (argc > 1 && *endptr == '\0' && repeat) {
|
||||
skipargs = 1;
|
||||
} else {
|
||||
repeat = 1;
|
||||
@@ -1343,7 +1454,9 @@ static void repl(void) {
|
||||
}
|
||||
|
||||
elapsed = mstime()-start_time;
|
||||
if (elapsed >= 500) {
|
||||
if (elapsed >= 500 &&
|
||||
config.output == OUTPUT_STANDARD)
|
||||
{
|
||||
printf("(%.2fs)\n",(double)elapsed/1000);
|
||||
}
|
||||
}
|
||||
@@ -1460,6 +1573,18 @@ static int evalMode(int argc, char **argv) {
|
||||
* Latency and latency history modes
|
||||
*--------------------------------------------------------------------------- */
|
||||
|
||||
static void latencyModePrint(long long min, long long max, double avg, long long count) {
|
||||
if (config.output == OUTPUT_STANDARD) {
|
||||
printf("min: %lld, max: %lld, avg: %.2f (%lld samples)",
|
||||
min, max, avg, count);
|
||||
fflush(stdout);
|
||||
} else if (config.output == OUTPUT_CSV) {
|
||||
printf("%lld,%lld,%.2f,%lld\n", min, max, avg, count);
|
||||
} else if (config.output == OUTPUT_RAW) {
|
||||
printf("%lld %lld %.2f %lld\n", min, max, avg, count);
|
||||
}
|
||||
}
|
||||
|
||||
#define LATENCY_SAMPLE_RATE 10 /* milliseconds. */
|
||||
#define LATENCY_HISTORY_DEFAULT_INTERVAL 15000 /* milliseconds. */
|
||||
static void latencyMode(void) {
|
||||
@@ -1471,6 +1596,14 @@ static void latencyMode(void) {
|
||||
double avg;
|
||||
long long history_start = mstime();
|
||||
|
||||
/* Set a default for the interval in case of --latency option
|
||||
* with --raw, --csv or when it is redirected to non tty. */
|
||||
if (config.interval == 0) {
|
||||
config.interval = 1000;
|
||||
} else {
|
||||
config.interval /= 1000; /* We need to convert to milliseconds. */
|
||||
}
|
||||
|
||||
if (!context) exit(1);
|
||||
while(1) {
|
||||
start = mstime();
|
||||
@@ -1491,9 +1624,19 @@ static void latencyMode(void) {
|
||||
tot += latency;
|
||||
avg = (double) tot/count;
|
||||
}
|
||||
printf("\x1b[0G\x1b[2Kmin: %lld, max: %lld, avg: %.2f (%lld samples)",
|
||||
min, max, avg, count);
|
||||
fflush(stdout);
|
||||
|
||||
if (config.output == OUTPUT_STANDARD) {
|
||||
printf("\x1b[0G\x1b[2K"); /* Clear the line. */
|
||||
latencyModePrint(min,max,avg,count);
|
||||
} else {
|
||||
if (config.latency_history) {
|
||||
latencyModePrint(min,max,avg,count);
|
||||
} else if (mstime()-history_start > config.interval) {
|
||||
latencyModePrint(min,max,avg,count);
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
if (config.latency_history && mstime()-history_start > history_interval)
|
||||
{
|
||||
printf(" -- %.2f seconds range\n", (float)(mstime()-history_start)/1000);
|
||||
@@ -1758,6 +1901,7 @@ static void getRDB(void) {
|
||||
}
|
||||
close(s); /* Close the file descriptor ASAP as fsync() may take time. */
|
||||
fsync(fd);
|
||||
close(fd);
|
||||
fprintf(stderr,"Transfer finished with success.\n");
|
||||
exit(0);
|
||||
}
|
||||
@@ -1931,7 +2075,8 @@ static void pipeMode(void) {
|
||||
#define TYPE_SET 2
|
||||
#define TYPE_HASH 3
|
||||
#define TYPE_ZSET 4
|
||||
#define TYPE_NONE 5
|
||||
#define TYPE_STREAM 5
|
||||
#define TYPE_NONE 6
|
||||
|
||||
static redisReply *sendScan(unsigned long long *it) {
|
||||
redisReply *reply = redisCommand(context, "SCAN %llu", *it);
|
||||
@@ -1990,6 +2135,8 @@ static int toIntType(char *key, char *type) {
|
||||
return TYPE_HASH;
|
||||
} else if(!strcmp(type, "zset")) {
|
||||
return TYPE_ZSET;
|
||||
} else if(!strcmp(type, "stream")) {
|
||||
return TYPE_STREAM;
|
||||
} else if(!strcmp(type, "none")) {
|
||||
return TYPE_NONE;
|
||||
} else {
|
||||
@@ -2014,8 +2161,13 @@ static void getKeyTypes(redisReply *keys, int *types) {
|
||||
keys->element[i]->str, context->err, context->errstr);
|
||||
exit(1);
|
||||
} else if(reply->type != REDIS_REPLY_STATUS) {
|
||||
fprintf(stderr, "Invalid reply type (%d) for TYPE on key '%s'!\n",
|
||||
reply->type, keys->element[i]->str);
|
||||
if(reply->type == REDIS_REPLY_ERROR) {
|
||||
fprintf(stderr, "TYPE returned an error: %s\n", reply->str);
|
||||
} else {
|
||||
fprintf(stderr,
|
||||
"Invalid reply type (%d) for TYPE on key '%s'!\n",
|
||||
reply->type, keys->element[i]->str);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
|
||||
@@ -2073,7 +2225,7 @@ static void findBigKeys(void) {
|
||||
unsigned long long biggest[5] = {0}, counts[5] = {0}, totalsize[5] = {0};
|
||||
unsigned long long sampled = 0, total_keys, totlen=0, *sizes=NULL, it=0;
|
||||
sds maxkeys[5] = {0};
|
||||
char *typename[] = {"string","list","set","hash","zset"};
|
||||
char *typename[] = {"string","list","set","hash","zset","stream"};
|
||||
char *typeunit[] = {"bytes","items","members","fields","members"};
|
||||
redisReply *reply, *keys;
|
||||
unsigned int arrsize=0, i;
|
||||
@@ -2200,6 +2352,129 @@ static void findBigKeys(void) {
|
||||
exit(0);
|
||||
}
|
||||
|
||||
static void getKeyFreqs(redisReply *keys, unsigned long long *freqs) {
|
||||
redisReply *reply;
|
||||
unsigned int i;
|
||||
|
||||
/* Pipeline OBJECT freq commands */
|
||||
for(i=0;i<keys->elements;i++) {
|
||||
redisAppendCommand(context, "OBJECT freq %s", keys->element[i]->str);
|
||||
}
|
||||
|
||||
/* Retrieve freqs */
|
||||
for(i=0;i<keys->elements;i++) {
|
||||
if(redisGetReply(context, (void**)&reply)!=REDIS_OK) {
|
||||
fprintf(stderr, "Error getting freq for key '%s' (%d: %s)\n",
|
||||
keys->element[i]->str, context->err, context->errstr);
|
||||
exit(1);
|
||||
} else if(reply->type != REDIS_REPLY_INTEGER) {
|
||||
if(reply->type == REDIS_REPLY_ERROR) {
|
||||
fprintf(stderr, "Error: %s\n", reply->str);
|
||||
exit(1);
|
||||
} else {
|
||||
fprintf(stderr, "Warning: OBJECT freq on '%s' failed (may have been deleted)\n", keys->element[i]->str);
|
||||
freqs[i] = 0;
|
||||
}
|
||||
} else {
|
||||
freqs[i] = reply->integer;
|
||||
}
|
||||
freeReplyObject(reply);
|
||||
}
|
||||
}
|
||||
|
||||
#define HOTKEYS_SAMPLE 16
|
||||
static void findHotKeys(void) {
|
||||
redisReply *keys, *reply;
|
||||
unsigned long long counters[HOTKEYS_SAMPLE] = {0};
|
||||
sds hotkeys[HOTKEYS_SAMPLE] = {NULL};
|
||||
unsigned long long sampled = 0, total_keys, *freqs = NULL, it = 0;
|
||||
unsigned int arrsize = 0, i, k;
|
||||
double pct;
|
||||
|
||||
/* Total keys pre scanning */
|
||||
total_keys = getDbSize();
|
||||
|
||||
/* Status message */
|
||||
printf("\n# Scanning the entire keyspace to find hot keys as well as\n");
|
||||
printf("# average sizes per key type. You can use -i 0.1 to sleep 0.1 sec\n");
|
||||
printf("# per 100 SCAN commands (not usually needed).\n\n");
|
||||
|
||||
/* SCAN loop */
|
||||
do {
|
||||
/* Calculate approximate percentage completion */
|
||||
pct = 100 * (double)sampled/total_keys;
|
||||
|
||||
/* Grab some keys and point to the keys array */
|
||||
reply = sendScan(&it);
|
||||
keys = reply->element[1];
|
||||
|
||||
/* Reallocate our freqs array if we need to */
|
||||
if(keys->elements > arrsize) {
|
||||
freqs = zrealloc(freqs, sizeof(unsigned long long)*keys->elements);
|
||||
|
||||
if(!freqs) {
|
||||
fprintf(stderr, "Failed to allocate storage for keys!\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
arrsize = keys->elements;
|
||||
}
|
||||
|
||||
getKeyFreqs(keys, freqs);
|
||||
|
||||
/* Now update our stats */
|
||||
for(i=0;i<keys->elements;i++) {
|
||||
sampled++;
|
||||
/* Update overall progress */
|
||||
if(sampled % 1000000 == 0) {
|
||||
printf("[%05.2f%%] Sampled %llu keys so far\n", pct, sampled);
|
||||
}
|
||||
|
||||
/* Use eviction pool here */
|
||||
k = 0;
|
||||
while (k < HOTKEYS_SAMPLE && freqs[i] > counters[k]) k++;
|
||||
if (k == 0) continue;
|
||||
k--;
|
||||
if (k == 0 || counters[k] == 0) {
|
||||
sdsfree(hotkeys[k]);
|
||||
} else {
|
||||
sdsfree(hotkeys[0]);
|
||||
memmove(counters,counters+1,sizeof(counters[0])*k);
|
||||
memmove(hotkeys,hotkeys+1,sizeof(hotkeys[0])*k);
|
||||
}
|
||||
counters[k] = freqs[i];
|
||||
hotkeys[k] = sdsnew(keys->element[i]->str);
|
||||
printf(
|
||||
"[%05.2f%%] Hot key '%s' found so far with counter %llu\n",
|
||||
pct, keys->element[i]->str, freqs[i]);
|
||||
}
|
||||
|
||||
/* Sleep if we've been directed to do so */
|
||||
if(sampled && (sampled %100) == 0 && config.interval) {
|
||||
usleep(config.interval);
|
||||
}
|
||||
|
||||
freeReplyObject(reply);
|
||||
} while(it != 0);
|
||||
|
||||
if (freqs) zfree(freqs);
|
||||
|
||||
/* We're done */
|
||||
printf("\n-------- summary -------\n\n");
|
||||
|
||||
printf("Sampled %llu keys in the keyspace!\n", sampled);
|
||||
|
||||
for (i=1; i<= HOTKEYS_SAMPLE; i++) {
|
||||
k = HOTKEYS_SAMPLE - i;
|
||||
if(counters[k]>0) {
|
||||
printf("hot key found with counter: %llu\tkeyname: %s\n", counters[k], hotkeys[k]);
|
||||
sdsfree(hotkeys[k]);
|
||||
}
|
||||
}
|
||||
|
||||
exit(0);
|
||||
}
|
||||
|
||||
/*------------------------------------------------------------------------------
|
||||
* Stats mode
|
||||
*--------------------------------------------------------------------------- */
|
||||
@@ -2310,7 +2585,7 @@ static void statMode(void) {
|
||||
sprintf(buf,"%ld",aux);
|
||||
printf("%-8s",buf);
|
||||
|
||||
/* Requets */
|
||||
/* Requests */
|
||||
aux = getLongInfoField(reply->str,"total_commands_processed");
|
||||
sprintf(buf,"%ld (+%ld)",aux,requests == 0 ? 0 : aux-requests);
|
||||
printf("%-19s",buf);
|
||||
@@ -2577,6 +2852,7 @@ int main(int argc, char **argv) {
|
||||
config.pipe_mode = 0;
|
||||
config.pipe_timeout = REDIS_CLI_DEFAULT_PIPE_TIMEOUT;
|
||||
config.bigkeys = 0;
|
||||
config.hotkeys = 0;
|
||||
config.stdinarg = 0;
|
||||
config.auth = NULL;
|
||||
config.eval = NULL;
|
||||
@@ -2601,11 +2877,6 @@ int main(int argc, char **argv) {
|
||||
argc -= firstarg;
|
||||
argv += firstarg;
|
||||
|
||||
/* Initialize the help and, if possible, use the COMMAND command in order
|
||||
* to retrieve missing entries. */
|
||||
cliInitHelp();
|
||||
cliIntegrateHelp();
|
||||
|
||||
/* Latency mode */
|
||||
if (config.latency_mode) {
|
||||
if (cliConnect(0) == REDIS_ERR) exit(1);
|
||||
@@ -2642,6 +2913,12 @@ int main(int argc, char **argv) {
|
||||
findBigKeys();
|
||||
}
|
||||
|
||||
/* Find hot keys */
|
||||
if (config.hotkeys) {
|
||||
if (cliConnect(0) == REDIS_ERR) exit(1);
|
||||
findHotKeys();
|
||||
}
|
||||
|
||||
/* Stat mode */
|
||||
if (config.stat_mode) {
|
||||
if (cliConnect(0) == REDIS_ERR) exit(1);
|
||||
|
||||
+132
-3
@@ -701,7 +701,12 @@ class RedisTrib
|
||||
|
||||
masters.each{|m| puts m}
|
||||
|
||||
# Alloc slots on masters
|
||||
# Rotating the list sometimes helps to get better initial
|
||||
# anti-affinity before the optimizer runs.
|
||||
interleaved.push interleaved.shift
|
||||
|
||||
# Alloc slots on masters. After interleaving to get just the first N
|
||||
# should be optimal. With slaves is more complex, see later...
|
||||
slots_per_node = ClusterHashSlots.to_f / masters_count
|
||||
first = 0
|
||||
cursor = 0.0
|
||||
@@ -769,6 +774,131 @@ class RedisTrib
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
optimize_anti_affinity
|
||||
end
|
||||
|
||||
def optimize_anti_affinity
|
||||
score,aux = get_anti_affinity_score
|
||||
return if score == 0
|
||||
|
||||
xputs ">>> Trying to optimize slaves allocation for anti-affinity"
|
||||
|
||||
maxiter = 500*@nodes.length # Effort is proportional to cluster size...
|
||||
while maxiter > 0
|
||||
score,offenders = get_anti_affinity_score
|
||||
break if score == 0 # Optimal anti affinity reached
|
||||
|
||||
# We'll try to randomly swap a slave's assigned master causing
|
||||
# an affinity problem with another random slave, to see if we
|
||||
# can improve the affinity.
|
||||
first = offenders.shuffle.first
|
||||
nodes = @nodes.select{|n| n != first && n.info[:replicate]}
|
||||
break if nodes.length == 0
|
||||
second = nodes.shuffle.first
|
||||
|
||||
first_master = first.info[:replicate]
|
||||
second_master = second.info[:replicate]
|
||||
first.set_as_replica(second_master)
|
||||
second.set_as_replica(first_master)
|
||||
|
||||
new_score,aux = get_anti_affinity_score
|
||||
# If the change actually makes thing worse, revert. Otherwise
|
||||
# leave as it is becuase the best solution may need a few
|
||||
# combined swaps.
|
||||
if new_score > score
|
||||
first.set_as_replica(first_master)
|
||||
second.set_as_replica(second_master)
|
||||
end
|
||||
|
||||
maxiter -= 1
|
||||
end
|
||||
|
||||
score,aux = get_anti_affinity_score
|
||||
if score == 0
|
||||
xputs "[OK] Perfect anti-affinity obtained!"
|
||||
elsif score >= 10000
|
||||
puts "[WARNING] Some slaves are in the same host as their master"
|
||||
else
|
||||
puts "[WARNING] Some slaves of the same master are in the same host"
|
||||
end
|
||||
end
|
||||
|
||||
# Return the anti-affinity score, which is a measure of the amount of
|
||||
# violations of anti-affinity in the current cluster layout, that is, how
|
||||
# badly the masters and slaves are distributed in the different IP
|
||||
# addresses so that slaves of the same master are not in the master
|
||||
# host and are also in different hosts.
|
||||
#
|
||||
# The score is calculated as follows:
|
||||
#
|
||||
# SAME_AS_MASTER = 10000 * each slave in the same IP of its master.
|
||||
# SAME_AS_SLAVE = 1 * each slave having the same IP as another slave
|
||||
# of the same master.
|
||||
# FINAL_SCORE = SAME_AS_MASTER + SAME_AS_SLAVE
|
||||
#
|
||||
# So a greater score means a worse anti-affinity level, while zero
|
||||
# means perfect anti-affinity.
|
||||
#
|
||||
# The anti affinity optimizator will try to get a score as low as
|
||||
# possible. Since we do not want to sacrifice the fact that slaves should
|
||||
# not be in the same host as the master, we assign 10000 times the score
|
||||
# to this violation, so that we'll optimize for the second factor only
|
||||
# if it does not impact the first one.
|
||||
#
|
||||
# The function returns two things: the above score, and the list of
|
||||
# offending slaves, so that the optimizer can try changing the
|
||||
# configuration of the slaves violating the anti-affinity goals.
|
||||
def get_anti_affinity_score
|
||||
score = 0
|
||||
offending = [] # List of offending slaves to return to the caller
|
||||
|
||||
# First, split nodes by host
|
||||
host_to_node = {}
|
||||
@nodes.each{|n|
|
||||
host = n.info[:host]
|
||||
host_to_node[host] = [] if host_to_node[host] == nil
|
||||
host_to_node[host] << n
|
||||
}
|
||||
|
||||
# Then, for each set of nodes in the same host, split by
|
||||
# related nodes (masters and slaves which are involved in
|
||||
# replication of each other)
|
||||
host_to_node.each{|host,nodes|
|
||||
related = {}
|
||||
nodes.each{|n|
|
||||
if !n.info[:replicate]
|
||||
name = n.info[:name]
|
||||
related[name] = [] if related[name] == nil
|
||||
related[name] << :m
|
||||
else
|
||||
name = n.info[:replicate]
|
||||
related[name] = [] if related[name] == nil
|
||||
related[name] << :s
|
||||
end
|
||||
}
|
||||
|
||||
# Now it's trivial to check, for each related group having the
|
||||
# same host, what is their local score.
|
||||
related.each{|id,types|
|
||||
next if types.length < 2
|
||||
types.sort! # Make sure :m if the first if any
|
||||
if types[0] == :m
|
||||
score += 10000 * (types.length-1)
|
||||
else
|
||||
score += 1 * types.length
|
||||
end
|
||||
|
||||
# Populate the list of offending nodes
|
||||
@nodes.each{|n|
|
||||
if n.info[:replicate] == id &&
|
||||
n.info[:host] == host
|
||||
offending << n
|
||||
end
|
||||
}
|
||||
}
|
||||
}
|
||||
return score,offending
|
||||
end
|
||||
|
||||
def flush_nodes_config
|
||||
@@ -1445,7 +1575,7 @@ class RedisTrib
|
||||
xputs ">>> Importing data from #{source_addr} to cluster #{argv[1]}"
|
||||
use_copy = opt['copy']
|
||||
use_replace = opt['replace']
|
||||
|
||||
|
||||
# Check the existing cluster.
|
||||
load_cluster_info_from_node(argv[0])
|
||||
check_cluster
|
||||
@@ -1669,7 +1799,6 @@ ALLOWED_OPTIONS={
|
||||
def show_help
|
||||
puts "Usage: redis-trib <command> <options> <arguments ...>\n\n"
|
||||
COMMANDS.each{|k,v|
|
||||
o = ""
|
||||
puts " #{k.ljust(15)} #{v[2]}"
|
||||
if ALLOWED_OPTIONS[k]
|
||||
ALLOWED_OPTIONS[k].each{|optname,has_arg|
|
||||
|
||||
@@ -41,7 +41,9 @@
|
||||
#include <unistd.h> /* for _exit() */
|
||||
|
||||
#define assert(_e) ((_e)?(void)0 : (_serverAssert(#_e,__FILE__,__LINE__),_exit(1)))
|
||||
#define panic(...) _serverPanic(__FILE__,__LINE__,__VA_ARGS__),_exit(1)
|
||||
|
||||
void _serverAssert(char *estr, char *file, int line);
|
||||
void _serverPanic(const char *file, int line, const char *msg, ...);
|
||||
|
||||
#endif
|
||||
|
||||
+81
-2
@@ -58,6 +58,30 @@
|
||||
#define REDISMODULE_HASH_CFIELDS (1<<2)
|
||||
#define REDISMODULE_HASH_EXISTS (1<<3)
|
||||
|
||||
/* Context Flags: Info about the current context returned by RM_GetContextFlags */
|
||||
|
||||
/* The command is running in the context of a Lua script */
|
||||
#define REDISMODULE_CTX_FLAGS_LUA 0x0001
|
||||
/* The command is running inside a Redis transaction */
|
||||
#define REDISMODULE_CTX_FLAGS_MULTI 0x0002
|
||||
/* The instance is a master */
|
||||
#define REDISMODULE_CTX_FLAGS_MASTER 0x0004
|
||||
/* The instance is a slave */
|
||||
#define REDISMODULE_CTX_FLAGS_SLAVE 0x0008
|
||||
/* The instance is read-only (usually meaning it's a slave as well) */
|
||||
#define REDISMODULE_CTX_FLAGS_READONLY 0x0010
|
||||
/* The instance is running in cluster mode */
|
||||
#define REDISMODULE_CTX_FLAGS_CLUSTER 0x0020
|
||||
/* The instance has AOF enabled */
|
||||
#define REDISMODULE_CTX_FLAGS_AOF 0x0040 //
|
||||
/* The instance has RDB enabled */
|
||||
#define REDISMODULE_CTX_FLAGS_RDB 0x0080 //
|
||||
/* The instance has Maxmemory set */
|
||||
#define REDISMODULE_CTX_FLAGS_MAXMEMORY 0x0100
|
||||
/* Maxmemory is set and has an eviction policy that may delete keys */
|
||||
#define REDISMODULE_CTX_FLAGS_EVICT 0x0200
|
||||
|
||||
|
||||
/* A special pointer that we can use between the core and the module to signal
|
||||
* field deletion, and that is impossible to be a valid pointer. */
|
||||
#define REDISMODULE_HASH_DELETE ((RedisModuleString*)(long)1)
|
||||
@@ -68,6 +92,8 @@
|
||||
#define REDISMODULE_POSITIVE_INFINITE (1.0/0.0)
|
||||
#define REDISMODULE_NEGATIVE_INFINITE (-1.0/0.0)
|
||||
|
||||
#define REDISMODULE_NOT_USED(V) ((void) V)
|
||||
|
||||
/* ------------------------- End of common defines ------------------------ */
|
||||
|
||||
#ifndef REDISMODULE_CORE
|
||||
@@ -82,15 +108,28 @@ typedef struct RedisModuleCallReply RedisModuleCallReply;
|
||||
typedef struct RedisModuleIO RedisModuleIO;
|
||||
typedef struct RedisModuleType RedisModuleType;
|
||||
typedef struct RedisModuleDigest RedisModuleDigest;
|
||||
typedef struct RedisModuleBlockedClient RedisModuleBlockedClient;
|
||||
|
||||
typedef int (*RedisModuleCmdFunc) (RedisModuleCtx *ctx, RedisModuleString **argv, int argc);
|
||||
|
||||
typedef void *(*RedisModuleTypeLoadFunc)(RedisModuleIO *rdb, int encver);
|
||||
typedef void (*RedisModuleTypeSaveFunc)(RedisModuleIO *rdb, void *value);
|
||||
typedef void (*RedisModuleTypeRewriteFunc)(RedisModuleIO *aof, RedisModuleString *key, void *value);
|
||||
typedef size_t (*RedisModuleTypeMemUsageFunc)(const void *value);
|
||||
typedef void (*RedisModuleTypeDigestFunc)(RedisModuleDigest *digest, void *value);
|
||||
typedef void (*RedisModuleTypeFreeFunc)(void *value);
|
||||
|
||||
#define REDISMODULE_TYPE_METHOD_VERSION 1
|
||||
typedef struct RedisModuleTypeMethods {
|
||||
uint64_t version;
|
||||
RedisModuleTypeLoadFunc rdb_load;
|
||||
RedisModuleTypeSaveFunc rdb_save;
|
||||
RedisModuleTypeRewriteFunc aof_rewrite;
|
||||
RedisModuleTypeMemUsageFunc mem_usage;
|
||||
RedisModuleTypeDigestFunc digest;
|
||||
RedisModuleTypeFreeFunc free;
|
||||
} RedisModuleTypeMethods;
|
||||
|
||||
#define REDISMODULE_GET_API(name) \
|
||||
RedisModule_GetApi("RedisModule_" #name, ((void **)&RedisModule_ ## name))
|
||||
|
||||
@@ -104,7 +143,8 @@ void *REDISMODULE_API_FUNC(RedisModule_Calloc)(size_t nmemb, size_t size);
|
||||
char *REDISMODULE_API_FUNC(RedisModule_Strdup)(const char *str);
|
||||
int REDISMODULE_API_FUNC(RedisModule_GetApi)(const char *, void *);
|
||||
int REDISMODULE_API_FUNC(RedisModule_CreateCommand)(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc cmdfunc, const char *strflags, int firstkey, int lastkey, int keystep);
|
||||
int REDISMODULE_API_FUNC(RedisModule_SetModuleAttribs)(RedisModuleCtx *ctx, const char *name, int ver, int apiver);
|
||||
void REDISMODULE_API_FUNC(RedisModule_SetModuleAttribs)(RedisModuleCtx *ctx, const char *name, int ver, int apiver);
|
||||
int REDISMODULE_API_FUNC(RedisModule_IsModuleNameBusy)(const char *name);
|
||||
int REDISMODULE_API_FUNC(RedisModule_WrongArity)(RedisModuleCtx *ctx);
|
||||
int REDISMODULE_API_FUNC(RedisModule_ReplyWithLongLong)(RedisModuleCtx *ctx, long long ll);
|
||||
int REDISMODULE_API_FUNC(RedisModule_GetSelectedDb)(RedisModuleCtx *ctx);
|
||||
@@ -168,8 +208,9 @@ int REDISMODULE_API_FUNC(RedisModule_HashGet)(RedisModuleKey *key, int flags, ..
|
||||
int REDISMODULE_API_FUNC(RedisModule_IsKeysPositionRequest)(RedisModuleCtx *ctx);
|
||||
void REDISMODULE_API_FUNC(RedisModule_KeyAtPos)(RedisModuleCtx *ctx, int pos);
|
||||
unsigned long long REDISMODULE_API_FUNC(RedisModule_GetClientId)(RedisModuleCtx *ctx);
|
||||
int REDISMODULE_API_FUNC(RedisModule_GetContextFlags)(RedisModuleCtx *ctx);
|
||||
void *REDISMODULE_API_FUNC(RedisModule_PoolAlloc)(RedisModuleCtx *ctx, size_t bytes);
|
||||
RedisModuleType *REDISMODULE_API_FUNC(RedisModule_CreateDataType)(RedisModuleCtx *ctx, const char *name, int encver, RedisModuleTypeLoadFunc rdb_load, RedisModuleTypeSaveFunc rdb_save, RedisModuleTypeRewriteFunc aof_rewrite, RedisModuleTypeDigestFunc digest, RedisModuleTypeFreeFunc free);
|
||||
RedisModuleType *REDISMODULE_API_FUNC(RedisModule_CreateDataType)(RedisModuleCtx *ctx, const char *name, int encver, RedisModuleTypeMethods *typemethods);
|
||||
int REDISMODULE_API_FUNC(RedisModule_ModuleTypeSetValue)(RedisModuleKey *key, RedisModuleType *mt, void *value);
|
||||
RedisModuleType *REDISMODULE_API_FUNC(RedisModule_ModuleTypeGetType)(RedisModuleKey *key);
|
||||
void *REDISMODULE_API_FUNC(RedisModule_ModuleTypeGetValue)(RedisModuleKey *key);
|
||||
@@ -192,6 +233,24 @@ int REDISMODULE_API_FUNC(RedisModule_StringAppendBuffer)(RedisModuleCtx *ctx, Re
|
||||
void REDISMODULE_API_FUNC(RedisModule_RetainString)(RedisModuleCtx *ctx, RedisModuleString *str);
|
||||
int REDISMODULE_API_FUNC(RedisModule_StringCompare)(RedisModuleString *a, RedisModuleString *b);
|
||||
RedisModuleCtx *REDISMODULE_API_FUNC(RedisModule_GetContextFromIO)(RedisModuleIO *io);
|
||||
long long REDISMODULE_API_FUNC(RedisModule_Milliseconds)(void);
|
||||
void REDISMODULE_API_FUNC(RedisModule_DigestAddStringBuffer)(RedisModuleDigest *md, unsigned char *ele, size_t len);
|
||||
void REDISMODULE_API_FUNC(RedisModule_DigestAddLongLong)(RedisModuleDigest *md, long long ele);
|
||||
void REDISMODULE_API_FUNC(RedisModule_DigestEndSequence)(RedisModuleDigest *md);
|
||||
|
||||
/* Experimental APIs */
|
||||
#ifdef REDISMODULE_EXPERIMENTAL_API
|
||||
RedisModuleBlockedClient *REDISMODULE_API_FUNC(RedisModule_BlockClient)(RedisModuleCtx *ctx, RedisModuleCmdFunc reply_callback, RedisModuleCmdFunc timeout_callback, void (*free_privdata)(void*), long long timeout_ms);
|
||||
int REDISMODULE_API_FUNC(RedisModule_UnblockClient)(RedisModuleBlockedClient *bc, void *privdata);
|
||||
int REDISMODULE_API_FUNC(RedisModule_IsBlockedReplyRequest)(RedisModuleCtx *ctx);
|
||||
int REDISMODULE_API_FUNC(RedisModule_IsBlockedTimeoutRequest)(RedisModuleCtx *ctx);
|
||||
void *REDISMODULE_API_FUNC(RedisModule_GetBlockedClientPrivateData)(RedisModuleCtx *ctx);
|
||||
int REDISMODULE_API_FUNC(RedisModule_AbortBlock)(RedisModuleBlockedClient *bc);
|
||||
RedisModuleCtx *REDISMODULE_API_FUNC(RedisModule_GetThreadSafeContext)(RedisModuleBlockedClient *bc);
|
||||
void REDISMODULE_API_FUNC(RedisModule_FreeThreadSafeContext)(RedisModuleCtx *ctx);
|
||||
void REDISMODULE_API_FUNC(RedisModule_ThreadSafeContextLock)(RedisModuleCtx *ctx);
|
||||
void REDISMODULE_API_FUNC(RedisModule_ThreadSafeContextUnlock)(RedisModuleCtx *ctx);
|
||||
#endif
|
||||
|
||||
/* This is included inline inside each Redis module. */
|
||||
static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int apiver) __attribute__((unused));
|
||||
@@ -205,6 +264,7 @@ static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int
|
||||
REDISMODULE_GET_API(Strdup);
|
||||
REDISMODULE_GET_API(CreateCommand);
|
||||
REDISMODULE_GET_API(SetModuleAttribs);
|
||||
REDISMODULE_GET_API(IsModuleNameBusy);
|
||||
REDISMODULE_GET_API(WrongArity);
|
||||
REDISMODULE_GET_API(ReplyWithLongLong);
|
||||
REDISMODULE_GET_API(ReplyWithError);
|
||||
@@ -269,6 +329,7 @@ static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int
|
||||
REDISMODULE_GET_API(IsKeysPositionRequest);
|
||||
REDISMODULE_GET_API(KeyAtPos);
|
||||
REDISMODULE_GET_API(GetClientId);
|
||||
REDISMODULE_GET_API(GetContextFlags);
|
||||
REDISMODULE_GET_API(PoolAlloc);
|
||||
REDISMODULE_GET_API(CreateDataType);
|
||||
REDISMODULE_GET_API(ModuleTypeSetValue);
|
||||
@@ -293,7 +354,25 @@ static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int
|
||||
REDISMODULE_GET_API(RetainString);
|
||||
REDISMODULE_GET_API(StringCompare);
|
||||
REDISMODULE_GET_API(GetContextFromIO);
|
||||
REDISMODULE_GET_API(Milliseconds);
|
||||
REDISMODULE_GET_API(DigestAddStringBuffer);
|
||||
REDISMODULE_GET_API(DigestAddLongLong);
|
||||
REDISMODULE_GET_API(DigestEndSequence);
|
||||
|
||||
#ifdef REDISMODULE_EXPERIMENTAL_API
|
||||
REDISMODULE_GET_API(GetThreadSafeContext);
|
||||
REDISMODULE_GET_API(FreeThreadSafeContext);
|
||||
REDISMODULE_GET_API(ThreadSafeContextLock);
|
||||
REDISMODULE_GET_API(ThreadSafeContextUnlock);
|
||||
REDISMODULE_GET_API(BlockClient);
|
||||
REDISMODULE_GET_API(UnblockClient);
|
||||
REDISMODULE_GET_API(IsBlockedReplyRequest);
|
||||
REDISMODULE_GET_API(IsBlockedTimeoutRequest);
|
||||
REDISMODULE_GET_API(GetBlockedClientPrivateData);
|
||||
REDISMODULE_GET_API(AbortBlock);
|
||||
#endif
|
||||
|
||||
if (RedisModule_IsModuleNameBusy(name)) return REDISMODULE_ERR;
|
||||
RedisModule_SetModuleAttribs(ctx,name,ver,apiver);
|
||||
return REDISMODULE_OK;
|
||||
}
|
||||
|
||||
+411
-126
@@ -79,11 +79,6 @@ void createReplicationBacklog(void) {
|
||||
server.repl_backlog = zmalloc(server.repl_backlog_size);
|
||||
server.repl_backlog_histlen = 0;
|
||||
server.repl_backlog_idx = 0;
|
||||
/* When a new backlog buffer is created, we increment the replication
|
||||
* offset by one to make sure we'll not be able to PSYNC with any
|
||||
* previous slave. This is needed because we avoid incrementing the
|
||||
* master_repl_offset if no backlog exists nor slaves are attached. */
|
||||
server.master_repl_offset++;
|
||||
|
||||
/* We don't have any data inside our buffer, but virtually the first
|
||||
* byte we have is the next byte that will be generated for the
|
||||
@@ -127,7 +122,7 @@ void freeReplicationBacklog(void) {
|
||||
/* Add data to the replication backlog.
|
||||
* This function also increments the global replication offset stored at
|
||||
* server.master_repl_offset, because there is no case where we want to feed
|
||||
* the backlog without incrementing the buffer. */
|
||||
* the backlog without incrementing the offset. */
|
||||
void feedReplicationBacklog(void *ptr, size_t len) {
|
||||
unsigned char *p = ptr;
|
||||
|
||||
@@ -170,12 +165,24 @@ void feedReplicationBacklogWithObject(robj *o) {
|
||||
feedReplicationBacklog(p,len);
|
||||
}
|
||||
|
||||
/* Propagate write commands to slaves, and populate the replication backlog
|
||||
* as well. This function is used if the instance is a master: we use
|
||||
* the commands received by our clients in order to create the replication
|
||||
* stream. Instead if the instance is a slave and has sub-slaves attached,
|
||||
* we use replicationFeedSlavesFromMaster() */
|
||||
void replicationFeedSlaves(list *slaves, int dictid, robj **argv, int argc) {
|
||||
listNode *ln;
|
||||
listIter li;
|
||||
int j, len;
|
||||
char llstr[LONG_STR_SIZE];
|
||||
|
||||
/* If the instance is not a top level master, return ASAP: we'll just proxy
|
||||
* the stream of data we receive from our master instead, in order to
|
||||
* propagate *identical* replication stream. In this way this slave can
|
||||
* advertise the same replication ID as the master (since it shares the
|
||||
* master replication history and has the same backlog and offsets). */
|
||||
if (server.masterhost != NULL) return;
|
||||
|
||||
/* If there aren't slaves, and there is no backlog buffer to populate,
|
||||
* we can return ASAP. */
|
||||
if (server.repl_backlog == NULL && listLength(slaves) == 0) return;
|
||||
@@ -244,7 +251,7 @@ void replicationFeedSlaves(list *slaves, int dictid, robj **argv, int argc) {
|
||||
}
|
||||
|
||||
/* Write the command to every slave. */
|
||||
listRewind(server.slaves,&li);
|
||||
listRewind(slaves,&li);
|
||||
while((ln = listNext(&li))) {
|
||||
client *slave = ln->value;
|
||||
|
||||
@@ -265,6 +272,34 @@ void replicationFeedSlaves(list *slaves, int dictid, robj **argv, int argc) {
|
||||
}
|
||||
}
|
||||
|
||||
/* This function is used in order to proxy what we receive from our master
|
||||
* to our sub-slaves. */
|
||||
#include <ctype.h>
|
||||
void replicationFeedSlavesFromMasterStream(list *slaves, char *buf, size_t buflen) {
|
||||
listNode *ln;
|
||||
listIter li;
|
||||
|
||||
/* Debugging: this is handy to see the stream sent from master
|
||||
* to slaves. Disabled with if(0). */
|
||||
if (0) {
|
||||
printf("%zu:",buflen);
|
||||
for (size_t j = 0; j < buflen; j++) {
|
||||
printf("%c", isprint(buf[j]) ? buf[j] : '.');
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
if (server.repl_backlog) feedReplicationBacklog(buf,buflen);
|
||||
listRewind(slaves,&li);
|
||||
while((ln = listNext(&li))) {
|
||||
client *slave = ln->value;
|
||||
|
||||
/* Don't feed slaves that are still waiting for BGSAVE to start */
|
||||
if (slave->replstate == SLAVE_STATE_WAIT_BGSAVE_START) continue;
|
||||
addReplyString(slave,buf,buflen);
|
||||
}
|
||||
}
|
||||
|
||||
void replicationFeedMonitors(client *c, list *monitors, int dictid, robj **argv, int argc) {
|
||||
listNode *ln;
|
||||
listIter li;
|
||||
@@ -329,7 +364,7 @@ long long addReplyReplicationBacklog(client *c, long long offset) {
|
||||
skip = offset - server.repl_backlog_off;
|
||||
serverLog(LL_DEBUG, "[PSYNC] Skipping: %lld", skip);
|
||||
|
||||
/* Point j to the oldest byte, that is actaully our
|
||||
/* Point j to the oldest byte, that is actually our
|
||||
* server.repl_backlog_off byte. */
|
||||
j = (server.repl_backlog_idx +
|
||||
(server.repl_backlog_size-server.repl_backlog_histlen)) %
|
||||
@@ -361,18 +396,14 @@ long long addReplyReplicationBacklog(client *c, long long offset) {
|
||||
* the BGSAVE process started and before executing any other command
|
||||
* from clients. */
|
||||
long long getPsyncInitialOffset(void) {
|
||||
long long psync_offset = server.master_repl_offset;
|
||||
/* Add 1 to psync_offset if it the replication backlog does not exists
|
||||
* as when it will be created later we'll increment the offset by one. */
|
||||
if (server.repl_backlog == NULL) psync_offset++;
|
||||
return psync_offset;
|
||||
return server.master_repl_offset;
|
||||
}
|
||||
|
||||
/* Send a FULLRESYNC reply in the specific case of a full resynchronization,
|
||||
* as a side effect setup the slave for a full sync in different ways:
|
||||
*
|
||||
* 1) Remember, into the slave client structure, the offset we sent
|
||||
* here, so that if new slaves will later attach to the same
|
||||
* 1) Remember, into the slave client structure, the replication offset
|
||||
* we sent here, so that if new slaves will later attach to the same
|
||||
* background RDB saving process (by duplicating this client output
|
||||
* buffer), we can get the right offset from this slave.
|
||||
* 2) Set the replication state of the slave to WAIT_BGSAVE_END so that
|
||||
@@ -392,14 +423,14 @@ int replicationSetupSlaveForFullResync(client *slave, long long offset) {
|
||||
slave->replstate = SLAVE_STATE_WAIT_BGSAVE_END;
|
||||
/* We are going to accumulate the incremental changes for this
|
||||
* slave as well. Set slaveseldb to -1 in order to force to re-emit
|
||||
* a SLEECT statement in the replication stream. */
|
||||
* a SELECT statement in the replication stream. */
|
||||
server.slaveseldb = -1;
|
||||
|
||||
/* Don't send this reply to slaves that approached us with
|
||||
* the old SYNC command. */
|
||||
if (!(slave->flags & CLIENT_PRE_PSYNC)) {
|
||||
buflen = snprintf(buf,sizeof(buf),"+FULLRESYNC %s %lld\r\n",
|
||||
server.runid,offset);
|
||||
server.replid,offset);
|
||||
if (write(slave->fd,buf,buflen) != buflen) {
|
||||
freeClientAsync(slave);
|
||||
return C_ERR;
|
||||
@@ -415,19 +446,40 @@ int replicationSetupSlaveForFullResync(client *slave, long long offset) {
|
||||
* with the usual full resync. */
|
||||
int masterTryPartialResynchronization(client *c) {
|
||||
long long psync_offset, psync_len;
|
||||
char *master_runid = c->argv[1]->ptr;
|
||||
char *master_replid = c->argv[1]->ptr;
|
||||
char buf[128];
|
||||
int buflen;
|
||||
|
||||
/* Is the runid of this master the same advertised by the wannabe slave
|
||||
* via PSYNC? If runid changed this master is a different instance and
|
||||
* there is no way to continue. */
|
||||
if (strcasecmp(master_runid, server.runid)) {
|
||||
/* Parse the replication offset asked by the slave. Go to full sync
|
||||
* on parse error: this should never happen but we try to handle
|
||||
* it in a robust way compared to aborting. */
|
||||
if (getLongLongFromObjectOrReply(c,c->argv[2],&psync_offset,NULL) !=
|
||||
C_OK) goto need_full_resync;
|
||||
|
||||
/* Is the replication ID of this master the same advertised by the wannabe
|
||||
* slave via PSYNC? If the replication ID changed this master has a
|
||||
* different replication history, and there is no way to continue.
|
||||
*
|
||||
* Note that there are two potentially valid replication IDs: the ID1
|
||||
* and the ID2. The ID2 however is only valid up to a specific offset. */
|
||||
if (strcasecmp(master_replid, server.replid) &&
|
||||
(strcasecmp(master_replid, server.replid2) ||
|
||||
psync_offset > server.second_replid_offset))
|
||||
{
|
||||
/* Run id "?" is used by slaves that want to force a full resync. */
|
||||
if (master_runid[0] != '?') {
|
||||
serverLog(LL_NOTICE,"Partial resynchronization not accepted: "
|
||||
"Runid mismatch (Client asked for runid '%s', my runid is '%s')",
|
||||
master_runid, server.runid);
|
||||
if (master_replid[0] != '?') {
|
||||
if (strcasecmp(master_replid, server.replid) &&
|
||||
strcasecmp(master_replid, server.replid2))
|
||||
{
|
||||
serverLog(LL_NOTICE,"Partial resynchronization not accepted: "
|
||||
"Replication ID mismatch (Slave asked for '%s', my "
|
||||
"replication IDs are '%s' and '%s')",
|
||||
master_replid, server.replid, server.replid2);
|
||||
} else {
|
||||
serverLog(LL_NOTICE,"Partial resynchronization not accepted: "
|
||||
"Requested offset for second ID was %lld, but I can reply "
|
||||
"up to %lld", psync_offset, server.second_replid_offset);
|
||||
}
|
||||
} else {
|
||||
serverLog(LL_NOTICE,"Full resync requested by slave %s",
|
||||
replicationGetSlaveName(c));
|
||||
@@ -436,8 +488,6 @@ int masterTryPartialResynchronization(client *c) {
|
||||
}
|
||||
|
||||
/* We still have the data our slave is asking for? */
|
||||
if (getLongLongFromObjectOrReply(c,c->argv[2],&psync_offset,NULL) !=
|
||||
C_OK) goto need_full_resync;
|
||||
if (!server.repl_backlog ||
|
||||
psync_offset < server.repl_backlog_off ||
|
||||
psync_offset > (server.repl_backlog_off + server.repl_backlog_histlen))
|
||||
@@ -463,7 +513,11 @@ int masterTryPartialResynchronization(client *c) {
|
||||
/* We can't use the connection buffers since they are used to accumulate
|
||||
* new commands at this stage. But we are sure the socket send buffer is
|
||||
* empty so this write will never fail actually. */
|
||||
buflen = snprintf(buf,sizeof(buf),"+CONTINUE\r\n");
|
||||
if (c->slave_capa & SLAVE_CAPA_PSYNC2) {
|
||||
buflen = snprintf(buf,sizeof(buf),"+CONTINUE %s\r\n", server.replid);
|
||||
} else {
|
||||
buflen = snprintf(buf,sizeof(buf),"+CONTINUE\r\n");
|
||||
}
|
||||
if (write(c->fd,buf,buflen) != buflen) {
|
||||
freeClientAsync(c);
|
||||
return C_OK;
|
||||
@@ -515,10 +569,19 @@ int startBgsaveForReplication(int mincapa) {
|
||||
serverLog(LL_NOTICE,"Starting BGSAVE for SYNC with target: %s",
|
||||
socket_target ? "slaves sockets" : "disk");
|
||||
|
||||
if (socket_target)
|
||||
retval = rdbSaveToSlavesSockets();
|
||||
else
|
||||
retval = rdbSaveBackground(server.rdb_filename);
|
||||
rdbSaveInfo rsi, *rsiptr;
|
||||
rsiptr = rdbPopulateSaveInfo(&rsi);
|
||||
/* Only do rdbSave* when rsiptr is not NULL,
|
||||
* otherwise slave will miss repl-stream-db. */
|
||||
if (rsiptr) {
|
||||
if (socket_target)
|
||||
retval = rdbSaveToSlavesSockets(rsiptr);
|
||||
else
|
||||
retval = rdbSaveBackground(server.rdb_filename,rsiptr);
|
||||
} else {
|
||||
serverLog(LL_WARNING,"BGSAVE for replication: replication information not available, can't generate the RDB file right now. Try later.");
|
||||
retval = C_ERR;
|
||||
}
|
||||
|
||||
/* If we failed to BGSAVE, remove the slaves waiting for a full
|
||||
* resynchorinization from the list of salves, inform them with
|
||||
@@ -568,7 +631,7 @@ void syncCommand(client *c) {
|
||||
/* Refuse SYNC requests if we are a slave but the link with our master
|
||||
* is not ok... */
|
||||
if (server.masterhost && server.repl_state != REPL_STATE_CONNECTED) {
|
||||
addReplyError(c,"Can't SYNC while not connected with my master");
|
||||
addReplySds(c,sdsnew("-NOMASTERLINK Can't SYNC while not connected with my master\r\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -589,22 +652,22 @@ void syncCommand(client *c) {
|
||||
* when this happens masterTryPartialResynchronization() already
|
||||
* replied with:
|
||||
*
|
||||
* +FULLRESYNC <runid> <offset>
|
||||
* +FULLRESYNC <replid> <offset>
|
||||
*
|
||||
* So the slave knows the new runid and offset to try a PSYNC later
|
||||
* So the slave knows the new replid and offset to try a PSYNC later
|
||||
* if the connection with the master is lost. */
|
||||
if (!strcasecmp(c->argv[0]->ptr,"psync")) {
|
||||
if (masterTryPartialResynchronization(c) == C_OK) {
|
||||
server.stat_sync_partial_ok++;
|
||||
return; /* No full resync needed, return. */
|
||||
} else {
|
||||
char *master_runid = c->argv[1]->ptr;
|
||||
char *master_replid = c->argv[1]->ptr;
|
||||
|
||||
/* Increment stats for failed PSYNCs, but only if the
|
||||
* runid is not "?", as this is used by slaves to force a full
|
||||
* replid is not "?", as this is used by slaves to force a full
|
||||
* resync on purpose when they are not albe to partially
|
||||
* resync. */
|
||||
if (master_runid[0] != '?') server.stat_sync_partial_err++;
|
||||
if (master_replid[0] != '?') server.stat_sync_partial_err++;
|
||||
}
|
||||
} else {
|
||||
/* If a slave uses SYNC, we are dealing with an old implementation
|
||||
@@ -625,6 +688,16 @@ void syncCommand(client *c) {
|
||||
c->flags |= CLIENT_SLAVE;
|
||||
listAddNodeTail(server.slaves,c);
|
||||
|
||||
/* Create the replication backlog if needed. */
|
||||
if (listLength(server.slaves) == 1 && server.repl_backlog == NULL) {
|
||||
/* When we create the backlog from scratch, we always use a new
|
||||
* replication ID and clear the ID2, since there is no valid
|
||||
* past history. */
|
||||
changeReplicationId();
|
||||
clearReplicationId2();
|
||||
createReplicationBacklog();
|
||||
}
|
||||
|
||||
/* CASE 1: BGSAVE is in progress, with disk target. */
|
||||
if (server.rdb_child_pid != -1 &&
|
||||
server.rdb_child_type == RDB_CHILD_TYPE_DISK)
|
||||
@@ -685,9 +758,6 @@ void syncCommand(client *c) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (listLength(server.slaves) == 1 && server.repl_backlog == NULL)
|
||||
createReplicationBacklog();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -735,6 +805,8 @@ void replconfCommand(client *c) {
|
||||
/* Ignore capabilities not understood by this master. */
|
||||
if (!strcasecmp(c->argv[j+1]->ptr,"eof"))
|
||||
c->slave_capa |= SLAVE_CAPA_EOF;
|
||||
else if (!strcasecmp(c->argv[j+1]->ptr,"psync2"))
|
||||
c->slave_capa |= SLAVE_CAPA_PSYNC2;
|
||||
} else if (!strcasecmp(c->argv[j]->ptr,"ack")) {
|
||||
/* REPLCONF ACK is used by slave to inform the master the amount
|
||||
* of replication stream that it processed so far. It is an
|
||||
@@ -758,7 +830,7 @@ void replconfCommand(client *c) {
|
||||
/* REPLCONF GETACK is used in order to request an ACK ASAP
|
||||
* to the slave. */
|
||||
if (server.masterhost && server.master) replicationSendAck();
|
||||
/* Note: this command does not reply anything! */
|
||||
return;
|
||||
} else {
|
||||
addReplyErrorFormat(c,"Unrecognized REPLCONF option: %s",
|
||||
(char*)c->argv[j]->ptr);
|
||||
@@ -928,6 +1000,43 @@ void updateSlavesWaitingBgsave(int bgsaveerr, int type) {
|
||||
if (startbgsave) startBgsaveForReplication(mincapa);
|
||||
}
|
||||
|
||||
/* Change the current instance replication ID with a new, random one.
|
||||
* This will prevent successful PSYNCs between this master and other
|
||||
* slaves, so the command should be called when something happens that
|
||||
* alters the current story of the dataset. */
|
||||
void changeReplicationId(void) {
|
||||
getRandomHexChars(server.replid,CONFIG_RUN_ID_SIZE);
|
||||
server.replid[CONFIG_RUN_ID_SIZE] = '\0';
|
||||
}
|
||||
|
||||
/* Clear (invalidate) the secondary replication ID. This happens, for
|
||||
* example, after a full resynchronization, when we start a new replication
|
||||
* history. */
|
||||
void clearReplicationId2(void) {
|
||||
memset(server.replid2,'0',sizeof(server.replid));
|
||||
server.replid2[CONFIG_RUN_ID_SIZE] = '\0';
|
||||
server.second_replid_offset = -1;
|
||||
}
|
||||
|
||||
/* Use the current replication ID / offset as secondary replication
|
||||
* ID, and change the current one in order to start a new history.
|
||||
* This should be used when an instance is switched from slave to master
|
||||
* so that it can serve PSYNC requests performed using the master
|
||||
* replication ID. */
|
||||
void shiftReplicationId(void) {
|
||||
memcpy(server.replid2,server.replid,sizeof(server.replid));
|
||||
/* We set the second replid offset to the master offset + 1, since
|
||||
* the slave will ask for the first byte it has not yet received, so
|
||||
* we need to add one to the offset: for example if, as a slave, we are
|
||||
* sure we have the same history as the master for 50 bytes, after we
|
||||
* are turned into a master, we can accept a PSYNC request with offset
|
||||
* 51, since the slave asking has the same history up to the 50th
|
||||
* byte, and is asking for the new bytes starting at offset 51. */
|
||||
server.second_replid_offset = server.master_repl_offset+1;
|
||||
changeReplicationId();
|
||||
serverLog(LL_WARNING,"Setting secondary replication ID to %s, valid up to offset: %lld. New replication ID is %s", server.replid2, server.second_replid_offset, server.replid);
|
||||
}
|
||||
|
||||
/* ----------------------------------- SLAVE -------------------------------- */
|
||||
|
||||
/* Returns 1 if the given replication state is a handshake state,
|
||||
@@ -965,18 +1074,31 @@ void replicationEmptyDbCallback(void *privdata) {
|
||||
/* Once we have a link with the master and the synchroniziation was
|
||||
* performed, this function materializes the master client we store
|
||||
* at server.master, starting from the specified file descriptor. */
|
||||
void replicationCreateMasterClient(int fd) {
|
||||
void replicationCreateMasterClient(int fd, int dbid) {
|
||||
server.master = createClient(fd);
|
||||
server.master->flags |= CLIENT_MASTER;
|
||||
server.master->authenticated = 1;
|
||||
server.repl_state = REPL_STATE_CONNECTED;
|
||||
server.master->reploff = server.repl_master_initial_offset;
|
||||
memcpy(server.master->replrunid, server.repl_master_runid,
|
||||
sizeof(server.repl_master_runid));
|
||||
server.master->reploff = server.master_initial_offset;
|
||||
server.master->read_reploff = server.master->reploff;
|
||||
memcpy(server.master->replid, server.master_replid,
|
||||
sizeof(server.master_replid));
|
||||
/* If master offset is set to -1, this master is old and is not
|
||||
* PSYNC capable, so we flag it accordingly. */
|
||||
if (server.master->reploff == -1)
|
||||
server.master->flags |= CLIENT_PRE_PSYNC;
|
||||
if (dbid != -1) selectDb(server.master,dbid);
|
||||
}
|
||||
|
||||
void restartAOF() {
|
||||
int retry = 10;
|
||||
while (retry-- && startAppendOnly() == C_ERR) {
|
||||
serverLog(LL_WARNING,"Failed enabling the AOF after successful master synchronization! Trying it again in one second.");
|
||||
sleep(1);
|
||||
}
|
||||
if (!retry) {
|
||||
serverLog(LL_WARNING,"FATAL: this slave instance finished the synchronization with its master, but the AOF can't be turned on. Exiting now.");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Asynchronously read the SYNC payload we receive from a master */
|
||||
@@ -1120,12 +1242,17 @@ void readSyncBulkPayload(aeEventLoop *el, int fd, void *privdata, int mask) {
|
||||
}
|
||||
|
||||
if (eof_reached) {
|
||||
int aof_is_enabled = server.aof_state != AOF_OFF;
|
||||
|
||||
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 <-> SLAVE synchronization: %s", strerror(errno));
|
||||
cancelReplicationHandshake();
|
||||
return;
|
||||
}
|
||||
serverLog(LL_NOTICE, "MASTER <-> SLAVE sync: Flushing old data");
|
||||
/* We need to stop any AOFRW fork before flusing and parsing
|
||||
* RDB, otherwise we'll create a copy-on-write disaster. */
|
||||
if(aof_is_enabled) stopAppendOnly();
|
||||
signalFlushedDb(-1);
|
||||
emptyDb(
|
||||
-1,
|
||||
@@ -1137,34 +1264,38 @@ void readSyncBulkPayload(aeEventLoop *el, int fd, void *privdata, int mask) {
|
||||
* time for non blocking loading. */
|
||||
aeDeleteFileEvent(server.el,server.repl_transfer_s,AE_READABLE);
|
||||
serverLog(LL_NOTICE, "MASTER <-> SLAVE sync: Loading DB in memory");
|
||||
if (rdbLoad(server.rdb_filename) != C_OK) {
|
||||
rdbSaveInfo rsi = RDB_SAVE_INFO_INIT;
|
||||
if (rdbLoad(server.rdb_filename,&rsi) != C_OK) {
|
||||
serverLog(LL_WARNING,"Failed trying to load the MASTER synchronization DB from disk");
|
||||
cancelReplicationHandshake();
|
||||
/* Re-enable the AOF if we disabled it earlier, in order to restore
|
||||
* the original configuration. */
|
||||
if (aof_is_enabled) restartAOF();
|
||||
return;
|
||||
}
|
||||
/* Final setup of the connected slave <- master link */
|
||||
zfree(server.repl_transfer_tmpfile);
|
||||
close(server.repl_transfer_fd);
|
||||
replicationCreateMasterClient(server.repl_transfer_s);
|
||||
replicationCreateMasterClient(server.repl_transfer_s,rsi.repl_stream_db);
|
||||
server.repl_state = REPL_STATE_CONNECTED;
|
||||
/* After a full resynchroniziation we use the replication ID and
|
||||
* offset of the master. The secondary ID / offset are cleared since
|
||||
* we are starting a new history. */
|
||||
memcpy(server.replid,server.master->replid,sizeof(server.replid));
|
||||
server.master_repl_offset = server.master->reploff;
|
||||
clearReplicationId2();
|
||||
/* Let's create the replication backlog if needed. Slaves need to
|
||||
* accumulate the backlog regardless of the fact they have sub-slaves
|
||||
* or not, in order to behave correctly if they are promoted to
|
||||
* masters after a failover. */
|
||||
if (server.repl_backlog == NULL) createReplicationBacklog();
|
||||
|
||||
serverLog(LL_NOTICE, "MASTER <-> SLAVE sync: Finished with success");
|
||||
/* Restart the AOF subsystem now that we finished the sync. This
|
||||
* will trigger an AOF rewrite, and when done will start appending
|
||||
* to the new file. */
|
||||
if (server.aof_state != AOF_OFF) {
|
||||
int retry = 10;
|
||||
|
||||
stopAppendOnly();
|
||||
while (retry-- && startAppendOnly() == C_ERR) {
|
||||
serverLog(LL_WARNING,"Failed enabling the AOF after successful master synchronization! Trying it again in one second.");
|
||||
sleep(1);
|
||||
}
|
||||
if (!retry) {
|
||||
serverLog(LL_WARNING,"FATAL: this slave instance finished the synchronization with its master, but the AOF can't be turned on. Exiting now.");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
if (aof_is_enabled) restartAOF();
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
error:
|
||||
@@ -1263,14 +1394,15 @@ char *sendSynchronousCommand(int flags, int fd, ...) {
|
||||
* offset is saved.
|
||||
* PSYNC_NOT_SUPPORTED: If the server does not understand PSYNC at all and
|
||||
* the caller should fall back to SYNC.
|
||||
* PSYNC_WRITE_ERR: There was an error writing the command to the socket.
|
||||
* PSYNC_WRITE_ERROR: There was an error writing the command to the socket.
|
||||
* PSYNC_WAIT_REPLY: Call again the function with read_reply set to 1.
|
||||
* PSYNC_TRY_LATER: Master is currently in a transient error condition.
|
||||
*
|
||||
* Notable side effects:
|
||||
*
|
||||
* 1) As a side effect of the function call the function removes the readable
|
||||
* event handler from "fd", unless the return value is PSYNC_WAIT_REPLY.
|
||||
* 2) server.repl_master_initial_offset is set to the right value according
|
||||
* 2) server.master_initial_offset is set to the right value according
|
||||
* to the master reply. This will be used to populate the 'server.master'
|
||||
* structure replication offset.
|
||||
*/
|
||||
@@ -1280,32 +1412,33 @@ char *sendSynchronousCommand(int flags, int fd, ...) {
|
||||
#define PSYNC_CONTINUE 2
|
||||
#define PSYNC_FULLRESYNC 3
|
||||
#define PSYNC_NOT_SUPPORTED 4
|
||||
#define PSYNC_TRY_LATER 5
|
||||
int slaveTryPartialResynchronization(int fd, int read_reply) {
|
||||
char *psync_runid;
|
||||
char *psync_replid;
|
||||
char psync_offset[32];
|
||||
sds reply;
|
||||
|
||||
/* Writing half */
|
||||
if (!read_reply) {
|
||||
/* Initially set repl_master_initial_offset to -1 to mark the current
|
||||
/* Initially set master_initial_offset to -1 to mark the current
|
||||
* master run_id and offset as not valid. Later if we'll be able to do
|
||||
* a FULL resync using the PSYNC command we'll set the offset at the
|
||||
* right value, so that this information will be propagated to the
|
||||
* client structure representing the master into server.master. */
|
||||
server.repl_master_initial_offset = -1;
|
||||
server.master_initial_offset = -1;
|
||||
|
||||
if (server.cached_master) {
|
||||
psync_runid = server.cached_master->replrunid;
|
||||
psync_replid = server.cached_master->replid;
|
||||
snprintf(psync_offset,sizeof(psync_offset),"%lld", server.cached_master->reploff+1);
|
||||
serverLog(LL_NOTICE,"Trying a partial resynchronization (request %s:%s).", psync_runid, psync_offset);
|
||||
serverLog(LL_NOTICE,"Trying a partial resynchronization (request %s:%s).", psync_replid, psync_offset);
|
||||
} else {
|
||||
serverLog(LL_NOTICE,"Partial resynchronization not possible (no cached master)");
|
||||
psync_runid = "?";
|
||||
psync_replid = "?";
|
||||
memcpy(psync_offset,"-1",3);
|
||||
}
|
||||
|
||||
/* Issue the PSYNC command */
|
||||
reply = sendSynchronousCommand(SYNC_CMD_WRITE,fd,"PSYNC",psync_runid,psync_offset,NULL);
|
||||
reply = sendSynchronousCommand(SYNC_CMD_WRITE,fd,"PSYNC",psync_replid,psync_offset,NULL);
|
||||
if (reply != NULL) {
|
||||
serverLog(LL_WARNING,"Unable to send PSYNC to master: %s",reply);
|
||||
sdsfree(reply);
|
||||
@@ -1327,31 +1460,31 @@ int slaveTryPartialResynchronization(int fd, int read_reply) {
|
||||
aeDeleteFileEvent(server.el,fd,AE_READABLE);
|
||||
|
||||
if (!strncmp(reply,"+FULLRESYNC",11)) {
|
||||
char *runid = NULL, *offset = NULL;
|
||||
char *replid = NULL, *offset = NULL;
|
||||
|
||||
/* FULL RESYNC, parse the reply in order to extract the run id
|
||||
* and the replication offset. */
|
||||
runid = strchr(reply,' ');
|
||||
if (runid) {
|
||||
runid++;
|
||||
offset = strchr(runid,' ');
|
||||
replid = strchr(reply,' ');
|
||||
if (replid) {
|
||||
replid++;
|
||||
offset = strchr(replid,' ');
|
||||
if (offset) offset++;
|
||||
}
|
||||
if (!runid || !offset || (offset-runid-1) != CONFIG_RUN_ID_SIZE) {
|
||||
if (!replid || !offset || (offset-replid-1) != CONFIG_RUN_ID_SIZE) {
|
||||
serverLog(LL_WARNING,
|
||||
"Master replied with wrong +FULLRESYNC syntax.");
|
||||
/* This is an unexpected condition, actually the +FULLRESYNC
|
||||
* reply means that the master supports PSYNC, but the reply
|
||||
* format seems wrong. To stay safe we blank the master
|
||||
* runid to make sure next PSYNCs will fail. */
|
||||
memset(server.repl_master_runid,0,CONFIG_RUN_ID_SIZE+1);
|
||||
* replid to make sure next PSYNCs will fail. */
|
||||
memset(server.master_replid,0,CONFIG_RUN_ID_SIZE+1);
|
||||
} else {
|
||||
memcpy(server.repl_master_runid, runid, offset-runid-1);
|
||||
server.repl_master_runid[CONFIG_RUN_ID_SIZE] = '\0';
|
||||
server.repl_master_initial_offset = strtoll(offset,NULL,10);
|
||||
memcpy(server.master_replid, replid, offset-replid-1);
|
||||
server.master_replid[CONFIG_RUN_ID_SIZE] = '\0';
|
||||
server.master_initial_offset = strtoll(offset,NULL,10);
|
||||
serverLog(LL_NOTICE,"Full resync from master: %s:%lld",
|
||||
server.repl_master_runid,
|
||||
server.repl_master_initial_offset);
|
||||
server.master_replid,
|
||||
server.master_initial_offset);
|
||||
}
|
||||
/* We are going to full resync, discard the cached master structure. */
|
||||
replicationDiscardCachedMaster();
|
||||
@@ -1360,17 +1493,69 @@ int slaveTryPartialResynchronization(int fd, int read_reply) {
|
||||
}
|
||||
|
||||
if (!strncmp(reply,"+CONTINUE",9)) {
|
||||
/* Partial resync was accepted, set the replication state accordingly */
|
||||
/* Partial resync was accepted. */
|
||||
serverLog(LL_NOTICE,
|
||||
"Successful partial resynchronization with master.");
|
||||
|
||||
/* Check the new replication ID advertised by the master. If it
|
||||
* changed, we need to set the new ID as primary ID, and set or
|
||||
* secondary ID as the old master ID up to the current offset, so
|
||||
* that our sub-slaves will be able to PSYNC with us after a
|
||||
* disconnection. */
|
||||
char *start = reply+10;
|
||||
char *end = reply+9;
|
||||
while(end[0] != '\r' && end[0] != '\n' && end[0] != '\0') end++;
|
||||
if (end-start == CONFIG_RUN_ID_SIZE) {
|
||||
char new[CONFIG_RUN_ID_SIZE+1];
|
||||
memcpy(new,start,CONFIG_RUN_ID_SIZE);
|
||||
new[CONFIG_RUN_ID_SIZE] = '\0';
|
||||
|
||||
if (strcmp(new,server.cached_master->replid)) {
|
||||
/* Master ID changed. */
|
||||
serverLog(LL_WARNING,"Master replication ID changed to %s",new);
|
||||
|
||||
/* Set the old ID as our ID2, up to the current offset+1. */
|
||||
memcpy(server.replid2,server.cached_master->replid,
|
||||
sizeof(server.replid2));
|
||||
server.second_replid_offset = server.master_repl_offset+1;
|
||||
|
||||
/* Update the cached master ID and our own primary ID to the
|
||||
* new one. */
|
||||
memcpy(server.replid,new,sizeof(server.replid));
|
||||
memcpy(server.cached_master->replid,new,sizeof(server.replid));
|
||||
|
||||
/* Disconnect all the sub-slaves: they need to be notified. */
|
||||
disconnectSlaves();
|
||||
}
|
||||
}
|
||||
|
||||
/* Setup the replication to continue. */
|
||||
sdsfree(reply);
|
||||
replicationResurrectCachedMaster(fd);
|
||||
|
||||
/* If this instance was restarted and we read the metadata to
|
||||
* PSYNC from the persistence file, our replication backlog could
|
||||
* be still not initialized. Create it. */
|
||||
if (server.repl_backlog == NULL) createReplicationBacklog();
|
||||
return PSYNC_CONTINUE;
|
||||
}
|
||||
|
||||
/* If we reach this point we received either an error since the master does
|
||||
* not understand PSYNC, or an unexpected reply from the master.
|
||||
* Return PSYNC_NOT_SUPPORTED to the caller in both cases. */
|
||||
/* If we reach this point we received either an error (since the master does
|
||||
* not understand PSYNC or because it is in a special state and cannot
|
||||
* serve our request), or an unexpected reply from the master.
|
||||
*
|
||||
* Return PSYNC_NOT_SUPPORTED on errors we don't understand, otherwise
|
||||
* return PSYNC_TRY_LATER if we believe this is a transient error. */
|
||||
|
||||
if (!strncmp(reply,"-NOMASTERLINK",13) ||
|
||||
!strncmp(reply,"-LOADING",8))
|
||||
{
|
||||
serverLog(LL_NOTICE,
|
||||
"Master is currently unable to PSYNC "
|
||||
"but should be in the future: %s", reply);
|
||||
sdsfree(reply);
|
||||
return PSYNC_TRY_LATER;
|
||||
}
|
||||
|
||||
if (strncmp(reply,"-ERR",4)) {
|
||||
/* If it's not an error, log the unexpected event. */
|
||||
@@ -1386,9 +1571,11 @@ int slaveTryPartialResynchronization(int fd, int read_reply) {
|
||||
return PSYNC_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
/* This handler fires when the non blocking connect was able to
|
||||
* establish a connection with the master. */
|
||||
void syncWithMaster(aeEventLoop *el, int fd, void *privdata, int mask) {
|
||||
char tmpfile[256], *err = NULL;
|
||||
int dfd, maxtries = 5;
|
||||
int dfd = -1, maxtries = 5;
|
||||
int sockerr = 0, psync_result;
|
||||
socklen_t errlen = sizeof(sockerr);
|
||||
UNUSED(el);
|
||||
@@ -1402,7 +1589,8 @@ void syncWithMaster(aeEventLoop *el, int fd, void *privdata, int mask) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Check for errors in the socket. */
|
||||
/* Check for errors in the socket: after a non blocking connect() we
|
||||
* may find that the socket is in error state. */
|
||||
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &sockerr, &errlen) == -1)
|
||||
sockerr = errno;
|
||||
if (sockerr) {
|
||||
@@ -1531,13 +1719,15 @@ void syncWithMaster(aeEventLoop *el, int fd, void *privdata, int mask) {
|
||||
server.repl_state = REPL_STATE_SEND_CAPA;
|
||||
}
|
||||
|
||||
/* Inform the master of our capabilities. While we currently send
|
||||
* just one capability, it is possible to chain new capabilities here
|
||||
* in the form of REPLCONF capa X capa Y capa Z ...
|
||||
/* Inform the master of our (slave) capabilities.
|
||||
*
|
||||
* EOF: supports EOF-style RDB transfer for diskless replication.
|
||||
* PSYNC2: supports PSYNC v2, so understands +CONTINUE <new repl ID>.
|
||||
*
|
||||
* The master will ignore capabilities it does not understand. */
|
||||
if (server.repl_state == REPL_STATE_SEND_CAPA) {
|
||||
err = sendSynchronousCommand(SYNC_CMD_WRITE,fd,"REPLCONF",
|
||||
"capa","eof",NULL);
|
||||
"capa","eof","capa","psync2",NULL);
|
||||
if (err) goto write_error;
|
||||
sdsfree(err);
|
||||
server.repl_state = REPL_STATE_RECEIVE_CAPA;
|
||||
@@ -1582,6 +1772,12 @@ void syncWithMaster(aeEventLoop *el, int fd, void *privdata, int mask) {
|
||||
psync_result = slaveTryPartialResynchronization(fd,1);
|
||||
if (psync_result == PSYNC_WAIT_REPLY) return; /* Try again later... */
|
||||
|
||||
/* If the master is in an transient error, we should try to PSYNC
|
||||
* from scratch later, so go to the error path. This happens when
|
||||
* the server is loading the dataset or is not connected with its
|
||||
* master and so forth. */
|
||||
if (psync_result == PSYNC_TRY_LATER) goto error;
|
||||
|
||||
/* Note: if PSYNC does not return WAIT_REPLY, it will take care of
|
||||
* uninstalling the read handler from the file descriptor. */
|
||||
|
||||
@@ -1591,14 +1787,14 @@ void syncWithMaster(aeEventLoop *el, int fd, void *privdata, int mask) {
|
||||
}
|
||||
|
||||
/* PSYNC failed or is not supported: we want our slaves to resync with us
|
||||
* as well, if we have any (chained replication case). The mater may
|
||||
* transfer us an entirely different data set and we have no way to
|
||||
* incrementally feed our slaves after that. */
|
||||
* as well, if we have any sub-slaves. The master may transfer us an
|
||||
* entirely different data set and we have no way to incrementally feed
|
||||
* our slaves after that. */
|
||||
disconnectSlaves(); /* Force our slaves to resync with us as well. */
|
||||
freeReplicationBacklog(); /* Don't allow our chained slaves to PSYNC. */
|
||||
|
||||
/* Fall back to SYNC if needed. Otherwise psync_result == PSYNC_FULLRESYNC
|
||||
* and the server.repl_master_runid and repl_master_initial_offset are
|
||||
* and the server.master_replid and master_initial_offset are
|
||||
* already populated. */
|
||||
if (psync_result == PSYNC_NOT_SUPPORTED) {
|
||||
serverLog(LL_NOTICE,"Retrying with SYNC...");
|
||||
@@ -1643,6 +1839,7 @@ void syncWithMaster(aeEventLoop *el, int fd, void *privdata, int mask) {
|
||||
|
||||
error:
|
||||
aeDeleteFileEvent(server.el,fd,AE_READABLE|AE_WRITABLE);
|
||||
if (dfd != -1) close(dfd);
|
||||
close(fd);
|
||||
server.repl_transfer_s = -1;
|
||||
server.repl_state = REPL_STATE_CONNECT;
|
||||
@@ -1727,17 +1924,24 @@ int cancelReplicationHandshake(void) {
|
||||
|
||||
/* Set replication to the specified master address and port. */
|
||||
void replicationSetMaster(char *ip, int port) {
|
||||
int was_master = server.masterhost == NULL;
|
||||
|
||||
sdsfree(server.masterhost);
|
||||
server.masterhost = sdsnew(ip);
|
||||
server.masterport = port;
|
||||
if (server.master) freeClient(server.master);
|
||||
if (server.master) {
|
||||
freeClient(server.master);
|
||||
}
|
||||
disconnectAllBlockedClients(); /* Clients blocked in master, now slave. */
|
||||
disconnectSlaves(); /* Force our slaves to resync with us as well. */
|
||||
replicationDiscardCachedMaster(); /* Don't try a PSYNC. */
|
||||
freeReplicationBacklog(); /* Don't allow our chained slaves to PSYNC. */
|
||||
|
||||
/* Force our slaves to resync with us as well. They may hopefully be able
|
||||
* to partially resync with us, but we can notify the replid change. */
|
||||
disconnectSlaves();
|
||||
cancelReplicationHandshake();
|
||||
/* Before destroying our master state, create a cached master using
|
||||
* our own parameters, to later PSYNC with the new master. */
|
||||
if (was_master) replicationCacheMasterUsingMyself();
|
||||
server.repl_state = REPL_STATE_CONNECT;
|
||||
server.master_repl_offset = 0;
|
||||
server.repl_down_since = 0;
|
||||
}
|
||||
|
||||
@@ -1746,20 +1950,26 @@ void replicationUnsetMaster(void) {
|
||||
if (server.masterhost == NULL) return; /* Nothing to do. */
|
||||
sdsfree(server.masterhost);
|
||||
server.masterhost = NULL;
|
||||
if (server.master) {
|
||||
if (listLength(server.slaves) == 0) {
|
||||
/* If this instance is turned into a master and there are no
|
||||
* slaves, it inherits the replication offset from the master.
|
||||
* Under certain conditions this makes replicas comparable by
|
||||
* replication offset to understand what is the most updated. */
|
||||
server.master_repl_offset = server.master->reploff;
|
||||
freeReplicationBacklog();
|
||||
}
|
||||
freeClient(server.master);
|
||||
}
|
||||
/* When a slave is turned into a master, the current replication ID
|
||||
* (that was inherited from the master at synchronization time) is
|
||||
* used as secondary ID up to the current offset, and a new replication
|
||||
* ID is created to continue with a new replication history. */
|
||||
shiftReplicationId();
|
||||
if (server.master) freeClient(server.master);
|
||||
replicationDiscardCachedMaster();
|
||||
cancelReplicationHandshake();
|
||||
/* Disconnecting all the slaves is required: we need to inform slaves
|
||||
* of the replication ID change (see shiftReplicationId() call). However
|
||||
* the slaves will be able to partially resync with us, so it will be
|
||||
* a very fast reconnection. */
|
||||
disconnectSlaves();
|
||||
server.repl_state = REPL_STATE_NONE;
|
||||
|
||||
/* We need to make sure the new master will start the replication stream
|
||||
* with a SELECT statement. This is forced after a full resync, but
|
||||
* with PSYNC version 2, there is no need for full resync after a
|
||||
* master switch. */
|
||||
server.slaveseldb = -1;
|
||||
}
|
||||
|
||||
/* This function is called when the slave lose the connection with the
|
||||
@@ -1915,6 +2125,18 @@ void replicationCacheMaster(client *c) {
|
||||
/* Unlink the client from the server structures. */
|
||||
unlinkClient(c);
|
||||
|
||||
/* Reset the master client so that's ready to accept new commands:
|
||||
* we want to discard te non processed query buffers and non processed
|
||||
* offsets, including pending transactions, already populated arguments,
|
||||
* pending outputs to the master. */
|
||||
sdsclear(server.master->querybuf);
|
||||
sdsclear(server.master->pending_querybuf);
|
||||
server.master->read_reploff = server.master->reploff;
|
||||
if (c->flags & CLIENT_MULTI) discardTransaction(c);
|
||||
listEmpty(c->reply);
|
||||
c->bufpos = 0;
|
||||
resetClient(c);
|
||||
|
||||
/* Save the master. Server.master will be set to null later by
|
||||
* replicationHandleMasterDisconnection(). */
|
||||
server.cached_master = server.master;
|
||||
@@ -1931,6 +2153,31 @@ void replicationCacheMaster(client *c) {
|
||||
replicationHandleMasterDisconnection();
|
||||
}
|
||||
|
||||
/* This function is called when a master is turend into a slave, in order to
|
||||
* create from scratch a cached master for the new client, that will allow
|
||||
* to PSYNC with the slave that was promoted as the new master after a
|
||||
* failover.
|
||||
*
|
||||
* Assuming this instance was previously the master instance of the new master,
|
||||
* the new master will accept its replication ID, and potentiall also the
|
||||
* current offset if no data was lost during the failover. So we use our
|
||||
* current replication ID and offset in order to synthesize a cached master. */
|
||||
void replicationCacheMasterUsingMyself(void) {
|
||||
/* The master client we create can be set to any DBID, because
|
||||
* the new master will start its replication stream with SELECT. */
|
||||
server.master_initial_offset = server.master_repl_offset;
|
||||
replicationCreateMasterClient(-1,-1);
|
||||
|
||||
/* Use our own ID / offset. */
|
||||
memcpy(server.master->replid, server.replid, sizeof(server.replid));
|
||||
|
||||
/* Set as cached master. */
|
||||
unlinkClient(server.master);
|
||||
server.cached_master = server.master;
|
||||
server.master = NULL;
|
||||
serverLog(LL_NOTICE,"Before turning into a slave, using my master parameters to synthesize a cached master: I may be able to synchronize with the new master with just a partial transfer.");
|
||||
}
|
||||
|
||||
/* Free a cached master, called when there are no longer the conditions for
|
||||
* a partial resync on reconnection. */
|
||||
void replicationDiscardCachedMaster(void) {
|
||||
@@ -1958,7 +2205,7 @@ void replicationResurrectCachedMaster(int newfd) {
|
||||
server.repl_state = REPL_STATE_CONNECTED;
|
||||
|
||||
/* Re-add to the list of clients. */
|
||||
listAddNodeTail(server.clients,server.master);
|
||||
linkClient(server.master);
|
||||
if (aeCreateFileEvent(server.el, newfd, AE_READABLE,
|
||||
readQueryFromClient, server.master)) {
|
||||
serverLog(LL_WARNING,"Error resurrecting the cached master, impossible to add the readable handler: %s", strerror(errno));
|
||||
@@ -2142,6 +2389,11 @@ void waitCommand(client *c) {
|
||||
long numreplicas, ackreplicas;
|
||||
long long offset = c->woff;
|
||||
|
||||
if (server.masterhost) {
|
||||
addReplyError(c,"WAIT cannot be used with slave instances. Please also note that since Redis 4.0 if a slave is configured to be writable (which is not the default) writes to slaves are just local and are not propagated.");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Argument parsing. */
|
||||
if (getLongFromObjectOrReply(c,c->argv[1],&numreplicas,NULL) != C_OK)
|
||||
return;
|
||||
@@ -2290,7 +2542,9 @@ void replicationCron(void) {
|
||||
robj *ping_argv[1];
|
||||
|
||||
/* First, send PING according to ping_slave_period. */
|
||||
if ((replication_cron_loops % server.repl_ping_slave_period) == 0) {
|
||||
if ((replication_cron_loops % server.repl_ping_slave_period) == 0 &&
|
||||
listLength(server.slaves))
|
||||
{
|
||||
ping_argv[0] = createStringObject("PING",4);
|
||||
replicationFeedSlaves(server.slaves, server.slaveseldb,
|
||||
ping_argv, 1);
|
||||
@@ -2299,20 +2553,30 @@ void replicationCron(void) {
|
||||
|
||||
/* Second, send a newline to all the slaves in pre-synchronization
|
||||
* stage, that is, slaves waiting for the master to create the RDB file.
|
||||
*
|
||||
* Also send the a newline to all the chained slaves we have, if we lost
|
||||
* connection from our master, to keep the slaves aware that their
|
||||
* master is online. This is needed since sub-slaves only receive proxied
|
||||
* data from top-level masters, so there is no explicit pinging in order
|
||||
* to avoid altering the replication offsets. This special out of band
|
||||
* pings (newlines) can be sent, they will have no effect in the offset.
|
||||
*
|
||||
* The newline will be ignored by the slave but will refresh the
|
||||
* last-io timer preventing a timeout. In this case we ignore the
|
||||
* last interaction timer preventing a timeout. In this case we ignore the
|
||||
* ping period and refresh the connection once per second since certain
|
||||
* timeouts are set at a few seconds (example: PSYNC response). */
|
||||
listRewind(server.slaves,&li);
|
||||
while((ln = listNext(&li))) {
|
||||
client *slave = ln->value;
|
||||
|
||||
if (slave->replstate == SLAVE_STATE_WAIT_BGSAVE_START ||
|
||||
int is_presync =
|
||||
(slave->replstate == SLAVE_STATE_WAIT_BGSAVE_START ||
|
||||
(slave->replstate == SLAVE_STATE_WAIT_BGSAVE_END &&
|
||||
server.rdb_child_type != RDB_CHILD_TYPE_SOCKET))
|
||||
{
|
||||
server.rdb_child_type != RDB_CHILD_TYPE_SOCKET));
|
||||
|
||||
if (is_presync) {
|
||||
if (write(slave->fd, "\n", 1) == -1) {
|
||||
/* Don't worry, it's just a ping. */
|
||||
/* Don't worry about socket errors, it's just a ping. */
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2337,14 +2601,35 @@ void replicationCron(void) {
|
||||
}
|
||||
}
|
||||
|
||||
/* If we have no attached slaves and there is a replication backlog
|
||||
* using memory, free it after some (configured) time. */
|
||||
/* If this is a master without attached slaves and there is a replication
|
||||
* backlog active, in order to reclaim memory we can free it after some
|
||||
* (configured) time. Note that this cannot be done for slaves: slaves
|
||||
* without sub-slaves attached should still accumulate data into the
|
||||
* backlog, in order to reply to PSYNC queries if they are turned into
|
||||
* masters after a failover. */
|
||||
if (listLength(server.slaves) == 0 && server.repl_backlog_time_limit &&
|
||||
server.repl_backlog)
|
||||
server.repl_backlog && server.masterhost == NULL)
|
||||
{
|
||||
time_t idle = server.unixtime - server.repl_no_slaves_since;
|
||||
|
||||
if (idle > server.repl_backlog_time_limit) {
|
||||
/* When we free the backlog, we always use a new
|
||||
* replication ID and clear the ID2. This is needed
|
||||
* because when there is no backlog, the master_repl_offset
|
||||
* is not updated, but we would still retain our replication
|
||||
* ID, leading to the following problem:
|
||||
*
|
||||
* 1. We are a master instance.
|
||||
* 2. Our slave is promoted to master. It's repl-id-2 will
|
||||
* be the same as our repl-id.
|
||||
* 3. We, yet as master, receive some updates, that will not
|
||||
* increment the master_repl_offset.
|
||||
* 4. Later we are turned into a slave, connecto to the new
|
||||
* master that will accept our PSYNC request by second
|
||||
* replication ID, but there will be data inconsistency
|
||||
* because we received writes. */
|
||||
changeReplicationId();
|
||||
clearReplicationId2();
|
||||
freeReplicationBacklog();
|
||||
serverLog(LL_NOTICE,
|
||||
"Replication backlog freed after %d seconds "
|
||||
|
||||
+74
-43
@@ -358,6 +358,13 @@ int luaRedisGenericCommand(lua_State *lua, int raise_error) {
|
||||
static size_t cached_objects_len[LUA_CMD_OBJCACHE_SIZE];
|
||||
static int inuse = 0; /* Recursive calls detection. */
|
||||
|
||||
/* Reflect MULTI state */
|
||||
if (server.lua_multi_emitted || (server.lua_caller->flags & CLIENT_MULTI)) {
|
||||
c->flags |= CLIENT_MULTI;
|
||||
} else {
|
||||
c->flags &= ~CLIENT_MULTI;
|
||||
}
|
||||
|
||||
/* By using Lua debug hooks it is possible to trigger a recursive call
|
||||
* to luaRedisGenericCommand(), which normally should never happen.
|
||||
* To make this function reentrant is futile and makes it slower, but
|
||||
@@ -443,6 +450,7 @@ int luaRedisGenericCommand(lua_State *lua, int raise_error) {
|
||||
if (j == 10) {
|
||||
cmdlog = sdscatprintf(cmdlog," ... (%d more)",
|
||||
c->argc-j-1);
|
||||
break;
|
||||
} else {
|
||||
cmdlog = sdscatlen(cmdlog," ",1);
|
||||
cmdlog = sdscatsds(cmdlog,c->argv[j]->ptr);
|
||||
@@ -534,6 +542,7 @@ int luaRedisGenericCommand(lua_State *lua, int raise_error) {
|
||||
* a Lua script in the context of AOF and slaves. */
|
||||
if (server.lua_replicate_commands &&
|
||||
!server.lua_multi_emitted &&
|
||||
!(server.lua_caller->flags & CLIENT_MULTI) &&
|
||||
server.lua_write_dirty &&
|
||||
server.lua_repl != PROPAGATE_NONE)
|
||||
{
|
||||
@@ -900,7 +909,6 @@ void scriptingInit(int setup) {
|
||||
server.lua_caller = NULL;
|
||||
server.lua_timedout = 0;
|
||||
server.lua_always_replicate_commands = 0; /* Only DEBUG can change it.*/
|
||||
server.lua_time_limit = LUA_SCRIPT_TIME_LIMIT;
|
||||
ldbInit();
|
||||
}
|
||||
|
||||
@@ -1133,18 +1141,38 @@ int redis_math_randomseed (lua_State *L) {
|
||||
* EVAL and SCRIPT commands implementation
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/* Define a lua function with the specified function name and body.
|
||||
* The function name musts be a 42 characters long string, since all the
|
||||
* functions we defined in the Lua context are in the form:
|
||||
/* Define a Lua function with the specified body.
|
||||
* The function name will be generated in the following form:
|
||||
*
|
||||
* f_<hex sha1 sum>
|
||||
*
|
||||
* On success C_OK is returned, and nothing is left on the Lua stack.
|
||||
* On error C_ERR is returned and an appropriate error is set in the
|
||||
* client context. */
|
||||
int luaCreateFunction(client *c, lua_State *lua, char *funcname, robj *body) {
|
||||
sds funcdef = sdsempty();
|
||||
* The function increments the reference count of the 'body' object as a
|
||||
* side effect of a successful call.
|
||||
*
|
||||
* On success a pointer to an SDS string representing the function SHA1 of the
|
||||
* just added function is returned (and will be valid until the next call
|
||||
* to scriptingReset() function), otherwise NULL is returned.
|
||||
*
|
||||
* The function handles the fact of being called with a script that already
|
||||
* exists, and in such a case, it behaves like in the success case.
|
||||
*
|
||||
* If 'c' is not NULL, on error the client is informed with an appropriate
|
||||
* error describing the nature of the problem and the Lua interpreter error. */
|
||||
sds luaCreateFunction(client *c, lua_State *lua, robj *body) {
|
||||
char funcname[43];
|
||||
dictEntry *de;
|
||||
|
||||
funcname[0] = 'f';
|
||||
funcname[1] = '_';
|
||||
sha1hex(funcname+2,body->ptr,sdslen(body->ptr));
|
||||
|
||||
sds sha = sdsnewlen(funcname+2,40);
|
||||
if ((de = dictFind(server.lua_scripts,sha)) != NULL) {
|
||||
sdsfree(sha);
|
||||
return dictGetKey(de);
|
||||
}
|
||||
|
||||
sds funcdef = sdsempty();
|
||||
funcdef = sdscat(funcdef,"function ");
|
||||
funcdef = sdscatlen(funcdef,funcname,42);
|
||||
funcdef = sdscatlen(funcdef,"() ",3);
|
||||
@@ -1152,30 +1180,35 @@ int luaCreateFunction(client *c, lua_State *lua, char *funcname, robj *body) {
|
||||
funcdef = sdscatlen(funcdef,"\nend",4);
|
||||
|
||||
if (luaL_loadbuffer(lua,funcdef,sdslen(funcdef),"@user_script")) {
|
||||
addReplyErrorFormat(c,"Error compiling script (new function): %s\n",
|
||||
lua_tostring(lua,-1));
|
||||
if (c != NULL) {
|
||||
addReplyErrorFormat(c,
|
||||
"Error compiling script (new function): %s\n",
|
||||
lua_tostring(lua,-1));
|
||||
}
|
||||
lua_pop(lua,1);
|
||||
sdsfree(sha);
|
||||
sdsfree(funcdef);
|
||||
return C_ERR;
|
||||
return NULL;
|
||||
}
|
||||
sdsfree(funcdef);
|
||||
|
||||
if (lua_pcall(lua,0,0,0)) {
|
||||
addReplyErrorFormat(c,"Error running script (new function): %s\n",
|
||||
lua_tostring(lua,-1));
|
||||
if (c != NULL) {
|
||||
addReplyErrorFormat(c,"Error running script (new function): %s\n",
|
||||
lua_tostring(lua,-1));
|
||||
}
|
||||
lua_pop(lua,1);
|
||||
return C_ERR;
|
||||
sdsfree(sha);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* We also save a SHA1 -> Original script map in a dictionary
|
||||
* so that we can replicate / write in the AOF all the
|
||||
* EVALSHA commands as EVAL using the original script. */
|
||||
{
|
||||
int retval = dictAdd(server.lua_scripts,
|
||||
sdsnewlen(funcname+2,40),body);
|
||||
serverAssertWithInfo(c,NULL,retval == DICT_OK);
|
||||
incrRefCount(body);
|
||||
}
|
||||
return C_OK;
|
||||
int retval = dictAdd(server.lua_scripts,sha,body);
|
||||
serverAssertWithInfo(c ? c : server.lua_client,NULL,retval == DICT_OK);
|
||||
incrRefCount(body);
|
||||
return sha;
|
||||
}
|
||||
|
||||
/* This is the Lua script "count" hook that we use to detect scripts timeout. */
|
||||
@@ -1274,10 +1307,10 @@ void evalGenericCommand(client *c, int evalsha) {
|
||||
addReply(c, shared.noscripterr);
|
||||
return;
|
||||
}
|
||||
if (luaCreateFunction(c,lua,funcname,c->argv[1]) == C_ERR) {
|
||||
if (luaCreateFunction(c,lua,c->argv[1]) == NULL) {
|
||||
lua_pop(lua,1); /* remove the error handler from the stack. */
|
||||
/* The error is sent to the client by luaCreateFunction()
|
||||
* itself when it returns C_ERR. */
|
||||
* itself when it returns NULL. */
|
||||
return;
|
||||
}
|
||||
/* Now the following is guaranteed to return non nil */
|
||||
@@ -1422,7 +1455,17 @@ void evalShaCommand(client *c) {
|
||||
}
|
||||
|
||||
void scriptCommand(client *c) {
|
||||
if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"flush")) {
|
||||
if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"help")) {
|
||||
const char *help[] = {
|
||||
"debug (yes|sync|no) -- Set the debug mode for subsequent scripts executed.",
|
||||
"exists <sha1> [<sha1> ...] -- Return information about the existence of the scripts in the script cache.",
|
||||
"flush -- Flush the Lua scripts cache. Very dangerous on slaves.",
|
||||
"kill -- Kill the currently executing Lua script.",
|
||||
"load <script> -- Load a script into the scripts cache, without executing it.",
|
||||
NULL
|
||||
};
|
||||
addReplyHelp(c, help);
|
||||
} else if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"flush")) {
|
||||
scriptingReset();
|
||||
addReply(c,shared.ok);
|
||||
replicationScriptCacheFlush();
|
||||
@@ -1438,22 +1481,9 @@ void scriptCommand(client *c) {
|
||||
addReply(c,shared.czero);
|
||||
}
|
||||
} else if (c->argc == 3 && !strcasecmp(c->argv[1]->ptr,"load")) {
|
||||
char funcname[43];
|
||||
sds sha;
|
||||
|
||||
funcname[0] = 'f';
|
||||
funcname[1] = '_';
|
||||
sha1hex(funcname+2,c->argv[2]->ptr,sdslen(c->argv[2]->ptr));
|
||||
sha = sdsnewlen(funcname+2,40);
|
||||
if (dictFind(server.lua_scripts,sha) == NULL) {
|
||||
if (luaCreateFunction(c,server.lua,funcname,c->argv[2])
|
||||
== C_ERR) {
|
||||
sdsfree(sha);
|
||||
return;
|
||||
}
|
||||
}
|
||||
addReplyBulkCBuffer(c,funcname+2,40);
|
||||
sdsfree(sha);
|
||||
sds sha = luaCreateFunction(c,server.lua,c->argv[2]);
|
||||
if (sha == NULL) return; /* The error was sent by luaCreateFunction(). */
|
||||
addReplyBulkCBuffer(c,sha,40);
|
||||
forceCommandPropagation(c,PROPAGATE_REPL|PROPAGATE_AOF);
|
||||
} else if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"kill")) {
|
||||
if (server.lua_caller == NULL) {
|
||||
@@ -1481,9 +1511,10 @@ void scriptCommand(client *c) {
|
||||
c->flags |= CLIENT_LUA_DEBUG_SYNC;
|
||||
} else {
|
||||
addReplyError(c,"Use SCRIPT DEBUG yes/sync/no");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
addReplyError(c, "Unknown SCRIPT subcommand or wrong # of args.");
|
||||
addReplyErrorFormat(c, "Unknown subcommand or wrong number of arguments for '%s'. Try SCRIPT HELP", (char*)c->argv[1]->ptr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2272,7 +2303,7 @@ ldbLog(sdsnew("[e]eval <code> Execute some Lua code (in a different callfr
|
||||
ldbLog(sdsnew("[r]edis <cmd> Execute a Redis command."));
|
||||
ldbLog(sdsnew("[m]axlen [len] Trim logged Redis replies and Lua var dumps to len."));
|
||||
ldbLog(sdsnew(" Specifying zero as <len> means unlimited."));
|
||||
ldbLog(sdsnew("[a]abort Stop the execution of the script. In sync"));
|
||||
ldbLog(sdsnew("[a]bort Stop the execution of the script. In sync"));
|
||||
ldbLog(sdsnew(" mode dataset changes will be retained."));
|
||||
ldbLog(sdsnew(""));
|
||||
ldbLog(sdsnew("Debugger functions you can call from Lua scripts:"));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user