diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..847abcf02 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: CI + +on: [push, pull_request] + +jobs: + build-ubuntu: + strategy: + matrix: + platform: [ubuntu-latest, ubuntu-16.04] + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v1 + - name: make + run: make + - name: test + run: | + sudo apt-get install tcl8.5 + make test + + build-macos-latest: + strategy: + matrix: + platform: [macos-latest, macOS-10.14] + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v1 + - name: make + run: make diff --git a/.gitignore b/.gitignore index a188cfc82..de626d61b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ .*.swp *.o +*.xo +*.so +*.d *.log dump.rdb redis-benchmark @@ -28,3 +31,4 @@ deps/lua/src/liblua.a .prerequisites *.dSYM Makefile.dep +.vscode/* diff --git a/CONTRIBUTING b/CONTRIBUTING index 7dee24c74..000edbeaf 100644 --- a/CONTRIBUTING +++ b/CONTRIBUTING @@ -14,9 +14,7 @@ each source file that you contribute. PLEASE DO NOT POST GENERAL QUESTIONS that are not about bugs or suspected bugs in the Github issues system. We'll be very happy to help you and provide - all the support at the Reddit sub: - - http://reddit.com/r/redis + all the support in the mailing list. There is also an active community of Redis users at Stack Overflow: @@ -24,7 +22,12 @@ each source file that you contribute. # How to provide a patch for a new feature -1. If it is a major feature or a semantical change, please post it as a new submission in r/redis on Reddit at http://reddit.com/r/redis. Try to be passionate about why the feature is needed, make users upvote your proposal to gain traction and so forth. Read feedbacks about the community. But in this first step **please don't write code yet**. +1. If it is a major feature or a semantical change, please don't start coding +straight away: if your feature is not a conceptual fit you'll lose a lot of +time writing the code without any reason. Start by posting in the mailing list +and creating an issue at Github with the description of, exactly, what you want +to accomplish and why. Use cases are important for features to be accepted. +Here you'll see if there is consensus about your idea. 2. If in step 1 you get an acknowledgment from the project leaders, use the following procedure to submit a patch: @@ -35,6 +38,13 @@ each source file that you contribute. d. Initiate a pull request on github ( https://help.github.com/articles/creating-a-pull-request/ ) e. Done :) -For minor fixes just open a pull request on Github. +3. Keep in mind that we are very overloaded, so issues and PRs sometimes wait +for a *very* long time. However this is not lack of interest, as the project +gets more and more users, we find ourselves in a constant need to prioritize +certain issues/PRs over others. If you think your issue/PR is very important +try to popularize it, have other users commenting and sharing their point of +view and so forth. This helps. + +4. For minor fixes just open a pull request on Github. Thanks! diff --git a/MANIFESTO b/MANIFESTO index 2b719057e..372789462 100644 --- a/MANIFESTO +++ b/MANIFESTO @@ -34,7 +34,21 @@ Redis Manifesto so that the complexity is obvious and more complex operations can be performed as the sum of the basic operations. -4 - Code is like a poem; it's not just something we write to reach some +4 - We believe in code efficiency. Computers get faster and faster, yet we + believe that abusing computing capabilities is not wise: the amount of + operations you can do for a given amount of energy remains anyway a + significant parameter: it allows to do more with less computers and, at + the same time, having a smaller environmental impact. Similarly Redis is + able to "scale down" to smaller devices. It is perfectly usable in a + Raspberry Pi and other small ARM based computers. Faster code having + just the layers of abstractions that are really needed will also result, + often, in more predictable performances. We think likewise about memory + usage, one of the fundamental goals of the Redis project is to + incrementally build more and more memory efficient data structures, so that + problems that were not approachable in RAM in the past will be perfectly + fine to handle in the future. + +5 - Code is like a poem; it's not just something we write to reach some practical result. Sometimes people that are far from the Redis philosophy suggest using other code written by other authors (frequently in other languages) in order to implement something Redis currently lacks. But to us @@ -45,23 +59,48 @@ Redis Manifesto when needed. At the same time, when writing the Redis story we're trying to write smaller stories that will fit in to other code. -5 - We're against complexity. We believe designing systems is a fight against +6 - We're against complexity. We believe designing systems is a fight against complexity. We'll accept to fight the complexity when it's worthwhile but we'll try hard to recognize when a small feature is not worth 1000s of lines of code. Most of the time the best way to fight complexity is by not - creating it at all. + creating it at all. Complexity is also a form of lock-in: code that is + very hard to understand cannot be modified by users in an independent way + regardless of the license. One of the main Redis goals is to remain + understandable, enough for a single programmer to have a clear idea of how + it works in detail just reading the source code for a couple of weeks. -6 - Two levels of API. The Redis API has two levels: 1) a subset of the API fits +7 - Threading is not a silver bullet. Instead of making Redis threaded we + believe on the idea of an efficient (mostly) single threaded Redis core. + Multiple of such cores, that may run in the same computer or may run + in multiple computers, are abstracted away as a single big system by + higher order protocols and features: Redis Cluster and the upcoming + Redis Proxy are our main goals. A shared nothing approach is not just + much simpler (see the previous point in this document), is also optimal + in NUMA systems. In the specific case of Redis it allows for each instance + to have a more limited amount of data, making the Redis persist-by-fork + approach more sounding. In the future we may explore parallelism only for + I/O, which is the low hanging fruit: minimal complexity could provide an + improved single process experience. + +8 - Two levels of API. The Redis API has two levels: 1) a subset of the API fits naturally into a distributed version of Redis and 2) a more complex API that supports multi-key operations. Both are useful if used judiciously but there's no way to make the more complex multi-keys API distributed in an opaque way without violating our other principles. We don't want to provide the illusion of something that will work magically when actually it can't in all cases. Instead we'll provide commands to quickly migrate keys from one - instance to another to perform multi-key operations and expose the tradeoffs - to the user. + instance to another to perform multi-key operations and expose the + trade-offs to the user. -7 - We optimize for joy. We believe writing code is a lot of hard work, and the +9 - We optimize for joy. We believe writing code is a lot of hard work, and the only way it can be worth is by enjoying it. When there is no longer joy in writing code, the best thing to do is stop. To prevent this, we'll avoid taking paths that will make Redis less of a joy to develop. + +10 - All the above points are put together in what we call opportunistic + programming: trying to get the most for the user with minimal increases + in complexity (hanging fruits). Solve 95% of the problem with 5% of the + code when it is acceptable. Avoid a fixed schedule but follow the flow of + user requests, inspiration, Redis internal readiness for certain features + (sometimes many past changes reach a critical point making a previously + complex feature very easy to obtain). diff --git a/README.md b/README.md index 2b4eeb19b..c08013416 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,11 @@ It is as simple as: % make +To build with TLS support, you'll need OpenSSL development libraries (e.g. +libssl-dev on Debian/Ubuntu) and run: + + % make BUILD_TLS=yes + You can run a 32 bit Redis binary using: % make 32bit @@ -43,6 +48,13 @@ After building Redis, it is a good idea to test it using: % make test +If TLS is built, running the tests with TLS enabled (you will need `tcl-tls` +installed): + + % ./utils/gen-test-certs.sh + % ./runtest --tls + + Fixing build problems with dependencies or cached build options --------- @@ -125,6 +137,12 @@ as options using the command line. Examples: All the options in redis.conf are also supported as options using the command line, with exactly the same name. +Running Redis with TLS: +------------------ + +Please consult the [TLS.md](TLS.md) file for more information on +how to use Redis with TLS. + Playing with Redis ------------------ @@ -166,6 +184,8 @@ for Ubuntu and Debian systems: % cd utils % ./install_server.sh +_Note_: `install_server.sh` will not work on Mac OSX; it is built for Linux only. + The script will ask you a few questions and will setup everything you need to run Redis properly as a background daemon that will start again on system reboots. @@ -404,7 +424,7 @@ replicas, or to continue the replication after a disconnection. Other C files --- -* `t_hash.c`, `t_list.c`, `t_set.c`, `t_string.c` and `t_zset.c` contains the implementation of the Redis data types. They implement both an API to access a given data type, and the client commands implementations for these data types. +* `t_hash.c`, `t_list.c`, `t_set.c`, `t_string.c`, `t_zset.c` and `t_stream.c` contains the implementation of the Redis data types. They implement both an API to access a given data type, and the client commands implementations for these data types. * `ae.c` implements the Redis event loop, it's a self contained library which is simple to read and understand. * `sds.c` is the Redis string library, check http://github.com/antirez/sds for more information. * `anet.c` is a library to use POSIX networking in a simpler way compared to the raw interface exposed by the kernel. diff --git a/TLS.md b/TLS.md new file mode 100644 index 000000000..e480c1e9d --- /dev/null +++ b/TLS.md @@ -0,0 +1,89 @@ +TLS Support +=========== + +Getting Started +--------------- + +### Building + +To build with TLS support you'll need OpenSSL development libraries (e.g. +libssl-dev on Debian/Ubuntu). + +Run `make BUILD_TLS=yes`. + +### Tests + +To run Redis test suite with TLS, you'll need TLS support for TCL (i.e. +`tcl-tls` package on Debian/Ubuntu). + +1. Run `./utils/gen-test-certs.sh` to generate a root CA and a server + certificate. + +2. Run `./runtest --tls` or `./runtest-cluster --tls` to run Redis and Redis + Cluster tests in TLS mode. + +### Running manually + +To manually run a Redis server with TLS mode (assuming `gen-test-certs.sh` was +invoked so sample certificates/keys are available): + + ./src/redis-server --tls-port 6379 --port 0 \ + --tls-cert-file ./tests/tls/redis.crt \ + --tls-key-file ./tests/tls/redis.key \ + --tls-ca-cert-file ./tests/tls/ca.crt + +To connect to this Redis server with `redis-cli`: + + ./src/redis-cli --tls \ + --cert ./tests/tls/redis.crt \ + --key ./tests/tls/redis.key \ + --cacert ./tests/tls/ca.crt + +This will disable TCP and enable TLS on port 6379. It's also possible to have +both TCP and TLS available, but you'll need to assign different ports. + +To make a Replica connect to the master using TLS, use `--tls-replication yes`, +and to make Redis Cluster use TLS across nodes use `--tls-cluster yes`. + +Connections +----------- + +All socket operations now go through a connection abstraction layer that hides +I/O and read/write event handling from the caller. + +**Multi-threading I/O is not currently supported for TLS**, as a TLS connection +needs to do its own manipulation of AE events which is not thread safe. The +solution is probably to manage independent AE loops for I/O threads and longer +term association of connections with threads. This may potentially improve +overall performance as well. + +Sync IO for TLS is currently implemented in a hackish way, i.e. making the +socket blocking and configuring socket-level timeout. This means the timeout +value may not be so accurate, and there would be a lot of syscall overhead. +However I believe that getting rid of syncio completely in favor of pure async +work is probably a better move than trying to fix that. For replication it would +probably not be so hard. For cluster keys migration it might be more difficult, +but there are probably other good reasons to improve that part anyway. + +To-Do List +---------- + +- [ ] Add session caching support. Check if/how it's handled by clients to + assess how useful/important it is. +- [ ] redis-benchmark support. The current implementation is a mix of using + hiredis for parsing and basic networking (establishing connections), but + directly manipulating sockets for most actions. This will need to be cleaned + up for proper TLS support. The best approach is probably to migrate to hiredis + async mode. +- [ ] redis-cli `--slave` and `--rdb` support. + +Multi-port +---------- + +Consider the implications of allowing TLS to be configured on a separate port, +making Redis listening on multiple ports: + +1. Startup banner port notification +2. Proctitle +3. How slaves announce themselves +4. Cluster bus port calculation diff --git a/deps/Makefile b/deps/Makefile index eb35c1e1f..700867f3b 100644 --- a/deps/Makefile +++ b/deps/Makefile @@ -41,9 +41,13 @@ distclean: .PHONY: distclean +ifeq ($(BUILD_TLS),yes) + HIREDIS_MAKE_FLAGS = USE_SSL=1 +endif + hiredis: .make-prerequisites @printf '%b %b\n' $(MAKECOLOR)MAKE$(ENDCOLOR) $(BINCOLOR)$@$(ENDCOLOR) - cd hiredis && $(MAKE) static + cd hiredis && $(MAKE) static $(HIREDIS_MAKE_FLAGS) .PHONY: hiredis diff --git a/deps/hiredis/.gitignore b/deps/hiredis/.gitignore index c44b5c537..8e50b5434 100644 --- a/deps/hiredis/.gitignore +++ b/deps/hiredis/.gitignore @@ -5,3 +5,4 @@ /*.dylib /*.a /*.pc +*.dSYM diff --git a/deps/hiredis/.travis.yml b/deps/hiredis/.travis.yml index faf2ce684..dd8e0e73d 100644 --- a/deps/hiredis/.travis.yml +++ b/deps/hiredis/.travis.yml @@ -26,20 +26,72 @@ addons: - libc6-dev-i386 - libc6-dbg:i386 - gcc-multilib + - g++-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" + - BITS="32" + - BITS="64" + +script: + - EXTRA_CMAKE_OPTS="-DENABLE_EXAMPLES:BOOL=ON -DHIREDIS_SSL:BOOL=ON"; + if [ "$TRAVIS_OS_NAME" == "osx" ]; then + if [ "$BITS" == "32" ]; then + CFLAGS="-m32 -Werror"; + CXXFLAGS="-m32 -Werror"; + LDFLAGS="-m32"; + EXTRA_CMAKE_OPTS=; + else + CFLAGS="-Werror"; + CXXFLAGS="-Werror"; + fi; + else + TEST_PREFIX="valgrind --track-origins=yes --leak-check=full"; + if [ "$BITS" == "32" ]; then + CFLAGS="-m32 -Werror"; + CXXFLAGS="-m32 -Werror"; + LDFLAGS="-m32"; + EXTRA_CMAKE_OPTS=; + else + CFLAGS="-Werror"; + CXXFLAGS="-Werror"; + fi; + fi; + export CFLAGS CXXFLAGS LDFLAGS TEST_PREFIX EXTRA_CMAKE_OPTS + - mkdir build/ && cd build/ + - cmake .. ${EXTRA_CMAKE_OPTS} + - make VERBOSE=1 + - ctest -V matrix: - exclude: - - os: osx - env: PRE="valgrind --track-origins=yes --leak-check=full" + include: + # Windows MinGW cross compile on Linux + - os: linux + dist: xenial + compiler: mingw + addons: + apt: + packages: + - ninja-build + - gcc-mingw-w64-x86-64 + - g++-mingw-w64-x86-64 + script: + - mkdir build && cd build + - CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++ cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_BUILD_WITH_INSTALL_RPATH=on + - ninja -v - - 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 + # Windows MSVC 2017 + - os: windows + compiler: msvc + env: + - MATRIX_EVAL="CC=cl.exe && CXX=cl.exe" + before_install: + - eval "${MATRIX_EVAL}" + install: + - choco install ninja + script: + - mkdir build && cd build + - cmd.exe /C '"C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools\VC\Auxiliary\Build\vcvarsall.bat" amd64 && + cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release && + ninja -v' + - ctest -V diff --git a/deps/hiredis/CHANGELOG.md b/deps/hiredis/CHANGELOG.md index a7fe3ac11..d1d37e515 100644 --- a/deps/hiredis/CHANGELOG.md +++ b/deps/hiredis/CHANGELOG.md @@ -12,6 +12,16 @@ compare to other values, casting might be necessary or can be removed, if casting was applied before. +### 0.x.x (unreleased) +**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. + +* `redisReplyObjectFunctions.createArray` now takes `size_t` for its length parameter. + ### 0.14.0 (2018-09-25) * Make string2ll static to fix conflict with Redis (Tom Lee [c3188b]) @@ -50,8 +60,9 @@ * 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**: +* Bulk and multi-bulk lengths less than -1 or greater than `LLONG_MAX` are now + protocol errors. This is consistent with the RESP specification. On 32-bit + platforms, the upper bound is lowered to `SIZE_MAX`. * Remove backwards compatibility macro's diff --git a/deps/hiredis/CMakeLists.txt b/deps/hiredis/CMakeLists.txt new file mode 100644 index 000000000..9e78894f3 --- /dev/null +++ b/deps/hiredis/CMakeLists.txt @@ -0,0 +1,90 @@ +CMAKE_MINIMUM_REQUIRED(VERSION 3.4.0) +INCLUDE(GNUInstallDirs) +PROJECT(hiredis) + +OPTION(ENABLE_SSL "Build hiredis_ssl for SSL support" OFF) + +MACRO(getVersionBit name) + SET(VERSION_REGEX "^#define ${name} (.+)$") + FILE(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/hiredis.h" + VERSION_BIT REGEX ${VERSION_REGEX}) + STRING(REGEX REPLACE ${VERSION_REGEX} "\\1" ${name} "${VERSION_BIT}") +ENDMACRO(getVersionBit) + +getVersionBit(HIREDIS_MAJOR) +getVersionBit(HIREDIS_MINOR) +getVersionBit(HIREDIS_PATCH) +getVersionBit(HIREDIS_SONAME) +SET(VERSION "${HIREDIS_MAJOR}.${HIREDIS_MINOR}.${HIREDIS_PATCH}") +MESSAGE("Detected version: ${VERSION}") + +PROJECT(hiredis VERSION "${VERSION}") + +SET(ENABLE_EXAMPLES OFF CACHE BOOL "Enable building hiredis examples") + +ADD_LIBRARY(hiredis SHARED + async.c + dict.c + hiredis.c + net.c + read.c + sds.c + sockcompat.c) + +SET_TARGET_PROPERTIES(hiredis + PROPERTIES + VERSION "${HIREDIS_SONAME}") +IF(WIN32 OR MINGW) + TARGET_LINK_LIBRARIES(hiredis PRIVATE ws2_32) +ENDIF() +TARGET_INCLUDE_DIRECTORIES(hiredis PUBLIC .) + +CONFIGURE_FILE(hiredis.pc.in hiredis.pc @ONLY) + +INSTALL(TARGETS hiredis + DESTINATION "${CMAKE_INSTALL_LIBDIR}") + +INSTALL(FILES hiredis.h read.h sds.h async.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/hiredis) + +INSTALL(DIRECTORY adapters + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/hiredis) + +INSTALL(FILES ${CMAKE_CURRENT_BINARY_DIR}/hiredis.pc + DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) + +IF(ENABLE_SSL) + IF (NOT OPENSSL_ROOT_DIR) + IF (APPLE) + SET(OPENSSL_ROOT_DIR "/usr/local/opt/openssl") + ENDIF() + ENDIF() + FIND_PACKAGE(OpenSSL REQUIRED) + ADD_LIBRARY(hiredis_ssl SHARED + ssl.c) + TARGET_INCLUDE_DIRECTORIES(hiredis_ssl PRIVATE "${OPENSSL_INCLUDE_DIR}") + TARGET_LINK_LIBRARIES(hiredis_ssl PRIVATE ${OPENSSL_LIBRARIES}) + CONFIGURE_FILE(hiredis_ssl.pc.in hiredis_ssl.pc @ONLY) + + INSTALL(TARGETS hiredis_ssl + DESTINATION "${CMAKE_INSTALL_LIBDIR}") + + INSTALL(FILES hiredis_ssl.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/hiredis) + + INSTALL(FILES ${CMAKE_CURRENT_BINARY_DIR}/hiredis_ssl.pc + DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) +ENDIF() + +IF(NOT (WIN32 OR MINGW)) + ENABLE_TESTING() + ADD_EXECUTABLE(hiredis-test test.c) + TARGET_LINK_LIBRARIES(hiredis-test hiredis) + ADD_TEST(NAME hiredis-test + COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/test.sh) +ENDIF() + +# Add examples +IF(ENABLE_EXAMPLES) + ADD_SUBDIRECTORY(examples) +ENDIF(ENABLE_EXAMPLES) diff --git a/deps/hiredis/Makefile b/deps/hiredis/Makefile index 06ca99468..25ac15464 100644 --- a/deps/hiredis/Makefile +++ b/deps/hiredis/Makefile @@ -3,11 +3,17 @@ # Copyright (C) 2010-2011 Pieter Noordhuis # This file is released under the BSD license, see the COPYING file -OBJ=net.o hiredis.o sds.o async.o read.o +OBJ=net.o hiredis.o sds.o async.o read.o sockcompat.o +SSL_OBJ=ssl.o EXAMPLES=hiredis-example hiredis-example-libevent hiredis-example-libev hiredis-example-glib +ifeq ($(USE_SSL),1) +EXAMPLES+=hiredis-example-ssl hiredis-example-libevent-ssl +endif TESTS=hiredis-test LIBNAME=libhiredis +SSL_LIBNAME=libhiredis_ssl PKGCONFNAME=hiredis.pc +SSL_PKGCONFNAME=hiredis_ssl.pc HIREDIS_MAJOR=$(shell grep HIREDIS_MAJOR hiredis.h | awk '{print $$3}') HIREDIS_MINOR=$(shell grep HIREDIS_MINOR hiredis.h | awk '{print $$3}') @@ -39,7 +45,7 @@ export REDIS_TEST_CONFIG 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 +WARNINGS=-Wall -W -Wstrict-prototypes -Wwrite-strings -Wno-missing-field-initializers DEBUG_FLAGS?= -g -ggdb REAL_CFLAGS=$(OPTIMIZATION) -fPIC $(CPPFLAGS) $(CFLAGS) $(WARNINGS) $(DEBUG_FLAGS) REAL_LDFLAGS=$(LDFLAGS) @@ -49,12 +55,30 @@ STLIBSUFFIX=a 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) +SSL_DYLIBNAME=$(SSL_LIBNAME).$(DYLIBSUFFIX) +DYLIB_MAKE_CMD=$(CC) -shared -Wl,-soname,$(DYLIB_MINOR_NAME) STLIBNAME=$(LIBNAME).$(STLIBSUFFIX) -STLIB_MAKE_CMD=ar rcs $(STLIBNAME) +SSL_STLIBNAME=$(SSL_LIBNAME).$(STLIBSUFFIX) +STLIB_MAKE_CMD=$(AR) rcs # Platform-specific overrides uname_S := $(shell sh -c 'uname -s 2>/dev/null || echo not') + +USE_SSL?=0 + +# This is required for test.c only +ifeq ($(USE_SSL),1) + CFLAGS+=-DHIREDIS_TEST_SSL +endif + +ifeq ($(uname_S),Linux) + SSL_LDFLAGS=-lssl -lcrypto +else + OPENSSL_PREFIX?=/usr/local/opt/openssl + CFLAGS+=-I$(OPENSSL_PREFIX)/include + SSL_LDFLAGS+=-L$(OPENSSL_PREFIX)/lib -lssl -lcrypto +endif + ifeq ($(uname_S),SunOS) REAL_LDFLAGS+= -ldl -lnsl -lsocket DYLIB_MAKE_CMD=$(CC) -G -o $(DYLIBNAME) -h $(DYLIB_MINOR_NAME) $(LDFLAGS) @@ -66,40 +90,61 @@ ifeq ($(uname_S),Darwin) endif all: $(DYLIBNAME) $(STLIBNAME) hiredis-test $(PKGCONFNAME) +ifeq ($(USE_SSL),1) +all: $(SSL_DYLIBNAME) $(SSL_STLIBNAME) $(SSL_PKGCONFNAME) +endif # Deps (use make dep to generate this) 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 +hiredis.o: hiredis.c fmacros.h hiredis.h read.h sds.h net.h win32.h +net.o: net.c fmacros.h net.h hiredis.h read.h sds.h sockcompat.h win32.h read.o: read.c fmacros.h read.h sds.h sds.o: sds.c sds.h +sockcompat.o: sockcompat.c sockcompat.h +ssl.o: ssl.c hiredis.h test.o: test.c fmacros.h hiredis.h read.h sds.h $(DYLIBNAME): $(OBJ) - $(DYLIB_MAKE_CMD) $(OBJ) + $(DYLIB_MAKE_CMD) -o $(DYLIBNAME) $(OBJ) $(REAL_LDFLAGS) $(STLIBNAME): $(OBJ) - $(STLIB_MAKE_CMD) $(OBJ) + $(STLIB_MAKE_CMD) $(STLIBNAME) $(OBJ) + +$(SSL_DYLIBNAME): $(SSL_OBJ) + $(DYLIB_MAKE_CMD) -o $(SSL_DYLIBNAME) $(SSL_OBJ) $(REAL_LDFLAGS) $(SSL_LDFLAGS) + +$(SSL_STLIBNAME): $(SSL_OBJ) + $(STLIB_MAKE_CMD) $(SSL_STLIBNAME) $(SSL_OBJ) dynamic: $(DYLIBNAME) static: $(STLIBNAME) +ifeq ($(USE_SSL),1) +dynamic: $(SSL_DYLIBNAME) +static: $(SSL_STLIBNAME) +endif # Binaries: hiredis-example-libevent: examples/example-libevent.c adapters/libevent.h $(STLIBNAME) - $(CC) -o examples/$@ $(REAL_CFLAGS) $(REAL_LDFLAGS) -I. $< -levent $(STLIBNAME) + $(CC) -o examples/$@ $(REAL_CFLAGS) -I. $< -levent $(STLIBNAME) $(REAL_LDFLAGS) + +hiredis-example-libevent-ssl: examples/example-libevent-ssl.c adapters/libevent.h $(STLIBNAME) $(SSL_STLIBNAME) + $(CC) -o examples/$@ $(REAL_CFLAGS) -I. $< -levent $(STLIBNAME) $(SSL_STLIBNAME) $(REAL_LDFLAGS) $(SSL_LDFLAGS) hiredis-example-libev: examples/example-libev.c adapters/libev.h $(STLIBNAME) - $(CC) -o examples/$@ $(REAL_CFLAGS) $(REAL_LDFLAGS) -I. $< -lev $(STLIBNAME) + $(CC) -o examples/$@ $(REAL_CFLAGS) -I. $< -lev $(STLIBNAME) $(REAL_LDFLAGS) hiredis-example-glib: examples/example-glib.c adapters/glib.h $(STLIBNAME) - $(CC) -o examples/$@ $(REAL_CFLAGS) $(REAL_LDFLAGS) -I. $< $(shell pkg-config --cflags --libs glib-2.0) $(STLIBNAME) + $(CC) -o examples/$@ $(REAL_CFLAGS) -I. $< $(shell pkg-config --cflags --libs glib-2.0) $(STLIBNAME) $(REAL_LDFLAGS) hiredis-example-ivykis: examples/example-ivykis.c adapters/ivykis.h $(STLIBNAME) - $(CC) -o examples/$@ $(REAL_CFLAGS) $(REAL_LDFLAGS) -I. $< -livykis $(STLIBNAME) + $(CC) -o examples/$@ $(REAL_CFLAGS) -I. $< -livykis $(STLIBNAME) $(REAL_LDFLAGS) hiredis-example-macosx: examples/example-macosx.c adapters/macosx.h $(STLIBNAME) - $(CC) -o examples/$@ $(REAL_CFLAGS) $(REAL_LDFLAGS) -I. $< -framework CoreFoundation $(STLIBNAME) + $(CC) -o examples/$@ $(REAL_CFLAGS) -I. $< -framework CoreFoundation $(STLIBNAME) $(REAL_LDFLAGS) + +hiredis-example-ssl: examples/example-ssl.c $(STLIBNAME) $(SSL_STLIBNAME) + $(CC) -o examples/$@ $(REAL_CFLAGS) -I. $< $(STLIBNAME) $(SSL_STLIBNAME) $(REAL_LDFLAGS) $(SSL_LDFLAGS) ifndef AE_DIR hiredis-example-ae: @@ -116,7 +161,7 @@ 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 -lrt $(STLIBNAME) + $(CC) -o examples/$@ $(REAL_CFLAGS) -I. -I$(LIBUV_DIR)/include $< $(LIBUV_DIR)/.libs/libuv.a -lpthread -lrt $(STLIBNAME) $(REAL_LDFLAGS) endif ifeq ($(and $(QT_MOC),$(QT_INCLUDE_DIR),$(QT_LIBRARY_DIR)),) @@ -133,32 +178,33 @@ hiredis-example-qt: examples/example-qt.cpp adapters/qt.h $(STLIBNAME) endif hiredis-example: examples/example.c $(STLIBNAME) - $(CC) -o examples/$@ $(REAL_CFLAGS) $(REAL_LDFLAGS) -I. $< $(STLIBNAME) + $(CC) -o examples/$@ $(REAL_CFLAGS) -I. $< $(STLIBNAME) $(REAL_LDFLAGS) examples: $(EXAMPLES) -hiredis-test: test.o $(STLIBNAME) +TEST_LIBS = $(STLIBNAME) +ifeq ($(USE_SSL),1) + TEST_LIBS += $(SSL_STLIBNAME) -lssl -lcrypto -lpthread +endif +hiredis-test: test.o $(TEST_LIBS) hiredis-%: %.o $(STLIBNAME) - $(CC) $(REAL_CFLAGS) -o $@ $(REAL_LDFLAGS) $< $(STLIBNAME) + $(CC) $(REAL_CFLAGS) -o $@ $< $(TEST_LIBS) $(REAL_LDFLAGS) test: hiredis-test ./hiredis-test check: hiredis-test - @echo "$$REDIS_TEST_CONFIG" | $(REDIS_SERVER) - - $(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` + TEST_SSL=$(USE_SSL) ./test.sh .c.o: $(CC) -std=c99 -pedantic -c $(REAL_CFLAGS) $< clean: - rm -rf $(DYLIBNAME) $(STLIBNAME) $(TESTS) $(PKGCONFNAME) examples/hiredis-example* *.o *.gcda *.gcno *.gcov + rm -rf $(DYLIBNAME) $(STLIBNAME) $(SSL_DYLIBNAME) $(SSL_STLIBNAME) $(TESTS) $(PKGCONFNAME) examples/hiredis-example* *.o *.gcda *.gcno *.gcov dep: - $(CC) -MM *.c + $(CC) $(CPPFLAGS) $(CFLAGS) -MM *.c INSTALL?= cp -pPR @@ -175,6 +221,20 @@ $(PKGCONFNAME): hiredis.h @echo Libs: -L\$${libdir} -lhiredis >> $@ @echo Cflags: -I\$${includedir} -D_FILE_OFFSET_BITS=64 >> $@ +$(SSL_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_ssl >> $@ + @echo Description: SSL Support for hiredis. >> $@ + @echo Version: $(HIREDIS_MAJOR).$(HIREDIS_MINOR).$(HIREDIS_PATCH) >> $@ + @echo Requires: hiredis >> $@ + @echo Libs: -L\$${libdir} -lhiredis_ssl >> $@ + @echo Libs.private: -lssl -lcrypto >> $@ + install: $(DYLIBNAME) $(STLIBNAME) $(PKGCONFNAME) mkdir -p $(INSTALL_INCLUDE_PATH) $(INSTALL_INCLUDE_PATH)/adapters $(INSTALL_LIBRARY_PATH) $(INSTALL) hiredis.h async.h read.h sds.h $(INSTALL_INCLUDE_PATH) diff --git a/deps/hiredis/README.md b/deps/hiredis/README.md index 01223ea59..c0b432f07 100644 --- a/deps/hiredis/README.md +++ b/deps/hiredis/README.md @@ -286,6 +286,7 @@ return `REDIS_ERR`. The function to set the disconnect callback has the followin ```c int redisAsyncSetDisconnectCallback(redisAsyncContext *ac, redisDisconnectCallback *fn); ``` +`ac->data` may be used to pass user data to this callback, the same can be done for redisConnectCallback. ### Sending commands and their callbacks In an asynchronous context, commands are automatically pipelined due to the nature of an event loop. @@ -406,6 +407,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) diff --git a/deps/hiredis/adapters/libevent.h b/deps/hiredis/adapters/libevent.h index 7d2bef180..a4952776c 100644 --- a/deps/hiredis/adapters/libevent.h +++ b/deps/hiredis/adapters/libevent.h @@ -34,48 +34,113 @@ #include "../hiredis.h" #include "../async.h" +#define REDIS_LIBEVENT_DELETED 0x01 +#define REDIS_LIBEVENT_ENTERED 0x02 + typedef struct redisLibeventEvents { redisAsyncContext *context; - struct event *rev, *wev; + struct event *ev; + struct event_base *base; + struct timeval tv; + short flags; + short state; } redisLibeventEvents; -static void redisLibeventReadEvent(int fd, short event, void *arg) { - ((void)fd); ((void)event); - redisLibeventEvents *e = (redisLibeventEvents*)arg; - redisAsyncHandleRead(e->context); +static void redisLibeventDestroy(redisLibeventEvents *e) { + free(e); } -static void redisLibeventWriteEvent(int fd, short event, void *arg) { - ((void)fd); ((void)event); +static void redisLibeventHandler(int fd, short event, void *arg) { + ((void)fd); redisLibeventEvents *e = (redisLibeventEvents*)arg; - redisAsyncHandleWrite(e->context); + e->state |= REDIS_LIBEVENT_ENTERED; + + #define CHECK_DELETED() if (e->state & REDIS_LIBEVENT_DELETED) {\ + redisLibeventDestroy(e);\ + return; \ + } + + if ((event & EV_TIMEOUT) && (e->state & REDIS_LIBEVENT_DELETED) == 0) { + redisAsyncHandleTimeout(e->context); + CHECK_DELETED(); + } + + if ((event & EV_READ) && e->context && (e->state & REDIS_LIBEVENT_DELETED) == 0) { + redisAsyncHandleRead(e->context); + CHECK_DELETED(); + } + + if ((event & EV_WRITE) && e->context && (e->state & REDIS_LIBEVENT_DELETED) == 0) { + redisAsyncHandleWrite(e->context); + CHECK_DELETED(); + } + + e->state &= ~REDIS_LIBEVENT_ENTERED; + #undef CHECK_DELETED +} + +static void redisLibeventUpdate(void *privdata, short flag, int isRemove) { + redisLibeventEvents *e = (redisLibeventEvents *)privdata; + const struct timeval *tv = e->tv.tv_sec || e->tv.tv_usec ? &e->tv : NULL; + + if (isRemove) { + if ((e->flags & flag) == 0) { + return; + } else { + e->flags &= ~flag; + } + } else { + if (e->flags & flag) { + return; + } else { + e->flags |= flag; + } + } + + event_del(e->ev); + event_assign(e->ev, e->base, e->context->c.fd, e->flags | EV_PERSIST, + redisLibeventHandler, privdata); + event_add(e->ev, tv); } static void redisLibeventAddRead(void *privdata) { - redisLibeventEvents *e = (redisLibeventEvents*)privdata; - event_add(e->rev,NULL); + redisLibeventUpdate(privdata, EV_READ, 0); } static void redisLibeventDelRead(void *privdata) { - redisLibeventEvents *e = (redisLibeventEvents*)privdata; - event_del(e->rev); + redisLibeventUpdate(privdata, EV_READ, 1); } static void redisLibeventAddWrite(void *privdata) { - redisLibeventEvents *e = (redisLibeventEvents*)privdata; - event_add(e->wev,NULL); + redisLibeventUpdate(privdata, EV_WRITE, 0); } static void redisLibeventDelWrite(void *privdata) { - redisLibeventEvents *e = (redisLibeventEvents*)privdata; - event_del(e->wev); + redisLibeventUpdate(privdata, EV_WRITE, 1); } static void redisLibeventCleanup(void *privdata) { redisLibeventEvents *e = (redisLibeventEvents*)privdata; - event_free(e->rev); - event_free(e->wev); - free(e); + if (!e) { + return; + } + event_del(e->ev); + event_free(e->ev); + e->ev = NULL; + + if (e->state & REDIS_LIBEVENT_ENTERED) { + e->state |= REDIS_LIBEVENT_DELETED; + } else { + redisLibeventDestroy(e); + } +} + +static void redisLibeventSetTimeout(void *privdata, struct timeval tv) { + redisLibeventEvents *e = (redisLibeventEvents *)privdata; + short flags = e->flags; + e->flags = 0; + e->tv = tv; + redisLibeventUpdate(e, flags, 0); } static int redisLibeventAttach(redisAsyncContext *ac, struct event_base *base) { @@ -87,7 +152,7 @@ static int redisLibeventAttach(redisAsyncContext *ac, struct event_base *base) { return REDIS_ERR; /* Create container for context and r/w events */ - e = (redisLibeventEvents*)malloc(sizeof(*e)); + e = (redisLibeventEvents*)calloc(1, sizeof(*e)); e->context = ac; /* Register functions to start/stop listening for events */ @@ -96,13 +161,12 @@ static int redisLibeventAttach(redisAsyncContext *ac, struct event_base *base) { ac->ev.addWrite = redisLibeventAddWrite; ac->ev.delWrite = redisLibeventDelWrite; ac->ev.cleanup = redisLibeventCleanup; + ac->ev.scheduleTimer = redisLibeventSetTimeout; ac->ev.data = e; /* Initialize and install read/write events */ - 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); + e->ev = event_new(base, c->fd, EV_READ | EV_WRITE, redisLibeventHandler, e); + e->base = base; return REDIS_OK; } #endif diff --git a/deps/hiredis/appveyor.yml b/deps/hiredis/appveyor.yml index 819efbd58..5b43fdbeb 100644 --- a/deps/hiredis/appveyor.yml +++ b/deps/hiredis/appveyor.yml @@ -5,8 +5,9 @@ environment: CC: gcc - CYG_BASH: C:\cygwin\bin\bash CC: gcc - TARGET: 32bit - TARGET_VARS: 32bit-vars + CFLAGS: -m32 + CXXFLAGS: -m32 + LDFLAGS: -m32 clone_depth: 1 @@ -20,4 +21,4 @@ install: build_script: - 'echo building...' - - '%CYG_BASH% -lc "cd $APPVEYOR_BUILD_FOLDER; exec 0 #include +#ifndef _MSC_VER #include +#endif #include #include #include @@ -40,22 +42,9 @@ #include "net.h" #include "dict.c" #include "sds.h" +#include "win32.h" -#define _EL_ADD_READ(ctx) do { \ - if ((ctx)->ev.addRead) (ctx)->ev.addRead((ctx)->ev.data); \ - } while(0) -#define _EL_DEL_READ(ctx) do { \ - if ((ctx)->ev.delRead) (ctx)->ev.delRead((ctx)->ev.data); \ - } while(0) -#define _EL_ADD_WRITE(ctx) do { \ - if ((ctx)->ev.addWrite) (ctx)->ev.addWrite((ctx)->ev.data); \ - } while(0) -#define _EL_DEL_WRITE(ctx) do { \ - if ((ctx)->ev.delWrite) (ctx)->ev.delWrite((ctx)->ev.data); \ - } while(0) -#define _EL_CLEANUP(ctx) do { \ - if ((ctx)->ev.cleanup) (ctx)->ev.cleanup((ctx)->ev.data); \ - } while(0); +#include "async_private.h" /* Forward declaration of function in hiredis.c */ int __redisAppendCommand(redisContext *c, const char *cmd, size_t len); @@ -126,6 +115,7 @@ static redisAsyncContext *redisAsyncInitialize(redisContext *c) { ac->ev.addWrite = NULL; ac->ev.delWrite = NULL; ac->ev.cleanup = NULL; + ac->ev.scheduleTimer = NULL; ac->onConnect = NULL; ac->onDisconnect = NULL; @@ -150,56 +140,52 @@ static void __redisAsyncCopyError(redisAsyncContext *ac) { ac->errstr = c->errstr; } -redisAsyncContext *redisAsyncConnect(const char *ip, int port) { +redisAsyncContext *redisAsyncConnectWithOptions(const redisOptions *options) { + redisOptions myOptions = *options; redisContext *c; redisAsyncContext *ac; - c = redisConnectNonBlock(ip,port); - if (c == NULL) + myOptions.options |= REDIS_OPT_NONBLOCK; + c = redisConnectWithOptions(&myOptions); + if (c == NULL) { return NULL; - + } ac = redisAsyncInitialize(c); if (ac == NULL) { redisFree(c); return NULL; } - __redisAsyncCopyError(ac); return ac; } +redisAsyncContext *redisAsyncConnect(const char *ip, int port) { + redisOptions options = {0}; + REDIS_OPTIONS_SET_TCP(&options, ip, port); + return redisAsyncConnectWithOptions(&options); +} + redisAsyncContext *redisAsyncConnectBind(const char *ip, int port, const char *source_addr) { - redisContext *c = redisConnectBindNonBlock(ip,port,source_addr); - redisAsyncContext *ac = redisAsyncInitialize(c); - __redisAsyncCopyError(ac); - return ac; + redisOptions options = {0}; + REDIS_OPTIONS_SET_TCP(&options, ip, port); + options.endpoint.tcp.source_addr = source_addr; + return redisAsyncConnectWithOptions(&options); } 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; + redisOptions options = {0}; + REDIS_OPTIONS_SET_TCP(&options, ip, port); + options.options |= REDIS_OPT_REUSEADDR; + options.endpoint.tcp.source_addr = source_addr; + return redisAsyncConnectWithOptions(&options); } redisAsyncContext *redisAsyncConnectUnix(const char *path) { - redisContext *c; - redisAsyncContext *ac; - - c = redisConnectUnixNonBlock(path); - if (c == NULL) - return NULL; - - ac = redisAsyncInitialize(c); - if (ac == NULL) { - redisFree(c); - return NULL; - } - - __redisAsyncCopyError(ac); - return ac; + redisOptions options = {0}; + REDIS_OPTIONS_SET_UNIX(&options, path); + return redisAsyncConnectWithOptions(&options); } int redisAsyncSetConnectCallback(redisAsyncContext *ac, redisConnectCallback *fn) { @@ -328,7 +314,7 @@ void redisAsyncFree(redisAsyncContext *ac) { } /* Helper function to make the disconnect happen and clean up. */ -static void __redisAsyncDisconnect(redisAsyncContext *ac) { +void __redisAsyncDisconnect(redisAsyncContext *ac) { redisContext *c = &(ac->c); /* Make sure error is accessible if there is any */ @@ -344,9 +330,15 @@ static void __redisAsyncDisconnect(redisAsyncContext *ac) { c->flags |= REDIS_DISCONNECTING; } + /* cleanup event library on disconnect. + * this is safe to call multiple times */ + _EL_CLEANUP(ac); + /* For non-clean disconnects, __redisAsyncFree() will execute pending * callbacks with a NULL-reply. */ - __redisAsyncFree(ac); + if (!(c->flags & REDIS_NO_AUTO_FREE)) { + __redisAsyncFree(ac); + } } /* Tries to do a clean disconnect from Redis, meaning it stops new commands @@ -358,6 +350,9 @@ static void __redisAsyncDisconnect(redisAsyncContext *ac) { void redisAsyncDisconnect(redisAsyncContext *ac) { redisContext *c = &(ac->c); c->flags |= REDIS_DISCONNECTING; + + /** unset the auto-free flag here, because disconnect undoes this */ + c->flags &= ~REDIS_NO_AUTO_FREE; if (!(c->flags & REDIS_IN_CALLBACK) && ac->replies.head == NULL) __redisAsyncDisconnect(ac); } @@ -408,7 +403,7 @@ static int __redisGetSubscribeCallback(redisAsyncContext *ac, redisReply *reply, assert(reply->element[2]->type == REDIS_REPLY_INTEGER); /* Unset subscribed flag only when no pipelined pending subscribe. */ - if (reply->element[2]->integer == 0 + if (reply->element[2]->integer == 0 && dictSize(ac->sub.channels) == 0 && dictSize(ac->sub.patterns) == 0) c->flags &= ~REDIS_SUBSCRIBED; @@ -524,6 +519,18 @@ static int __redisAsyncHandleConnect(redisAsyncContext *ac) { } } +void redisAsyncRead(redisAsyncContext *ac) { + redisContext *c = &(ac->c); + + if (redisBufferRead(c) == REDIS_ERR) { + __redisAsyncDisconnect(ac); + } else { + /* Always re-schedule reads */ + _EL_ADD_READ(ac); + redisProcessCallbacks(ac); + } +} + /* This function should be called when the socket is readable. * It processes all replies that can be read and executes their callbacks. */ @@ -539,28 +546,13 @@ void redisAsyncHandleRead(redisAsyncContext *ac) { return; } - if (redisBufferRead(c) == REDIS_ERR) { - __redisAsyncDisconnect(ac); - } else { - /* Always re-schedule reads */ - _EL_ADD_READ(ac); - redisProcessCallbacks(ac); - } + c->funcs->async_read(ac); } -void redisAsyncHandleWrite(redisAsyncContext *ac) { +void redisAsyncWrite(redisAsyncContext *ac) { redisContext *c = &(ac->c); int done = 0; - if (!(c->flags & REDIS_CONNECTED)) { - /* Abort connect was not successful. */ - if (__redisAsyncHandleConnect(ac) != REDIS_OK) - return; - /* Try again later when the context is still not connected. */ - if (!(c->flags & REDIS_CONNECTED)) - return; - } - if (redisBufferWrite(c,&done) == REDIS_ERR) { __redisAsyncDisconnect(ac); } else { @@ -575,6 +567,51 @@ void redisAsyncHandleWrite(redisAsyncContext *ac) { } } +void redisAsyncHandleWrite(redisAsyncContext *ac) { + redisContext *c = &(ac->c); + + if (!(c->flags & REDIS_CONNECTED)) { + /* Abort connect was not successful. */ + if (__redisAsyncHandleConnect(ac) != REDIS_OK) + return; + /* Try again later when the context is still not connected. */ + if (!(c->flags & REDIS_CONNECTED)) + return; + } + + c->funcs->async_write(ac); +} + +void __redisSetError(redisContext *c, int type, const char *str); + +void redisAsyncHandleTimeout(redisAsyncContext *ac) { + redisContext *c = &(ac->c); + redisCallback cb; + + if ((c->flags & REDIS_CONNECTED) && ac->replies.head == NULL) { + /* Nothing to do - just an idle timeout */ + return; + } + + if (!c->err) { + __redisSetError(c, REDIS_ERR_TIMEOUT, "Timeout"); + } + + if (!(c->flags & REDIS_CONNECTED) && ac->onConnect) { + ac->onConnect(ac, REDIS_ERR); + } + + while (__redisShiftCallback(&ac->replies, &cb) == REDIS_OK) { + __redisRunCallback(ac, &cb, NULL); + } + + /** + * TODO: Don't automatically sever the connection, + * rather, allow to ignore responses before the queue is clear + */ + __redisAsyncDisconnect(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 const char *nextArgument(const char *start, const char **str, size_t *len) { @@ -714,3 +751,16 @@ int redisAsyncFormattedCommand(redisAsyncContext *ac, redisCallbackFn *fn, void int status = __redisAsyncCommand(ac,fn,privdata,cmd,len); return status; } + +void redisAsyncSetTimeout(redisAsyncContext *ac, struct timeval tv) { + if (!ac->c.timeout) { + ac->c.timeout = calloc(1, sizeof(tv)); + } + + if (tv.tv_sec == ac->c.timeout->tv_sec && + tv.tv_usec == ac->c.timeout->tv_usec) { + return; + } + + *ac->c.timeout = tv; +} diff --git a/deps/hiredis/async.h b/deps/hiredis/async.h index 740555c24..4f6b3b783 100644 --- a/deps/hiredis/async.h +++ b/deps/hiredis/async.h @@ -57,6 +57,7 @@ typedef struct redisCallbackList { /* Connection callback prototypes */ typedef void (redisDisconnectCallback)(const struct redisAsyncContext*, int status); typedef void (redisConnectCallback)(const struct redisAsyncContext*, int status); +typedef void(redisTimerCallback)(void *timer, void *privdata); /* Context for an async connection to Redis */ typedef struct redisAsyncContext { @@ -81,6 +82,7 @@ typedef struct redisAsyncContext { void (*addWrite)(void *privdata); void (*delWrite)(void *privdata); void (*cleanup)(void *privdata); + void (*scheduleTimer)(void *privdata, struct timeval tv); } ev; /* Called when either the connection is terminated due to an error or per @@ -106,6 +108,7 @@ typedef struct redisAsyncContext { } redisAsyncContext; /* Functions that proxy to hiredis */ +redisAsyncContext *redisAsyncConnectWithOptions(const redisOptions *options); 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, @@ -113,12 +116,17 @@ redisAsyncContext *redisAsyncConnectBindWithReuse(const char *ip, int port, redisAsyncContext *redisAsyncConnectUnix(const char *path); int redisAsyncSetConnectCallback(redisAsyncContext *ac, redisConnectCallback *fn); int redisAsyncSetDisconnectCallback(redisAsyncContext *ac, redisDisconnectCallback *fn); + +void redisAsyncSetTimeout(redisAsyncContext *ac, struct timeval tv); void redisAsyncDisconnect(redisAsyncContext *ac); void redisAsyncFree(redisAsyncContext *ac); /* Handle read/write events */ void redisAsyncHandleRead(redisAsyncContext *ac); void redisAsyncHandleWrite(redisAsyncContext *ac); +void redisAsyncHandleTimeout(redisAsyncContext *ac); +void redisAsyncRead(redisAsyncContext *ac); +void redisAsyncWrite(redisAsyncContext *ac); /* Command functions for an async context. Write the command to the * output buffer and register the provided callback. */ diff --git a/deps/hiredis/async_private.h b/deps/hiredis/async_private.h new file mode 100644 index 000000000..d0133ae18 --- /dev/null +++ b/deps/hiredis/async_private.h @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2009-2011, Salvatore Sanfilippo + * Copyright (c) 2010-2011, Pieter Noordhuis + * + * 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_ASYNC_PRIVATE_H +#define __HIREDIS_ASYNC_PRIVATE_H + +#define _EL_ADD_READ(ctx) \ + do { \ + refreshTimeout(ctx); \ + if ((ctx)->ev.addRead) (ctx)->ev.addRead((ctx)->ev.data); \ + } while (0) +#define _EL_DEL_READ(ctx) do { \ + if ((ctx)->ev.delRead) (ctx)->ev.delRead((ctx)->ev.data); \ + } while(0) +#define _EL_ADD_WRITE(ctx) \ + do { \ + refreshTimeout(ctx); \ + if ((ctx)->ev.addWrite) (ctx)->ev.addWrite((ctx)->ev.data); \ + } while (0) +#define _EL_DEL_WRITE(ctx) do { \ + if ((ctx)->ev.delWrite) (ctx)->ev.delWrite((ctx)->ev.data); \ + } while(0) +#define _EL_CLEANUP(ctx) do { \ + if ((ctx)->ev.cleanup) (ctx)->ev.cleanup((ctx)->ev.data); \ + ctx->ev.cleanup = NULL; \ + } while(0); + +static inline void refreshTimeout(redisAsyncContext *ctx) { + if (ctx->c.timeout && ctx->ev.scheduleTimer && + (ctx->c.timeout->tv_sec || ctx->c.timeout->tv_usec)) { + ctx->ev.scheduleTimer(ctx->ev.data, *ctx->c.timeout); + // } else { + // printf("Not scheduling timer.. (tmo=%p)\n", ctx->c.timeout); + // if (ctx->c.timeout){ + // printf("tv_sec: %u. tv_usec: %u\n", ctx->c.timeout->tv_sec, + // ctx->c.timeout->tv_usec); + // } + } +} + +void __redisAsyncDisconnect(redisAsyncContext *ac); +void redisProcessCallbacks(redisAsyncContext *ac); + +#endif /* __HIREDIS_ASYNC_PRIVATE_H */ diff --git a/deps/hiredis/examples/CMakeLists.txt b/deps/hiredis/examples/CMakeLists.txt new file mode 100644 index 000000000..dd3a313ac --- /dev/null +++ b/deps/hiredis/examples/CMakeLists.txt @@ -0,0 +1,46 @@ +INCLUDE(FindPkgConfig) +# Check for GLib + +PKG_CHECK_MODULES(GLIB2 glib-2.0) +if (GLIB2_FOUND) + INCLUDE_DIRECTORIES(${GLIB2_INCLUDE_DIRS}) + LINK_DIRECTORIES(${GLIB2_LIBRARY_DIRS}) + ADD_EXECUTABLE(example-glib example-glib.c) + TARGET_LINK_LIBRARIES(example-glib hiredis ${GLIB2_LIBRARIES}) +ENDIF(GLIB2_FOUND) + +FIND_PATH(LIBEV ev.h + HINTS /usr/local /usr/opt/local + ENV LIBEV_INCLUDE_DIR) + +if (LIBEV) + # Just compile and link with libev + ADD_EXECUTABLE(example-libev example-libev.c) + TARGET_LINK_LIBRARIES(example-libev hiredis ev) +ENDIF() + +FIND_PATH(LIBEVENT event.h) +if (LIBEVENT) + ADD_EXECUTABLE(example-libevent example-libevent) + TARGET_LINK_LIBRARIES(example-libevent hiredis event) +ENDIF() + +FIND_PATH(LIBUV uv.h) +IF (LIBUV) + ADD_EXECUTABLE(example-libuv example-libuv.c) + TARGET_LINK_LIBRARIES(example-libuv hiredis uv) +ENDIF() + +IF (APPLE) + FIND_LIBRARY(CF CoreFoundation) + ADD_EXECUTABLE(example-macosx example-macosx.c) + TARGET_LINK_LIBRARIES(example-macosx hiredis ${CF}) +ENDIF() + +IF (ENABLE_SSL) + ADD_EXECUTABLE(example-ssl example-ssl.c) + TARGET_LINK_LIBRARIES(example-ssl hiredis hiredis_ssl) +ENDIF() + +ADD_EXECUTABLE(example example.c) +TARGET_LINK_LIBRARIES(example hiredis) diff --git a/deps/hiredis/examples/example-libevent-ssl.c b/deps/hiredis/examples/example-libevent-ssl.c new file mode 100644 index 000000000..1021113b9 --- /dev/null +++ b/deps/hiredis/examples/example-libevent-ssl.c @@ -0,0 +1,73 @@ +#include +#include +#include +#include + +#include +#include +#include +#include + +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); + struct event_base *base = event_base_new(); + if (argc < 5) { + fprintf(stderr, + "Usage: %s [ca]\n", argv[0]); + exit(1); + } + + const char *value = argv[1]; + size_t nvalue = strlen(value); + + const char *hostname = argv[2]; + int port = atoi(argv[3]); + + const char *cert = argv[4]; + const char *certKey = argv[5]; + const char *caCert = argc > 5 ? argv[6] : NULL; + + redisAsyncContext *c = redisAsyncConnect(hostname, port); + if (c->err) { + /* Let *c leak for now... */ + printf("Error: %s\n", c->errstr); + return 1; + } + if (redisSecureConnection(&c->c, caCert, cert, certKey, "sni") != REDIS_OK) { + printf("SSL Error!\n"); + exit(1); + } + + redisLibeventAttach(c,base); + redisAsyncSetConnectCallback(c,connectCallback); + redisAsyncSetDisconnectCallback(c,disconnectCallback); + redisAsyncCommand(c, NULL, NULL, "SET key %b", value, nvalue); + redisAsyncCommand(c, getCallback, (char*)"end-1", "GET key"); + event_base_dispatch(base); + return 0; +} diff --git a/deps/hiredis/examples/example-libevent.c b/deps/hiredis/examples/example-libevent.c index d333c22b7..1fe71ae4e 100644 --- a/deps/hiredis/examples/example-libevent.c +++ b/deps/hiredis/examples/example-libevent.c @@ -9,7 +9,12 @@ void getCallback(redisAsyncContext *c, void *r, void *privdata) { redisReply *reply = r; - if (reply == NULL) return; + if (reply == NULL) { + if (c->errstr) { + printf("errstr: %s\n", c->errstr); + } + return; + } printf("argv[%s]: %s\n", (char*)privdata, reply->str); /* Disconnect after receiving the reply to GET */ @@ -35,8 +40,14 @@ void disconnectCallback(const redisAsyncContext *c, int status) { int main (int argc, char **argv) { signal(SIGPIPE, SIG_IGN); struct event_base *base = event_base_new(); + redisOptions options = {0}; + REDIS_OPTIONS_SET_TCP(&options, "127.0.0.1", 6379); + struct timeval tv = {0}; + tv.tv_sec = 1; + options.timeout = &tv; - redisAsyncContext *c = redisAsyncConnect("127.0.0.1", 6379); + + redisAsyncContext *c = redisAsyncConnectWithOptions(&options); if (c->err) { /* Let *c leak for now... */ printf("Error: %s\n", c->errstr); diff --git a/deps/hiredis/examples/example-ssl.c b/deps/hiredis/examples/example-ssl.c new file mode 100644 index 000000000..81f4648c6 --- /dev/null +++ b/deps/hiredis/examples/example-ssl.c @@ -0,0 +1,97 @@ +#include +#include +#include + +#include +#include + +int main(int argc, char **argv) { + unsigned int j; + redisContext *c; + redisReply *reply; + if (argc < 4) { + printf("Usage: %s [ca]\n", argv[0]); + exit(1); + } + const char *hostname = (argc > 1) ? argv[1] : "127.0.0.1"; + int port = atoi(argv[2]); + const char *cert = argv[3]; + const char *key = argv[4]; + const char *ca = argc > 4 ? argv[5] : NULL; + + struct timeval tv = { 1, 500000 }; // 1.5 seconds + redisOptions options = {0}; + REDIS_OPTIONS_SET_TCP(&options, hostname, port); + options.timeout = &tv; + c = redisConnectWithOptions(&options); + + if (c == NULL || c->err) { + if (c) { + printf("Connection error: %s\n", c->errstr); + redisFree(c); + } else { + printf("Connection error: can't allocate redis context\n"); + } + exit(1); + } + + if (redisSecureConnection(c, ca, cert, key, "sni") != REDIS_OK) { + printf("Couldn't initialize SSL!\n"); + printf("Error: %s\n", c->errstr); + redisFree(c); + exit(1); + } + + /* PING server */ + reply = redisCommand(c,"PING"); + printf("PING: %s\n", reply->str); + freeReplyObject(reply); + + /* Set a key */ + reply = redisCommand(c,"SET %s %s", "foo", "hello world"); + printf("SET: %s\n", reply->str); + freeReplyObject(reply); + + /* Set a key using binary safe API */ + reply = redisCommand(c,"SET %b %b", "bar", (size_t) 3, "hello", (size_t) 5); + printf("SET (binary API): %s\n", reply->str); + freeReplyObject(reply); + + /* Try a GET and two INCR */ + reply = redisCommand(c,"GET foo"); + printf("GET foo: %s\n", reply->str); + freeReplyObject(reply); + + reply = redisCommand(c,"INCR counter"); + printf("INCR counter: %lld\n", reply->integer); + freeReplyObject(reply); + /* again ... */ + reply = redisCommand(c,"INCR counter"); + printf("INCR counter: %lld\n", reply->integer); + freeReplyObject(reply); + + /* Create a list of numbers, from 0 to 9 */ + reply = redisCommand(c,"DEL mylist"); + freeReplyObject(reply); + for (j = 0; j < 10; j++) { + char buf[64]; + + snprintf(buf,64,"%u",j); + reply = redisCommand(c,"LPUSH mylist element-%s", buf); + freeReplyObject(reply); + } + + /* Let's check what we have inside the list */ + reply = redisCommand(c,"LRANGE mylist 0 -1"); + if (reply->type == REDIS_REPLY_ARRAY) { + for (j = 0; j < reply->elements; j++) { + printf("%u) %s\n", j, reply->element[j]->str); + } + } + freeReplyObject(reply); + + /* Disconnects and frees the context */ + redisFree(c); + + return 0; +} diff --git a/deps/hiredis/examples/example.c b/deps/hiredis/examples/example.c index 4d494c55a..0e93fc8b3 100644 --- a/deps/hiredis/examples/example.c +++ b/deps/hiredis/examples/example.c @@ -5,14 +5,27 @@ #include int main(int argc, char **argv) { - unsigned int j; + unsigned int j, isunix = 0; redisContext *c; redisReply *reply; const char *hostname = (argc > 1) ? argv[1] : "127.0.0.1"; + + if (argc > 2) { + if (*argv[2] == 'u' || *argv[2] == 'U') { + isunix = 1; + /* in this case, host is the path to the unix socket */ + printf("Will connect to unix socket @%s\n", hostname); + } + } + int port = (argc > 2) ? atoi(argv[2]) : 6379; struct timeval timeout = { 1, 500000 }; // 1.5 seconds - c = redisConnectWithTimeout(hostname, port, timeout); + if (isunix) { + c = redisConnectUnixWithTimeout(hostname, timeout); + } else { + c = redisConnectWithTimeout(hostname, port, timeout); + } if (c == NULL || c->err) { if (c) { printf("Connection error: %s\n", c->errstr); diff --git a/deps/hiredis/hiredis.c b/deps/hiredis/hiredis.c index 0947d1ed7..abd94c01d 100644 --- a/deps/hiredis/hiredis.c +++ b/deps/hiredis/hiredis.c @@ -34,7 +34,6 @@ #include "fmacros.h" #include #include -#include #include #include #include @@ -42,10 +41,20 @@ #include "hiredis.h" #include "net.h" #include "sds.h" +#include "async.h" +#include "win32.h" + +static redisContextFuncs redisContextDefaultFuncs = { + .free_privdata = NULL, + .async_read = redisAsyncRead, + .async_write = redisAsyncWrite, + .read = redisNetRead, + .write = redisNetWrite +}; static redisReply *createReplyObject(int type); static void *createStringObject(const redisReadTask *task, char *str, size_t len); -static void *createArrayObject(const redisReadTask *task, int elements); +static void *createArrayObject(const redisReadTask *task, size_t elements); static void *createIntegerObject(const redisReadTask *task, long long value); static void *createDoubleObject(const redisReadTask *task, double value, char *str, size_t len); static void *createNilObject(const redisReadTask *task); @@ -112,21 +121,34 @@ static void *createStringObject(const redisReadTask *task, char *str, size_t len if (r == NULL) return NULL; - buf = malloc(len+1); - if (buf == NULL) { - freeReplyObject(r); - return NULL; - } - assert(task->type == REDIS_REPLY_ERROR || task->type == REDIS_REPLY_STATUS || - task->type == REDIS_REPLY_STRING); + task->type == REDIS_REPLY_STRING || + task->type == REDIS_REPLY_VERB); /* Copy string value */ - memcpy(buf,str,len); - buf[len] = '\0'; + if (task->type == REDIS_REPLY_VERB) { + buf = malloc(len-4+1); /* Skip 4 bytes of verbatim type header. */ + if (buf == NULL) { + freeReplyObject(r); + return NULL; + } + memcpy(r->vtype,str,3); + r->vtype[3] = '\0'; + memcpy(buf,str+4,len-4); + buf[len-4] = '\0'; + r->len = len-4; + } else { + buf = malloc(len+1); + if (buf == NULL) { + freeReplyObject(r); + return NULL; + } + memcpy(buf,str,len); + buf[len] = '\0'; + r->len = len; + } r->str = buf; - r->len = len; if (task->parent) { parent = task->parent->obj; @@ -138,7 +160,7 @@ static void *createStringObject(const redisReadTask *task, char *str, size_t len return r; } -static void *createArrayObject(const redisReadTask *task, int elements) { +static void *createArrayObject(const redisReadTask *task, size_t elements) { redisReply *r, *parent; r = createReplyObject(task->type); @@ -649,29 +671,30 @@ redisReader *redisReaderCreate(void) { return redisReaderCreateWithFunctions(&defaultFunctions); } -static redisContext *redisContextInit(void) { +static redisContext *redisContextInit(const redisOptions *options) { redisContext *c; - c = calloc(1,sizeof(redisContext)); + c = calloc(1, sizeof(*c)); if (c == NULL) return NULL; + c->funcs = &redisContextDefaultFuncs; c->obuf = sdsempty(); c->reader = redisReaderCreate(); + c->fd = REDIS_INVALID_FD; if (c->obuf == NULL || c->reader == NULL) { redisFree(c); return NULL; } - + (void)options; /* options are used in other functions */ return c; } void redisFree(redisContext *c) { if (c == NULL) return; - if (c->fd > 0) - close(c->fd); + redisNetClose(c); sdsfree(c->obuf); redisReaderFree(c->reader); @@ -680,12 +703,16 @@ void redisFree(redisContext *c) { free(c->unix_sock.path); free(c->timeout); free(c->saddr); + if (c->funcs->free_privdata) { + c->funcs->free_privdata(c->privdata); + } + memset(c, 0xff, sizeof(*c)); free(c); } -int redisFreeKeepFd(redisContext *c) { - int fd = c->fd; - c->fd = -1; +redisFD redisFreeKeepFd(redisContext *c) { + redisFD fd = c->fd; + c->fd = REDIS_INVALID_FD; redisFree(c); return fd; } @@ -694,10 +721,13 @@ int redisReconnect(redisContext *c) { c->err = 0; memset(c->errstr, '\0', strlen(c->errstr)); - if (c->fd > 0) { - close(c->fd); + if (c->privdata && c->funcs->free_privdata) { + c->funcs->free_privdata(c->privdata); + c->privdata = NULL; } + redisNetClose(c); + sdsfree(c->obuf); redisReaderFree(c->reader); @@ -718,112 +748,107 @@ int redisReconnect(redisContext *c) { return REDIS_ERR; } +redisContext *redisConnectWithOptions(const redisOptions *options) { + redisContext *c = redisContextInit(options); + if (c == NULL) { + return NULL; + } + if (!(options->options & REDIS_OPT_NONBLOCK)) { + c->flags |= REDIS_BLOCK; + } + if (options->options & REDIS_OPT_REUSEADDR) { + c->flags |= REDIS_REUSEADDR; + } + if (options->options & REDIS_OPT_NOAUTOFREE) { + c->flags |= REDIS_NO_AUTO_FREE; + } + + if (options->type == REDIS_CONN_TCP) { + redisContextConnectBindTcp(c, options->endpoint.tcp.ip, + options->endpoint.tcp.port, options->timeout, + options->endpoint.tcp.source_addr); + } else if (options->type == REDIS_CONN_UNIX) { + redisContextConnectUnix(c, options->endpoint.unix_socket, + options->timeout); + } else if (options->type == REDIS_CONN_USERFD) { + c->fd = options->endpoint.fd; + c->flags |= REDIS_CONNECTED; + } else { + // Unknown type - FIXME - FREE + return NULL; + } + if (options->timeout != NULL && (c->flags & REDIS_BLOCK) && c->fd != REDIS_INVALID_FD) { + redisContextSetTimeout(c, *options->timeout); + } + return c; +} + /* Connect to a Redis instance. On error the field error in the returned * context will be set to the return value of the error function. * When no set of reply functions is given, the default set will be used. */ redisContext *redisConnect(const char *ip, int port) { - redisContext *c; - - c = redisContextInit(); - if (c == NULL) - return NULL; - - c->flags |= REDIS_BLOCK; - redisContextConnectTcp(c,ip,port,NULL); - return c; + redisOptions options = {0}; + REDIS_OPTIONS_SET_TCP(&options, ip, port); + return redisConnectWithOptions(&options); } redisContext *redisConnectWithTimeout(const char *ip, int port, const struct timeval tv) { - redisContext *c; - - c = redisContextInit(); - if (c == NULL) - return NULL; - - c->flags |= REDIS_BLOCK; - redisContextConnectTcp(c,ip,port,&tv); - return c; + redisOptions options = {0}; + REDIS_OPTIONS_SET_TCP(&options, ip, port); + options.timeout = &tv; + return redisConnectWithOptions(&options); } redisContext *redisConnectNonBlock(const char *ip, int port) { - redisContext *c; - - c = redisContextInit(); - if (c == NULL) - return NULL; - - c->flags &= ~REDIS_BLOCK; - redisContextConnectTcp(c,ip,port,NULL); - return c; + redisOptions options = {0}; + REDIS_OPTIONS_SET_TCP(&options, ip, port); + options.options |= REDIS_OPT_NONBLOCK; + return redisConnectWithOptions(&options); } redisContext *redisConnectBindNonBlock(const char *ip, int port, const char *source_addr) { - redisContext *c = redisContextInit(); - if (c == NULL) - return NULL; - c->flags &= ~REDIS_BLOCK; - redisContextConnectBindTcp(c,ip,port,NULL,source_addr); - return c; + redisOptions options = {0}; + REDIS_OPTIONS_SET_TCP(&options, ip, port); + options.endpoint.tcp.source_addr = source_addr; + options.options |= REDIS_OPT_NONBLOCK; + return redisConnectWithOptions(&options); } redisContext *redisConnectBindNonBlockWithReuse(const char *ip, int port, const char *source_addr) { - redisContext *c = redisContextInit(); - if (c == NULL) - return NULL; - c->flags &= ~REDIS_BLOCK; - c->flags |= REDIS_REUSEADDR; - redisContextConnectBindTcp(c,ip,port,NULL,source_addr); - return c; + redisOptions options = {0}; + REDIS_OPTIONS_SET_TCP(&options, ip, port); + options.endpoint.tcp.source_addr = source_addr; + options.options |= REDIS_OPT_NONBLOCK|REDIS_OPT_REUSEADDR; + return redisConnectWithOptions(&options); } redisContext *redisConnectUnix(const char *path) { - redisContext *c; - - c = redisContextInit(); - if (c == NULL) - return NULL; - - c->flags |= REDIS_BLOCK; - redisContextConnectUnix(c,path,NULL); - return c; + redisOptions options = {0}; + REDIS_OPTIONS_SET_UNIX(&options, path); + return redisConnectWithOptions(&options); } redisContext *redisConnectUnixWithTimeout(const char *path, const struct timeval tv) { - redisContext *c; - - c = redisContextInit(); - if (c == NULL) - return NULL; - - c->flags |= REDIS_BLOCK; - redisContextConnectUnix(c,path,&tv); - return c; + redisOptions options = {0}; + REDIS_OPTIONS_SET_UNIX(&options, path); + options.timeout = &tv; + return redisConnectWithOptions(&options); } redisContext *redisConnectUnixNonBlock(const char *path) { - redisContext *c; - - c = redisContextInit(); - if (c == NULL) - return NULL; - - c->flags &= ~REDIS_BLOCK; - redisContextConnectUnix(c,path,NULL); - return c; + redisOptions options = {0}; + REDIS_OPTIONS_SET_UNIX(&options, path); + options.options |= REDIS_OPT_NONBLOCK; + return redisConnectWithOptions(&options); } -redisContext *redisConnectFd(int fd) { - redisContext *c; - - c = redisContextInit(); - if (c == NULL) - return NULL; - - c->fd = fd; - c->flags |= REDIS_BLOCK | REDIS_CONNECTED; - return c; +redisContext *redisConnectFd(redisFD fd) { + redisOptions options = {0}; + options.type = REDIS_CONN_USERFD; + options.endpoint.fd = fd; + return redisConnectWithOptions(&options); } /* Set read/write timeout on a blocking socket. */ @@ -853,22 +878,15 @@ int redisBufferRead(redisContext *c) { if (c->err) return REDIS_ERR; - nread = read(c->fd,buf,sizeof(buf)); - if (nread == -1) { - if ((errno == EAGAIN && !(c->flags & REDIS_BLOCK)) || (errno == EINTR)) { - /* Try again later */ + nread = c->funcs->read(c, buf, sizeof(buf)); + if (nread > 0) { + if (redisReaderFeed(c->reader, buf, nread) != REDIS_OK) { + __redisSetError(c, c->reader->err, c->reader->errstr); + return REDIS_ERR; } else { - __redisSetError(c,REDIS_ERR_IO,NULL); - return REDIS_ERR; } - } else if (nread == 0) { - __redisSetError(c,REDIS_ERR_EOF,"Server closed the connection"); + } else if (nread < 0) { return REDIS_ERR; - } else { - if (redisReaderFeed(c->reader,buf,nread) != REDIS_OK) { - __redisSetError(c,c->reader->err,c->reader->errstr); - return REDIS_ERR; - } } return REDIS_OK; } @@ -883,21 +901,15 @@ int redisBufferRead(redisContext *c) { * c->errstr to hold the appropriate error string. */ int redisBufferWrite(redisContext *c, int *done) { - int nwritten; /* Return early when the context has seen an error. */ if (c->err) return REDIS_ERR; if (sdslen(c->obuf) > 0) { - nwritten = write(c->fd,c->obuf,sdslen(c->obuf)); - if (nwritten == -1) { - if ((errno == EAGAIN && !(c->flags & REDIS_BLOCK)) || (errno == EINTR)) { - /* Try again later */ - } else { - __redisSetError(c,REDIS_ERR_IO,NULL); - return REDIS_ERR; - } + int nwritten = c->funcs->write(c); + if (nwritten < 0) { + return REDIS_ERR; } else if (nwritten > 0) { if (nwritten == (signed)sdslen(c->obuf)) { sdsfree(c->obuf); diff --git a/deps/hiredis/hiredis.h b/deps/hiredis/hiredis.h index 47d7982e9..69dc39c5e 100644 --- a/deps/hiredis/hiredis.h +++ b/deps/hiredis/hiredis.h @@ -35,7 +35,11 @@ #define __HIREDIS_H #include "read.h" #include /* for va_list */ +#ifndef _MSC_VER #include /* for struct timeval */ +#else +struct timeval; /* forward declaration */ +#endif #include /* uintXX_t, etc */ #include "sds.h" /* for sds */ @@ -74,6 +78,12 @@ /* Flag that is set when we should set SO_REUSEADDR before calling bind() */ #define REDIS_REUSEADDR 0x80 +/** + * Flag that indicates the user does not want the context to + * be automatically freed upon error + */ +#define REDIS_NO_AUTO_FREE 0x200 + #define REDIS_KEEPALIVE_INTERVAL 15 /* seconds */ /* number of times we retry to connect in the case of EADDRNOTAVAIL and @@ -92,6 +102,8 @@ typedef struct redisReply { size_t len; /* Length of string */ char *str; /* Used for REDIS_REPLY_ERROR, REDIS_REPLY_STRING and REDIS_REPLY_DOUBLE (in additionl to dval). */ + char vtype[4]; /* Used for REDIS_REPLY_VERB, contains the null + terminated 3 character content type, such as "txt". */ size_t elements; /* number of elements, for REDIS_REPLY_ARRAY */ struct redisReply **element; /* elements vector for REDIS_REPLY_ARRAY */ } redisReply; @@ -111,14 +123,93 @@ void redisFreeSdsCommand(sds cmd); enum redisConnectionType { REDIS_CONN_TCP, - REDIS_CONN_UNIX + REDIS_CONN_UNIX, + REDIS_CONN_USERFD }; +struct redisSsl; + +#define REDIS_OPT_NONBLOCK 0x01 +#define REDIS_OPT_REUSEADDR 0x02 + +/** + * Don't automatically free the async object on a connection failure, + * or other implicit conditions. Only free on an explicit call to disconnect() or free() + */ +#define REDIS_OPT_NOAUTOFREE 0x04 + +/* In Unix systems a file descriptor is a regular signed int, with -1 + * representing an invalid descriptor. In Windows it is a SOCKET + * (32- or 64-bit unsigned integer depending on the architecture), where + * all bits set (~0) is INVALID_SOCKET. */ +#ifndef _WIN32 +typedef int redisFD; +#define REDIS_INVALID_FD -1 +#else +#ifdef _WIN64 +typedef unsigned long long redisFD; /* SOCKET = 64-bit UINT_PTR */ +#else +typedef unsigned long redisFD; /* SOCKET = 32-bit UINT_PTR */ +#endif +#define REDIS_INVALID_FD ((redisFD)(~0)) /* INVALID_SOCKET */ +#endif + +typedef struct { + /* + * the type of connection to use. This also indicates which + * `endpoint` member field to use + */ + int type; + /* bit field of REDIS_OPT_xxx */ + int options; + /* timeout value. if NULL, no timeout is used */ + const struct timeval *timeout; + union { + /** use this field for tcp/ip connections */ + struct { + const char *source_addr; + const char *ip; + int port; + } tcp; + /** use this field for unix domain sockets */ + const char *unix_socket; + /** + * use this field to have hiredis operate an already-open + * file descriptor */ + redisFD fd; + } endpoint; +} redisOptions; + +/** + * Helper macros to initialize options to their specified fields. + */ +#define REDIS_OPTIONS_SET_TCP(opts, ip_, port_) \ + (opts)->type = REDIS_CONN_TCP; \ + (opts)->endpoint.tcp.ip = ip_; \ + (opts)->endpoint.tcp.port = port_; + +#define REDIS_OPTIONS_SET_UNIX(opts, path) \ + (opts)->type = REDIS_CONN_UNIX; \ + (opts)->endpoint.unix_socket = path; + +struct redisAsyncContext; +struct redisContext; + +typedef struct redisContextFuncs { + void (*free_privdata)(void *); + void (*async_read)(struct redisAsyncContext *); + void (*async_write)(struct redisAsyncContext *); + int (*read)(struct redisContext *, char *, size_t); + int (*write)(struct redisContext *); +} redisContextFuncs; + /* Context for a connection to Redis */ typedef struct redisContext { + const redisContextFuncs *funcs; /* Function table */ + int err; /* Error flags, 0 when there is no error */ char errstr[128]; /* String representation of error when applicable */ - int fd; + redisFD fd; int flags; char *obuf; /* Write buffer */ redisReader *reader; /* Protocol reader */ @@ -139,8 +230,12 @@ typedef struct redisContext { /* For non-blocking connect */ struct sockadr *saddr; size_t addrlen; + + /* Additional private data for hiredis addons such as SSL */ + void *privdata; } redisContext; +redisContext *redisConnectWithOptions(const redisOptions *options); 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); @@ -151,7 +246,7 @@ redisContext *redisConnectBindNonBlockWithReuse(const char *ip, int port, redisContext *redisConnectUnix(const char *path); redisContext *redisConnectUnixWithTimeout(const char *path, const struct timeval tv); redisContext *redisConnectUnixNonBlock(const char *path); -redisContext *redisConnectFd(int fd); +redisContext *redisConnectFd(redisFD fd); /** * Reconnect the given context using the saved information. @@ -167,7 +262,7 @@ int redisReconnect(redisContext *c); int redisSetTimeout(redisContext *c, const struct timeval tv); int redisEnableKeepAlive(redisContext *c); void redisFree(redisContext *c); -int redisFreeKeepFd(redisContext *c); +redisFD redisFreeKeepFd(redisContext *c); int redisBufferRead(redisContext *c); int redisBufferWrite(redisContext *c, int *done); diff --git a/deps/hiredis/hiredis.pc.in b/deps/hiredis/hiredis.pc.in new file mode 100644 index 000000000..140b040f1 --- /dev/null +++ b/deps/hiredis/hiredis.pc.in @@ -0,0 +1,11 @@ +prefix=@CMAKE_INSTALL_PREFIX@ +exec_prefix=${prefix} +libdir=${exec_prefix}/lib +includedir=${prefix}/include +pkgincludedir=${includedir}/hiredis + +Name: hiredis +Description: Minimalistic C client library for Redis. +Version: @PROJECT_VERSION@ +Libs: -L${libdir} -lhiredis +Cflags: -I${pkgincludedir} -D_FILE_OFFSET_BITS=64 diff --git a/deps/hiredis/hiredis_ssl.h b/deps/hiredis/hiredis_ssl.h new file mode 100644 index 000000000..f844f9548 --- /dev/null +++ b/deps/hiredis/hiredis_ssl.h @@ -0,0 +1,53 @@ + +/* + * Copyright (c) 2019, Redis Labs + * + * 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_SSL_H +#define __HIREDIS_SSL_H + +/* This is the underlying struct for SSL in ssl.h, which is not included to + * keep build dependencies short here. + */ +struct ssl_st; + +/** + * Secure the connection using SSL. This should be done before any command is + * executed on the connection. + */ +int redisSecureConnection(redisContext *c, const char *capath, const char *certpath, + const char *keypath, const char *servername); + +/** + * Initiate SSL/TLS negotiation on a provided context. + */ + +int redisInitiateSSL(redisContext *c, struct ssl_st *ssl); + +#endif /* __HIREDIS_SSL_H */ diff --git a/deps/hiredis/hiredis_ssl.pc.in b/deps/hiredis/hiredis_ssl.pc.in new file mode 100644 index 000000000..588a978a5 --- /dev/null +++ b/deps/hiredis/hiredis_ssl.pc.in @@ -0,0 +1,12 @@ +prefix=@CMAKE_INSTALL_PREFIX@ +exec_prefix=${prefix} +libdir=${exec_prefix}/lib +includedir=${prefix}/include +pkgincludedir=${includedir}/hiredis + +Name: hiredis_ssl +Description: SSL Support for hiredis. +Version: @PROJECT_VERSION@ +Requires: hiredis +Libs: -L${libdir} -lhiredis_ssl +Libs.private: -lssl -lcrypto diff --git a/deps/hiredis/net.c b/deps/hiredis/net.c index a4b3abc6d..e5f40b0a4 100644 --- a/deps/hiredis/net.c +++ b/deps/hiredis/net.c @@ -34,36 +34,64 @@ #include "fmacros.h" #include -#include -#include -#include -#include -#include -#include -#include #include #include -#include #include #include #include -#include #include #include #include "net.h" #include "sds.h" +#include "sockcompat.h" +#include "win32.h" /* Defined in hiredis.c */ void __redisSetError(redisContext *c, int type, const char *str); -static void redisContextCloseFd(redisContext *c) { - if (c && c->fd >= 0) { +void redisNetClose(redisContext *c) { + if (c && c->fd != REDIS_INVALID_FD) { close(c->fd); - c->fd = -1; + c->fd = REDIS_INVALID_FD; } } +int redisNetRead(redisContext *c, char *buf, size_t bufcap) { + int nread = recv(c->fd, buf, bufcap, 0); + if (nread == -1) { + if ((errno == EWOULDBLOCK && !(c->flags & REDIS_BLOCK)) || (errno == EINTR)) { + /* Try again later */ + return 0; + } else if(errno == ETIMEDOUT && (c->flags & REDIS_BLOCK)) { + /* especially in windows */ + __redisSetError(c, REDIS_ERR_TIMEOUT, "recv timeout"); + return -1; + } else { + __redisSetError(c, REDIS_ERR_IO, NULL); + return -1; + } + } else if (nread == 0) { + __redisSetError(c, REDIS_ERR_EOF, "Server closed the connection"); + return -1; + } else { + return nread; + } +} + +int redisNetWrite(redisContext *c) { + int nwritten = send(c->fd, c->obuf, sdslen(c->obuf), 0); + if (nwritten < 0) { + if ((errno == EWOULDBLOCK && !(c->flags & REDIS_BLOCK)) || (errno == EINTR)) { + /* Try again later */ + } else { + __redisSetError(c, REDIS_ERR_IO, NULL); + return -1; + } + } + return nwritten; +} + static void __redisSetErrorFromErrno(redisContext *c, int type, const char *prefix) { int errorno = errno; /* snprintf() may change errno */ char buf[128] = { 0 }; @@ -79,15 +107,15 @@ static int redisSetReuseAddr(redisContext *c) { int on = 1; if (setsockopt(c->fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1) { __redisSetErrorFromErrno(c,REDIS_ERR_IO,NULL); - redisContextCloseFd(c); + redisNetClose(c); return REDIS_ERR; } return REDIS_OK; } static int redisCreateSocket(redisContext *c, int type) { - int s; - if ((s = socket(type, SOCK_STREAM, 0)) == -1) { + redisFD s; + if ((s = socket(type, SOCK_STREAM, 0)) == REDIS_INVALID_FD) { __redisSetErrorFromErrno(c,REDIS_ERR_IO,NULL); return REDIS_ERR; } @@ -101,6 +129,7 @@ static int redisCreateSocket(redisContext *c, int type) { } static int redisSetBlocking(redisContext *c, int blocking) { +#ifndef _WIN32 int flags; /* Set the socket nonblocking. @@ -108,7 +137,7 @@ static int redisSetBlocking(redisContext *c, int blocking) { * interrupted by a signal. */ if ((flags = fcntl(c->fd, F_GETFL)) == -1) { __redisSetErrorFromErrno(c,REDIS_ERR_IO,"fcntl(F_GETFL)"); - redisContextCloseFd(c); + redisNetClose(c); return REDIS_ERR; } @@ -119,15 +148,23 @@ static int redisSetBlocking(redisContext *c, int blocking) { if (fcntl(c->fd, F_SETFL, flags) == -1) { __redisSetErrorFromErrno(c,REDIS_ERR_IO,"fcntl(F_SETFL)"); - redisContextCloseFd(c); + redisNetClose(c); return REDIS_ERR; } +#else + u_long mode = blocking ? 0 : 1; + if (ioctl(c->fd, FIONBIO, &mode) == -1) { + __redisSetErrorFromErrno(c, REDIS_ERR_IO, "ioctl(FIONBIO)"); + redisNetClose(c); + return REDIS_ERR; + } +#endif /* _WIN32 */ return REDIS_OK; } int redisKeepAlive(redisContext *c, int interval) { int val = 1; - int fd = c->fd; + redisFD fd = c->fd; if (setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &val, sizeof(val)) == -1){ __redisSetError(c,REDIS_ERR_OTHER,strerror(errno)); @@ -170,7 +207,7 @@ static int redisSetTcpNoDelay(redisContext *c) { int yes = 1; if (setsockopt(c->fd, IPPROTO_TCP, TCP_NODELAY, &yes, sizeof(yes)) == -1) { __redisSetErrorFromErrno(c,REDIS_ERR_IO,"setsockopt(TCP_NODELAY)"); - redisContextCloseFd(c); + redisNetClose(c); return REDIS_ERR; } return REDIS_OK; @@ -212,12 +249,12 @@ static int redisContextWaitReady(redisContext *c, long msec) { if ((res = poll(wfd, 1, msec)) == -1) { __redisSetErrorFromErrno(c, REDIS_ERR_IO, "poll(2)"); - redisContextCloseFd(c); + redisNetClose(c); return REDIS_ERR; } else if (res == 0) { errno = ETIMEDOUT; __redisSetErrorFromErrno(c,REDIS_ERR_IO,NULL); - redisContextCloseFd(c); + redisNetClose(c); return REDIS_ERR; } @@ -230,7 +267,7 @@ static int redisContextWaitReady(redisContext *c, long msec) { } __redisSetErrorFromErrno(c,REDIS_ERR_IO,NULL); - redisContextCloseFd(c); + redisNetClose(c); return REDIS_ERR; } @@ -277,11 +314,18 @@ int redisCheckSocketError(redisContext *c) { } int redisContextSetTimeout(redisContext *c, const struct timeval tv) { - if (setsockopt(c->fd,SOL_SOCKET,SO_RCVTIMEO,&tv,sizeof(tv)) == -1) { + const void *to_ptr = &tv; + size_t to_sz = sizeof(tv); +#ifdef _WIN32 + DWORD timeout_msec = tv.tv_sec * 1000 + tv.tv_usec / 1000; + to_ptr = &timeout_msec; + to_sz = sizeof(timeout_msec); +#endif + if (setsockopt(c->fd,SOL_SOCKET,SO_RCVTIMEO,to_ptr,to_sz) == -1) { __redisSetErrorFromErrno(c,REDIS_ERR_IO,"setsockopt(SO_RCVTIMEO)"); return REDIS_ERR; } - if (setsockopt(c->fd,SOL_SOCKET,SO_SNDTIMEO,&tv,sizeof(tv)) == -1) { + if (setsockopt(c->fd,SOL_SOCKET,SO_SNDTIMEO,to_ptr,to_sz) == -1) { __redisSetErrorFromErrno(c,REDIS_ERR_IO,"setsockopt(SO_SNDTIMEO)"); return REDIS_ERR; } @@ -291,7 +335,8 @@ 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, n; + redisFD s; + int rv, n; char _port[6]; /* strlen("65535"); */ struct addrinfo hints, *servinfo, *bservinfo, *p, *b; int blocking = (c->flags & REDIS_BLOCK); @@ -360,7 +405,7 @@ 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) + if ((s = socket(p->ai_family,p->ai_socktype,p->ai_protocol)) == REDIS_INVALID_FD) continue; c->fd = s; @@ -401,16 +446,14 @@ addrretry: } /* For repeat connection */ - if (c->saddr) { - free(c->saddr); - } + free(c->saddr); c->saddr = malloc(p->ai_addrlen); memcpy(c->saddr, p->ai_addr, p->ai_addrlen); c->addrlen = p->ai_addrlen; if (connect(s,p->ai_addr,p->ai_addrlen) == -1) { if (errno == EHOSTUNREACH) { - redisContextCloseFd(c); + redisNetClose(c); continue; } else if (errno == EINPROGRESS) { if (blocking) { @@ -424,7 +467,7 @@ addrretry: if (++reuses >= REDIS_CONNECT_RETRIES) { goto error; } else { - redisContextCloseFd(c); + redisNetClose(c); goto addrretry; } } else { @@ -471,8 +514,9 @@ int redisContextConnectBindTcp(redisContext *c, const char *addr, int port, } int redisContextConnectUnix(redisContext *c, const char *path, const struct timeval *timeout) { +#ifndef _WIN32 int blocking = (c->flags & REDIS_BLOCK); - struct sockaddr_un sa; + struct sockaddr_un *sa; long timeout_msec = -1; if (redisCreateSocket(c,AF_UNIX) < 0) @@ -499,9 +543,11 @@ int redisContextConnectUnix(redisContext *c, const char *path, const struct time if (redisContextTimeoutMsec(c,&timeout_msec) != REDIS_OK) return REDIS_ERR; - sa.sun_family = AF_UNIX; - strncpy(sa.sun_path,path,sizeof(sa.sun_path)-1); - if (connect(c->fd, (struct sockaddr*)&sa, sizeof(sa)) == -1) { + sa = (struct sockaddr_un*)(c->saddr = malloc(sizeof(struct sockaddr_un))); + c->addrlen = sizeof(struct sockaddr_un); + sa->sun_family = AF_UNIX; + 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 { @@ -516,4 +562,10 @@ int redisContextConnectUnix(redisContext *c, const char *path, const struct time c->flags |= REDIS_CONNECTED; return REDIS_OK; +#else + /* We currently do not support Unix sockets for Windows. */ + /* TODO(m): https://devblogs.microsoft.com/commandline/af_unix-comes-to-windows/ */ + errno = EPROTONOSUPPORT; + return REDIS_ERR; +#endif /* _WIN32 */ } diff --git a/deps/hiredis/net.h b/deps/hiredis/net.h index a11594e68..a4393c06b 100644 --- a/deps/hiredis/net.h +++ b/deps/hiredis/net.h @@ -37,6 +37,10 @@ #include "hiredis.h" +void redisNetClose(redisContext *c); +int redisNetRead(redisContext *c, char *buf, size_t bufcap); +int redisNetWrite(redisContext *c); + int redisCheckSocketError(redisContext *c); int redisContextSetTimeout(redisContext *c, const struct timeval tv); int redisContextConnectTcp(redisContext *c, const char *addr, int port, const struct timeval *timeout); diff --git a/deps/hiredis/read.c b/deps/hiredis/read.c index c75c3435f..b9853ea9a 100644 --- a/deps/hiredis/read.c +++ b/deps/hiredis/read.c @@ -34,6 +34,7 @@ #include #ifndef _MSC_VER #include +#include #endif #include #include @@ -43,6 +44,7 @@ #include "read.h" #include "sds.h" +#include "win32.h" static void __redisReaderSetError(redisReader *r, int type, const char *str) { size_t len; @@ -293,9 +295,9 @@ static int processLineItem(redisReader *r) { buf[len] = '\0'; if (strcasecmp(buf,",inf") == 0) { - d = 1.0/0.0; /* Positive infinite. */ + d = INFINITY; /* Positive infinite. */ } else if (strcasecmp(buf,",-inf") == 0) { - d = -1.0/0.0; /* Nevative infinite. */ + d = -INFINITY; /* Nevative infinite. */ } else { d = strtod((char*)buf,&eptr); if (buf[0] == '\0' || eptr[0] != '\0' || isnan(d)) { @@ -378,10 +380,18 @@ static int processBulkItem(redisReader *r) { /* Only continue when the buffer contains the entire bulk item. */ bytelen += len+2; /* include \r\n */ if (r->pos+bytelen <= r->len) { + if ((cur->type == REDIS_REPLY_VERB && len < 4) || + (cur->type == REDIS_REPLY_VERB && s[5] != ':')) + { + __redisReaderSetError(r,REDIS_ERR_PROTOCOL, + "Verbatim string 4 bytes of content type are " + "missing or incorrectly encoded."); + return REDIS_ERR; + } if (r->fn && r->fn->createString) obj = r->fn->createString(cur,s+2,len); else - obj = (void*)REDIS_REPLY_STRING; + obj = (void*)(long)cur->type; success = 1; } } @@ -429,7 +439,7 @@ static int processAggregateItem(redisReader *r) { root = (r->ridx == 0); - if (elements < -1 || elements > INT_MAX) { + if (elements < -1 || (LLONG_MAX > SIZE_MAX && elements > SIZE_MAX)) { __redisReaderSetError(r,REDIS_ERR_PROTOCOL, "Multi-bulk length out of range"); return REDIS_ERR; @@ -522,6 +532,9 @@ static int processItem(redisReader *r) { case '#': cur->type = REDIS_REPLY_BOOL; break; + case '=': + cur->type = REDIS_REPLY_VERB; + break; default: __redisReaderSetErrorProtocolByte(r,*p); return REDIS_ERR; @@ -542,6 +555,7 @@ static int processItem(redisReader *r) { case REDIS_REPLY_BOOL: return processLineItem(r); case REDIS_REPLY_STRING: + case REDIS_REPLY_VERB: return processBulkItem(r); case REDIS_REPLY_ARRAY: case REDIS_REPLY_MAP: @@ -656,8 +670,11 @@ int redisReaderGetReply(redisReader *r, void **reply) { /* Emit a reply when there is one. */ if (r->ridx == -1) { - if (reply != NULL) + if (reply != NULL) { *reply = r->reply; + } else if (r->reply != NULL && r->fn && r->fn->freeObject) { + r->fn->freeObject(r->reply); + } r->reply = NULL; } return REDIS_OK; diff --git a/deps/hiredis/read.h b/deps/hiredis/read.h index f3d075843..58105312a 100644 --- a/deps/hiredis/read.h +++ b/deps/hiredis/read.h @@ -45,6 +45,7 @@ #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_TIMEOUT 6 /* Timed out */ #define REDIS_ERR_OTHER 2 /* Everything else... */ #define REDIS_REPLY_STRING 1 @@ -55,12 +56,12 @@ #define REDIS_REPLY_ERROR 6 #define REDIS_REPLY_DOUBLE 7 #define REDIS_REPLY_BOOL 8 -#define REDIS_REPLY_VERB 9 #define REDIS_REPLY_MAP 9 #define REDIS_REPLY_SET 10 #define REDIS_REPLY_ATTR 11 #define REDIS_REPLY_PUSH 12 #define REDIS_REPLY_BIGNUM 13 +#define REDIS_REPLY_VERB 14 #define REDIS_READER_MAX_BUF (1024*16) /* Default max unused reader buffer. */ @@ -79,7 +80,7 @@ typedef struct redisReadTask { typedef struct redisReplyObjectFunctions { void *(*createString)(const redisReadTask*, char*, size_t); - void *(*createArray)(const redisReadTask*, int); + void *(*createArray)(const redisReadTask*, size_t); void *(*createInteger)(const redisReadTask*, long long); void *(*createDouble)(const redisReadTask*, double, char*, size_t); void *(*createNil)(const redisReadTask*); diff --git a/deps/hiredis/sds.c b/deps/hiredis/sds.c index 44777b10c..6cf75841c 100644 --- a/deps/hiredis/sds.c +++ b/deps/hiredis/sds.c @@ -1035,7 +1035,7 @@ sds *sdssplitargs(const char *line, int *argc) { s_free(vector); return NULL; } - + vector = new_vector; vector[*argc] = current; (*argc)++; diff --git a/deps/hiredis/sds.h b/deps/hiredis/sds.h index 13be75a9f..3f9a96457 100644 --- a/deps/hiredis/sds.h +++ b/deps/hiredis/sds.h @@ -34,6 +34,9 @@ #define __SDS_H #define SDS_MAX_PREALLOC (1024*1024) +#ifdef _MSC_VER +#define __attribute__(x) +#endif #include #include @@ -132,20 +135,20 @@ static inline void sdssetlen(sds s, size_t newlen) { case SDS_TYPE_5: { unsigned char *fp = ((unsigned char*)s)-1; - *fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS); + *fp = (unsigned char)(SDS_TYPE_5 | (newlen << SDS_TYPE_BITS)); } break; case SDS_TYPE_8: - SDS_HDR(8,s)->len = newlen; + SDS_HDR(8,s)->len = (uint8_t)newlen; break; case SDS_TYPE_16: - SDS_HDR(16,s)->len = newlen; + SDS_HDR(16,s)->len = (uint16_t)newlen; break; case SDS_TYPE_32: - SDS_HDR(32,s)->len = newlen; + SDS_HDR(32,s)->len = (uint32_t)newlen; break; case SDS_TYPE_64: - SDS_HDR(64,s)->len = newlen; + SDS_HDR(64,s)->len = (uint64_t)newlen; break; } } @@ -156,21 +159,21 @@ static inline void sdsinclen(sds s, size_t inc) { case SDS_TYPE_5: { unsigned char *fp = ((unsigned char*)s)-1; - unsigned char newlen = SDS_TYPE_5_LEN(flags)+inc; + unsigned char newlen = SDS_TYPE_5_LEN(flags)+(unsigned char)inc; *fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS); } break; case SDS_TYPE_8: - SDS_HDR(8,s)->len += inc; + SDS_HDR(8,s)->len += (uint8_t)inc; break; case SDS_TYPE_16: - SDS_HDR(16,s)->len += inc; + SDS_HDR(16,s)->len += (uint16_t)inc; break; case SDS_TYPE_32: - SDS_HDR(32,s)->len += inc; + SDS_HDR(32,s)->len += (uint32_t)inc; break; case SDS_TYPE_64: - SDS_HDR(64,s)->len += inc; + SDS_HDR(64,s)->len += (uint64_t)inc; break; } } @@ -200,16 +203,16 @@ static inline void sdssetalloc(sds s, size_t newlen) { /* Nothing to do, this type has no total allocation info. */ break; case SDS_TYPE_8: - SDS_HDR(8,s)->alloc = newlen; + SDS_HDR(8,s)->alloc = (uint8_t)newlen; break; case SDS_TYPE_16: - SDS_HDR(16,s)->alloc = newlen; + SDS_HDR(16,s)->alloc = (uint16_t)newlen; break; case SDS_TYPE_32: - SDS_HDR(32,s)->alloc = newlen; + SDS_HDR(32,s)->alloc = (uint32_t)newlen; break; case SDS_TYPE_64: - SDS_HDR(64,s)->alloc = newlen; + SDS_HDR(64,s)->alloc = (uint64_t)newlen; break; } } diff --git a/deps/hiredis/sockcompat.c b/deps/hiredis/sockcompat.c new file mode 100644 index 000000000..4cc2f414f --- /dev/null +++ b/deps/hiredis/sockcompat.c @@ -0,0 +1,248 @@ +/* + * Copyright (c) 2019, Marcus Geelnard + * + * 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 REDIS_SOCKCOMPAT_IMPLEMENTATION +#include "sockcompat.h" + +#ifdef _WIN32 +static int _wsaErrorToErrno(int err) { + switch (err) { + case WSAEWOULDBLOCK: + return EWOULDBLOCK; + case WSAEINPROGRESS: + return EINPROGRESS; + case WSAEALREADY: + return EALREADY; + case WSAENOTSOCK: + return ENOTSOCK; + case WSAEDESTADDRREQ: + return EDESTADDRREQ; + case WSAEMSGSIZE: + return EMSGSIZE; + case WSAEPROTOTYPE: + return EPROTOTYPE; + case WSAENOPROTOOPT: + return ENOPROTOOPT; + case WSAEPROTONOSUPPORT: + return EPROTONOSUPPORT; + case WSAEOPNOTSUPP: + return EOPNOTSUPP; + case WSAEAFNOSUPPORT: + return EAFNOSUPPORT; + case WSAEADDRINUSE: + return EADDRINUSE; + case WSAEADDRNOTAVAIL: + return EADDRNOTAVAIL; + case WSAENETDOWN: + return ENETDOWN; + case WSAENETUNREACH: + return ENETUNREACH; + case WSAENETRESET: + return ENETRESET; + case WSAECONNABORTED: + return ECONNABORTED; + case WSAECONNRESET: + return ECONNRESET; + case WSAENOBUFS: + return ENOBUFS; + case WSAEISCONN: + return EISCONN; + case WSAENOTCONN: + return ENOTCONN; + case WSAETIMEDOUT: + return ETIMEDOUT; + case WSAECONNREFUSED: + return ECONNREFUSED; + case WSAELOOP: + return ELOOP; + case WSAENAMETOOLONG: + return ENAMETOOLONG; + case WSAEHOSTUNREACH: + return EHOSTUNREACH; + case WSAENOTEMPTY: + return ENOTEMPTY; + default: + /* We just return a generic I/O error if we could not find a relevant error. */ + return EIO; + } +} + +static void _updateErrno(int success) { + errno = success ? 0 : _wsaErrorToErrno(WSAGetLastError()); +} + +static int _initWinsock() { + static int s_initialized = 0; + if (!s_initialized) { + static WSADATA wsadata; + int err = WSAStartup(MAKEWORD(2,2), &wsadata); + if (err != 0) { + errno = _wsaErrorToErrno(err); + return 0; + } + s_initialized = 1; + } + return 1; +} + +int win32_getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res) { + /* Note: This function is likely to be called before other functions, so run init here. */ + if (!_initWinsock()) { + return EAI_FAIL; + } + + switch (getaddrinfo(node, service, hints, res)) { + case 0: return 0; + case WSATRY_AGAIN: return EAI_AGAIN; + case WSAEINVAL: return EAI_BADFLAGS; + case WSAEAFNOSUPPORT: return EAI_FAMILY; + case WSA_NOT_ENOUGH_MEMORY: return EAI_MEMORY; + case WSAHOST_NOT_FOUND: return EAI_NONAME; + case WSATYPE_NOT_FOUND: return EAI_SERVICE; + case WSAESOCKTNOSUPPORT: return EAI_SOCKTYPE; + default: return EAI_FAIL; /* Including WSANO_RECOVERY */ + } +} + +const char *win32_gai_strerror(int errcode) { + switch (errcode) { + case 0: errcode = 0; break; + case EAI_AGAIN: errcode = WSATRY_AGAIN; break; + case EAI_BADFLAGS: errcode = WSAEINVAL; break; + case EAI_FAMILY: errcode = WSAEAFNOSUPPORT; break; + case EAI_MEMORY: errcode = WSA_NOT_ENOUGH_MEMORY; break; + case EAI_NONAME: errcode = WSAHOST_NOT_FOUND; break; + case EAI_SERVICE: errcode = WSATYPE_NOT_FOUND; break; + case EAI_SOCKTYPE: errcode = WSAESOCKTNOSUPPORT; break; + default: errcode = WSANO_RECOVERY; break; /* Including EAI_FAIL */ + } + return gai_strerror(errcode); +} + +void win32_freeaddrinfo(struct addrinfo *res) { + freeaddrinfo(res); +} + +SOCKET win32_socket(int domain, int type, int protocol) { + SOCKET s; + + /* Note: This function is likely to be called before other functions, so run init here. */ + if (!_initWinsock()) { + return INVALID_SOCKET; + } + + _updateErrno((s = socket(domain, type, protocol)) != INVALID_SOCKET); + return s; +} + +int win32_ioctl(SOCKET fd, unsigned long request, unsigned long *argp) { + int ret = ioctlsocket(fd, (long)request, argp); + _updateErrno(ret != SOCKET_ERROR); + return ret != SOCKET_ERROR ? ret : -1; +} + +int win32_bind(SOCKET sockfd, const struct sockaddr *addr, socklen_t addrlen) { + int ret = bind(sockfd, addr, addrlen); + _updateErrno(ret != SOCKET_ERROR); + return ret != SOCKET_ERROR ? ret : -1; +} + +int win32_connect(SOCKET sockfd, const struct sockaddr *addr, socklen_t addrlen) { + int ret = connect(sockfd, addr, addrlen); + _updateErrno(ret != SOCKET_ERROR); + + /* For Winsock connect(), the WSAEWOULDBLOCK error means the same thing as + * EINPROGRESS for POSIX connect(), so we do that translation to keep POSIX + * logic consistent. */ + if (errno == EWOULDBLOCK) { + errno = EINPROGRESS; + } + + return ret != SOCKET_ERROR ? ret : -1; +} + +int win32_getsockopt(SOCKET sockfd, int level, int optname, void *optval, socklen_t *optlen) { + int ret = 0; + if ((level == SOL_SOCKET) && ((optname == SO_RCVTIMEO) || (optname == SO_SNDTIMEO))) { + if (*optlen >= sizeof (struct timeval)) { + struct timeval *tv = optval; + DWORD timeout = 0; + socklen_t dwlen = 0; + ret = getsockopt(sockfd, level, optname, (char *)&timeout, &dwlen); + tv->tv_sec = timeout / 1000; + tv->tv_usec = (timeout * 1000) % 1000000; + } else { + ret = WSAEFAULT; + } + *optlen = sizeof (struct timeval); + } else { + ret = getsockopt(sockfd, level, optname, (char*)optval, optlen); + } + _updateErrno(ret != SOCKET_ERROR); + return ret != SOCKET_ERROR ? ret : -1; +} + +int win32_setsockopt(SOCKET sockfd, int level, int optname, const void *optval, socklen_t optlen) { + int ret = 0; + if ((level == SOL_SOCKET) && ((optname == SO_RCVTIMEO) || (optname == SO_SNDTIMEO))) { + struct timeval *tv = optval; + DWORD timeout = tv->tv_sec * 1000 + tv->tv_usec / 1000; + ret = setsockopt(sockfd, level, optname, (const char*)&timeout, sizeof(DWORD)); + } else { + ret = setsockopt(sockfd, level, optname, (const char*)optval, optlen); + } + _updateErrno(ret != SOCKET_ERROR); + return ret != SOCKET_ERROR ? ret : -1; +} + +int win32_close(SOCKET fd) { + int ret = closesocket(fd); + _updateErrno(ret != SOCKET_ERROR); + return ret != SOCKET_ERROR ? ret : -1; +} + +ssize_t win32_recv(SOCKET sockfd, void *buf, size_t len, int flags) { + int ret = recv(sockfd, (char*)buf, (int)len, flags); + _updateErrno(ret != SOCKET_ERROR); + return ret != SOCKET_ERROR ? ret : -1; +} + +ssize_t win32_send(SOCKET sockfd, const void *buf, size_t len, int flags) { + int ret = send(sockfd, (const char*)buf, (int)len, flags); + _updateErrno(ret != SOCKET_ERROR); + return ret != SOCKET_ERROR ? ret : -1; +} + +int win32_poll(struct pollfd *fds, nfds_t nfds, int timeout) { + int ret = WSAPoll(fds, nfds, timeout); + _updateErrno(ret != SOCKET_ERROR); + return ret != SOCKET_ERROR ? ret : -1; +} +#endif /* _WIN32 */ diff --git a/deps/hiredis/sockcompat.h b/deps/hiredis/sockcompat.h new file mode 100644 index 000000000..56006c163 --- /dev/null +++ b/deps/hiredis/sockcompat.h @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2019, Marcus Geelnard + * + * 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 __SOCKCOMPAT_H +#define __SOCKCOMPAT_H + +#ifndef _WIN32 +/* For POSIX systems we use the standard BSD socket API. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#else +/* For Windows we use winsock. */ +#undef _WIN32_WINNT +#define _WIN32_WINNT 0x0600 /* To get WSAPoll etc. */ +#include +#include +#include + +#ifdef _MSC_VER +typedef signed long ssize_t; +#endif + +/* Emulate the parts of the BSD socket API that we need (override the winsock signatures). */ +int win32_getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res); +const char *win32_gai_strerror(int errcode); +void win32_freeaddrinfo(struct addrinfo *res); +SOCKET win32_socket(int domain, int type, int protocol); +int win32_ioctl(SOCKET fd, unsigned long request, unsigned long *argp); +int win32_bind(SOCKET sockfd, const struct sockaddr *addr, socklen_t addrlen); +int win32_connect(SOCKET sockfd, const struct sockaddr *addr, socklen_t addrlen); +int win32_getsockopt(SOCKET sockfd, int level, int optname, void *optval, socklen_t *optlen); +int win32_setsockopt(SOCKET sockfd, int level, int optname, const void *optval, socklen_t optlen); +int win32_close(SOCKET fd); +ssize_t win32_recv(SOCKET sockfd, void *buf, size_t len, int flags); +ssize_t win32_send(SOCKET sockfd, const void *buf, size_t len, int flags); +typedef ULONG nfds_t; +int win32_poll(struct pollfd *fds, nfds_t nfds, int timeout); + +#ifndef REDIS_SOCKCOMPAT_IMPLEMENTATION +#define getaddrinfo(node, service, hints, res) win32_getaddrinfo(node, service, hints, res) +#undef gai_strerror +#define gai_strerror(errcode) win32_gai_strerror(errcode) +#define freeaddrinfo(res) win32_freeaddrinfo(res) +#define socket(domain, type, protocol) win32_socket(domain, type, protocol) +#define ioctl(fd, request, argp) win32_ioctl(fd, request, argp) +#define bind(sockfd, addr, addrlen) win32_bind(sockfd, addr, addrlen) +#define connect(sockfd, addr, addrlen) win32_connect(sockfd, addr, addrlen) +#define getsockopt(sockfd, level, optname, optval, optlen) win32_getsockopt(sockfd, level, optname, optval, optlen) +#define setsockopt(sockfd, level, optname, optval, optlen) win32_setsockopt(sockfd, level, optname, optval, optlen) +#define close(fd) win32_close(fd) +#define recv(sockfd, buf, len, flags) win32_recv(sockfd, buf, len, flags) +#define send(sockfd, buf, len, flags) win32_send(sockfd, buf, len, flags) +#define poll(fds, nfds, timeout) win32_poll(fds, nfds, timeout) +#endif /* REDIS_SOCKCOMPAT_IMPLEMENTATION */ +#endif /* _WIN32 */ + +#endif /* __SOCKCOMPAT_H */ diff --git a/deps/hiredis/ssl.c b/deps/hiredis/ssl.c new file mode 100644 index 000000000..78ab9e43e --- /dev/null +++ b/deps/hiredis/ssl.c @@ -0,0 +1,448 @@ +/* + * Copyright (c) 2009-2011, Salvatore Sanfilippo + * Copyright (c) 2010-2011, Pieter Noordhuis + * Copyright (c) 2019, Redis Labs + * + * 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 "hiredis.h" +#include "async.h" + +#include +#include +#include +#include + +#include +#include + +#include "async_private.h" + +void __redisSetError(redisContext *c, int type, const char *str); + +/* The SSL context is attached to SSL/TLS connections as a privdata. */ +typedef struct redisSSLContext { + /** + * OpenSSL SSL_CTX; It is optional and will not be set when using + * user-supplied SSL. + */ + SSL_CTX *ssl_ctx; + + /** + * OpenSSL SSL object. + */ + SSL *ssl; + + /** + * SSL_write() requires to be called again with the same arguments it was + * previously called with in the event of an SSL_read/SSL_write situation + */ + size_t lastLen; + + /** Whether the SSL layer requires read (possibly before a write) */ + int wantRead; + + /** + * Whether a write was requested prior to a read. If set, the write() + * should resume whenever a read takes place, if possible + */ + int pendingWrite; +} redisSSLContext; + +/* Forward declaration */ +redisContextFuncs redisContextSSLFuncs; + +#ifdef HIREDIS_SSL_TRACE +/** + * Callback used for debugging + */ +static void sslLogCallback(const SSL *ssl, int where, int ret) { + const char *retstr = ""; + int should_log = 1; + /* Ignore low-level SSL stuff */ + + if (where & SSL_CB_ALERT) { + should_log = 1; + } + if (where == SSL_CB_HANDSHAKE_START || where == SSL_CB_HANDSHAKE_DONE) { + should_log = 1; + } + if ((where & SSL_CB_EXIT) && ret == 0) { + should_log = 1; + } + + if (!should_log) { + return; + } + + retstr = SSL_alert_type_string(ret); + printf("ST(0x%x). %s. R(0x%x)%s\n", where, SSL_state_string_long(ssl), ret, retstr); + + if (where == SSL_CB_HANDSHAKE_DONE) { + printf("Using SSL version %s. Cipher=%s\n", SSL_get_version(ssl), SSL_get_cipher_name(ssl)); + } +} +#endif + +/** + * OpenSSL global initialization and locking handling callbacks. + * Note that this is only required for OpenSSL < 1.1.0. + */ + +#if OPENSSL_VERSION_NUMBER < 0x10100000L +#define HIREDIS_USE_CRYPTO_LOCKS +#endif + +#ifdef HIREDIS_USE_CRYPTO_LOCKS +typedef pthread_mutex_t sslLockType; +static void sslLockInit(sslLockType *l) { + pthread_mutex_init(l, NULL); +} +static void sslLockAcquire(sslLockType *l) { + pthread_mutex_lock(l); +} +static void sslLockRelease(sslLockType *l) { + pthread_mutex_unlock(l); +} +static pthread_mutex_t *ossl_locks; + +static void opensslDoLock(int mode, int lkid, const char *f, int line) { + sslLockType *l = ossl_locks + lkid; + + if (mode & CRYPTO_LOCK) { + sslLockAcquire(l); + } else { + sslLockRelease(l); + } + + (void)f; + (void)line; +} + +static void initOpensslLocks(void) { + unsigned ii, nlocks; + if (CRYPTO_get_locking_callback() != NULL) { + /* Someone already set the callback before us. Don't destroy it! */ + return; + } + nlocks = CRYPTO_num_locks(); + ossl_locks = malloc(sizeof(*ossl_locks) * nlocks); + for (ii = 0; ii < nlocks; ii++) { + sslLockInit(ossl_locks + ii); + } + CRYPTO_set_locking_callback(opensslDoLock); +} +#endif /* HIREDIS_USE_CRYPTO_LOCKS */ + +/** + * SSL Connection initialization. + */ + +static int redisSSLConnect(redisContext *c, SSL_CTX *ssl_ctx, SSL *ssl) { + if (c->privdata) { + __redisSetError(c, REDIS_ERR_OTHER, "redisContext was already associated"); + return REDIS_ERR; + } + c->privdata = calloc(1, sizeof(redisSSLContext)); + + c->funcs = &redisContextSSLFuncs; + redisSSLContext *rssl = c->privdata; + + rssl->ssl_ctx = ssl_ctx; + rssl->ssl = ssl; + + SSL_set_mode(rssl->ssl, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); + SSL_set_fd(rssl->ssl, c->fd); + SSL_set_connect_state(rssl->ssl); + + ERR_clear_error(); + int rv = SSL_connect(rssl->ssl); + if (rv == 1) { + return REDIS_OK; + } + + rv = SSL_get_error(rssl->ssl, rv); + if (((c->flags & REDIS_BLOCK) == 0) && + (rv == SSL_ERROR_WANT_READ || rv == SSL_ERROR_WANT_WRITE)) { + return REDIS_OK; + } + + if (c->err == 0) { + char err[512]; + if (rv == SSL_ERROR_SYSCALL) + snprintf(err,sizeof(err)-1,"SSL_connect failed: %s",strerror(errno)); + else { + unsigned long e = ERR_peek_last_error(); + snprintf(err,sizeof(err)-1,"SSL_connect failed: %s", + ERR_reason_error_string(e)); + } + __redisSetError(c, REDIS_ERR_IO, err); + } + return REDIS_ERR; +} + +int redisInitiateSSL(redisContext *c, SSL *ssl) { + return redisSSLConnect(c, NULL, ssl); +} + +int redisSecureConnection(redisContext *c, const char *capath, + const char *certpath, const char *keypath, const char *servername) { + + SSL_CTX *ssl_ctx = NULL; + SSL *ssl = NULL; + + /* Initialize global OpenSSL stuff */ + static int isInit = 0; + if (!isInit) { + isInit = 1; + SSL_library_init(); +#ifdef HIREDIS_USE_CRYPTO_LOCKS + initOpensslLocks(); +#endif + } + + ssl_ctx = SSL_CTX_new(SSLv23_client_method()); + if (!ssl_ctx) { + __redisSetError(c, REDIS_ERR_OTHER, "Failed to create SSL_CTX"); + goto error; + } + +#ifdef HIREDIS_SSL_TRACE + SSL_CTX_set_info_callback(ssl_ctx, sslLogCallback); +#endif + SSL_CTX_set_options(ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3); + SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, NULL); + if ((certpath != NULL && keypath == NULL) || (keypath != NULL && certpath == NULL)) { + __redisSetError(c, REDIS_ERR_OTHER, "certpath and keypath must be specified together"); + goto error; + } + + if (capath) { + if (!SSL_CTX_load_verify_locations(ssl_ctx, capath, NULL)) { + __redisSetError(c, REDIS_ERR_OTHER, "Invalid CA certificate"); + goto error; + } + } + if (certpath) { + if (!SSL_CTX_use_certificate_chain_file(ssl_ctx, certpath)) { + __redisSetError(c, REDIS_ERR_OTHER, "Invalid client certificate"); + goto error; + } + if (!SSL_CTX_use_PrivateKey_file(ssl_ctx, keypath, SSL_FILETYPE_PEM)) { + __redisSetError(c, REDIS_ERR_OTHER, "Invalid client key"); + goto error; + } + } + + ssl = SSL_new(ssl_ctx); + if (!ssl) { + __redisSetError(c, REDIS_ERR_OTHER, "Couldn't create new SSL instance"); + goto error; + } + if (servername) { + if (!SSL_set_tlsext_host_name(ssl, servername)) { + __redisSetError(c, REDIS_ERR_OTHER, "Couldn't set server name indication"); + goto error; + } + } + + return redisSSLConnect(c, ssl_ctx, ssl); + +error: + if (ssl) SSL_free(ssl); + if (ssl_ctx) SSL_CTX_free(ssl_ctx); + return REDIS_ERR; +} + +static int maybeCheckWant(redisSSLContext *rssl, int rv) { + /** + * If the error is WANT_READ or WANT_WRITE, the appropriate flags are set + * and true is returned. False is returned otherwise + */ + if (rv == SSL_ERROR_WANT_READ) { + rssl->wantRead = 1; + return 1; + } else if (rv == SSL_ERROR_WANT_WRITE) { + rssl->pendingWrite = 1; + return 1; + } else { + return 0; + } +} + +/** + * Implementation of redisContextFuncs for SSL connections. + */ + +static void redisSSLFreeContext(void *privdata){ + redisSSLContext *rsc = privdata; + + if (!rsc) return; + if (rsc->ssl) { + SSL_free(rsc->ssl); + rsc->ssl = NULL; + } + if (rsc->ssl_ctx) { + SSL_CTX_free(rsc->ssl_ctx); + rsc->ssl_ctx = NULL; + } + free(rsc); +} + +static int redisSSLRead(redisContext *c, char *buf, size_t bufcap) { + redisSSLContext *rssl = c->privdata; + + int nread = SSL_read(rssl->ssl, buf, bufcap); + if (nread > 0) { + return nread; + } else if (nread == 0) { + __redisSetError(c, REDIS_ERR_EOF, "Server closed the connection"); + return -1; + } else { + int err = SSL_get_error(rssl->ssl, nread); + if (c->flags & REDIS_BLOCK) { + /** + * In blocking mode, we should never end up in a situation where + * we get an error without it being an actual error, except + * in the case of EINTR, which can be spuriously received from + * debuggers or whatever. + */ + if (errno == EINTR) { + return 0; + } else { + const char *msg = NULL; + if (errno == EAGAIN) { + msg = "Resource temporarily unavailable"; + } + __redisSetError(c, REDIS_ERR_IO, msg); + return -1; + } + } + + /** + * We can very well get an EWOULDBLOCK/EAGAIN, however + */ + if (maybeCheckWant(rssl, err)) { + return 0; + } else { + __redisSetError(c, REDIS_ERR_IO, NULL); + return -1; + } + } +} + +static int redisSSLWrite(redisContext *c) { + redisSSLContext *rssl = c->privdata; + + size_t len = rssl->lastLen ? rssl->lastLen : sdslen(c->obuf); + int rv = SSL_write(rssl->ssl, c->obuf, len); + + if (rv > 0) { + rssl->lastLen = 0; + } else if (rv < 0) { + rssl->lastLen = len; + + int err = SSL_get_error(rssl->ssl, rv); + if ((c->flags & REDIS_BLOCK) == 0 && maybeCheckWant(rssl, err)) { + return 0; + } else { + __redisSetError(c, REDIS_ERR_IO, NULL); + return -1; + } + } + return rv; +} + +static void redisSSLAsyncRead(redisAsyncContext *ac) { + int rv; + redisSSLContext *rssl = ac->c.privdata; + redisContext *c = &ac->c; + + rssl->wantRead = 0; + + if (rssl->pendingWrite) { + int done; + + /* This is probably just a write event */ + rssl->pendingWrite = 0; + rv = redisBufferWrite(c, &done); + if (rv == REDIS_ERR) { + __redisAsyncDisconnect(ac); + return; + } else if (!done) { + _EL_ADD_WRITE(ac); + } + } + + rv = redisBufferRead(c); + if (rv == REDIS_ERR) { + __redisAsyncDisconnect(ac); + } else { + _EL_ADD_READ(ac); + redisProcessCallbacks(ac); + } +} + +static void redisSSLAsyncWrite(redisAsyncContext *ac) { + int rv, done = 0; + redisSSLContext *rssl = ac->c.privdata; + redisContext *c = &ac->c; + + rssl->pendingWrite = 0; + rv = redisBufferWrite(c, &done); + if (rv == REDIS_ERR) { + __redisAsyncDisconnect(ac); + return; + } + + if (!done) { + if (rssl->wantRead) { + /* Need to read-before-write */ + rssl->pendingWrite = 1; + _EL_DEL_WRITE(ac); + } else { + /* No extra reads needed, just need to write more */ + _EL_ADD_WRITE(ac); + } + } else { + /* Already done! */ + _EL_DEL_WRITE(ac); + } + + /* Always reschedule a read */ + _EL_ADD_READ(ac); +} + +redisContextFuncs redisContextSSLFuncs = { + .free_privdata = redisSSLFreeContext, + .async_read = redisSSLAsyncRead, + .async_write = redisSSLAsyncWrite, + .read = redisSSLRead, + .write = redisSSLWrite +}; + diff --git a/deps/hiredis/test.c b/deps/hiredis/test.c index 79cff4308..8668e1856 100644 --- a/deps/hiredis/test.c +++ b/deps/hiredis/test.c @@ -13,12 +13,16 @@ #include #include "hiredis.h" +#ifdef HIREDIS_TEST_SSL +#include "hiredis_ssl.h" +#endif #include "net.h" enum connection_type { CONN_TCP, CONN_UNIX, - CONN_FD + CONN_FD, + CONN_SSL }; struct config { @@ -33,6 +37,14 @@ struct config { struct { const char *path; } unix_sock; + + struct { + const char *host; + int port; + const char *ca_cert; + const char *cert; + const char *key; + } ssl; }; /* The following lines make up our testing "framework" :) */ @@ -93,11 +105,27 @@ static int disconnect(redisContext *c, int keep_fd) { return -1; } +static void do_ssl_handshake(redisContext *c, struct config config) { +#ifdef HIREDIS_TEST_SSL + redisSecureConnection(c, config.ssl.ca_cert, config.ssl.cert, config.ssl.key, NULL); + if (c->err) { + printf("SSL error: %s\n", c->errstr); + redisFree(c); + exit(1); + } +#else + (void) c; + (void) config; +#endif +} + static redisContext *do_connect(struct config config) { redisContext *c = NULL; if (config.type == CONN_TCP) { c = redisConnect(config.tcp.host, config.tcp.port); + } else if (config.type == CONN_SSL) { + c = redisConnect(config.ssl.host, config.ssl.port); } else if (config.type == CONN_UNIX) { c = redisConnectUnix(config.unix_sock.path); } else if (config.type == CONN_FD) { @@ -121,9 +149,21 @@ static redisContext *do_connect(struct config config) { exit(1); } + if (config.type == CONN_SSL) { + do_ssl_handshake(c, config); + } + return select_database(c); } +static void do_reconnect(redisContext *c, struct config config) { + redisReconnect(c); + + if (config.type == CONN_SSL) { + do_ssl_handshake(c, config); + } +} + static void test_format_commands(void) { char *cmd; int len; @@ -360,7 +400,8 @@ static void test_reply_reader(void) { freeReplyObject(reply); redisReaderFree(reader); - test("Set error when array > INT_MAX: "); +#if LLONG_MAX > SIZE_MAX + test("Set error when array > SIZE_MAX: "); reader = redisReaderCreate(); redisReaderFeed(reader, "*9223372036854775807\r\n+asdf\r\n",29); ret = redisReaderGetReply(reader,&reply); @@ -369,7 +410,6 @@ static void test_reply_reader(void) { freeReplyObject(reply); redisReaderFree(reader); -#if LLONG_MAX > SIZE_MAX test("Set error when bulk > SIZE_MAX: "); reader = redisReaderCreate(); redisReaderFeed(reader, "$9223372036854775807\r\nasdf\r\n",28); @@ -434,22 +474,23 @@ static void test_free_null(void) { test_cond(reply == NULL); } +#define HIREDIS_BAD_DOMAIN "idontexist-noreally.com" static void test_blocking_connection_errors(void) { redisContext *c; struct addrinfo hints = {.ai_family = AF_INET}; struct addrinfo *ai_tmp = NULL; - const char *bad_domain = "idontexist.com"; - int rv = getaddrinfo(bad_domain, "6379", &hints, &ai_tmp); + int rv = getaddrinfo(HIREDIS_BAD_DOMAIN, "6379", &hints, &ai_tmp); if (rv != 0) { // Address does *not* exist test("Returns error when host cannot be resolved: "); // First see if this domain name *actually* resolves to NXDOMAIN - c = redisConnect("dontexist.com", 6379); + c = redisConnect(HIREDIS_BAD_DOMAIN, 6379); test_cond( c->err == REDIS_ERR_OTHER && (strcmp(c->errstr, "Name or service not known") == 0 || - strcmp(c->errstr, "Can't resolve: sadkfjaskfjsa.com") == 0 || + strcmp(c->errstr, "Can't resolve: " HIREDIS_BAD_DOMAIN) == 0 || + strcmp(c->errstr, "Name does not resolve") == 0 || strcmp(c->errstr, "nodename nor servname provided, or not known") == 0 || strcmp(c->errstr, "No address associated with hostname") == 0 || @@ -574,7 +615,8 @@ static void test_blocking_connection_timeouts(struct config config) { c = do_connect(config); test("Does not return a reply when the command times out: "); - s = write(c->fd, cmd, strlen(cmd)); + redisAppendFormattedCommand(c, cmd, strlen(cmd)); + s = c->funcs->write(c); tv.tv_sec = 0; tv.tv_usec = 10000; redisSetTimeout(c, tv); @@ -583,7 +625,7 @@ static void test_blocking_connection_timeouts(struct config config) { freeReplyObject(reply); test("Reconnect properly reconnects after a timeout: "); - redisReconnect(c); + do_reconnect(c, config); reply = redisCommand(c, "PING"); test_cond(reply != NULL && reply->type == REDIS_REPLY_STATUS && strcmp(reply->str, "PONG") == 0); freeReplyObject(reply); @@ -591,7 +633,7 @@ static void test_blocking_connection_timeouts(struct config config) { test("Reconnect properly uses owned parameters: "); config.tcp.host = "foo"; config.unix_sock.path = "foo"; - redisReconnect(c); + do_reconnect(c, config); reply = redisCommand(c, "PING"); test_cond(reply != NULL && reply->type == REDIS_REPLY_STATUS && strcmp(reply->str, "PONG") == 0); freeReplyObject(reply); @@ -894,6 +936,23 @@ int main(int argc, char **argv) { throughput = 0; } else if (argc >= 1 && !strcmp(argv[0],"--skip-inherit-fd")) { test_inherit_fd = 0; +#ifdef HIREDIS_TEST_SSL + } else if (argc >= 2 && !strcmp(argv[0],"--ssl-port")) { + argv++; argc--; + cfg.ssl.port = atoi(argv[0]); + } else if (argc >= 2 && !strcmp(argv[0],"--ssl-host")) { + argv++; argc--; + cfg.ssl.host = argv[0]; + } else if (argc >= 2 && !strcmp(argv[0],"--ssl-ca-cert")) { + argv++; argc--; + cfg.ssl.ca_cert = argv[0]; + } else if (argc >= 2 && !strcmp(argv[0],"--ssl-cert")) { + argv++; argc--; + cfg.ssl.cert = argv[0]; + } else if (argc >= 2 && !strcmp(argv[0],"--ssl-key")) { + argv++; argc--; + cfg.ssl.key = argv[0]; +#endif } else { fprintf(stderr, "Invalid argument: %s\n", argv[0]); exit(1); @@ -922,6 +981,20 @@ int main(int argc, char **argv) { test_blocking_io_errors(cfg); if (throughput) test_throughput(cfg); +#ifdef HIREDIS_TEST_SSL + if (cfg.ssl.port && cfg.ssl.host) { + printf("\nTesting against SSL connection (%s:%d):\n", cfg.ssl.host, cfg.ssl.port); + cfg.type = CONN_SSL; + + 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); + } +#endif + if (test_inherit_fd) { printf("\nTesting against inherited fd (%s):\n", cfg.unix_sock.path); cfg.type = CONN_FD; diff --git a/deps/hiredis/test.sh b/deps/hiredis/test.sh new file mode 100755 index 000000000..2cab9e6fb --- /dev/null +++ b/deps/hiredis/test.sh @@ -0,0 +1,70 @@ +#!/bin/sh -ue + +REDIS_SERVER=${REDIS_SERVER:-redis-server} +REDIS_PORT=${REDIS_PORT:-56379} +REDIS_SSL_PORT=${REDIS_SSL_PORT:-56443} +TEST_SSL=${TEST_SSL:-0} +SSL_TEST_ARGS= + +tmpdir=$(mktemp -d) +PID_FILE=${tmpdir}/hiredis-test-redis.pid +SOCK_FILE=${tmpdir}/hiredis-test-redis.sock + +if [ "$TEST_SSL" = "1" ]; then + SSL_CA_CERT=${tmpdir}/ca.crt + SSL_CA_KEY=${tmpdir}/ca.key + SSL_CERT=${tmpdir}/redis.crt + SSL_KEY=${tmpdir}/redis.key + + openssl genrsa -out ${tmpdir}/ca.key 4096 + openssl req \ + -x509 -new -nodes -sha256 \ + -key ${SSL_CA_KEY} \ + -days 3650 \ + -subj '/CN=Hiredis Test CA' \ + -out ${SSL_CA_CERT} + openssl genrsa -out ${SSL_KEY} 2048 + openssl req \ + -new -sha256 \ + -key ${SSL_KEY} \ + -subj '/CN=Hiredis Test Cert' | \ + openssl x509 \ + -req -sha256 \ + -CA ${SSL_CA_CERT} \ + -CAkey ${SSL_CA_KEY} \ + -CAserial ${tmpdir}/ca.txt \ + -CAcreateserial \ + -days 365 \ + -out ${SSL_CERT} + + SSL_TEST_ARGS="--ssl-host 127.0.0.1 --ssl-port ${REDIS_SSL_PORT} --ssl-ca-cert ${SSL_CA_CERT} --ssl-cert ${SSL_CERT} --ssl-key ${SSL_KEY}" +fi + +cleanup() { + set +e + kill $(cat ${PID_FILE}) + rm -rf ${tmpdir} +} +trap cleanup INT TERM EXIT + +cat > ${tmpdir}/redis.conf <> ${tmpdir}/redis.conf < /* for struct timeval */ + #ifndef inline #define inline __inline #endif +#ifndef strcasecmp +#define strcasecmp stricmp +#endif + +#ifndef strncasecmp +#define strncasecmp strnicmp +#endif + #ifndef va_copy #define va_copy(d,s) ((d) = (s)) #endif @@ -37,6 +47,10 @@ __inline int c99_snprintf(char* str, size_t size, const char* format, ...) return count; } #endif +#endif /* _MSC_VER */ -#endif -#endif \ No newline at end of file +#ifdef _WIN32 +#define strerror_r(errno,buf,len) strerror_s(buf,len,errno) +#endif /* _WIN32 */ + +#endif /* _WIN32_HELPER_INCLUDE */ diff --git a/deps/jemalloc/src/background_thread.c b/deps/jemalloc/src/background_thread.c index 3517a3bb8..457669c9e 100644 --- a/deps/jemalloc/src/background_thread.c +++ b/deps/jemalloc/src/background_thread.c @@ -787,7 +787,13 @@ background_thread_stats_read(tsdn_t *tsdn, background_thread_stats_t *stats) { nstime_init(&stats->run_interval, 0); for (unsigned i = 0; i < max_background_threads; i++) { background_thread_info_t *info = &background_thread_info[i]; - malloc_mutex_lock(tsdn, &info->mtx); + if (malloc_mutex_trylock(tsdn, &info->mtx)) { + /* + * Each background thread run may take a long time; + * avoid waiting on the stats if the thread is active. + */ + continue; + } if (info->state != background_thread_stopped) { num_runs += info->tot_n_runs; nstime_add(&stats->run_interval, &info->tot_sleep_time); diff --git a/deps/lua/src/lua_struct.c b/deps/lua/src/lua_struct.c index 4d5f027b8..c58c8e72b 100644 --- a/deps/lua/src/lua_struct.c +++ b/deps/lua/src/lua_struct.c @@ -89,12 +89,14 @@ typedef struct Header { } Header; -static int getnum (const char **fmt, int df) { +static int getnum (lua_State *L, const char **fmt, int df) { if (!isdigit(**fmt)) /* no number? */ return df; /* return default value */ else { int a = 0; do { + if (a > (INT_MAX / 10) || a * 10 > (INT_MAX - (**fmt - '0'))) + luaL_error(L, "integral size overflow"); a = a*10 + *((*fmt)++) - '0'; } while (isdigit(**fmt)); return a; @@ -115,9 +117,9 @@ static size_t optsize (lua_State *L, char opt, const char **fmt) { case 'f': return sizeof(float); case 'd': return sizeof(double); case 'x': return 1; - case 'c': return getnum(fmt, 1); + case 'c': return getnum(L, fmt, 1); case 'i': case 'I': { - int sz = getnum(fmt, sizeof(int)); + int sz = getnum(L, fmt, sizeof(int)); if (sz > MAXINTSIZE) luaL_error(L, "integral size %d is larger than limit of %d", sz, MAXINTSIZE); @@ -150,7 +152,7 @@ static void controloptions (lua_State *L, int opt, const char **fmt, case '>': h->endian = BIG; return; case '<': h->endian = LITTLE; return; case '!': { - int a = getnum(fmt, MAXALIGN); + int a = getnum(L, fmt, MAXALIGN); if (!isp2(a)) luaL_error(L, "alignment %d is not a power of 2", a); h->align = a; diff --git a/redis.conf b/redis.conf index 5ea915905..c04880f32 100644 --- a/redis.conf +++ b/redis.conf @@ -129,6 +129,75 @@ timeout 0 # Redis default starting with Redis 3.2.1. tcp-keepalive 300 +################################# TLS/SSL ##################################### + +# By default, TLS/SSL is disabled. To enable it, the "tls-port" configuration +# directive can be used to define TLS-listening ports. To enable TLS on the +# default port, use: +# +# port 0 +# tls-port 6379 + +# Configure a X.509 certificate and private key to use for authenticating the +# server to connected clients, masters or cluster peers. These files should be +# PEM formatted. +# +# tls-cert-file redis.crt tls-key-file redis.key + +# Configure a DH parameters file to enable Diffie-Hellman (DH) key exchange: +# +# tls-dh-params-file redis.dh + +# Configure a CA certificate(s) bundle or directory to authenticate TLS/SSL +# clients and peers. Redis requires an explicit configuration of at least one +# of these, and will not implicitly use the system wide configuration. +# +# tls-ca-cert-file ca.crt +# tls-ca-cert-dir /etc/ssl/certs + +# By default, clients (including replica servers) on a TLS port are required +# to authenticate using valid client side certificates. +# +# It is possible to disable authentication using this directive. +# +# tls-auth-clients no + +# By default, a Redis replica does not attempt to establish a TLS connection +# with its master. +# +# Use the following directive to enable TLS on replication links. +# +# tls-replication yes + +# By default, the Redis Cluster bus uses a plain TCP connection. To enable +# TLS for the bus protocol, use the following directive: +# +# tls-cluster yes + +# Explicitly specify TLS versions to support. Allowed values are case insensitive +# and include "TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3" (OpenSSL >= 1.1.1) or +# "default" which is currently >= TLSv1.1. +# +# tls-protocols TLSv1.2 + +# Configure allowed ciphers. See the ciphers(1ssl) manpage for more information +# about the syntax of this string. +# +# Note: this configuration applies only to <= TLSv1.2. +# +# tls-ciphers DEFAULT:!MEDIUM + +# Configure allowed TLSv1.3 ciphersuites. See the ciphers(1ssl) manpage for more +# information about the syntax of this string, and specifically for TLSv1.3 +# ciphersuites. +# +# tls-ciphersuites TLS_CHACHA20_POLY1305_SHA256 + +# When choosing a cipher, use the server's preference instead of the client +# preference. By default, the server follows the client's preference. +# +# tls-prefer-server-ciphers yes + ################################# GENERAL ##################################### # By default Redis does not run as a daemon. Use 'yes' if you need it. @@ -336,13 +405,11 @@ replica-read-only yes # Replication SYNC strategy: disk or socket. # -# ------------------------------------------------------- -# WARNING: DISKLESS REPLICATION IS EXPERIMENTAL CURRENTLY -# ------------------------------------------------------- +# New replicas and reconnecting replicas that are not able to continue the +# replication process just receiving differences, need to do what is called a +# "full synchronization". An RDB file is transmitted from the master to the +# replicas. # -# New replicas and reconnecting replicas that are not able to continue the replication -# process just receiving differences, need to do what is called a "full -# synchronization". An RDB file is transmitted from the master to the replicas. # The transmission can happen in two different ways: # # 1) Disk-backed: The Redis master creates a new process that writes the RDB @@ -352,14 +419,14 @@ replica-read-only yes # RDB file to replica sockets, without touching the disk at all. # # With disk-backed replication, while the RDB file is generated, more replicas -# can be queued and served with the RDB file as soon as the current child producing -# the RDB file finishes its work. With diskless replication instead once -# the transfer starts, new replicas arriving will be queued and a new transfer -# will start when the current one terminates. +# can be queued and served with the RDB file as soon as the current child +# producing the RDB file finishes its work. With diskless replication instead +# once the transfer starts, new replicas arriving will be queued and a new +# transfer will start when the current one terminates. # # When diskless replication is used, the master waits a configurable amount of -# time (in seconds) before starting the transfer in the hope that multiple replicas -# will arrive and the transfer can be parallelized. +# time (in seconds) before starting the transfer in the hope that multiple +# replicas will arrive and the transfer can be parallelized. # # With slow disks and fast (large bandwidth) networks, diskless replication # works better. @@ -370,16 +437,42 @@ repl-diskless-sync no # to the replicas. # # This is important since once the transfer starts, it is not possible to serve -# new replicas arriving, that will be queued for the next RDB transfer, so the server -# waits a delay in order to let more replicas arrive. +# new replicas arriving, that will be queued for the next RDB transfer, so the +# server waits a delay in order to let more replicas arrive. # # The delay is specified in seconds, and by default is 5 seconds. To disable # it entirely just set it to 0 seconds and the transfer will start ASAP. repl-diskless-sync-delay 5 -# Replicas send PINGs to server in a predefined interval. It's possible to change -# this interval with the repl_ping_replica_period option. The default value is 10 -# seconds. +# ----------------------------------------------------------------------------- +# WARNING: RDB diskless load is experimental. Since in this setup the replica +# does not immediately store an RDB on disk, it may cause data loss during +# failovers. RDB diskless load + Redis modules not handling I/O reads may also +# cause Redis to abort in case of I/O errors during the initial synchronization +# stage with the master. Use only if your do what you are doing. +# ----------------------------------------------------------------------------- +# +# Replica can load the RDB it reads from the replication link directly from the +# socket, or store the RDB to a file and read that file after it was completely +# recived from the master. +# +# In many cases the disk is slower than the network, and storing and loading +# the RDB file may increase replication time (and even increase the master's +# Copy on Write memory and salve buffers). +# However, parsing the RDB file directly from the socket may mean that we have +# to flush the contents of the current database before the full rdb was +# received. For this reason we have the following options: +# +# "disabled" - Don't use diskless load (store the rdb file to the disk first) +# "on-empty-db" - Use diskless load only when it is completely safe. +# "swapdb" - Keep a copy of the current db contents in RAM while parsing +# the data directly from the socket. note that this requires +# sufficient memory, if you don't have it, you risk an OOM kill. +repl-diskless-load disabled + +# Replicas send PINGs to server in a predefined interval. It's possible to +# change this interval with the repl_ping_replica_period option. The default +# value is 10 seconds. # # repl-ping-replica-period 10 @@ -411,10 +504,10 @@ repl-diskless-sync-delay 5 repl-disable-tcp-nodelay no # Set the replication backlog size. The backlog is a buffer that accumulates -# replica data when replicas are disconnected for some time, so that when a replica -# wants to reconnect again, often a full resync is not needed, but a partial -# resync is enough, just passing the portion of data the replica missed while -# disconnected. +# replica data when replicas are disconnected for some time, so that when a +# replica wants to reconnect again, often a full resync is not needed, but a +# partial resync is enough, just passing the portion of data the replica +# missed while disconnected. # # The bigger the replication backlog, the longer the time the replica can be # disconnected and later be able to perform a partial resynchronization. @@ -436,13 +529,13 @@ repl-disable-tcp-nodelay no # # repl-backlog-ttl 3600 -# The replica priority is an integer number published by Redis in the INFO output. -# It is used by Redis Sentinel in order to select a replica to promote into a -# master if the master is no longer working correctly. +# The replica priority is an integer number published by Redis in the INFO +# output. It is used by Redis Sentinel in order to select a replica to promote +# into a master if the master is no longer working correctly. # # A replica with a low priority number is considered better for promotion, so -# for instance if there are three replicas with priority 10, 100, 25 Sentinel will -# pick the one with priority 10, that is the lowest. +# for instance if there are three replicas with priority 10, 100, 25 Sentinel +# will pick the one with priority 10, that is the lowest. # # However a special priority of 0 marks the replica as not able to perform the # role of master, so a replica with priority of 0 will never be selected by @@ -502,6 +595,39 @@ replica-priority 100 # replica-announce-ip 5.5.5.5 # replica-announce-port 1234 +############################### KEYS TRACKING ################################# + +# Redis implements server assisted support for client side caching of values. +# This is implemented using an invalidation table that remembers, using +# 16 millions of slots, what clients may have certain subsets of keys. In turn +# this is used in order to send invalidation messages to clients. Please +# to understand more about the feature check this page: +# +# https://redis.io/topics/client-side-caching +# +# When tracking is enabled for a client, all the read only queries are assumed +# to be cached: this will force Redis to store information in the invalidation +# table. When keys are modified, such information is flushed away, and +# invalidation messages are sent to the clients. However if the workload is +# heavily dominated by reads, Redis could use more and more memory in order +# to track the keys fetched by many clients. +# +# For this reason it is possible to configure a maximum fill value for the +# invalidation table. By default it is set to 10%, and once this limit is +# reached, Redis will start to evict caching slots in the invalidation table +# even if keys are not modified, just to reclaim memory: this will in turn +# force the clients to invalidate the cached values. Basically the table +# maximum fill rate is a trade off between the memory you want to spend server +# side to track information about who cached what, and the ability of clients +# to retain cached objects in memory. +# +# If you set the value to 0, it means there are no limits, and all the 16 +# millions of caching slots can be used at the same time. In the "stats" +# INFO section, you can find information about the amount of caching slots +# used at every given moment. +# +# tracking-table-max-fill 10 + ################################## SECURITY ################################### # Warning: since Redis is pretty fast an outside user can try up to @@ -684,13 +810,13 @@ replica-priority 100 # maxmemory # MAXMEMORY POLICY: how Redis will select what to remove when maxmemory -# is reached. You can select among five behaviors: +# is reached. You can select one from the following behaviors: # -# volatile-lru -> Evict using approximated LRU among the keys with an expire set. +# volatile-lru -> Evict using approximated LRU, only keys with an expire set. # allkeys-lru -> Evict any key using approximated LRU. -# volatile-lfu -> Evict using approximated LFU among the keys with an expire set. +# volatile-lfu -> Evict using approximated LFU, only keys with an expire set. # allkeys-lfu -> Evict any key using approximated LFU. -# volatile-random -> Remove a random key among the ones with an expire set. +# volatile-random -> Remove a random key having an expire set. # allkeys-random -> Remove a random key, any key. # volatile-ttl -> Remove the key with the nearest expire time (minor TTL) # noeviction -> Don't evict anything, just return an error on write operations. @@ -731,20 +857,37 @@ replica-priority 100 # DEL commands to the replica as keys evict in the master side. # # This behavior ensures that masters and replicas stay consistent, and is usually -# what you want, however if your replica is writable, or you want the replica to have -# a different memory setting, and you are sure all the writes performed to the -# replica are idempotent, then you may change this default (but be sure to understand -# what you are doing). +# what you want, however if your replica is writable, or you want the replica +# to have a different memory setting, and you are sure all the writes performed +# to the replica are idempotent, then you may change this default (but be sure +# to understand what you are doing). # # Note that since the replica by default does not evict, it may end using more # memory than the one set via maxmemory (there are certain buffers that may -# be larger on the replica, or data structures may sometimes take more memory and so -# forth). So make sure you monitor your replicas and make sure they have enough -# memory to never hit a real out-of-memory condition before the master hits -# the configured maxmemory setting. +# be larger on the replica, or data structures may sometimes take more memory +# and so forth). So make sure you monitor your replicas and make sure they +# have enough memory to never hit a real out-of-memory condition before the +# master hits the configured maxmemory setting. # # replica-ignore-maxmemory yes +# Redis reclaims expired keys in two ways: upon access when those keys are +# found to be expired, and also in background, in what is called the +# "active expire key". The key space is slowly and interactively scanned +# looking for expired keys to reclaim, so that it is possible to free memory +# of keys that are expired and will never be accessed again in a short time. +# +# The default effort of the expire cycle will try to avoid having more than +# ten percent of expired keys still in memory, and will try to avoid consuming +# more than 25% of total memory and to add latency to the system. However +# it is possible to increase the expire "effort" that is normally set to +# "1", to a greater value, up to the value "10". At its maximum value the +# system will use more CPU, longer cycles (and technically may introduce +# more latency), and will tollerate less already expired keys still present +# in the system. It's a tradeoff betweeen memory, CPU and latecy. +# +# active-expire-effort 1 + ############################# LAZY FREEING #################################### # Redis has two primitives to delete keys. One is called DEL and is a blocking @@ -794,6 +937,52 @@ lazyfree-lazy-expire no lazyfree-lazy-server-del no replica-lazy-flush no +################################ THREADED I/O ################################# + +# Redis is mostly single threaded, however there are certain threaded +# operations such as UNLINK, slow I/O accesses and other things that are +# performed on side threads. +# +# Now it is also possible to handle Redis clients socket reads and writes +# in different I/O threads. Since especially writing is so slow, normally +# Redis users use pipelining in order to speedup the Redis performances per +# core, and spawn multiple instances in order to scale more. Using I/O +# threads it is possible to easily speedup two times Redis without resorting +# to pipelining nor sharding of the instance. +# +# By default threading is disabled, we suggest enabling it only in machines +# that have at least 4 or more cores, leaving at least one spare core. +# Using more than 8 threads is unlikely to help much. We also recommend using +# threaded I/O only if you actually have performance problems, with Redis +# instances being able to use a quite big percentage of CPU time, otherwise +# there is no point in using this feature. +# +# So for instance if you have a four cores boxes, try to use 2 or 3 I/O +# threads, if you have a 8 cores, try to use 6 threads. In order to +# enable I/O threads use the following configuration directive: +# +# io-threads 4 +# +# Setting io-threads to 1 will just use the main thread as usually. +# When I/O threads are enabled, we only use threads for writes, that is +# to thread the write(2) syscall and transfer the client buffers to the +# socket. However it is also possible to enable threading of reads and +# protocol parsing using the following configuration directive, by setting +# it to yes: +# +# io-threads-do-reads no +# +# Usually threading reads doesn't help much. +# +# NOTE 1: This configuration directive cannot be changed at runtime via +# CONFIG SET. Aso this feature currently does not work when SSL is +# enabled. +# +# NOTE 2: If you want to test the Redis speedup using redis-benchmark, make +# sure you also run the benchmark itself in threaded mode, using the +# --threads option to match the number of Redis theads, otherwise you'll not +# be able to notice the improvements. + ############################## APPEND ONLY MODE ############################### # By default Redis asynchronously dumps the dataset on disk. This mode is @@ -942,13 +1131,7 @@ aof-use-rdb-preamble yes lua-time-limit 5000 ################################ REDIS CLUSTER ############################### -# -# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -# WARNING EXPERIMENTAL: Redis Cluster is considered to be stable code, however -# in order to mark it as "mature" we need to wait for a non trivial percentage -# of users to deploy it in production. -# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -# + # Normal Redis instances can't be part of a Redis Cluster; only nodes that are # started as cluster nodes can. In order to start a Redis instance as a # cluster node enable the cluster support uncommenting the following: @@ -1056,6 +1239,22 @@ lua-time-limit 5000 # # cluster-replica-no-failover no +# This option, when set to yes, allows nodes to serve read traffic while the +# the cluster is in a down state, as long as it believes it owns the slots. +# +# This is useful for two cases. The first case is for when an application +# doesn't require consistency of data during node failures or network partitions. +# One example of this is a cache, where as long as the node has the data it +# should be able to serve it. +# +# The second use case is for configurations that don't meet the recommended +# three shards but want to enable cluster mode and scale later. A +# master outage in a 1 or 2 shard configuration causes a read/write outage to the +# entire cluster without this option set, with it set there is only a write outage. +# Without a quorum of masters, slot ownership will not change automatically. +# +# cluster-allow-reads-when-down no + # In order to setup your cluster make sure to read the documentation # available at http://redis.io web site. @@ -1162,7 +1361,11 @@ latency-monitor-threshold 0 # z Sorted set commands # x Expired events (events generated every time a key expires) # e Evicted events (events generated when a key is evicted for maxmemory) -# A Alias for g$lshzxe, so that the "AKE" string means all the events. +# t Stream commands +# m Key-miss events (Note: It is not included in the 'A' class) +# A Alias for g$lshzxet, so that the "AKE" string means all the events +# (Except key-miss events which are excluded from 'A' due to their +# unique nature). # # The "notify-keyspace-events" takes as argument a string that is composed # of zero or multiple characters. The empty string means that notifications @@ -1188,7 +1391,7 @@ notify-keyspace-events "" # Redis contains an implementation of the Gopher protocol, as specified in # the RFC 1436 (https://www.ietf.org/rfc/rfc1436.txt). # -# The Gopher protocol was very popular in the late '90s. It is an alternative +# The Gopher protocol was very popular in the late '90s. It is an alternative # to the web, and the implementation both server and client side is so simple # that the Redis server has just 100 lines of code in order to implement this # support. @@ -1226,7 +1429,7 @@ notify-keyspace-events "" # to server Gopher pages MAKE SURE TO SET A PASSWORD to the instance. # Once a password is set: # -# 1. The Gopher server (when enabled, not by default) will kill serve +# 1. The Gopher server (when enabled, not by default) will still serve # content via Gopher. # 2. However other commands cannot be called before the client will # authenticate. @@ -1485,10 +1688,6 @@ rdb-save-incremental-fsync yes ########################### 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? # ------------------------------- # @@ -1528,7 +1727,7 @@ rdb-save-incremental-fsync yes # a good idea to leave the defaults untouched. # Enabled active defragmentation -# activedefrag yes +# activedefrag no # Minimum amount of fragmentation waste to start active defrag # active-defrag-ignore-bytes 100mb @@ -1539,13 +1738,14 @@ rdb-save-incremental-fsync yes # 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 5 +# Minimal effort for defrag in CPU percentage, to be used when the lower +# threshold is reached +# active-defrag-cycle-min 1 -# Maximal effort for defrag in CPU percentage -# active-defrag-cycle-max 75 +# Maximal effort for defrag in CPU percentage, to be used when the upper +# threshold is reached +# active-defrag-cycle-max 25 # Maximum number of set/hash/zset/list fields that will be processed from # the main dictionary scan # active-defrag-max-scan-fields 1000 - diff --git a/runtest-moduleapi b/runtest-moduleapi new file mode 100755 index 000000000..f6cc0a258 --- /dev/null +++ b/runtest-moduleapi @@ -0,0 +1,28 @@ +#!/bin/sh +TCL_VERSIONS="8.5 8.6" +TCLSH="" + +for VERSION in $TCL_VERSIONS; do + TCL=`which tclsh$VERSION 2>/dev/null` && TCLSH=$TCL +done + +if [ -z $TCLSH ] +then + echo "You need tcl 8.5 or newer in order to run the Redis test" + exit 1 +fi + +make -C tests/modules && \ +$TCLSH tests/test_helper.tcl \ +--single unit/moduleapi/commandfilter \ +--single unit/moduleapi/fork \ +--single unit/moduleapi/testrdb \ +--single unit/moduleapi/infotest \ +--single unit/moduleapi/propagate \ +--single unit/moduleapi/hooks \ +--single unit/moduleapi/misc \ +--single unit/moduleapi/blockonkeys \ +--single unit/moduleapi/scan \ +--single unit/moduleapi/datatype \ +--single unit/moduleapi/auth \ +"${@}" diff --git a/src/Makefile b/src/Makefile index d4874f7cf..00b623a4b 100644 --- a/src/Makefile +++ b/src/Makefile @@ -20,7 +20,7 @@ DEPENDENCY_TARGETS=hiredis linenoise lua NODEPS:=clean distclean # Default settings -STD=-std=c99 -pedantic -DREDIS_STATIC='' +STD=-std=c11 -pedantic -DREDIS_STATIC='' ifneq (,$(findstring clang,$(CC))) ifneq (,$(findstring FreeBSD,$(uname_S))) STD+=-Wno-c11-extensions @@ -32,6 +32,7 @@ OPT=$(OPTIMIZATION) PREFIX?=/usr/local INSTALL_BIN=$(PREFIX)/bin INSTALL=install +PKG_CONFIG?=pkg-config # Default allocator defaults to Jemalloc if it's not an ARM MALLOC=libc @@ -77,6 +78,15 @@ FINAL_LDFLAGS=$(LDFLAGS) $(REDIS_LDFLAGS) $(DEBUG) FINAL_LIBS=-lm DEBUG=-g -ggdb +# Linux ARM needs -latomic at linking time +ifneq (,$(filter aarch64 armv,$(uname_M))) + FINAL_LIBS+=-latomic +else +ifneq (,$(findstring armv,$(uname_M))) + FINAL_LIBS+=-latomic +endif +endif + ifeq ($(uname_S),SunOS) # SunOS ifneq ($(@@),32bit) @@ -93,6 +103,8 @@ else ifeq ($(uname_S),Darwin) # Darwin FINAL_LIBS+= -ldl + OPENSSL_CFLAGS=-I/usr/local/opt/openssl/include + OPENSSL_LDFLAGS=-L/usr/local/opt/openssl/lib else ifeq ($(uname_S),AIX) # AIX @@ -129,6 +141,30 @@ endif # Include paths to dependencies FINAL_CFLAGS+= -I../deps/hiredis -I../deps/linenoise -I../deps/lua/src +# Determine systemd support and/or build preference (defaulting to auto-detection) +BUILD_WITH_SYSTEMD=no +# If 'USE_SYSTEMD' in the environment is neither "no" nor "yes", try to +# auto-detect libsystemd's presence and link accordingly. +ifneq ($(USE_SYSTEMD),no) + LIBSYSTEMD_PKGCONFIG := $(shell $(PKG_CONFIG) --exists libsystemd && echo $$?) +# If libsystemd cannot be detected, continue building without support for it +# (unless a later check tells us otherwise) +ifeq ($(LIBSYSTEMD_PKGCONFIG),0) + BUILD_WITH_SYSTEMD=yes +endif +endif +ifeq ($(USE_SYSTEMD),yes) +ifneq ($(LIBSYSTEMD_PKGCONFIG),0) +$(error USE_SYSTEMD is set to "$(USE_SYSTEMD)", but $(PKG_CONFIG) cannot find libsystemd) +endif +# Force building with libsystemd + BUILD_WITH_SYSTEMD=yes +endif +ifeq ($(BUILD_WITH_SYSTEMD),yes) + FINAL_LIBS+=$(shell $(PKG_CONFIG) --libs libsystemd) + FINAL_CFLAGS+= -DHAVE_LIBSYSTEMD +endif + ifeq ($(MALLOC),tcmalloc) FINAL_CFLAGS+= -DUSE_TCMALLOC FINAL_LIBS+= -ltcmalloc @@ -145,6 +181,12 @@ ifeq ($(MALLOC),jemalloc) FINAL_LIBS := ../deps/jemalloc/lib/libjemalloc.a $(FINAL_LIBS) endif +ifeq ($(BUILD_TLS),yes) + FINAL_CFLAGS+=-DUSE_OPENSSL $(OPENSSL_CFLAGS) + FINAL_LDFLAGS+=$(OPENSSL_LDFLAGS) + FINAL_LIBS += ../deps/hiredis/libhiredis_ssl.a -lssl -lcrypto +endif + REDIS_CC=$(QUIET_CC)$(CC) $(FINAL_CFLAGS) REDIS_LD=$(QUIET_LINK)$(CC) $(FINAL_LDFLAGS) REDIS_INSTALL=$(QUIET_INSTALL)$(INSTALL) @@ -164,11 +206,11 @@ 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 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.o localtime.o lolwut.o lolwut5.o acl.o gopher.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.o localtime.o lolwut.o lolwut5.o lolwut6.o acl.o gopher.o tracking.o connection.o tls.o sha256.o REDIS_CLI_NAME=redis-cli REDIS_CLI_OBJ=anet.o adlist.o dict.o redis-cli.o zmalloc.o release.o anet.o ae.o crc64.o siphash.o crc16.o REDIS_BENCHMARK_NAME=redis-benchmark -REDIS_BENCHMARK_OBJ=ae.o anet.o redis-benchmark.o adlist.o zmalloc.o redis-benchmark.o +REDIS_BENCHMARK_OBJ=ae.o anet.o redis-benchmark.o adlist.o dict.o zmalloc.o siphash.o redis-benchmark.o REDIS_CHECK_RDB_NAME=redis-check-rdb REDIS_CHECK_AOF_NAME=redis-check-aof @@ -241,14 +283,18 @@ $(REDIS_BENCHMARK_NAME): $(REDIS_BENCHMARK_OBJ) dict-benchmark: dict.c zmalloc.c sds.c siphash.c $(REDIS_CC) $(FINAL_CFLAGS) $^ -D DICT_BENCHMARK_MAIN -o $@ $(FINAL_LIBS) +DEP = $(REDIS_SERVER_OBJ:%.o=%.d) $(REDIS_CLI_OBJ:%.o=%.d) $(REDIS_BENCHMARK_OBJ:%.o=%.d) +-include $(DEP) + # 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 # depending on a single artifact, build all dependencies first. %.o: %.c .make-prerequisites - $(REDIS_CC) -c $< + $(REDIS_CC) -MMD -o $@ -c $< clean: rm -rf $(REDIS_SERVER_NAME) $(REDIS_SENTINEL_NAME) $(REDIS_CLI_NAME) $(REDIS_BENCHMARK_NAME) $(REDIS_CHECK_RDB_NAME) $(REDIS_CHECK_AOF_NAME) *.o *.gcda *.gcno *.gcov redis.info lcov-html Makefile.dep dict-benchmark + rm -f $(DEP) .PHONY: clean @@ -310,3 +356,6 @@ install: all $(REDIS_INSTALL) $(REDIS_CHECK_RDB_NAME) $(INSTALL_BIN) $(REDIS_INSTALL) $(REDIS_CHECK_AOF_NAME) $(INSTALL_BIN) @ln -sf $(REDIS_SERVER_NAME) $(INSTALL_BIN)/$(REDIS_SENTINEL_NAME) + +uninstall: + rm -f $(INSTALL_BIN)/{$(REDIS_SERVER_NAME),$(REDIS_BENCHMARK_NAME),$(REDIS_CLI_NAME),$(REDIS_CHECK_RDB_NAME),$(REDIS_CHECK_AOF_NAME),$(REDIS_SENTINEL_NAME)} diff --git a/src/acl.c b/src/acl.c index 3cca50027..b046785ff 100644 --- a/src/acl.c +++ b/src/acl.c @@ -28,6 +28,7 @@ */ #include "server.h" +#include "sha256.h" #include /* ============================================================================= @@ -48,6 +49,8 @@ list *UsersToLoad; /* This is a list of users found in the configuration file array of SDS pointers: the first is the user name, all the remaining pointers are ACL rules in the same format as ACLSetUser(). */ +list *ACLLog; /* Our security log, the user is able to inspect that + using the ACL LOG command .*/ struct ACLCategoryItem { const char *name; @@ -92,6 +95,10 @@ struct ACLUserFlag { void ACLResetSubcommandsForCommand(user *u, unsigned long id); void ACLResetSubcommands(user *u); void ACLAddAllowedSubcommand(user *u, unsigned long id, const char *sub); +void ACLFreeLogEntry(void *le); + +/* The length of the string representation of a hashed password. */ +#define HASH_PASSWORD_LEN SHA256_BLOCK_SIZE*2 /* ============================================================================= * Helper functions for the rest of the ACL implementation @@ -139,6 +146,25 @@ int time_independent_strcmp(char *a, char *b) { return diff; /* If zero strings are the same. */ } +/* Given an SDS string, returns the SHA256 hex representation as a + * new SDS string. */ +sds ACLHashPassword(unsigned char *cleartext, size_t len) { + SHA256_CTX ctx; + unsigned char hash[SHA256_BLOCK_SIZE]; + char hex[HASH_PASSWORD_LEN]; + char *cset = "0123456789abcdef"; + + sha256_init(&ctx); + sha256_update(&ctx,(unsigned char*)cleartext,len); + sha256_final(&ctx,hash); + + for (int j = 0; j < SHA256_BLOCK_SIZE; j++) { + hex[j*2] = cset[((hash[j]&0xF0)>>4)]; + hex[j*2+1] = cset[(hash[j]&0xF)]; + } + return sdsnewlen(hex,HASH_PASSWORD_LEN); +} + /* ============================================================================= * Low level ACL API * ==========================================================================*/ @@ -160,12 +186,12 @@ int ACLListMatchSds(void *a, void *b) { return sdscmp(a,b) == 0; } -/* Method to free list elements from ACL users password/ptterns lists. */ +/* Method to free list elements from ACL users password/patterns lists. */ void ACLListFreeSds(void *item) { sdsfree(item); } -/* Method to duplicate list elements from ACL users password/ptterns lists. */ +/* Method to duplicate list elements from ACL users password/patterns lists. */ void *ACLListDupSds(void *item) { return sdsdup(item); } @@ -295,7 +321,7 @@ int ACLGetCommandBitCoordinates(uint64_t id, uint64_t *word, uint64_t *bit) { * Note that this function does not check the ALLCOMMANDS flag of the user * but just the lowlevel bitmask. * - * If the bit overflows the user internal represetation, zero is returned + * If the bit overflows the user internal representation, zero is returned * in order to disallow the execution of the command in such edge case. */ int ACLGetUserCommandBit(user *u, unsigned long id) { uint64_t word, bit; @@ -311,7 +337,7 @@ int ACLUserCanExecuteFutureCommands(user *u) { } /* Set the specified command bit for the specified user to 'value' (0 or 1). - * If the bit overflows the user internal represetation, no operation + * If the bit overflows the user internal representation, no operation * is performed. As a side effect of calling this function with a value of * zero, the user flag ALLCOMMANDS is cleared since it is no longer possible * to skip the command bit explicit test. */ @@ -350,7 +376,7 @@ int ACLSetUserCommandBitsForCategory(user *u, const char *category, int value) { /* Return the number of commands allowed (on) and denied (off) for the user 'u' * in the subset of commands flagged with the specified category name. - * If the categoty name is not valid, C_ERR is returend, otherwise C_OK is + * If the category name is not valid, C_ERR is returned, otherwise C_OK is * returned and on and off are populated by reference. */ int ACLCountCategoryBitsForUser(user *u, unsigned long *on, unsigned long *off, const char *category) @@ -502,7 +528,7 @@ sds ACLDescribeUser(user *u) { listRewind(u->passwords,&li); while((ln = listNext(&li))) { sds thispass = listNodeValue(ln); - res = sdscatlen(res,">",1); + res = sdscatlen(res,"#",1); res = sdscatsds(res,thispass); res = sdscatlen(res," ",1); } @@ -542,6 +568,8 @@ struct redisCommand *ACLLookupCommand(const char *name) { * and command ID. */ void ACLResetSubcommandsForCommand(user *u, unsigned long id) { if (u->allowed_subcommands && u->allowed_subcommands[id]) { + for (int i = 0; u->allowed_subcommands[id][i]; i++) + sdsfree(u->allowed_subcommands[id][i]); zfree(u->allowed_subcommands[id]); u->allowed_subcommands[id] = NULL; } @@ -624,10 +652,17 @@ void ACLAddAllowedSubcommand(user *u, unsigned long id, const char *sub) { * It is possible to specify multiple patterns. * allkeys Alias for ~* * resetkeys Flush the list of allowed keys patterns. - * > Add this passowrd to the list of valid password for the user. + * > Add this password to the list of valid password for the user. * For example >mypass will add "mypass" to the list. * This directive clears the "nopass" flag (see later). + * # Add this password hash to the list of valid hashes for + * the user. This is useful if you have previously computed + * the hash, and don't want to store it in plaintext. + * This directive clears the "nopass" flag (see later). * < Remove this password from the list of valid passwords. + * ! Remove this hashed password from the list of valid passwords. + * This is useful when you want to remove a password just by + * hash without knowing its plaintext version at all. * nopass All the set passwords of the user are removed, and the user * is flagged as requiring no password: it means that every * password will work against this user. If this directive is @@ -663,6 +698,7 @@ void ACLAddAllowedSubcommand(user *u, unsigned long id, const char *sub) { * EEXIST: You are adding a key pattern after "*" was already added. This is * almost surely an error on the user side. * ENODEV: The password you are trying to remove from the user does not exist. + * EBADMSG: The hash you are trying to add is not a valid hash. */ int ACLSetUser(user *u, const char *op, ssize_t oplen) { if (oplen == -1) oplen = strlen(op); @@ -698,14 +734,48 @@ int ACLSetUser(user *u, const char *op, ssize_t oplen) { } else if (!strcasecmp(op,"resetpass")) { u->flags &= ~USER_FLAG_NOPASS; listEmpty(u->passwords); - } else if (op[0] == '>') { - sds newpass = sdsnewlen(op+1,oplen-1); + } else if (op[0] == '>' || op[0] == '#') { + sds newpass; + if (op[0] == '>') { + newpass = ACLHashPassword((unsigned char*)op+1,oplen-1); + } else { + if (oplen != HASH_PASSWORD_LEN + 1) { + errno = EBADMSG; + return C_ERR; + } + + /* Password hashes can only be characters that represent + * hexadecimal values, which are numbers and lowercase + * characters 'a' through 'f'. + */ + for(int i = 1; i < HASH_PASSWORD_LEN + 1; i++) { + char c = op[i]; + if ((c < 'a' || c > 'f') && (c < '0' || c > '9')) { + errno = EBADMSG; + return C_ERR; + } + } + newpass = sdsnewlen(op+1,oplen-1); + } + listNode *ln = listSearchKey(u->passwords,newpass); /* Avoid re-adding the same password multiple times. */ - if (ln == NULL) listAddNodeTail(u->passwords,newpass); + if (ln == NULL) + listAddNodeTail(u->passwords,newpass); + else + sdsfree(newpass); u->flags &= ~USER_FLAG_NOPASS; - } else if (op[0] == '<') { - sds delpass = sdsnewlen(op+1,oplen-1); + } else if (op[0] == '<' || op[0] == '!') { + sds delpass; + if (op[0] == '<') { + delpass = ACLHashPassword((unsigned char*)op+1,oplen-1); + } else { + if (oplen != HASH_PASSWORD_LEN + 1) { + errno = EBADMSG; + return C_ERR; + } + delpass = sdsnewlen(op+1,oplen-1); + } listNode *ln = listSearchKey(u->passwords,delpass); sdsfree(delpass); if (ln) { @@ -722,7 +792,10 @@ int ACLSetUser(user *u, const char *op, ssize_t oplen) { sds newpat = sdsnewlen(op+1,oplen-1); listNode *ln = listSearchKey(u->patterns,newpat); /* Avoid re-adding the same pattern multiple times. */ - if (ln == NULL) listAddNodeTail(u->patterns,newpat); + if (ln == NULL) + listAddNodeTail(u->patterns,newpat); + else + sdsfree(newpat); u->flags &= ~USER_FLAG_ALLKEYS; } else if (op[0] == '+' && op[1] != '@') { if (strchr(op,'|') == NULL) { @@ -820,6 +893,9 @@ char *ACLSetUserStringError(void) { else if (errno == ENODEV) errmsg = "The password you are trying to remove from the user does " "not exist"; + else if (errno == EBADMSG) + errmsg = "The password hash must be exactly 64 characters and contain " + "only lowercase hexadecimal characters"; return errmsg; } @@ -847,6 +923,7 @@ void ACLInitDefaultUser(void) { void ACLInit(void) { Users = raxNew(); UsersToLoad = listCreate(); + ACLLog = listCreate(); ACLInitDefaultUser(); } @@ -877,11 +954,15 @@ int ACLCheckUserCredentials(robj *username, robj *password) { listIter li; listNode *ln; listRewind(u->passwords,&li); + sds hashed = ACLHashPassword(password->ptr,sdslen(password->ptr)); while((ln = listNext(&li))) { sds thispass = listNodeValue(ln); - if (!time_independent_strcmp(password->ptr, thispass)) + if (!time_independent_strcmp(hashed, thispass)) { + sdsfree(hashed); return C_OK; + } } + sdsfree(hashed); /* If we reached this point, no password matched. */ errno = EINVAL; @@ -898,8 +979,10 @@ int ACLAuthenticateUser(client *c, robj *username, robj *password) { if (ACLCheckUserCredentials(username,password) == C_OK) { c->authenticated = 1; c->user = ACLGetUserByName(username->ptr,sdslen(username->ptr)); + moduleNotifyUserChanged(c); return C_OK; } else { + addACLLogEntry(c,ACL_DENIED_AUTH,0,username->ptr); return C_ERR; } } @@ -947,16 +1030,16 @@ user *ACLGetUserByName(const char *name, size_t namelen) { return myuser; } -/* Check if the command ready to be excuted in the client 'c', and already - * referenced by c->cmd, can be executed by this client according to the - * ACls associated to the client user c->user. +/* Check if the command is ready to be executed in the client 'c', already + * referenced by c->cmd, and can be executed by this client according to the + * ACLs associated to the client user c->user. * * If the user can execute the command ACL_OK is returned, otherwise * ACL_DENIED_CMD or ACL_DENIED_KEY is returned: the first in case the * command cannot be executed because the user is not allowed to run such * command, the second if the command is denied because the user is trying * to access keys that are not among the specified patterns. */ -int ACLCheckCommandPerm(client *c) { +int ACLCheckCommandPerm(client *c, int *keyidxptr) { user *u = c->user; uint64_t id = c->cmd->id; @@ -1016,6 +1099,7 @@ int ACLCheckCommandPerm(client *c) { } } if (!match) { + if (keyidxptr) *keyidxptr = keyidx[j]; getKeysFreeResult(keyidx); return ACL_DENIED_KEY; } @@ -1120,7 +1204,7 @@ int ACLLoadConfiguredUsers(void) { } /* This function loads the ACL from the specified filename: every line - * is validated and shold be either empty or in the format used to specify + * is validated and should be either empty or in the format used to specify * users in the redis.conf configuration or in the ACL file, that is: * * user ... rules ... @@ -1170,7 +1254,7 @@ sds ACLLoadFromFile(const char *filename) { * to the real user mentioned in the ACL line. */ user *fakeuser = ACLCreateUnlinkedUser(); - /* We do all the loading in a fresh insteance of the Users radix tree, + /* We do all the loading in a fresh instance of the Users radix tree, * so if there are errors loading the ACL file we can rollback to the * old version. */ rax *old_users = Users; @@ -1246,7 +1330,7 @@ sds ACLLoadFromFile(const char *filename) { } /* Note that the same rules already applied to the fake user, so - * we just assert that everything goess well: it should. */ + * we just assert that everything goes well: it should. */ for (j = 2; j < argc; j++) serverAssert(ACLSetUser(u,argv[j],sdslen(argv[j])) == C_OK); @@ -1376,6 +1460,131 @@ void ACLLoadUsersAtStartup(void) { } } +/* ============================================================================= + * ACL log + * ==========================================================================*/ + +#define ACL_LOG_CTX_TOPLEVEL 0 +#define ACL_LOG_CTX_LUA 1 +#define ACL_LOG_CTX_MULTI 2 +#define ACL_LOG_GROUPING_MAX_TIME_DELTA 60000 + +/* This structure defines an entry inside the ACL log. */ +typedef struct ACLLogEntry { + uint64_t count; /* Number of times this happened recently. */ + int reason; /* Reason for denying the command. ACL_DENIED_*. */ + int context; /* Toplevel, Lua or MULTI/EXEC? ACL_LOG_CTX_*. */ + sds object; /* The key name or command name. */ + sds username; /* User the client is authenticated with. */ + mstime_t ctime; /* Milliseconds time of last update to this entry. */ + sds cinfo; /* Client info (last client if updated). */ +} ACLLogEntry; + +/* This function will check if ACL entries 'a' and 'b' are similar enough + * that we should actually update the existing entry in our ACL log instead + * of creating a new one. */ +int ACLLogMatchEntry(ACLLogEntry *a, ACLLogEntry *b) { + if (a->reason != b->reason) return 0; + if (a->context != b->context) return 0; + mstime_t delta = a->ctime - b->ctime; + if (delta < 0) delta = -delta; + if (delta > ACL_LOG_GROUPING_MAX_TIME_DELTA) return 0; + if (sdscmp(a->object,b->object) != 0) return 0; + if (sdscmp(a->username,b->username) != 0) return 0; + return 1; +} + +/* Release an ACL log entry. */ +void ACLFreeLogEntry(void *leptr) { + ACLLogEntry *le = leptr; + sdsfree(le->object); + sdsfree(le->username); + sdsfree(le->cinfo); + zfree(le); +} + +/* Adds a new entry in the ACL log, making sure to delete the old entry + * if we reach the maximum length allowed for the log. This function attempts + * to find similar entries in the current log in order to bump the counter of + * the log entry instead of creating many entries for very similar ACL + * rules issues. + * + * The keypos argument is only used when the reason is ACL_DENIED_KEY, since + * it allows the function to log the key name that caused the problem. + * Similarly the username is only passed when we failed to authenticate the + * user with AUTH or HELLO, for the ACL_DENIED_AUTH reason. Otherwise + * it will just be NULL. + */ +void addACLLogEntry(client *c, int reason, int keypos, sds username) { + /* Create a new entry. */ + struct ACLLogEntry *le = zmalloc(sizeof(*le)); + le->count = 1; + le->reason = reason; + le->username = sdsdup(reason == ACL_DENIED_AUTH ? username : c->user->name); + le->ctime = mstime(); + + switch(reason) { + case ACL_DENIED_CMD: le->object = sdsnew(c->cmd->name); break; + case ACL_DENIED_KEY: le->object = sdsnew(c->argv[keypos]->ptr); break; + case ACL_DENIED_AUTH: le->object = sdsnew(c->argv[0]->ptr); break; + default: le->object = sdsempty(); + } + + client *realclient = c; + if (realclient->flags & CLIENT_LUA) realclient = server.lua_caller; + + le->cinfo = catClientInfoString(sdsempty(),realclient); + if (c->flags & CLIENT_MULTI) { + le->context = ACL_LOG_CTX_MULTI; + } else if (c->flags & CLIENT_LUA) { + le->context = ACL_LOG_CTX_LUA; + } else { + le->context = ACL_LOG_CTX_TOPLEVEL; + } + + /* Try to match this entry with past ones, to see if we can just + * update an existing entry instead of creating a new one. */ + long toscan = 10; /* Do a limited work trying to find duplicated. */ + listIter li; + listNode *ln; + listRewind(ACLLog,&li); + ACLLogEntry *match = NULL; + while (toscan-- && (ln = listNext(&li)) != NULL) { + ACLLogEntry *current = listNodeValue(ln); + if (ACLLogMatchEntry(current,le)) { + match = current; + listDelNode(ACLLog,ln); + listAddNodeHead(ACLLog,current); + break; + } + } + + /* If there is a match update the entry, otherwise add it as a + * new one. */ + if (match) { + /* We update a few fields of the existing entry and bump the + * counter of events for this entry. */ + sdsfree(match->cinfo); + match->cinfo = le->cinfo; + match->ctime = le->ctime; + match->count++; + + /* Release the old entry. */ + le->cinfo = NULL; + ACLFreeLogEntry(le); + } else { + /* Add it to our list of entires. We'll have to trim the list + * to its maximum size. */ + listAddNodeHead(ACLLog, le); + while(listLength(ACLLog) > server.acllog_max_len) { + listNode *ln = listLast(ACLLog); + ACLLogEntry *le = listNodeValue(ln); + ACLFreeLogEntry(le); + listDelNode(ACLLog,ln); + } + } +} + /* ============================================================================= * ACL related commands * ==========================================================================*/ @@ -1383,12 +1592,16 @@ void ACLLoadUsersAtStartup(void) { /* ACL -- show and modify the configuration of ACL users. * ACL HELP * ACL LOAD + * ACL SAVE * ACL LIST * ACL USERS * ACL CAT [] * ACL SETUSER ... acl rules ... * ACL DELUSER [...] * ACL GETUSER + * ACL GENPASS + * ACL WHOAMI + * ACL LOG [ | RESET] */ void aclCommand(client *c) { char *sub = c->argv[1]->ptr; @@ -1571,9 +1784,79 @@ void aclCommand(client *c) { } dictReleaseIterator(di); setDeferredArrayLen(c,dl,arraylen); + } else if (!strcasecmp(sub,"genpass") && c->argc == 2) { + char pass[32]; /* 128 bits of actual pseudo random data. */ + getRandomHexChars(pass,sizeof(pass)); + addReplyBulkCBuffer(c,pass,sizeof(pass)); + } else if (!strcasecmp(sub,"log") && (c->argc == 2 || c->argc ==3)) { + long count = 10; /* Number of entries to emit by default. */ + + /* Parse the only argument that LOG may have: it could be either + * the number of entires the user wants to display, or alternatively + * the "RESET" command in order to flush the old entires. */ + if (c->argc == 3) { + if (!strcasecmp(c->argv[2]->ptr,"reset")) { + listSetFreeMethod(ACLLog,ACLFreeLogEntry); + listEmpty(ACLLog); + listSetFreeMethod(ACLLog,NULL); + addReply(c,shared.ok); + return; + } else if (getLongFromObjectOrReply(c,c->argv[2],&count,NULL) + != C_OK) + { + return; + } + if (count < 0) count = 0; + } + + /* Fix the count according to the number of entries we got. */ + if ((size_t)count > listLength(ACLLog)) + count = listLength(ACLLog); + + addReplyArrayLen(c,count); + listIter li; + listNode *ln; + listRewind(ACLLog,&li); + mstime_t now = mstime(); + while (count-- && (ln = listNext(&li)) != NULL) { + ACLLogEntry *le = listNodeValue(ln); + addReplyMapLen(c,7); + addReplyBulkCString(c,"count"); + addReplyLongLong(c,le->count); + + addReplyBulkCString(c,"reason"); + char *reasonstr; + switch(le->reason) { + case ACL_DENIED_CMD: reasonstr="command"; break; + case ACL_DENIED_KEY: reasonstr="key"; break; + case ACL_DENIED_AUTH: reasonstr="auth"; break; + } + addReplyBulkCString(c,reasonstr); + + addReplyBulkCString(c,"context"); + char *ctxstr; + switch(le->context) { + case ACL_LOG_CTX_TOPLEVEL: ctxstr="toplevel"; break; + case ACL_LOG_CTX_MULTI: ctxstr="multi"; break; + case ACL_LOG_CTX_LUA: ctxstr="lua"; break; + default: ctxstr="unknown"; + } + addReplyBulkCString(c,ctxstr); + + addReplyBulkCString(c,"object"); + addReplyBulkCBuffer(c,le->object,sdslen(le->object)); + addReplyBulkCString(c,"username"); + addReplyBulkCBuffer(c,le->username,sdslen(le->username)); + addReplyBulkCString(c,"age-seconds"); + double age = (double)(now - le->ctime)/1000; + addReplyDouble(c,age); + addReplyBulkCString(c,"client-info"); + addReplyBulkCBuffer(c,le->cinfo,sdslen(le->cinfo)); + } } else if (!strcasecmp(sub,"help")) { const char *help[] = { "LOAD -- Reload users from the ACL file.", +"SAVE -- Save the current config to the ACL file." "LIST -- Show user details in config file format.", "USERS -- List all the registered usernames.", "SETUSER [attribs ...] -- Create or modify a user.", @@ -1581,7 +1864,9 @@ void aclCommand(client *c) { "DELUSER [...] -- Delete a list of users.", "CAT -- List available categories.", "CAT -- List commands inside category.", +"GENPASS -- Generate a secure user password.", "WHOAMI -- Return the current connection username.", +"LOG [ | RESET] -- Show the ACL log entries.", NULL }; addReplyHelp(c,help); @@ -1602,7 +1887,7 @@ void addReplyCommandCategories(client *c, struct redisCommand *cmd) { setDeferredSetLen(c, flaglen, flagcount); } -/* AUTH +/* AUTH * AUTH (Redis >= 6.0 form) * * When the user is omitted it means that we are trying to authenticate diff --git a/src/adlist.h b/src/adlist.h index c954fac87..28b9016ce 100644 --- a/src/adlist.h +++ b/src/adlist.h @@ -66,7 +66,7 @@ typedef struct list { #define listSetMatchMethod(l,m) ((l)->match = (m)) #define listGetDupMethod(l) ((l)->dup) -#define listGetFree(l) ((l)->free) +#define listGetFreeMethod(l) ((l)->free) #define listGetMatchMethod(l) ((l)->match) /* Prototypes */ diff --git a/src/ae.c b/src/ae.c index 53629ef77..2c1dae512 100644 --- a/src/ae.c +++ b/src/ae.c @@ -76,6 +76,7 @@ aeEventLoop *aeCreateEventLoop(int setsize) { eventLoop->maxfd = -1; eventLoop->beforesleep = NULL; eventLoop->aftersleep = NULL; + eventLoop->flags = 0; if (aeApiCreate(eventLoop) == -1) goto err; /* Events with mask == AE_NONE are not set. So let's initialize the * vector with it. */ @@ -97,6 +98,14 @@ int aeGetSetSize(aeEventLoop *eventLoop) { return eventLoop->setsize; } +/* Tells the next iteration/s of the event processing to set timeout of 0. */ +void aeSetDontWait(aeEventLoop *eventLoop, int noWait) { + if (noWait) + eventLoop->flags |= AE_DONT_WAIT; + else + eventLoop->flags &= ~AE_DONT_WAIT; +} + /* Resize the maximum set size of the event loop. * If the requested set size is smaller than the current set size, but * there is already a file descriptor in use that is >= the requested @@ -406,6 +415,11 @@ int aeProcessEvents(aeEventLoop *eventLoop, int flags) } } + if (eventLoop->flags & AE_DONT_WAIT) { + tv.tv_sec = tv.tv_usec = 0; + tvp = &tv; + } + /* Call the multiplexing API, will return only on timeout or when * some event fires. */ numevents = aeApiPoll(eventLoop, tvp); diff --git a/src/ae.h b/src/ae.h index 184fe3d1b..9acd72434 100644 --- a/src/ae.h +++ b/src/ae.h @@ -106,6 +106,7 @@ typedef struct aeEventLoop { void *apidata; /* This is used for polling API specific data */ aeBeforeSleepProc *beforesleep; aeBeforeSleepProc *aftersleep; + int flags; } aeEventLoop; /* Prototypes */ @@ -128,5 +129,6 @@ void aeSetBeforeSleepProc(aeEventLoop *eventLoop, aeBeforeSleepProc *beforesleep void aeSetAfterSleepProc(aeEventLoop *eventLoop, aeBeforeSleepProc *aftersleep); int aeGetSetSize(aeEventLoop *eventLoop); int aeResizeSetSize(aeEventLoop *eventLoop, int setsize); +void aeSetDontWait(aeEventLoop *eventLoop, int noWait); #endif diff --git a/src/ae_epoll.c b/src/ae_epoll.c index 410aac70d..fa197297e 100644 --- a/src/ae_epoll.c +++ b/src/ae_epoll.c @@ -121,8 +121,8 @@ static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) { if (e->events & EPOLLIN) mask |= AE_READABLE; if (e->events & EPOLLOUT) mask |= AE_WRITABLE; - if (e->events & EPOLLERR) mask |= AE_WRITABLE; - if (e->events & EPOLLHUP) mask |= AE_WRITABLE; + if (e->events & EPOLLERR) mask |= AE_WRITABLE|AE_READABLE; + if (e->events & EPOLLHUP) mask |= AE_WRITABLE|AE_READABLE; eventLoop->fired[j].fd = e->data.fd; eventLoop->fired[j].mask = mask; } diff --git a/src/anet.c b/src/anet.c index 2981fca13..46ea7e145 100644 --- a/src/anet.c +++ b/src/anet.c @@ -193,6 +193,20 @@ int anetSendTimeout(char *err, int fd, long long ms) { return ANET_OK; } +/* Set the socket receive timeout (SO_RCVTIMEO socket option) to the specified + * number of milliseconds, or disable it if the 'ms' argument is zero. */ +int anetRecvTimeout(char *err, int fd, long long ms) { + struct timeval tv; + + tv.tv_sec = ms/1000; + tv.tv_usec = (ms%1000)*1000; + if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) == -1) { + anetSetError(err, "setsockopt SO_RCVTIMEO: %s", strerror(errno)); + return ANET_ERR; + } + return ANET_OK; +} + /* anetGenericResolve() is called by anetResolve() and anetResolveIP() to * do the actual work. It resolves the hostname "host" and set the string * representation of the IP address into the buffer pointed by "ipbuf". @@ -265,8 +279,8 @@ static int anetCreateSocket(char *err, int domain) { #define ANET_CONNECT_NONE 0 #define ANET_CONNECT_NONBLOCK 1 #define ANET_CONNECT_BE_BINDING 2 /* Best effort binding. */ -static int anetTcpGenericConnect(char *err, char *addr, int port, - char *source_addr, int flags) +static int anetTcpGenericConnect(char *err, const char *addr, int port, + const char *source_addr, int flags) { int s = ANET_ERR, rv; char portstr[6]; /* strlen("65535") + 1; */ @@ -345,31 +359,31 @@ end: } } -int anetTcpConnect(char *err, char *addr, int port) +int anetTcpConnect(char *err, const char *addr, int port) { return anetTcpGenericConnect(err,addr,port,NULL,ANET_CONNECT_NONE); } -int anetTcpNonBlockConnect(char *err, char *addr, int port) +int anetTcpNonBlockConnect(char *err, const char *addr, int port) { return anetTcpGenericConnect(err,addr,port,NULL,ANET_CONNECT_NONBLOCK); } -int anetTcpNonBlockBindConnect(char *err, char *addr, int port, - char *source_addr) +int anetTcpNonBlockBindConnect(char *err, const char *addr, int port, + const char *source_addr) { return anetTcpGenericConnect(err,addr,port,source_addr, ANET_CONNECT_NONBLOCK); } -int anetTcpNonBlockBestEffortBindConnect(char *err, char *addr, int port, - char *source_addr) +int anetTcpNonBlockBestEffortBindConnect(char *err, const char *addr, int port, + const char *source_addr) { return anetTcpGenericConnect(err,addr,port,source_addr, ANET_CONNECT_NONBLOCK|ANET_CONNECT_BE_BINDING); } -int anetUnixGenericConnect(char *err, char *path, int flags) +int anetUnixGenericConnect(char *err, const char *path, int flags) { int s; struct sockaddr_un sa; @@ -397,12 +411,12 @@ int anetUnixGenericConnect(char *err, char *path, int flags) return s; } -int anetUnixConnect(char *err, char *path) +int anetUnixConnect(char *err, const char *path) { return anetUnixGenericConnect(err,path,ANET_CONNECT_NONE); } -int anetUnixNonBlockConnect(char *err, char *path) +int anetUnixNonBlockConnect(char *err, const char *path) { return anetUnixGenericConnect(err,path,ANET_CONNECT_NONBLOCK); } diff --git a/src/anet.h b/src/anet.h index 7142f78d2..23f19643c 100644 --- a/src/anet.h +++ b/src/anet.h @@ -49,12 +49,12 @@ #undef ip_len #endif -int anetTcpConnect(char *err, char *addr, int port); -int anetTcpNonBlockConnect(char *err, char *addr, int port); -int anetTcpNonBlockBindConnect(char *err, char *addr, int port, char *source_addr); -int anetTcpNonBlockBestEffortBindConnect(char *err, char *addr, int port, char *source_addr); -int anetUnixConnect(char *err, char *path); -int anetUnixNonBlockConnect(char *err, char *path); +int anetTcpConnect(char *err, const char *addr, int port); +int anetTcpNonBlockConnect(char *err, const char *addr, int port); +int anetTcpNonBlockBindConnect(char *err, const char *addr, int port, const char *source_addr); +int anetTcpNonBlockBestEffortBindConnect(char *err, const char *addr, int port, const char *source_addr); +int anetUnixConnect(char *err, const char *path); +int anetUnixNonBlockConnect(char *err, const char *path); int anetRead(int fd, char *buf, int count); int anetResolve(char *err, char *host, char *ipbuf, size_t ipbuf_len); int anetResolveIP(char *err, char *host, char *ipbuf, size_t ipbuf_len); @@ -70,6 +70,7 @@ int anetEnableTcpNoDelay(char *err, int fd); int anetDisableTcpNoDelay(char *err, int fd); int anetTcpKeepAlive(char *err, int fd); int anetSendTimeout(char *err, int fd, long long ms); +int anetRecvTimeout(char *err, int fd, long long ms); int anetPeerToString(int fd, char *ip, size_t ip_len, int *port); int anetKeepAlive(char *err, int fd, int interval); int anetSockName(int fd, char *ip, size_t ip_len, int *port); diff --git a/src/aof.c b/src/aof.c index 46ae58324..3682c4568 100644 --- a/src/aof.c +++ b/src/aof.c @@ -197,6 +197,12 @@ ssize_t aofRewriteBufferWrite(int fd) { * AOF file implementation * ------------------------------------------------------------------------- */ +/* Return true if an AOf fsync is currently already in progress in a + * BIO thread. */ +int aofFsyncInProgress(void) { + return bioPendingJobsOfType(BIO_AOF_FSYNC) != 0; +} + /* Starts a background task that performs fsync() against the specified * file descriptor (the one of the AOF file) in another thread. */ void aof_background_fsync(int fd) { @@ -236,6 +242,7 @@ void stopAppendOnly(void) { server.aof_fd = -1; server.aof_selected_db = -1; server.aof_state = AOF_OFF; + server.aof_rewrite_scheduled = 0; killAppendOnlyChild(); } @@ -258,9 +265,9 @@ int startAppendOnly(void) { strerror(errno)); return C_ERR; } - if (server.rdb_child_pid != -1) { + if (hasActiveChildProcess() && server.aof_child_pid == -1) { 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."); + serverLog(LL_WARNING,"AOF was enabled but there is already another background operation. An AOF background was scheduled to start when possible."); } else { /* If there is a pending AOF rewrite, we need to switch it off and * start a new one: the old one cannot be reused because it is not @@ -297,9 +304,7 @@ ssize_t aofWrite(int fd, const char *buf, size_t len) { nwritten = write(fd, buf, len); if (nwritten < 0) { - if (errno == EINTR) { - continue; - } + if (errno == EINTR) continue; return totwritten ? totwritten : -1; } @@ -335,10 +340,24 @@ void flushAppendOnlyFile(int force) { int sync_in_progress = 0; mstime_t latency; - if (sdslen(server.aof_buf) == 0) return; + if (sdslen(server.aof_buf) == 0) { + /* Check if we need to do fsync even the aof buffer is empty, + * because previously in AOF_FSYNC_EVERYSEC mode, fsync is + * called only when aof buffer is not empty, so if users + * stop write commands before fsync called in one second, + * the data in page cache cannot be flushed in time. */ + if (server.aof_fsync == AOF_FSYNC_EVERYSEC && + server.aof_fsync_offset != server.aof_current_size && + server.unixtime > server.aof_last_fsync && + !(sync_in_progress = aofFsyncInProgress())) { + goto try_fsync; + } else { + return; + } + } if (server.aof_fsync == AOF_FSYNC_EVERYSEC) - sync_in_progress = bioPendingJobsOfType(BIO_AOF_FSYNC) != 0; + sync_in_progress = aofFsyncInProgress(); if (server.aof_fsync == AOF_FSYNC_EVERYSEC && !force) { /* With this append fsync policy we do background fsyncing. @@ -367,6 +386,10 @@ void flushAppendOnlyFile(int force) { * there is much to do about the whole server stopping for power problems * or alike */ + if (server.aof_flush_sleep && sdslen(server.aof_buf)) { + usleep(server.aof_flush_sleep); + } + latencyStartMonitor(latency); nwritten = aofWrite(server.aof_fd,server.aof_buf,sdslen(server.aof_buf)); latencyEndMonitor(latency); @@ -377,7 +400,7 @@ void flushAppendOnlyFile(int force) { * useful for graphing / monitoring purposes. */ if (sync_in_progress) { latencyAddSampleIfNeeded("aof-write-pending-fsync",latency); - } else if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) { + } else if (hasActiveChildProcess()) { latencyAddSampleIfNeeded("aof-write-active-child",latency); } else { latencyAddSampleIfNeeded("aof-write-alone",latency); @@ -470,11 +493,11 @@ void flushAppendOnlyFile(int force) { server.aof_buf = sdsempty(); } +try_fsync: /* Don't fsync if no-appendfsync-on-rewrite is set to yes and there are * children doing I/O in the background. */ - if (server.aof_no_fsync_on_rewrite && - (server.aof_child_pid != -1 || server.rdb_child_pid != -1)) - return; + if (server.aof_no_fsync_on_rewrite && hasActiveChildProcess()) + return; /* Perform the fsync if needed. */ if (server.aof_fsync == AOF_FSYNC_ALWAYS) { @@ -484,10 +507,14 @@ void flushAppendOnlyFile(int force) { redis_fsync(server.aof_fd); /* Let's try to get this data on the disk */ latencyEndMonitor(latency); latencyAddSampleIfNeeded("aof-fsync-always",latency); + server.aof_fsync_offset = server.aof_current_size; server.aof_last_fsync = server.unixtime; } else if ((server.aof_fsync == AOF_FSYNC_EVERYSEC && server.unixtime > server.aof_last_fsync)) { - if (!sync_in_progress) aof_background_fsync(server.aof_fd); + if (!sync_in_progress) { + aof_background_fsync(server.aof_fd); + server.aof_fsync_offset = server.aof_current_size; + } server.aof_last_fsync = server.unixtime; } } @@ -626,11 +653,12 @@ void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int a /* In Redis commands are always executed in the context of a client, so in * order to load the append only file we need to create a fake client. */ -struct client *createFakeClient(void) { +struct client *createAOFClient(void) { struct client *c = zmalloc(sizeof(*c)); selectDb(c,0); - c->fd = -1; + c->id = CLIENT_ID_AOF; /* So modules can identify it's the AOF client. */ + c->conn = NULL; c->name = NULL; c->querybuf = sdsempty(); c->querybuf_peak = 0; @@ -694,6 +722,7 @@ int loadAppendOnlyFile(char *filename) { * operation is received. */ if (fp && redis_fstat(fileno(fp),&sb) != -1 && sb.st_size == 0) { server.aof_current_size = 0; + server.aof_fsync_offset = server.aof_current_size; fclose(fp); return C_ERR; } @@ -702,8 +731,8 @@ int loadAppendOnlyFile(char *filename) { * to the same file we're about to read. */ server.aof_state = AOF_OFF; - fakeClient = createFakeClient(); - startLoading(fp); + fakeClient = createAOFClient(); + startLoadingFile(fp, filename, RDBFLAGS_AOF_PREAMBLE); /* Check if this AOF file has an RDB preamble. In that case we need to * load the RDB file and later continue loading the AOF tail. */ @@ -718,7 +747,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,NULL,1) != C_OK) { + if (rdbLoadRio(&rdb,RDBFLAGS_AOF_PREAMBLE,NULL) != C_OK) { serverLog(LL_WARNING,"Error reading the RDB preamble of the AOF file, AOF loading aborted"); goto readerr; } else { @@ -739,6 +768,7 @@ int loadAppendOnlyFile(char *filename) { if (!(loops++ % 1000)) { loadingProgress(ftello(fp)); processEventsWhileBlocked(); + processModuleLoadingProgressEvent(1); } if (fgets(buf,sizeof(buf),fp) == NULL) { @@ -752,18 +782,26 @@ int loadAppendOnlyFile(char *filename) { argc = atoi(buf+1); if (argc < 1) goto fmterr; + /* Load the next command in the AOF as our fake client + * argv. */ argv = zmalloc(sizeof(robj*)*argc); fakeClient->argc = argc; fakeClient->argv = argv; for (j = 0; j < argc; j++) { - if (fgets(buf,sizeof(buf),fp) == NULL) { + /* Parse the argument len. */ + char *readres = fgets(buf,sizeof(buf),fp); + if (readres == NULL || buf[0] != '$') { fakeClient->argc = j; /* Free up to j-1. */ freeFakeClientArgv(fakeClient); - goto readerr; + if (readres == NULL) + goto readerr; + else + goto fmterr; } - if (buf[0] != '$') goto fmterr; len = strtol(buf+1,NULL,10); + + /* Read it into a string object. */ argsds = sdsnewlen(SDS_NOINIT,len); if (len && fread(argsds,len,1,fp) == 0) { sdsfree(argsds); @@ -772,10 +810,12 @@ int loadAppendOnlyFile(char *filename) { goto readerr; } argv[j] = createObject(OBJ_STRING,argsds); + + /* Discard CRLF. */ if (fread(buf,2,1,fp) == 0) { fakeClient->argc = j+1; /* Free up to j. */ freeFakeClientArgv(fakeClient); - goto readerr; /* discard CRLF */ + goto readerr; } } @@ -812,6 +852,8 @@ int loadAppendOnlyFile(char *filename) { freeFakeClientArgv(fakeClient); fakeClient->cmd = NULL; if (server.aof_load_truncated) valid_up_to = ftello(fp); + if (server.key_load_delay) + usleep(server.key_load_delay); } /* This point can only be reached when EOF is reached without errors. @@ -829,14 +871,16 @@ loaded_ok: /* DB loaded, cleanup and return C_OK to the caller. */ fclose(fp); freeFakeClient(fakeClient); server.aof_state = old_aof_state; - stopLoading(); + stopLoading(1); aofUpdateCurrentSize(); server.aof_rewrite_base_size = server.aof_current_size; + server.aof_fsync_offset = server.aof_current_size; return C_OK; readerr: /* Read error. If feof(fp) is true, fall through to unexpected EOF. */ if (!feof(fp)) { if (fakeClient) freeFakeClient(fakeClient); /* avoid valgrind warning */ + fclose(fp); serverLog(LL_WARNING,"Unrecoverable error reading the append only file: %s", strerror(errno)); exit(1); } @@ -867,11 +911,13 @@ uxeof: /* Unexpected AOF end of file. */ } } if (fakeClient) freeFakeClient(fakeClient); /* avoid valgrind warning */ + fclose(fp); serverLog(LL_WARNING,"Unexpected end of file reading the append only file. You can: 1) Make a backup of your AOF file, then use ./redis-check-aof --fix . 2) Alternatively you can set the 'aof-load-truncated' configuration option to yes and restart the server."); exit(1); fmterr: /* Format error. */ if (fakeClient) freeFakeClient(fakeClient); /* avoid valgrind warning */ + fclose(fp); serverLog(LL_WARNING,"Bad file format reading the append only file: make a backup of your AOF file, then use ./redis-check-aof --fix "); exit(1); } @@ -1104,7 +1150,7 @@ int rioWriteBulkStreamID(rio *r,streamID *id) { int retval; sds replyid = sdscatfmt(sdsempty(),"%U-%U",id->ms,id->seq); - if ((retval = rioWriteBulkString(r,replyid,sdslen(replyid))) == 0) return 0; + retval = rioWriteBulkString(r,replyid,sdslen(replyid)); sdsfree(replyid); return retval; } @@ -1239,7 +1285,7 @@ int rewriteModuleObject(rio *r, robj *key, robj *o) { RedisModuleIO io; moduleValue *mv = o->ptr; moduleType *mt = mv->type; - moduleInitIOContext(io,mt,r); + moduleInitIOContext(io,mt,r,key); mt->aof_rewrite(&io,key,mv->value); if (io.ctx) { moduleFreeContext(io.ctx); @@ -1366,9 +1412,11 @@ int rewriteAppendOnlyFile(char *filename) { if (server.aof_rewrite_incremental_fsync) rioSetAutoSync(&aof,REDIS_AUTOSYNC_BYTES); + startSaving(RDBFLAGS_AOF_PREAMBLE); + if (server.aof_use_rdb_preamble) { int error; - if (rdbSaveRio(&aof,&error,RDB_SAVE_AOF_PREAMBLE,NULL) == C_ERR) { + if (rdbSaveRio(&aof,&error,RDBFLAGS_AOF_PREAMBLE,NULL) == C_ERR) { errno = error; goto werr; } @@ -1431,15 +1479,18 @@ int rewriteAppendOnlyFile(char *filename) { if (rename(tmpfile,filename) == -1) { serverLog(LL_WARNING,"Error moving temp append only file on the final destination: %s", strerror(errno)); unlink(tmpfile); + stopSaving(0); return C_ERR; } serverLog(LL_NOTICE,"SYNC append only file rewrite performed"); + stopSaving(1); return C_OK; werr: serverLog(LL_WARNING,"Write error writing append only file on disk: %s", strerror(errno)); fclose(fp); unlink(tmpfile); + stopSaving(0); return C_ERR; } @@ -1535,39 +1586,24 @@ void aofClosePipes(void) { */ int rewriteAppendOnlyFileBackground(void) { pid_t childpid; - long long start; - if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR; + if (hasActiveChildProcess()) return C_ERR; if (aofCreatePipes() != C_OK) return C_ERR; openChildInfoPipe(); - start = ustime(); - if ((childpid = fork()) == 0) { + if ((childpid = redisFork()) == 0) { char tmpfile[256]; /* Child */ - closeListeningSockets(0); redisSetProcTitle("redis-aof-rewrite"); snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof", (int) getpid()); if (rewriteAppendOnlyFile(tmpfile) == C_OK) { - size_t private_dirty = zmalloc_get_private_dirty(-1); - - if (private_dirty) { - serverLog(LL_NOTICE, - "AOF rewrite: %zu MB of memory used by copy-on-write", - private_dirty/(1024*1024)); - } - - server.child_info_data.cow_size = private_dirty; - sendChildInfo(CHILD_INFO_TYPE_AOF); + sendChildCOWInfo(CHILD_INFO_TYPE_AOF, "AOF rewrite"); exitFromChild(0); } else { exitFromChild(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) { closeChildInfoPipe(); serverLog(LL_WARNING, @@ -1581,7 +1617,6 @@ int rewriteAppendOnlyFileBackground(void) { server.aof_rewrite_scheduled = 0; server.aof_rewrite_time_start = time(NULL); server.aof_child_pid = childpid; - updateDictResizePolicy(); /* We set appendseldb to -1 in order to force the next call to the * feedAppendOnlyFile() to issue a SELECT command, so the differences * accumulated by the parent into server.aof_rewrite_buf will start @@ -1596,13 +1631,14 @@ int rewriteAppendOnlyFileBackground(void) { void bgrewriteaofCommand(client *c) { if (server.aof_child_pid != -1) { addReplyError(c,"Background append only file rewriting already in progress"); - } else if (server.rdb_child_pid != -1) { + } else if (hasActiveChildProcess()) { server.aof_rewrite_scheduled = 1; addReplyStatus(c,"Background append only file rewriting scheduled"); } else if (rewriteAppendOnlyFileBackground() == C_OK) { addReplyStatus(c,"Background append only file rewriting started"); } else { - addReply(c,shared.err); + addReplyError(c,"Can't execute an AOF background rewriting. " + "Please check the server logs for more information."); } } @@ -1611,6 +1647,9 @@ void aofRemoveTempFile(pid_t childpid) { snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof", (int) childpid); unlink(tmpfile); + + snprintf(tmpfile,256,"temp-rewriteaof-%d.aof", (int) childpid); + unlink(tmpfile); } /* Update the server.aof_current_size field explicitly using stat(2) @@ -1738,6 +1777,7 @@ void backgroundRewriteDoneHandler(int exitcode, int bysignal) { server.aof_selected_db = -1; /* Make sure SELECT is re-issued */ aofUpdateCurrentSize(); server.aof_rewrite_base_size = server.aof_current_size; + server.aof_fsync_offset = server.aof_current_size; /* Clear regular AOF buffer since its contents was just written to * the new AOF from the background rewrite buffer. */ diff --git a/src/bio.h b/src/bio.h index 4b15d1c4d..6c2155941 100644 --- a/src/bio.h +++ b/src/bio.h @@ -27,6 +27,9 @@ * POSSIBILITY OF SUCH DAMAGE. */ +#ifndef __BIO_H +#define __BIO_H + /* Exported API */ void bioInit(void); void bioCreateBackgroundJob(int type, void *arg1, void *arg2, void *arg3); @@ -40,3 +43,5 @@ void bioKillThreads(void); #define BIO_AOF_FSYNC 1 /* Deferred AOF fsync. */ #define BIO_LAZY_FREE 2 /* Deferred objects freeing. */ #define BIO_NUM_OPS 3 + +#endif diff --git a/src/bitops.c b/src/bitops.c index 8d03a7699..ee1ce0460 100644 --- a/src/bitops.c +++ b/src/bitops.c @@ -994,12 +994,18 @@ void bitfieldCommand(client *c) { /* Lookup for read is ok if key doesn't exit, but errors * if it's not a string. */ o = lookupKeyRead(c->db,c->argv[1]); - if (o != NULL && checkType(c,o,OBJ_STRING)) return; + if (o != NULL && checkType(c,o,OBJ_STRING)) { + zfree(ops); + return; + } } else { /* Lookup by making room up to the farest bit reached by * this operation. */ if ((o = lookupStringForBitCommand(c, - highest_write_offset)) == NULL) return; + highest_write_offset)) == NULL) { + zfree(ops); + return; + } } addReplyArrayLen(c,numops); diff --git a/src/blocked.c b/src/blocked.c index f9e196626..06aa5850e 100644 --- a/src/blocked.c +++ b/src/blocked.c @@ -77,10 +77,18 @@ int serveClientBlockedOnList(client *receiver, robj *key, robj *dstkey, redisDb * is zero. */ int getTimeoutFromObjectOrReply(client *c, robj *object, mstime_t *timeout, int unit) { long long tval; + long double ftval; - if (getLongLongFromObjectOrReply(c,object,&tval, - "timeout is not an integer or out of range") != C_OK) - return C_ERR; + if (unit == UNIT_SECONDS) { + if (getLongDoubleFromObjectOrReply(c,object,&ftval, + "timeout is not an float or out of range") != C_OK) + return C_ERR; + tval = (long long) (ftval * 1000.0); + } else { + if (getLongLongFromObjectOrReply(c,object,&tval, + "timeout is not an integer or out of range") != C_OK) + return C_ERR; + } if (tval < 0) { addReplyError(c,"timeout is negative"); @@ -88,7 +96,6 @@ int getTimeoutFromObjectOrReply(client *c, robj *object, mstime_t *timeout, int } if (tval > 0) { - if (unit == UNIT_SECONDS) tval *= 1000; tval += mstime(); } *timeout = tval; @@ -167,6 +174,7 @@ void unblockClient(client *c) { } else if (c->btype == BLOCKED_WAIT) { unblockClientWaitingReplicas(c); } else if (c->btype == BLOCKED_MODULE) { + if (moduleClientIsBlockedOnKeys(c)) unblockClientWaitingData(c); unblockClientFromModule(c); } else { serverPanic("Unknown btype in unblockClient()."); @@ -222,6 +230,250 @@ void disconnectAllBlockedClients(void) { } } +/* Helper function for handleClientsBlockedOnKeys(). This function is called + * when there may be clients blocked on a list key, and there may be new + * data to fetch (the key is ready). */ +void serveClientsBlockedOnListKey(robj *o, readyList *rl) { + /* We serve clients in the same order they blocked for + * this key, from the first blocked to the last. */ + dictEntry *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 at 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); + notifyKeyspaceEvent(NOTIFY_GENERIC,"del",rl->key,rl->db->id); + } + /* We don't call signalModifiedKey() as it was already called + * when an element was pushed on the list. */ +} + +/* Helper function for handleClientsBlockedOnKeys(). This function is called + * when there may be clients blocked on a sorted set key, and there may be new + * data to fetch (the key is ready). */ +void serveClientsBlockedOnSortedSetKey(robj *o, readyList *rl) { + /* We serve clients in the same order they blocked for + * this key, from the first blocked to the last. */ + dictEntry *de = dictFind(rl->db->blocking_keys,rl->key); + if (de) { + list *clients = dictGetVal(de); + int numclients = listLength(clients); + unsigned long zcard = zsetLength(o); + + while(numclients-- && zcard) { + listNode *clientnode = listFirst(clients); + client *receiver = clientnode->value; + + if (receiver->btype != BLOCKED_ZSET) { + /* Put at the tail, so that at the next call + * we'll not run into it again. */ + listDelNode(clients,clientnode); + listAddNodeTail(clients,receiver); + continue; + } + + int where = (receiver->lastcmd && + receiver->lastcmd->proc == bzpopminCommand) + ? ZSET_MIN : ZSET_MAX; + unblockClient(receiver); + genericZpopCommand(receiver,&rl->key,1,where,1,NULL); + zcard--; + + /* Replicate the command. */ + robj *argv[2]; + struct redisCommand *cmd = where == ZSET_MIN ? + server.zpopminCommand : + server.zpopmaxCommand; + argv[0] = createStringObject(cmd->name,strlen(cmd->name)); + argv[1] = rl->key; + incrRefCount(rl->key); + propagate(cmd,receiver->db->id, + argv,2,PROPAGATE_AOF|PROPAGATE_REPL); + decrRefCount(argv[0]); + decrRefCount(argv[1]); + } + } +} + +/* Helper function for handleClientsBlockedOnKeys(). This function is called + * when there may be clients blocked on a stream key, and there may be new + * data to fetch (the key is ready). */ +void serveClientsBlockedOnStreamKey(robj *o, readyList *rl) { + 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 we blocked in the context of a consumer + * group, we need to resolve the group and update the + * last ID the client is blocked for: this is needed + * because serving other clients in the same consumer + * group will alter the "last ID" of the consumer + * group, and clients blocked in a consumer group are + * always blocked for the ">" ID: we need to deliver + * only new messages and avoid unblocking the client + * otherwise. */ + streamCG *group = NULL; + if (receiver->bpop.xread_group) { + group = streamLookupCG(s, + receiver->bpop.xread_group->ptr); + /* If the group was not found, send an error + * to the consumer. */ + if (!group) { + addReplyError(receiver, + "-NOGROUP the consumer group this client " + "was blocked on no longer exists"); + unblockClient(receiver); + continue; + } else { + *gt = group->last_id; + } + } + + if (streamCompareID(&s->last_id, gt) > 0) { + streamID start = *gt; + streamIncrID(&start); + + /* Lookup the consumer for the group, if any. */ + streamConsumer *consumer = NULL; + int noack = 0; + + if (group) { + consumer = streamLookupConsumer(group, + receiver->bpop.xread_consumer->ptr, + 1); + noack = receiver->bpop.xread_group_noack; + } + + /* 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. */ + if (receiver->resp == 2) { + addReplyArrayLen(receiver,1); + addReplyArrayLen(receiver,2); + } else { + addReplyMapLen(receiver,1); + } + addReplyBulk(receiver,rl->key); + + streamPropInfo pi = { + rl->key, + receiver->bpop.xread_group + }; + streamReplyWithRange(receiver,s,&start,NULL, + receiver->bpop.xread_count, + 0, group, consumer, noack, &pi); + + /* Note that after we unblock the client, 'gt' + * and other receiver->bpop stuff are no longer + * valid, so we must do the setup above before + * this call. */ + unblockClient(receiver); + } + } + } +} + +/* Helper function for handleClientsBlockedOnKeys(). This function is called + * in order to check if we can serve clients blocked by modules using + * RM_BlockClientOnKeys(), when the corresponding key was signaled as ready: + * our goal here is to call the RedisModuleBlockedClient reply() callback to + * see if the key is really able to serve the client, and in that case, + * unblock it. */ +void serveClientsBlockedOnKeyByModule(readyList *rl) { + 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; + + /* Put at the tail, so that at the next call + * we'll not run into it again: clients here may not be + * ready to be served, so they'll remain in the list + * sometimes. We want also be able to skip clients that are + * not blocked for the MODULE type safely. */ + listDelNode(clients,clientnode); + listAddNodeTail(clients,receiver); + + if (receiver->btype != BLOCKED_MODULE) continue; + + /* Note that if *this* client cannot be served by this key, + * it does not mean that another client that is next into the + * list cannot be served as well: they may be blocked by + * different modules with different triggers to consider if a key + * is ready or not. This means we can't exit the loop but need + * to continue after the first failure. */ + if (!moduleTryServeClientBlockedOnKey(receiver, rl->key)) continue; + + moduleUnblockClient(receiver); + } + } +} + /* 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. It handles serving clients blocked in @@ -262,205 +514,32 @@ void handleClientsBlockedOnKeys(void) { * we can safely call signalKeyAsReady() against this key. */ dictDelete(rl->db->ready_keys,rl->key); + /* Even if we are not inside call(), increment the call depth + * in order to make sure that keys are expired against a fixed + * reference time, and not against the wallclock time. This + * way we can lookup an object multiple times (BRPOPLPUSH does + * that) without the risk of it being freed in the second + * lookup, invalidating the first one. + * See https://github.com/antirez/redis/pull/6554. */ + server.fixed_time_expire++; + updateCachedTime(0); + /* 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 at 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); - notifyKeyspaceEvent(NOTIFY_GENERIC,"del",rl->key,rl->db->id); - } - /* We don't call signalModifiedKey() as it was already called - * when an element was pushed on the list. */ - } - - /* Serve clients blocked on sorted set key. */ - else if (o != NULL && o->type == OBJ_ZSET) { - 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); - unsigned long zcard = zsetLength(o); - - while(numclients-- && zcard) { - listNode *clientnode = listFirst(clients); - client *receiver = clientnode->value; - - if (receiver->btype != BLOCKED_ZSET) { - /* Put at the tail, so that at the next call - * we'll not run into it again. */ - listDelNode(clients,clientnode); - listAddNodeTail(clients,receiver); - continue; - } - - int where = (receiver->lastcmd && - receiver->lastcmd->proc == bzpopminCommand) - ? ZSET_MIN : ZSET_MAX; - unblockClient(receiver); - genericZpopCommand(receiver,&rl->key,1,where,1,NULL); - zcard--; - - /* Replicate the command. */ - robj *argv[2]; - struct redisCommand *cmd = where == ZSET_MIN ? - server.zpopminCommand : - server.zpopmaxCommand; - argv[0] = createStringObject(cmd->name,strlen(cmd->name)); - argv[1] = rl->key; - incrRefCount(rl->key); - propagate(cmd,receiver->db->id, - argv,2,PROPAGATE_AOF|PROPAGATE_REPL); - decrRefCount(argv[0]); - decrRefCount(argv[1]); - } - } - } - - /* 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 we blocked in the context of a consumer - * group, we need to resolve the group and update the - * last ID the client is blocked for: this is needed - * because serving other clients in the same consumer - * group will alter the "last ID" of the consumer - * group, and clients blocked in a consumer group are - * always blocked for the ">" ID: we need to deliver - * only new messages and avoid unblocking the client - * otherwise. */ - streamCG *group = NULL; - if (receiver->bpop.xread_group) { - group = streamLookupCG(s, - receiver->bpop.xread_group->ptr); - /* If the group was not found, send an error - * to the consumer. */ - if (!group) { - addReplyError(receiver, - "-NOGROUP the consumer group this client " - "was blocked on no longer exists"); - unblockClient(receiver); - continue; - } else { - *gt = group->last_id; - } - } - - if (streamCompareID(&s->last_id, gt) > 0) { - streamID start = *gt; - start.seq++; /* Can't overflow, it's an uint64_t */ - - /* Lookup the consumer for the group, if any. */ - streamConsumer *consumer = NULL; - int noack = 0; - - if (group) { - consumer = streamLookupConsumer(group, - receiver->bpop.xread_consumer->ptr, - 1); - noack = receiver->bpop.xread_group_noack; - } - - /* 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. */ - if (receiver->resp == 2) { - addReplyArrayLen(receiver,1); - addReplyArrayLen(receiver,2); - } else { - addReplyMapLen(receiver,1); - } - addReplyBulk(receiver,rl->key); - - streamPropInfo pi = { - rl->key, - receiver->bpop.xread_group - }; - streamReplyWithRange(receiver,s,&start,NULL, - receiver->bpop.xread_count, - 0, group, consumer, noack, &pi); - - /* Note that after we unblock the client, 'gt' - * and other receiver->bpop stuff are no longer - * valid, so we must do the setup above before - * this call. */ - unblockClient(receiver); - } - } - } + if (o != NULL) { + if (o->type == OBJ_LIST) + serveClientsBlockedOnListKey(o,rl); + else if (o->type == OBJ_ZSET) + serveClientsBlockedOnSortedSetKey(o,rl); + else if (o->type == OBJ_STREAM) + serveClientsBlockedOnStreamKey(o,rl); + /* We want to serve clients blocked on module keys + * regardless of the object type: we don't know what the + * module is trying to accomplish right now. */ + serveClientsBlockedOnKeyByModule(rl); } + server.fixed_time_expire--; /* Free this item. */ decrRefCount(rl->key); @@ -585,7 +664,7 @@ void unblockClientWaitingData(client *c) { * 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() */ + * The list will be finally processed by handleClientsBlockedOnKeys() */ void signalKeyAsReady(redisDb *db, robj *key) { readyList *rl; diff --git a/src/childinfo.c b/src/childinfo.c index 719025e8c..fa0600552 100644 --- a/src/childinfo.c +++ b/src/childinfo.c @@ -80,6 +80,8 @@ void receiveChildInfo(void) { server.stat_rdb_cow_bytes = server.child_info_data.cow_size; } else if (server.child_info_data.process_type == CHILD_INFO_TYPE_AOF) { server.stat_aof_cow_bytes = server.child_info_data.cow_size; + } else if (server.child_info_data.process_type == CHILD_INFO_TYPE_MODULE) { + server.stat_module_cow_bytes = server.child_info_data.cow_size; } } } diff --git a/src/cluster.c b/src/cluster.c index 1a3a348b5..c05e46f76 100644 --- a/src/cluster.c +++ b/src/cluster.c @@ -49,7 +49,7 @@ clusterNode *myself = NULL; clusterNode *createClusterNode(char *nodename, int flags); int clusterAddNode(clusterNode *node); void clusterAcceptHandler(aeEventLoop *el, int fd, void *privdata, int mask); -void clusterReadHandler(aeEventLoop *el, int fd, void *privdata, int mask); +void clusterReadHandler(connection *conn); void clusterSendPing(clusterLink *link, int type); void clusterSendFail(char *nodename); void clusterSendFailoverAuthIfNeeded(clusterNode *node, clusterMsg *request); @@ -138,6 +138,7 @@ int clusterLoadConfig(char *filename) { /* Handle the special "vars" line. Don't pretend it is the last * line even if it actually is when generated by Redis. */ if (strcasecmp(argv[0],"vars") == 0) { + if (!(argc % 2)) goto fmterr; for (j = 1; j < argc; j += 2) { if (strcasecmp(argv[j],"currentEpoch") == 0) { server.cluster->currentEpoch = @@ -156,7 +157,10 @@ int clusterLoadConfig(char *filename) { } /* Regular config lines have at least eight fields */ - if (argc < 8) goto fmterr; + if (argc < 8) { + sdsfreesplitres(argv,argc); + goto fmterr; + } /* Create this node if it does not exist */ n = clusterLookupNode(argv[0]); @@ -165,7 +169,10 @@ int clusterLoadConfig(char *filename) { clusterAddNode(n); } /* Address and port */ - if ((p = strrchr(argv[1],':')) == NULL) goto fmterr; + if ((p = strrchr(argv[1],':')) == NULL) { + sdsfreesplitres(argv,argc); + goto fmterr; + } *p = '\0'; memcpy(n->ip,argv[1],strlen(argv[1])+1); char *port = p+1; @@ -246,7 +253,10 @@ int clusterLoadConfig(char *filename) { *p = '\0'; direction = p[1]; /* Either '>' or '<' */ slot = atoi(argv[j]+1); - if (slot < 0 || slot >= CLUSTER_SLOTS) goto fmterr; + if (slot < 0 || slot >= CLUSTER_SLOTS) { + sdsfreesplitres(argv,argc); + goto fmterr; + } p += 3; cn = clusterLookupNode(p); if (!cn) { @@ -266,8 +276,12 @@ 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; + if (start < 0 || start >= CLUSTER_SLOTS || + stop < 0 || stop >= CLUSTER_SLOTS) + { + sdsfreesplitres(argv,argc); + goto fmterr; + } while(start <= stop) clusterAddSlot(n, start++); } @@ -476,7 +490,8 @@ void clusterInit(void) { /* Port sanity check II * The other handshake port check is triggered too late to stop * us from trying to use a too-high cluster port number. */ - if (server.port > (65535-CLUSTER_PORT_INCR)) { + int port = server.tls_cluster ? server.tls_port : server.port; + if (port > (65535-CLUSTER_PORT_INCR)) { serverLog(LL_WARNING, "Redis port number too high. " "Cluster communication port is 10,000 port " "numbers higher than your Redis port. " @@ -484,8 +499,7 @@ void clusterInit(void) { "lower than 55535."); exit(1); } - - if (listenToPort(server.port+CLUSTER_PORT_INCR, + if (listenToPort(port+CLUSTER_PORT_INCR, server.cfd,&server.cfd_count) == C_ERR) { exit(1); @@ -507,8 +521,8 @@ void clusterInit(void) { /* Set myself->port / cport to my listening ports, we'll just need to * discover the IP address via MEET messages. */ - myself->port = server.port; - myself->cport = server.port+CLUSTER_PORT_INCR; + myself->port = port; + myself->cport = port+CLUSTER_PORT_INCR; if (server.cluster_announce_port) myself->port = server.cluster_announce_port; if (server.cluster_announce_bus_port) @@ -592,7 +606,7 @@ clusterLink *createClusterLink(clusterNode *node) { link->sndbuf = sdsempty(); link->rcvbuf = sdsempty(); link->node = node; - link->fd = -1; + link->conn = NULL; return link; } @@ -600,23 +614,45 @@ clusterLink *createClusterLink(clusterNode *node) { * This function will just make sure that the original node associated * with this link will have the 'link' field set to NULL. */ void freeClusterLink(clusterLink *link) { - if (link->fd != -1) { - aeDeleteFileEvent(server.el, link->fd, AE_READABLE|AE_WRITABLE); + if (link->conn) { + connClose(link->conn); + link->conn = NULL; } sdsfree(link->sndbuf); sdsfree(link->rcvbuf); if (link->node) link->node->link = NULL; - close(link->fd); zfree(link); } +static void clusterConnAcceptHandler(connection *conn) { + clusterLink *link; + + if (connGetState(conn) != CONN_STATE_CONNECTED) { + serverLog(LL_VERBOSE, + "Error accepting cluster node connection: %s", connGetLastError(conn)); + connClose(conn); + return; + } + + /* Create a link object we use to handle the connection. + * It gets passed to the readable handler when data is available. + * Initiallly the link->node pointer is set to NULL as we don't know + * which node is, but the right node is references once we know the + * node identity. */ + link = createClusterLink(NULL); + link->conn = conn; + connSetPrivateData(conn, link); + + /* Register read handler */ + connSetReadHandler(conn, clusterReadHandler); +} + #define MAX_CLUSTER_ACCEPTS_PER_CALL 1000 void clusterAcceptHandler(aeEventLoop *el, int fd, void *privdata, int mask) { int cport, cfd; int max = MAX_CLUSTER_ACCEPTS_PER_CALL; char cip[NET_IP_STR_LEN]; - clusterLink *link; UNUSED(el); UNUSED(mask); UNUSED(privdata); @@ -633,19 +669,24 @@ void clusterAcceptHandler(aeEventLoop *el, int fd, void *privdata, int mask) { "Error accepting cluster node: %s", server.neterr); return; } - anetNonBlock(NULL,cfd); - anetEnableTcpNoDelay(NULL,cfd); + + connection *conn = server.tls_cluster ? connCreateAcceptedTLS(cfd,1) : connCreateAcceptedSocket(cfd); + connNonBlock(conn); + connEnableTcpNoDelay(conn); /* Use non-blocking I/O for cluster messages. */ - serverLog(LL_VERBOSE,"Accepted cluster node %s:%d", cip, cport); - /* Create a link object we use to handle the connection. - * It gets passed to the readable handler when data is available. - * Initiallly the link->node pointer is set to NULL as we don't know - * which node is, but the right node is references once we know the - * node identity. */ - link = createClusterLink(NULL); - link->fd = cfd; - aeCreateFileEvent(server.el,cfd,AE_READABLE,clusterReadHandler,link); + serverLog(LL_VERBOSE,"Accepting cluster node connection from %s:%d", cip, cport); + + /* Accept the connection now. connAccept() may call our handler directly + * or schedule it for later depending on connection implementation. + */ + if (connAccept(conn, clusterConnAcceptHandler) == C_ERR) { + serverLog(LL_VERBOSE, + "Error accepting cluster node connection: %s", + connGetLastError(conn)); + connClose(conn); + return; + } } } @@ -1446,7 +1487,7 @@ void nodeIp2String(char *buf, clusterLink *link, char *announced_ip) { memcpy(buf,announced_ip,NET_IP_STR_LEN); buf[NET_IP_STR_LEN-1] = '\0'; /* We are not sure the input is sane. */ } else { - anetPeerToString(link->fd, buf, NET_IP_STR_LEN, NULL); + connPeerToString(link->conn, buf, NET_IP_STR_LEN, NULL); } } @@ -1750,7 +1791,7 @@ int clusterProcessPacket(clusterLink *link) { { char ip[NET_IP_STR_LEN]; - if (anetSockName(link->fd,ip,sizeof(ip),NULL) != -1 && + if (connSockName(link->conn,ip,sizeof(ip),NULL) != -1 && strcmp(ip,myself->ip)) { memcpy(myself->ip,ip,NET_IP_STR_LEN); @@ -2117,35 +2158,76 @@ void handleLinkIOError(clusterLink *link) { /* Send data. This is handled using a trivial send buffer that gets * consumed by write(). We don't try to optimize this for speed too much * as this is a very low traffic channel. */ -void clusterWriteHandler(aeEventLoop *el, int fd, void *privdata, int mask) { - clusterLink *link = (clusterLink*) privdata; +void clusterWriteHandler(connection *conn) { + clusterLink *link = connGetPrivateData(conn); ssize_t nwritten; - UNUSED(el); - UNUSED(mask); - nwritten = write(fd, link->sndbuf, sdslen(link->sndbuf)); + nwritten = connWrite(conn, link->sndbuf, sdslen(link->sndbuf)); if (nwritten <= 0) { serverLog(LL_DEBUG,"I/O error writing to node link: %s", - (nwritten == -1) ? strerror(errno) : "short write"); + (nwritten == -1) ? connGetLastError(conn) : "short write"); handleLinkIOError(link); return; } sdsrange(link->sndbuf,nwritten,-1); if (sdslen(link->sndbuf) == 0) - aeDeleteFileEvent(server.el, link->fd, AE_WRITABLE); + connSetWriteHandler(link->conn, NULL); +} + +/* A connect handler that gets called when a connection to another node + * gets established. + */ +void clusterLinkConnectHandler(connection *conn) { + clusterLink *link = connGetPrivateData(conn); + clusterNode *node = link->node; + + /* Check if connection succeeded */ + if (connGetState(conn) != CONN_STATE_CONNECTED) { + serverLog(LL_VERBOSE, "Connection with Node %.40s at %s:%d failed: %s", + node->name, node->ip, node->cport, + connGetLastError(conn)); + freeClusterLink(link); + return; + } + + /* Register a read handler from now on */ + connSetReadHandler(conn, clusterReadHandler); + + /* Queue a PING in the new connection ASAP: this is crucial + * to avoid false positives in failure detection. + * + * If the node is flagged as MEET, we send a MEET message instead + * of a PING one, to force the receiver to add us in its node + * table. */ + mstime_t old_ping_sent = node->ping_sent; + clusterSendPing(link, node->flags & CLUSTER_NODE_MEET ? + CLUSTERMSG_TYPE_MEET : CLUSTERMSG_TYPE_PING); + if (old_ping_sent) { + /* If there was an active ping before the link was + * disconnected, we want to restore the ping time, otherwise + * replaced by the clusterSendPing() call. */ + node->ping_sent = old_ping_sent; + } + /* We can clear the flag after the first packet is sent. + * If we'll never receive a PONG, we'll never send new packets + * to this node. Instead after the PONG is received and we + * are no longer in meet/handshake status, we want to send + * normal PING packets. */ + node->flags &= ~CLUSTER_NODE_MEET; + + serverLog(LL_DEBUG,"Connecting with Node %.40s at %s:%d", + node->name, node->ip, node->cport); } /* Read data. Try to read the first field of the header first to check the * full length of the packet. When a whole packet is in memory this function * will call the function to process the packet. And so forth. */ -void clusterReadHandler(aeEventLoop *el, int fd, void *privdata, int mask) { - char buf[sizeof(clusterMsg)]; +void clusterReadHandler(connection *conn) { + clusterMsg buf[1]; ssize_t nread; clusterMsg *hdr; - clusterLink *link = (clusterLink*) privdata; + clusterLink *link = connGetPrivateData(conn); unsigned int readlen, rcvbuflen; - UNUSED(el); - UNUSED(mask); while(1) { /* Read as long as there is data to read. */ rcvbuflen = sdslen(link->rcvbuf); @@ -2173,13 +2255,13 @@ void clusterReadHandler(aeEventLoop *el, int fd, void *privdata, int mask) { if (readlen > sizeof(buf)) readlen = sizeof(buf); } - nread = read(fd,buf,readlen); - if (nread == -1 && errno == EAGAIN) return; /* No more data ready. */ + nread = connRead(conn,buf,readlen); + if (nread == -1 && (connGetState(conn) == CONN_STATE_CONNECTED)) return; /* No more data ready. */ if (nread <= 0) { /* I/O error... */ serverLog(LL_DEBUG,"I/O error reading from node link: %s", - (nread == 0) ? "connection closed" : strerror(errno)); + (nread == 0) ? "connection closed" : connGetLastError(conn)); handleLinkIOError(link); return; } else { @@ -2208,8 +2290,7 @@ void clusterReadHandler(aeEventLoop *el, int fd, void *privdata, int mask) { * from event handlers that will do stuff with the same link later. */ void clusterSendMessage(clusterLink *link, unsigned char *msg, size_t msglen) { if (sdslen(link->sndbuf) == 0 && msglen != 0) - aeCreateFileEvent(server.el,link->fd,AE_WRITABLE|AE_BARRIER, - clusterWriteHandler,link); + connSetWriteHandlerWithBarrier(link->conn, clusterWriteHandler, 1); link->sndbuf = sdscatlen(link->sndbuf, msg, msglen); @@ -2275,11 +2356,12 @@ void clusterBuildMessageHdr(clusterMsg *hdr, int type) { } /* Handle cluster-announce-port as well. */ + int port = server.tls_cluster ? server.tls_port : server.port; int announced_port = server.cluster_announce_port ? - server.cluster_announce_port : server.port; + server.cluster_announce_port : port; int announced_cport = server.cluster_announce_bus_port ? server.cluster_announce_bus_port : - (server.port + CLUSTER_PORT_INCR); + (port + CLUSTER_PORT_INCR); memcpy(hdr->myslots,master->slots,sizeof(hdr->myslots)); memset(hdr->slaveof,0,CLUSTER_NAMELEN); @@ -2516,7 +2598,8 @@ void clusterBroadcastPong(int target) { * * If link is NULL, then the message is broadcasted to the whole cluster. */ void clusterSendPublish(clusterLink *link, robj *channel, robj *message) { - unsigned char buf[sizeof(clusterMsg)], *payload; + unsigned char *payload; + clusterMsg buf[1]; clusterMsg *hdr = (clusterMsg*) buf; uint32_t totlen; uint32_t channel_len, message_len; @@ -2536,7 +2619,7 @@ void clusterSendPublish(clusterLink *link, robj *channel, robj *message) { /* Try to use the local buffer if possible */ if (totlen < sizeof(buf)) { - payload = buf; + payload = (unsigned char*)buf; } else { payload = zmalloc(totlen); memcpy(payload,hdr,sizeof(*hdr)); @@ -2553,7 +2636,7 @@ void clusterSendPublish(clusterLink *link, robj *channel, robj *message) { decrRefCount(channel); decrRefCount(message); - if (payload != buf) zfree(payload); + if (payload != (unsigned char*)buf) zfree(payload); } /* Send a FAIL message to all the nodes we are able to contact. @@ -2562,7 +2645,7 @@ void clusterSendPublish(clusterLink *link, robj *channel, robj *message) { * we switch the node state to CLUSTER_NODE_FAIL and ask all the other * nodes to do the same ASAP. */ void clusterSendFail(char *nodename) { - unsigned char buf[sizeof(clusterMsg)]; + clusterMsg buf[1]; clusterMsg *hdr = (clusterMsg*) buf; clusterBuildMessageHdr(hdr,CLUSTERMSG_TYPE_FAIL); @@ -2574,7 +2657,7 @@ void clusterSendFail(char *nodename) { * slots configuration. The node name, slots bitmap, and configEpoch info * are included. */ void clusterSendUpdate(clusterLink *link, clusterNode *node) { - unsigned char buf[sizeof(clusterMsg)]; + clusterMsg buf[1]; clusterMsg *hdr = (clusterMsg*) buf; if (link == NULL) return; @@ -2582,7 +2665,7 @@ void clusterSendUpdate(clusterLink *link, clusterNode *node) { memcpy(hdr->data.update.nodecfg.nodename,node->name,CLUSTER_NAMELEN); hdr->data.update.nodecfg.configEpoch = htonu64(node->configEpoch); memcpy(hdr->data.update.nodecfg.slots,node->slots,sizeof(node->slots)); - clusterSendMessage(link,buf,ntohl(hdr->totlen)); + clusterSendMessage(link,(unsigned char*)buf,ntohl(hdr->totlen)); } /* Send a MODULE message. @@ -2590,7 +2673,8 @@ void clusterSendUpdate(clusterLink *link, clusterNode *node) { * If link is NULL, then the message is broadcasted to the whole cluster. */ void clusterSendModule(clusterLink *link, uint64_t module_id, uint8_t type, unsigned char *payload, uint32_t len) { - unsigned char buf[sizeof(clusterMsg)], *heapbuf; + unsigned char *heapbuf; + clusterMsg buf[1]; clusterMsg *hdr = (clusterMsg*) buf; uint32_t totlen; @@ -2605,7 +2689,7 @@ void clusterSendModule(clusterLink *link, uint64_t module_id, uint8_t type, /* Try to use the local buffer if possible */ if (totlen < sizeof(buf)) { - heapbuf = buf; + heapbuf = (unsigned char*)buf; } else { heapbuf = zmalloc(totlen); memcpy(heapbuf,hdr,sizeof(*hdr)); @@ -2618,7 +2702,7 @@ void clusterSendModule(clusterLink *link, uint64_t module_id, uint8_t type, else clusterBroadcastMessage(heapbuf,totlen); - if (heapbuf != buf) zfree(heapbuf); + if (heapbuf != (unsigned char*)buf) zfree(heapbuf); } /* This function gets a cluster node ID string as target, the same way the nodes @@ -2662,7 +2746,7 @@ void clusterPropagatePublish(robj *channel, robj *message) { * Note that we send the failover request to everybody, master and slave nodes, * but only the masters are supposed to reply to our query. */ void clusterRequestFailoverAuth(void) { - unsigned char buf[sizeof(clusterMsg)]; + clusterMsg buf[1]; clusterMsg *hdr = (clusterMsg*) buf; uint32_t totlen; @@ -2678,7 +2762,7 @@ void clusterRequestFailoverAuth(void) { /* Send a FAILOVER_AUTH_ACK message to the specified node. */ void clusterSendFailoverAuth(clusterNode *node) { - unsigned char buf[sizeof(clusterMsg)]; + clusterMsg buf[1]; clusterMsg *hdr = (clusterMsg*) buf; uint32_t totlen; @@ -2686,12 +2770,12 @@ void clusterSendFailoverAuth(clusterNode *node) { clusterBuildMessageHdr(hdr,CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK); totlen = sizeof(clusterMsg)-sizeof(union clusterMsgData); hdr->totlen = htonl(totlen); - clusterSendMessage(node->link,buf,totlen); + clusterSendMessage(node->link,(unsigned char*)buf,totlen); } /* Send a MFSTART message to the specified node. */ void clusterSendMFStart(clusterNode *node) { - unsigned char buf[sizeof(clusterMsg)]; + clusterMsg buf[1]; clusterMsg *hdr = (clusterMsg*) buf; uint32_t totlen; @@ -2699,7 +2783,7 @@ void clusterSendMFStart(clusterNode *node) { clusterBuildMessageHdr(hdr,CLUSTERMSG_TYPE_MFSTART); totlen = sizeof(clusterMsg)-sizeof(union clusterMsgData); hdr->totlen = htonl(totlen); - clusterSendMessage(node->link,buf,totlen); + clusterSendMessage(node->link,(unsigned char*)buf,totlen); } /* Vote for the node asking for our vote if there are the conditions. */ @@ -3031,6 +3115,7 @@ void clusterHandleSlaveFailover(void) { if (server.cluster->mf_end) { server.cluster->failover_auth_time = mstime(); server.cluster->failover_auth_rank = 0; + clusterDoBeforeSleep(CLUSTER_TODO_HANDLE_FAILOVER); } serverLog(LL_WARNING, "Start of election delayed for %lld milliseconds " @@ -3381,13 +3466,11 @@ void clusterCron(void) { } if (node->link == NULL) { - int fd; - mstime_t old_ping_sent; - clusterLink *link; - - fd = anetTcpNonBlockBindConnect(server.neterr, node->ip, - node->cport, NET_FIRST_BIND_ADDR); - if (fd == -1) { + clusterLink *link = createClusterLink(node); + link->conn = server.tls_cluster ? connCreateTLS() : connCreateSocket(); + connSetPrivateData(link->conn, link); + if (connConnect(link->conn, node->ip, node->cport, NET_FIRST_BIND_ADDR, + clusterLinkConnectHandler) == -1) { /* We got a synchronous error from connect before * clusterSendPing() had a chance to be called. * If node->ping_sent is zero, failure detection can't work, @@ -3397,37 +3480,11 @@ void clusterCron(void) { serverLog(LL_DEBUG, "Unable to connect to " "Cluster Node [%s]:%d -> %s", node->ip, node->cport, server.neterr); + + freeClusterLink(link); continue; } - link = createClusterLink(node); - link->fd = fd; node->link = link; - aeCreateFileEvent(server.el,link->fd,AE_READABLE, - clusterReadHandler,link); - /* Queue a PING in the new connection ASAP: this is crucial - * to avoid false positives in failure detection. - * - * If the node is flagged as MEET, we send a MEET message instead - * of a PING one, to force the receiver to add us in its node - * table. */ - old_ping_sent = node->ping_sent; - clusterSendPing(link, node->flags & CLUSTER_NODE_MEET ? - CLUSTERMSG_TYPE_MEET : CLUSTERMSG_TYPE_PING); - if (old_ping_sent) { - /* If there was an active ping before the link was - * disconnected, we want to restore the ping time, otherwise - * replaced by the clusterSendPing() call. */ - node->ping_sent = old_ping_sent; - } - /* We can clear the flag after the first packet is sent. - * If we'll never receive a PONG, we'll never send new packets - * to this node. Instead after the PONG is received and we - * are no longer in meet/handshake status, we want to send - * normal PING packets. */ - node->flags &= ~CLUSTER_NODE_MEET; - - serverLog(LL_DEBUG,"Connecting with Node %.40s at %s:%d", - node->name, node->ip, node->cport); } } dictReleaseIterator(di); @@ -4250,12 +4307,9 @@ NULL } } else if (!strcasecmp(c->argv[1]->ptr,"nodes") && c->argc == 2) { /* CLUSTER NODES */ - robj *o; - sds ci = clusterGenNodesDescription(0); - - o = createObject(OBJ_STRING,ci); - addReplyBulk(c,o); - decrRefCount(o); + sds nodes = clusterGenNodesDescription(0); + addReplyVerbatim(c,nodes,sdslen(nodes),"txt"); + sdsfree(nodes); } else if (!strcasecmp(c->argv[1]->ptr,"myid") && c->argc == 2) { /* CLUSTER MYID */ addReplyBulkCBuffer(c,myself->name, CLUSTER_NAMELEN); @@ -4497,10 +4551,8 @@ NULL "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); - addReply(c,shared.crlf); + addReplyVerbatim(c,info,sdslen(info),"txt"); + sdsfree(info); } else if (!strcasecmp(c->argv[1]->ptr,"saveconfig") && c->argc == 2) { int retval = clusterSaveConfig(1); @@ -4775,7 +4827,7 @@ NULL /* Generates a DUMP-format representation of the object 'o', adding it to the * io stream pointed by 'rio'. This function can't fail. */ -void createDumpPayload(rio *payload, robj *o) { +void createDumpPayload(rio *payload, robj *o, robj *key) { unsigned char buf[2]; uint64_t crc; @@ -4783,7 +4835,7 @@ void createDumpPayload(rio *payload, robj *o) { * byte followed by the serialized object. This is understood by RESTORE. */ rioInitWithBuffer(payload,sdsempty()); serverAssert(rdbSaveObjectType(payload,o)); - serverAssert(rdbSaveObject(payload,o)); + serverAssert(rdbSaveObject(payload,o,key)); /* Write the footer, this is how it looks like: * ----------------+---------------------+---------------+ @@ -4831,7 +4883,7 @@ int verifyDumpPayload(unsigned char *p, size_t len) { * DUMP is actually not used by Redis Cluster but it is the obvious * complement of RESTORE and can be useful for different applications. */ void dumpCommand(client *c) { - robj *o, *dumpobj; + robj *o; rio payload; /* Check if the key is here. */ @@ -4841,12 +4893,10 @@ void dumpCommand(client *c) { } /* Create the DUMP encoded representation. */ - createDumpPayload(&payload,o); + createDumpPayload(&payload,o,c->argv[1]); /* Transfer to the client */ - dumpobj = createObject(OBJ_STRING,payload.io.buffer.ptr); - addReplyBulk(c,dumpobj); - decrRefCount(dumpobj); + addReplyBulkSds(c,payload.io.buffer.ptr); return; } @@ -4914,7 +4964,7 @@ void restoreCommand(client *c) { rioInitWithBuffer(&payload,c->argv[3]->ptr); if (((type = rdbLoadObjectType(&payload)) == -1) || - ((obj = rdbLoadObject(type,&payload)) == NULL)) + ((obj = rdbLoadObject(type,&payload,c->argv[1])) == NULL)) { addReplyError(c,"Bad data format"); return; @@ -4929,7 +4979,7 @@ void restoreCommand(client *c) { if (!absttl) ttl+=mstime(); setExpire(c,c->db,c->argv[1],ttl); } - objectSetLRUOrLFU(obj,lfu_freq,lru_idle,lru_clock); + objectSetLRUOrLFU(obj,lfu_freq,lru_idle,lru_clock,1000); signalModifiedKey(c->db,c->argv[1]); addReply(c,shared.ok); server.dirty++; @@ -4945,7 +4995,7 @@ void restoreCommand(client *c) { #define MIGRATE_SOCKET_CACHE_TTL 10 /* close cached sockets after 10 sec. */ typedef struct migrateCachedSocket { - int fd; + connection *conn; long last_dbid; time_t last_use_time; } migrateCachedSocket; @@ -4962,7 +5012,7 @@ typedef struct migrateCachedSocket { * should be called so that the connection will be created from scratch * the next time. */ migrateCachedSocket* migrateGetSocket(client *c, robj *host, robj *port, long timeout) { - int fd; + connection *conn; sds name = sdsempty(); migrateCachedSocket *cs; @@ -4982,34 +5032,27 @@ migrateCachedSocket* migrateGetSocket(client *c, robj *host, robj *port, long ti /* Too many items, drop one at random. */ dictEntry *de = dictGetRandomKey(server.migrate_cached_sockets); cs = dictGetVal(de); - close(cs->fd); + connClose(cs->conn); zfree(cs); dictDelete(server.migrate_cached_sockets,dictGetKey(de)); } /* Create the socket */ - fd = anetTcpNonBlockConnect(server.neterr,c->argv[1]->ptr, - atoi(c->argv[2]->ptr)); - if (fd == -1) { - sdsfree(name); - addReplyErrorFormat(c,"Can't connect to target node: %s", - server.neterr); - return NULL; - } - anetEnableTcpNoDelay(server.neterr,fd); - - /* Check if it connects within the specified timeout. */ - if ((aeWait(fd,AE_WRITABLE,timeout) & AE_WRITABLE) == 0) { - sdsfree(name); + conn = server.tls_cluster ? connCreateTLS() : connCreateSocket(); + if (connBlockingConnect(conn, c->argv[1]->ptr, atoi(c->argv[2]->ptr), timeout) + != C_OK) { addReplySds(c, sdsnew("-IOERR error or timeout connecting to the client\r\n")); - close(fd); + connClose(conn); + sdsfree(name); return NULL; } + connEnableTcpNoDelay(conn); /* Add to the cache and return it to the caller. */ cs = zmalloc(sizeof(*cs)); - cs->fd = fd; + cs->conn = conn; + cs->last_dbid = -1; cs->last_use_time = server.unixtime; dictAdd(server.migrate_cached_sockets,name,cs); @@ -5030,7 +5073,7 @@ void migrateCloseSocket(robj *host, robj *port) { return; } - close(cs->fd); + connClose(cs->conn); zfree(cs); dictDelete(server.migrate_cached_sockets,name); sdsfree(name); @@ -5044,7 +5087,7 @@ void migrateCloseTimedoutSockets(void) { migrateCachedSocket *cs = dictGetVal(de); if ((server.unixtime - cs->last_use_time) > MIGRATE_SOCKET_CACHE_TTL) { - close(cs->fd); + connClose(cs->conn); zfree(cs); dictDelete(server.migrate_cached_sockets,dictGetKey(de)); } @@ -5202,7 +5245,7 @@ try_again: /* Emit the payload argument, that is the serialized object using * the DUMP format. */ - createDumpPayload(&payload,ov[j]); + createDumpPayload(&payload,ov[j],kv[j]); serverAssertWithInfo(c,NULL, rioWriteBulkString(&cmd,payload.io.buffer.ptr, sdslen(payload.io.buffer.ptr))); @@ -5226,7 +5269,7 @@ try_again: while ((towrite = sdslen(buf)-pos) > 0) { towrite = (towrite > (64*1024) ? (64*1024) : towrite); - nwritten = syncWrite(cs->fd,buf+pos,towrite,timeout); + nwritten = connSyncWrite(cs->conn,buf+pos,towrite,timeout); if (nwritten != (signed)towrite) { write_error = 1; goto socket_err; @@ -5240,11 +5283,11 @@ try_again: char buf2[1024]; /* Restore reply. */ /* Read the AUTH reply if needed. */ - if (password && syncReadLine(cs->fd, buf0, sizeof(buf0), timeout) <= 0) + if (password && connSyncReadLine(cs->conn, buf0, sizeof(buf0), timeout) <= 0) goto socket_err; /* Read the SELECT reply if needed. */ - if (select && syncReadLine(cs->fd, buf1, sizeof(buf1), timeout) <= 0) + if (select && connSyncReadLine(cs->conn, buf1, sizeof(buf1), timeout) <= 0) goto socket_err; /* Read the RESTORE replies. */ @@ -5259,7 +5302,7 @@ try_again: if (!copy) newargv = zmalloc(sizeof(robj*)*(num_keys+1)); for (j = 0; j < num_keys; j++) { - if (syncReadLine(cs->fd, buf2, sizeof(buf2), timeout) <= 0) { + if (connSyncReadLine(cs->conn, buf2, sizeof(buf2), timeout) <= 0) { socket_error = 1; break; } @@ -5446,8 +5489,8 @@ void readwriteCommand(client *c) { * already "down" but it is fragile to rely on the update of the global state, * so we also handle it here. * - * CLUSTER_REDIR_DOWN_STATE if the cluster is down but the user attempts to - * execute a command that addresses one or more keys. */ + * CLUSTER_REDIR_DOWN_STATE and CLUSTER_REDIR_DOWN_RO_STATE if the cluster is + * down but the user attempts to execute a command that addresses one or more keys. */ clusterNode *getNodeByQuery(client *c, struct redisCommand *cmd, robj **argv, int argc, int *hashslot, int *error_code) { clusterNode *n = NULL; robj *firstkey = NULL; @@ -5565,10 +5608,27 @@ clusterNode *getNodeByQuery(client *c, struct redisCommand *cmd, robj **argv, in * without redirections or errors in all the cases. */ if (n == NULL) return myself; - /* Cluster is globally down but we got keys? We can't serve the request. */ + /* Cluster is globally down but we got keys? We only serve the request + * if it is a read command and when allow_reads_when_down is enabled. */ if (server.cluster->state != CLUSTER_OK) { - if (error_code) *error_code = CLUSTER_REDIR_DOWN_STATE; - return NULL; + if (!server.cluster_allow_reads_when_down) { + /* The cluster is configured to block commands when the + * cluster is down. */ + if (error_code) *error_code = CLUSTER_REDIR_DOWN_STATE; + return NULL; + } else if (!(cmd->flags & CMD_READONLY) && !(cmd->proc == evalCommand) + && !(cmd->proc == evalShaCommand)) + { + /* The cluster is configured to allow read only commands + * but this command is neither readonly, nor EVAL or + * EVALSHA. */ + if (error_code) *error_code = CLUSTER_REDIR_DOWN_RO_STATE; + return NULL; + } else { + /* Fall through and allow the command to be executed: + * this happens when server.cluster_allow_reads_when_down is + * true and the command is a readonly command or EVAL / EVALSHA. */ + } } /* Return the hashslot by reference. */ @@ -5637,6 +5697,8 @@ void clusterRedirectClient(client *c, clusterNode *n, int hashslot, int error_co addReplySds(c,sdsnew("-TRYAGAIN Multiple keys request during rehashing of slot\r\n")); } else if (error_code == CLUSTER_REDIR_DOWN_STATE) { addReplySds(c,sdsnew("-CLUSTERDOWN The cluster is down\r\n")); + } else if (error_code == CLUSTER_REDIR_DOWN_RO_STATE) { + addReplySds(c,sdsnew("-CLUSTERDOWN The cluster is down and only accepts read commands\r\n")); } else if (error_code == CLUSTER_REDIR_DOWN_UNBOUND) { addReplySds(c,sdsnew("-CLUSTERDOWN Hash slot not served\r\n")); } else if (error_code == CLUSTER_REDIR_MOVED || @@ -5671,7 +5733,10 @@ int clusterRedirectBlockedClientIfNeeded(client *c) { dictEntry *de; dictIterator *di; - /* If the cluster is down, unblock the client with the right error. */ + /* If the cluster is down, unblock the client with the right error. + * If the cluster is configured to allow reads on cluster down, we + * still want to emit this error since a write will be required + * to unblock them which may never come. */ if (server.cluster->state == CLUSTER_FAIL) { clusterRedirectClient(c,NULL,0,CLUSTER_REDIR_DOWN_STATE); return 1; diff --git a/src/cluster.h b/src/cluster.h index 571b9c543..35fc0cbfa 100644 --- a/src/cluster.h +++ b/src/cluster.h @@ -13,15 +13,10 @@ /* The following defines are amount of time, sometimes expressed as * multiplicators of the node timeout value (when ending with MULT). */ -#define CLUSTER_DEFAULT_NODE_TIMEOUT 15000 -#define CLUSTER_DEFAULT_SLAVE_VALIDITY 10 /* Slave max data age factor. */ -#define CLUSTER_DEFAULT_REQUIRE_FULL_COVERAGE 1 -#define CLUSTER_DEFAULT_SLAVE_NO_FAILOVER 0 /* Failover by default. */ #define CLUSTER_FAIL_REPORT_VALIDITY_MULT 2 /* Fail report validity. */ #define CLUSTER_FAIL_UNDO_TIME_MULT 2 /* Undo fail if master is back. */ #define CLUSTER_FAIL_UNDO_TIME_ADD 10 /* Some additional time. */ #define CLUSTER_FAILOVER_DELAY 5 /* Seconds */ -#define CLUSTER_DEFAULT_MIGRATION_BARRIER 1 #define CLUSTER_MF_TIMEOUT 5000 /* Milliseconds to do a manual failover. */ #define CLUSTER_MF_PAUSE_MULT 2 /* Master pause manual failover mult. */ #define CLUSTER_SLAVE_MIGRATION_DELAY 5000 /* Delay for slave migration. */ @@ -34,13 +29,14 @@ #define CLUSTER_REDIR_MOVED 4 /* -MOVED redirection required. */ #define CLUSTER_REDIR_DOWN_STATE 5 /* -CLUSTERDOWN, global state. */ #define CLUSTER_REDIR_DOWN_UNBOUND 6 /* -CLUSTERDOWN, unbound slot. */ +#define CLUSTER_REDIR_DOWN_RO_STATE 7 /* -CLUSTERDOWN, allow reads. */ struct clusterNode; /* clusterLink encapsulates everything needed to talk with a remote node. */ typedef struct clusterLink { mstime_t ctime; /* Link creation time */ - int fd; /* TCP socket file descriptor */ + connection *conn; /* Connection to remote node */ sds sndbuf; /* Packet send buffer */ sds rcvbuf; /* Packet reception buffer */ struct clusterNode *node; /* Node related to this link if any, or NULL */ diff --git a/src/config.c b/src/config.c index 8fe5cdbb7..d55d1f8b5 100644 --- a/src/config.c +++ b/src/config.c @@ -91,6 +91,13 @@ configEnum aof_fsync_enum[] = { {NULL, 0} }; +configEnum repl_diskless_load_enum[] = { + {"disabled", REPL_DISKLESS_LOAD_DISABLED}, + {"on-empty-db", REPL_DISKLESS_LOAD_WHEN_DB_EMPTY}, + {"swapdb", REPL_DISKLESS_LOAD_SWAPDB}, + {NULL, 0} +}; + /* Output buffer limits presets. */ clientBufferLimitsConfig clientBufferLimitsDefaults[CLIENT_TYPE_OBUF_COUNT] = { {0, 0, 0}, /* normal */ @@ -98,6 +105,110 @@ clientBufferLimitsConfig clientBufferLimitsDefaults[CLIENT_TYPE_OBUF_COUNT] = { {1024*1024*32, 1024*1024*8, 60} /* pubsub */ }; +/* Generic config infrastructure function pointers + * int is_valid_fn(val, err) + * Return 1 when val is valid, and 0 when invalid. + * Optionslly set err to a static error string. + * int update_fn(val, prev, err) + * This function is called only for CONFIG SET command (not at config file parsing) + * It is called after the actual config is applied, + * Return 1 for success, and 0 for failure. + * Optionslly set err to a static error string. + * On failure the config change will be reverted. + */ + +/* Configuration values that require no special handling to set, get, load or + * rewrite. */ +typedef struct boolConfigData { + int *config; /* The pointer to the server config this value is stored in */ + const int default_value; /* The default value of the config on rewrite */ + int (*is_valid_fn)(int val, char **err); /* Optional function to check validity of new value (generic doc above) */ + int (*update_fn)(int val, int prev, char **err); /* Optional function to apply new value at runtime (generic doc above) */ +} boolConfigData; + +typedef struct stringConfigData { + char **config; /* Pointer to the server config this value is stored in. */ + const char *default_value; /* Default value of the config on rewrite. */ + int (*is_valid_fn)(char* val, char **err); /* Optional function to check validity of new value (generic doc above) */ + int (*update_fn)(char* val, char* prev, char **err); /* Optional function to apply new value at runtime (generic doc above) */ + int convert_empty_to_null; /* Boolean indicating if empty strings should + be stored as a NULL value. */ +} stringConfigData; + +typedef struct enumConfigData { + int *config; /* The pointer to the server config this value is stored in */ + configEnum *enum_value; /* The underlying enum type this data represents */ + const int default_value; /* The default value of the config on rewrite */ + int (*is_valid_fn)(int val, char **err); /* Optional function to check validity of new value (generic doc above) */ + int (*update_fn)(int val, int prev, char **err); /* Optional function to apply new value at runtime (generic doc above) */ +} enumConfigData; + +typedef enum numericType { + NUMERIC_TYPE_INT, + NUMERIC_TYPE_UINT, + NUMERIC_TYPE_LONG, + NUMERIC_TYPE_ULONG, + NUMERIC_TYPE_LONG_LONG, + NUMERIC_TYPE_ULONG_LONG, + NUMERIC_TYPE_SIZE_T, + NUMERIC_TYPE_SSIZE_T, + NUMERIC_TYPE_OFF_T, + NUMERIC_TYPE_TIME_T, +} numericType; + +typedef struct numericConfigData { + union { + int *i; + unsigned int *ui; + long *l; + unsigned long *ul; + long long *ll; + unsigned long long *ull; + size_t *st; + ssize_t *sst; + off_t *ot; + time_t *tt; + } config; /* The pointer to the numeric config this value is stored in */ + int is_memory; /* Indicates if this value can be loaded as a memory value */ + numericType numeric_type; /* An enum indicating the type of this value */ + long long lower_bound; /* The lower bound of this numeric value */ + long long upper_bound; /* The upper bound of this numeric value */ + const long long default_value; /* The default value of the config on rewrite */ + int (*is_valid_fn)(long long val, char **err); /* Optional function to check validity of new value (generic doc above) */ + int (*update_fn)(long long val, long long prev, char **err); /* Optional function to apply new value at runtime (generic doc above) */ +} numericConfigData; + +typedef union typeData { + boolConfigData yesno; + stringConfigData string; + enumConfigData enumd; + numericConfigData numeric; +} typeData; + +typedef struct typeInterface { + /* Called on server start, to init the server with default value */ + void (*init)(typeData data); + /* Called on server start, should return 1 on success, 0 on error and should set err */ + int (*load)(typeData data, sds *argc, int argv, char **err); + /* Called on server startup and CONFIG SET, returns 1 on success, 0 on error + * and can set a verbose err string, update is true when called from CONFIG SET */ + int (*set)(typeData data, sds value, int update, char **err); + /* Called on CONFIG GET, required to add output to the client */ + void (*get)(client *c, typeData data); + /* Called on CONFIG REWRITE, required to rewrite the config state */ + void (*rewrite)(typeData data, const char *name, struct rewriteConfigState *state); +} typeInterface; + +typedef struct standardConfig { + const char *name; /* The user visible name of this config */ + const char *alias; /* An alias that can also be used for this config */ + const int modifiable; /* Can this value be updated by CONFIG SET? */ + typeInterface interface; /* The function pointers that define the type interface */ + typeData data; /* The type specific data exposed used by the interface */ +} standardConfig; + +standardConfig configs[]; + /*----------------------------------------------------------------------------- * Enum access functions *----------------------------------------------------------------------------*/ @@ -169,6 +280,12 @@ void queueLoadModule(sds path, sds *argv, int argc) { listAddNodeTail(server.loadmodule_queue,loadmod); } +void initConfigValues() { + for (standardConfig *config = configs; config->name != NULL; config++) { + config->interface.init(config->data); + } +} + void loadServerConfigFromString(char *config) { char *err = NULL; int linenum = 0, totlines, i; @@ -201,46 +318,44 @@ void loadServerConfigFromString(char *config) { } sdstolower(argv[0]); + /* Iterate the configs that are standard */ + int match = 0; + for (standardConfig *config = configs; config->name != NULL; config++) { + if ((!strcasecmp(argv[0],config->name) || + (config->alias && !strcasecmp(argv[0],config->alias)))) + { + if (argc != 2) { + err = "wrong number of arguments"; + goto loaderr; + } + if (!config->interface.set(config->data, argv[1], 0, &err)) { + goto loaderr; + } + + match = 1; + break; + } + } + + if (match) { + sdsfreesplitres(argv,argc); + continue; + } + /* Execute config directives */ - if (!strcasecmp(argv[0],"timeout") && argc == 2) { - server.maxidletime = atoi(argv[1]); - if (server.maxidletime < 0) { - err = "Invalid timeout value"; goto loaderr; - } - } else if (!strcasecmp(argv[0],"tcp-keepalive") && argc == 2) { - server.tcpkeepalive = atoi(argv[1]); - if (server.tcpkeepalive < 0) { - err = "Invalid tcp-keepalive value"; goto loaderr; - } - } else if (!strcasecmp(argv[0],"protected-mode") && argc == 2) { - if ((server.protected_mode = yesnotoi(argv[1])) == -1) { - err = "argument must be 'yes' or 'no'"; goto loaderr; - } - } else if (!strcasecmp(argv[0],"gopher-enabled") && argc == 2) { - if ((server.gopher_enabled = yesnotoi(argv[1])) == -1) { - err = "argument must be 'yes' or 'no'"; goto loaderr; - } - } else if (!strcasecmp(argv[0],"port") && argc == 2) { - server.port = atoi(argv[1]); - if (server.port < 0 || server.port > 65535) { - err = "Invalid port"; goto loaderr; - } - } else if (!strcasecmp(argv[0],"tcp-backlog") && argc == 2) { - server.tcp_backlog = atoi(argv[1]); - if (server.tcp_backlog < 0) { - err = "Invalid backlog value"; goto loaderr; - } - } else if (!strcasecmp(argv[0],"bind") && argc >= 2) { + if (!strcasecmp(argv[0],"bind") && argc >= 2) { int j, addresses = argc-1; if (addresses > CONFIG_BINDADDR_MAX) { err = "Too many bind addresses specified"; goto loaderr; } + /* Free old bind addresses */ + for (j = 0; j < server.bindaddr_count; j++) { + zfree(server.bindaddr[j]); + } for (j = 0; j < addresses; j++) server.bindaddr[j] = zstrdup(argv[j+1]); server.bindaddr_count = addresses; - } else if (!strcasecmp(argv[0],"unixsocket") && argc == 2) { - server.unixsocket = zstrdup(argv[1]); } else if (!strcasecmp(argv[0],"unixsocketperm") && argc == 2) { errno = 0; server.unixsocketperm = (mode_t)strtol(argv[1], NULL, 8); @@ -264,13 +379,6 @@ void loadServerConfigFromString(char *config) { argv[1], strerror(errno)); exit(1); } - } else if (!strcasecmp(argv[0],"loglevel") && argc == 2) { - server.verbosity = configEnumGetValue(loglevel_enum,argv[1]); - if (server.verbosity == INT_MIN) { - err = "Invalid log level. " - "Must be one of debug, verbose, notice, warning"; - goto loaderr; - } } else if (!strcasecmp(argv[0],"logfile") && argc == 2) { FILE *logfp; @@ -287,255 +395,16 @@ void loadServerConfigFromString(char *config) { } fclose(logfp); } - } else if (!strcasecmp(argv[0],"aclfile") && argc == 2) { - zfree(server.acl_filename); - server.acl_filename = zstrdup(argv[1]); - } 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; - } - } else if (!strcasecmp(argv[0],"syslog-ident") && argc == 2) { - if (server.syslog_ident) zfree(server.syslog_ident); - server.syslog_ident = zstrdup(argv[1]); - } else if (!strcasecmp(argv[0],"syslog-facility") && argc == 2) { - server.syslog_facility = - configEnumGetValue(syslog_facility_enum,argv[1]); - if (server.syslog_facility == INT_MIN) { - err = "Invalid log facility. Must be one of USER or between LOCAL0-LOCAL7"; - goto loaderr; - } - } else if (!strcasecmp(argv[0],"databases") && argc == 2) { - server.dbnum = atoi(argv[1]); - if (server.dbnum < 1) { - err = "Invalid number of databases"; goto loaderr; - } } else if (!strcasecmp(argv[0],"include") && argc == 2) { loadServerConfig(argv[1],NULL); - } else if (!strcasecmp(argv[0],"maxclients") && argc == 2) { - server.maxclients = atoi(argv[1]); - if (server.maxclients < 1) { - err = "Invalid max clients limit"; goto loaderr; - } - } else if (!strcasecmp(argv[0],"maxmemory") && argc == 2) { - server.maxmemory = memtoll(argv[1],NULL); - } else if (!strcasecmp(argv[0],"maxmemory-policy") && argc == 2) { - server.maxmemory_policy = - configEnumGetValue(maxmemory_policy_enum,argv[1]); - if (server.maxmemory_policy == INT_MIN) { - err = "Invalid maxmemory policy"; - goto loaderr; - } - } else if (!strcasecmp(argv[0],"maxmemory-samples") && argc == 2) { - server.maxmemory_samples = atoi(argv[1]); - if (server.maxmemory_samples <= 0) { - err = "maxmemory-samples must be 1 or greater"; - goto loaderr; - } - } else if ((!strcasecmp(argv[0],"proto-max-bulk-len")) && argc == 2) { - server.proto_max_bulk_len = memtoll(argv[1],NULL); } else if ((!strcasecmp(argv[0],"client-query-buffer-limit")) && argc == 2) { - server.client_max_querybuf_len = memtoll(argv[1],NULL); - } else if (!strcasecmp(argv[0],"lfu-log-factor") && argc == 2) { - server.lfu_log_factor = atoi(argv[1]); - 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.lfu_decay_time < 0) { - err = "lfu-decay-time must be 0 or greater"; - goto loaderr; - } + server.client_max_querybuf_len = memtoll(argv[1],NULL); } else if ((!strcasecmp(argv[0],"slaveof") || !strcasecmp(argv[0],"replicaof")) && argc == 3) { slaveof_linenum = linenum; server.masterhost = sdsnew(argv[1]); server.masterport = atoi(argv[2]); server.repl_state = REPL_STATE_CONNECT; - } else if ((!strcasecmp(argv[0],"repl-ping-slave-period") || - !strcasecmp(argv[0],"repl-ping-replica-period")) && - argc == 2) - { - server.repl_ping_slave_period = atoi(argv[1]); - if (server.repl_ping_slave_period <= 0) { - err = "repl-ping-replica-period must be 1 or greater"; - goto loaderr; - } - } else if (!strcasecmp(argv[0],"repl-timeout") && argc == 2) { - server.repl_timeout = atoi(argv[1]); - if (server.repl_timeout <= 0) { - err = "repl-timeout must be 1 or greater"; - goto loaderr; - } - } else if (!strcasecmp(argv[0],"repl-disable-tcp-nodelay") && argc==2) { - if ((server.repl_disable_tcp_nodelay = yesnotoi(argv[1])) == -1) { - err = "argument must be 'yes' or 'no'"; goto loaderr; - } - } else if (!strcasecmp(argv[0],"repl-diskless-sync") && argc==2) { - if ((server.repl_diskless_sync = yesnotoi(argv[1])) == -1) { - err = "argument must be 'yes' or 'no'"; goto loaderr; - } - } else if (!strcasecmp(argv[0],"repl-diskless-sync-delay") && argc==2) { - server.repl_diskless_sync_delay = atoi(argv[1]); - if (server.repl_diskless_sync_delay < 0) { - err = "repl-diskless-sync-delay can't be negative"; - goto loaderr; - } - } else if (!strcasecmp(argv[0],"repl-backlog-size") && argc == 2) { - long long size = memtoll(argv[1],NULL); - if (size <= 0) { - err = "repl-backlog-size must be 1 or greater."; - goto loaderr; - } - resizeReplicationBacklog(size); - } else if (!strcasecmp(argv[0],"repl-backlog-ttl") && argc == 2) { - server.repl_backlog_time_limit = atoi(argv[1]); - if (server.repl_backlog_time_limit < 0) { - err = "repl-backlog-ttl can't be negative "; - goto loaderr; - } - } else if (!strcasecmp(argv[0],"masteruser") && argc == 2) { - zfree(server.masteruser); - server.masteruser = argv[1][0] ? zstrdup(argv[1]) : NULL; - } else if (!strcasecmp(argv[0],"masterauth") && argc == 2) { - zfree(server.masterauth); - server.masterauth = argv[1][0] ? zstrdup(argv[1]) : NULL; - } else if ((!strcasecmp(argv[0],"slave-serve-stale-data") || - !strcasecmp(argv[0],"replica-serve-stale-data")) - && argc == 2) - { - if ((server.repl_serve_stale_data = yesnotoi(argv[1])) == -1) { - err = "argument must be 'yes' or 'no'"; goto loaderr; - } - } else if ((!strcasecmp(argv[0],"slave-read-only") || - !strcasecmp(argv[0],"replica-read-only")) - && argc == 2) - { - if ((server.repl_slave_ro = yesnotoi(argv[1])) == -1) { - err = "argument must be 'yes' or 'no'"; goto loaderr; - } - } else if ((!strcasecmp(argv[0],"slave-ignore-maxmemory") || - !strcasecmp(argv[0],"replica-ignore-maxmemory")) - && argc == 2) - { - if ((server.repl_slave_ignore_maxmemory = yesnotoi(argv[1])) == -1) { - err = "argument must be 'yes' or 'no'"; goto loaderr; - } - } else if (!strcasecmp(argv[0],"rdbcompression") && argc == 2) { - if ((server.rdb_compression = yesnotoi(argv[1])) == -1) { - err = "argument must be 'yes' or 'no'"; goto loaderr; - } - } else if (!strcasecmp(argv[0],"rdbchecksum") && argc == 2) { - if ((server.rdb_checksum = yesnotoi(argv[1])) == -1) { - err = "argument must be 'yes' or 'no'"; goto loaderr; - } - } else if (!strcasecmp(argv[0],"activerehashing") && argc == 2) { - if ((server.activerehashing = yesnotoi(argv[1])) == -1) { - err = "argument must be 'yes' or 'no'"; goto loaderr; - } - } else if (!strcasecmp(argv[0],"lazyfree-lazy-eviction") && argc == 2) { - if ((server.lazyfree_lazy_eviction = yesnotoi(argv[1])) == -1) { - err = "argument must be 'yes' or 'no'"; goto loaderr; - } - } else if (!strcasecmp(argv[0],"lazyfree-lazy-expire") && argc == 2) { - if ((server.lazyfree_lazy_expire = yesnotoi(argv[1])) == -1) { - err = "argument must be 'yes' or 'no'"; goto loaderr; - } - } else if (!strcasecmp(argv[0],"lazyfree-lazy-server-del") && argc == 2){ - if ((server.lazyfree_lazy_server_del = yesnotoi(argv[1])) == -1) { - err = "argument must be 'yes' or 'no'"; goto loaderr; - } - } else if ((!strcasecmp(argv[0],"slave-lazy-flush") || - !strcasecmp(argv[0],"replica-lazy-flush")) && argc == 2) - { - 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; - } - if (server.active_defrag_enabled) { -#ifndef HAVE_DEFRAG - err = "active defrag can't be enabled without proper jemalloc support"; goto loaderr; -#endif - } - } else if (!strcasecmp(argv[0],"daemonize") && argc == 2) { - if ((server.daemonize = yesnotoi(argv[1])) == -1) { - err = "argument must be 'yes' or 'no'"; goto loaderr; - } - } else if (!strcasecmp(argv[0],"dynamic-hz") && argc == 2) { - if ((server.dynamic_hz = yesnotoi(argv[1])) == -1) { - err = "argument must be 'yes' or 'no'"; goto loaderr; - } - } else if (!strcasecmp(argv[0],"hz") && argc == 2) { - server.config_hz = atoi(argv[1]); - if (server.config_hz < CONFIG_MIN_HZ) server.config_hz = CONFIG_MIN_HZ; - if (server.config_hz > CONFIG_MAX_HZ) server.config_hz = CONFIG_MAX_HZ; - } else if (!strcasecmp(argv[0],"appendonly") && argc == 2) { - int yes; - - if ((yes = yesnotoi(argv[1])) == -1) { - err = "argument must be 'yes' or 'no'"; goto loaderr; - } - server.aof_state = yes ? AOF_ON : AOF_OFF; - } else if (!strcasecmp(argv[0],"appendfilename") && argc == 2) { - if (!pathIsBaseName(argv[1])) { - err = "appendfilename can't be a path, just a filename"; - goto loaderr; - } - zfree(server.aof_filename); - server.aof_filename = zstrdup(argv[1]); - } else if (!strcasecmp(argv[0],"no-appendfsync-on-rewrite") - && argc == 2) { - if ((server.aof_no_fsync_on_rewrite= yesnotoi(argv[1])) == -1) { - err = "argument must be 'yes' or 'no'"; goto loaderr; - } - } else if (!strcasecmp(argv[0],"appendfsync") && argc == 2) { - server.aof_fsync = configEnumGetValue(aof_fsync_enum,argv[1]); - if (server.aof_fsync == INT_MIN) { - err = "argument must be 'no', 'always' or 'everysec'"; - goto loaderr; - } - } else if (!strcasecmp(argv[0],"auto-aof-rewrite-percentage") && - argc == 2) - { - server.aof_rewrite_perc = atoi(argv[1]); - if (server.aof_rewrite_perc < 0) { - err = "Invalid negative percentage for AOF auto rewrite"; - goto loaderr; - } - } else if (!strcasecmp(argv[0],"auto-aof-rewrite-min-size") && - argc == 2) - { - server.aof_rewrite_min_size = memtoll(argv[1],NULL); - } else if (!strcasecmp(argv[0],"aof-rewrite-incremental-fsync") && - argc == 2) - { - if ((server.aof_rewrite_incremental_fsync = - yesnotoi(argv[1])) == -1) { - err = "argument must be 'yes' or 'no'"; goto loaderr; - } - } else if (!strcasecmp(argv[0],"rdb-save-incremental-fsync") && - argc == 2) - { - if ((server.rdb_save_incremental_fsync = - yesnotoi(argv[1])) == -1) { - err = "argument must be 'yes' or 'no'"; goto loaderr; - } - } else if (!strcasecmp(argv[0],"aof-load-truncated") && argc == 2) { - if ((server.aof_load_truncated = yesnotoi(argv[1])) == -1) { - err = "argument must be 'yes' or 'no'"; goto loaderr; - } - } else if (!strcasecmp(argv[0],"aof-use-rdb-preamble") && argc == 2) { - if ((server.aof_use_rdb_preamble = yesnotoi(argv[1])) == -1) { - err = "argument must be 'yes' or 'no'"; goto loaderr; - } } else if (!strcasecmp(argv[0],"requirepass") && argc == 2) { if (strlen(argv[1]) > CONFIG_AUTHPASS_MAX_LEN) { err = "Password is longer than CONFIG_AUTHPASS_MAX_LEN"; @@ -547,78 +416,10 @@ void loadServerConfigFromString(char *config) { sds aclop = sdscatprintf(sdsempty(),">%s",argv[1]); ACLSetUser(DefaultUser,aclop,sdslen(aclop)); sdsfree(aclop); - } else if (!strcasecmp(argv[0],"pidfile") && argc == 2) { - zfree(server.pidfile); - server.pidfile = zstrdup(argv[1]); - } else if (!strcasecmp(argv[0],"dbfilename") && argc == 2) { - if (!pathIsBaseName(argv[1])) { - err = "dbfilename can't be a path, just a filename"; - goto loaderr; - } - 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 || - server.active_defrag_threshold_lower > 1000) { - err = "active-defrag-threshold-lower must be between 0 and 1000"; - 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 || - server.active_defrag_threshold_upper > 1000) { - err = "active-defrag-threshold-upper must be between 0 and 1000"; - 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],"active-defrag-max-scan-fields") && argc == 2) { - server.active_defrag_max_scan_fields = strtoll(argv[1],NULL,10); - if (server.active_defrag_max_scan_fields < 1) { - err = "active-defrag-max-scan-fields must be positive"; - 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) { - server.hash_max_ziplist_value = memtoll(argv[1], NULL); - } else if (!strcasecmp(argv[0],"stream-node-max-bytes") && argc == 2) { - server.stream_node_max_bytes = memtoll(argv[1], NULL); - } else if (!strcasecmp(argv[0],"stream-node-max-entries") && argc == 2) { - server.stream_node_max_entries = atoi(argv[1]); } else if (!strcasecmp(argv[0],"list-max-ziplist-entries") && argc == 2){ /* DEAD OPTION */ } else if (!strcasecmp(argv[0],"list-max-ziplist-value") && argc == 2) { /* DEAD OPTION */ - } else if (!strcasecmp(argv[0],"list-max-ziplist-size") && argc == 2) { - server.list_max_ziplist_size = atoi(argv[1]); - } else if (!strcasecmp(argv[0],"list-compress-depth") && argc == 2) { - server.list_compress_depth = atoi(argv[1]); - } else if (!strcasecmp(argv[0],"set-max-intset-entries") && argc == 2) { - server.set_max_intset_entries = memtoll(argv[1], NULL); - } else if (!strcasecmp(argv[0],"zset-max-ziplist-entries") && argc == 2) { - server.zset_max_ziplist_entries = memtoll(argv[1], NULL); - } else if (!strcasecmp(argv[0],"zset-max-ziplist-value") && argc == 2) { - server.zset_max_ziplist_value = memtoll(argv[1], NULL); - } else if (!strcasecmp(argv[0],"hll-sparse-max-bytes") && argc == 2) { - server.hll_sparse_max_bytes = memtoll(argv[1], NULL); } else if (!strcasecmp(argv[0],"rename-command") && argc == 3) { struct redisCommand *cmd = lookupCommand(argv[1]); int retval; @@ -643,88 +444,9 @@ void loadServerConfigFromString(char *config) { err = "Target command name already exists"; goto loaderr; } } - } else if (!strcasecmp(argv[0],"cluster-enabled") && argc == 2) { - if ((server.cluster_enabled = yesnotoi(argv[1])) == -1) { - err = "argument must be 'yes' or 'no'"; goto loaderr; - } } else if (!strcasecmp(argv[0],"cluster-config-file") && argc == 2) { zfree(server.cluster_configfile); server.cluster_configfile = zstrdup(argv[1]); - } else if (!strcasecmp(argv[0],"cluster-announce-ip") && argc == 2) { - zfree(server.cluster_announce_ip); - server.cluster_announce_ip = zstrdup(argv[1]); - } else if (!strcasecmp(argv[0],"cluster-announce-port") && argc == 2) { - server.cluster_announce_port = atoi(argv[1]); - if (server.cluster_announce_port < 0 || - server.cluster_announce_port > 65535) - { - err = "Invalid port"; goto loaderr; - } - } else if (!strcasecmp(argv[0],"cluster-announce-bus-port") && - argc == 2) - { - server.cluster_announce_bus_port = atoi(argv[1]); - if (server.cluster_announce_bus_port < 0 || - server.cluster_announce_bus_port > 65535) - { - err = "Invalid port"; goto loaderr; - } - } else if (!strcasecmp(argv[0],"cluster-require-full-coverage") && - argc == 2) - { - if ((server.cluster_require_full_coverage = yesnotoi(argv[1])) == -1) - { - err = "argument must be 'yes' or 'no'"; goto loaderr; - } - } else if (!strcasecmp(argv[0],"cluster-node-timeout") && argc == 2) { - server.cluster_node_timeout = strtoll(argv[1],NULL,10); - if (server.cluster_node_timeout <= 0) { - err = "cluster node timeout must be 1 or greater"; goto loaderr; - } - } else if (!strcasecmp(argv[0],"cluster-migration-barrier") - && argc == 2) - { - server.cluster_migration_barrier = atoi(argv[1]); - if (server.cluster_migration_barrier < 0) { - err = "cluster migration barrier must zero or positive"; - goto loaderr; - } - } else if ((!strcasecmp(argv[0],"cluster-slave-validity-factor") || - !strcasecmp(argv[0],"cluster-replica-validity-factor")) - && argc == 2) - { - server.cluster_slave_validity_factor = atoi(argv[1]); - if (server.cluster_slave_validity_factor < 0) { - err = "cluster replica validity factor must be zero or positive"; - goto loaderr; - } - } else if ((!strcasecmp(argv[0],"cluster-slave-no-failover") || - !strcasecmp(argv[0],"cluster-replica-no-failover")) && - argc == 2) - { - server.cluster_slave_no_failover = yesnotoi(argv[1]); - if (server.cluster_slave_no_failover == -1) { - err = "argument must be 'yes' or 'no'"; - goto loaderr; - } - } else if (!strcasecmp(argv[0],"lua-time-limit") && argc == 2) { - server.lua_time_limit = strtoll(argv[1],NULL,10); - } else if (!strcasecmp(argv[0],"lua-replicate-commands") && argc == 2) { - server.lua_always_replicate_commands = yesnotoi(argv[1]); - } else if (!strcasecmp(argv[0],"slowlog-log-slower-than") && - argc == 2) - { - server.slowlog_log_slower_than = strtoll(argv[1],NULL,10); - } else if (!strcasecmp(argv[0],"latency-monitor-threshold") && - argc == 2) - { - server.latency_monitor_threshold = strtoll(argv[1],NULL,10); - if (server.latency_monitor_threshold < 0) { - err = "The latency threshold can't be negative"; - goto loaderr; - } - } else if (!strcasecmp(argv[0],"slowlog-max-len") && argc == 2) { - server.slowlog_max_len = strtoll(argv[1],NULL,10); } else if (!strcasecmp(argv[0],"client-output-buffer-limit") && argc == 5) { @@ -747,43 +469,6 @@ void loadServerConfigFromString(char *config) { server.client_obuf_limits[class].hard_limit_bytes = hard; server.client_obuf_limits[class].soft_limit_bytes = soft; server.client_obuf_limits[class].soft_limit_seconds = soft_seconds; - } else if (!strcasecmp(argv[0],"stop-writes-on-bgsave-error") && - argc == 2) { - if ((server.stop_writes_on_bgsave_err = yesnotoi(argv[1])) == -1) { - err = "argument must be 'yes' or 'no'"; goto loaderr; - } - } else if ((!strcasecmp(argv[0],"slave-priority") || - !strcasecmp(argv[0],"replica-priority")) && argc == 2) - { - server.slave_priority = atoi(argv[1]); - } else if ((!strcasecmp(argv[0],"slave-announce-ip") || - !strcasecmp(argv[0],"replica-announce-ip")) && argc == 2) - { - zfree(server.slave_announce_ip); - server.slave_announce_ip = zstrdup(argv[1]); - } else if ((!strcasecmp(argv[0],"slave-announce-port") || - !strcasecmp(argv[0],"replica-announce-port")) && argc == 2) - { - server.slave_announce_port = atoi(argv[1]); - if (server.slave_announce_port < 0 || - server.slave_announce_port > 65535) - { - err = "Invalid port"; goto loaderr; - } - } else if ((!strcasecmp(argv[0],"min-slaves-to-write") || - !strcasecmp(argv[0],"min-replicas-to-write")) && argc == 2) - { - server.repl_min_slaves_to_write = atoi(argv[1]); - if (server.repl_min_slaves_to_write < 0) { - err = "Invalid value for min-replicas-to-write."; goto loaderr; - } - } else if ((!strcasecmp(argv[0],"min-slaves-max-lag") || - !strcasecmp(argv[0],"min-replicas-max-lag")) && argc == 2) - { - server.repl_min_slaves_max_lag = atoi(argv[1]); - if (server.repl_min_slaves_max_lag < 0) { - err = "Invalid value for min-replicas-max-lag."; goto loaderr; - } } else if (!strcasecmp(argv[0],"notify-keyspace-events") && argc == 2) { int flags = keyspaceEventsStringToFlags(argv[1]); @@ -792,15 +477,6 @@ void loadServerConfigFromString(char *config) { goto loaderr; } server.notify_keyspace_events = flags; - } else if (!strcasecmp(argv[0],"supervised") && argc == 2) { - server.supervised_mode = - configEnumGetValue(supervised_mode_enum,argv[1]); - - if (server.supervised_mode == INT_MIN) { - err = "Invalid option for 'supervised'. " - "Allowed values: 'upstart', 'systemd', 'auto', or 'no'"; - goto loaderr; - } } else if (!strcasecmp(argv[0],"user") && argc >= 2) { int argc_err; if (ACLAppendUserForLoading(argv,argc,&argc_err) == C_ERR) { @@ -909,12 +585,6 @@ void loadServerConfig(char *filename, char *options) { if (err || ll < 0) goto badfmt; \ _var = ll; -#define config_set_enum_field(_name,_var,_enumvar) \ - } else if (!strcasecmp(c->argv[2]->ptr,_name)) { \ - int enumval = configEnumGetValue(_enumvar,o->ptr); \ - if (enumval == INT_MIN) goto badfmt; \ - _var = enumval; - #define config_set_special_field(_name) \ } else if (!strcasecmp(c->argv[2]->ptr,_name)) { @@ -928,21 +598,28 @@ void configSetCommand(client *c) { robj *o; long long ll; int err; + char *errstr = NULL; serverAssertWithInfo(c,c->argv[2],sdsEncodedObject(c->argv[2])); serverAssertWithInfo(c,c->argv[3],sdsEncodedObject(c->argv[3])); o = c->argv[3]; + /* Iterate the configs that are standard */ + for (standardConfig *config = configs; config->name != NULL; config++) { + if(config->modifiable && (!strcasecmp(c->argv[2]->ptr,config->name) || + (config->alias && !strcasecmp(c->argv[2]->ptr,config->alias)))) + { + if (!config->interface.set(config->data,o->ptr,1,&errstr)) { + goto badfmt; + } + addReply(c,shared.ok); + return; + } + } + if (0) { /* this starts the config_set macros else-if chain. */ /* Special fields that can't be handled with general macros. */ - config_set_special_field("dbfilename") { - if (!pathIsBaseName(o->ptr)) { - addReplyError(c, "dbfilename can't be a path, just a filename"); - return; - } - zfree(server.rdb_filename); - server.rdb_filename = zstrdup(o->ptr); - } config_set_special_field("requirepass") { + config_set_special_field("requirepass") { if (sdslen(o->ptr) > CONFIG_AUTHPASS_MAX_LEN) goto badfmt; /* The old "requirepass" directive just translates to setting * a password to the default user. */ @@ -950,54 +627,6 @@ void configSetCommand(client *c) { sds aclop = sdscatprintf(sdsempty(),">%s",(char*)o->ptr); ACLSetUser(DefaultUser,aclop,sdslen(aclop)); sdsfree(aclop); - } config_set_special_field("masteruser") { - zfree(server.masteruser); - server.masteruser = ((char*)o->ptr)[0] ? zstrdup(o->ptr) : NULL; - } config_set_special_field("masterauth") { - zfree(server.masterauth); - server.masterauth = ((char*)o->ptr)[0] ? zstrdup(o->ptr) : NULL; - } config_set_special_field("cluster-announce-ip") { - zfree(server.cluster_announce_ip); - server.cluster_announce_ip = ((char*)o->ptr)[0] ? zstrdup(o->ptr) : NULL; - } config_set_special_field("maxclients") { - int orig_value = server.maxclients; - - if (getLongLongFromObject(o,&ll) == C_ERR || ll < 1) goto badfmt; - - /* Try to check if the OS is capable of supporting so many FDs. */ - server.maxclients = ll; - if (ll > orig_value) { - adjustOpenFilesLimit(); - if (server.maxclients != ll) { - addReplyErrorFormat(c,"The operating system is not able to handle the specified number of clients, try with %d", server.maxclients); - server.maxclients = orig_value; - return; - } - if ((unsigned int) aeGetSetSize(server.el) < - server.maxclients + CONFIG_FDSET_INCR) - { - if (aeResizeSetSize(server.el, - server.maxclients + CONFIG_FDSET_INCR) == AE_ERR) - { - addReplyError(c,"The event loop API used by Redis is not able to handle the specified number of clients"); - server.maxclients = orig_value; - return; - } - } - } - } config_set_special_field("appendonly") { - int enable = yesnotoi(o->ptr); - - if (enable == -1) goto badfmt; - if (enable == 0 && server.aof_state != AOF_OFF) { - stopAppendOnly(); - } else if (enable && server.aof_state == AOF_OFF) { - if (startAppendOnly() == C_ERR) { - addReplyError(c, - "Unable to turn on AOF. Check server logs."); - return; - } - } } config_set_special_field("save") { int vlen, j; sds *v = sdssplitlen(o->ptr,sdslen(o->ptr)," ",1,&vlen); @@ -1074,8 +703,8 @@ void configSetCommand(client *c) { int soft_seconds; class = getClientTypeByName(v[j]); - hard = strtoll(v[j+1],NULL,10); - soft = strtoll(v[j+2],NULL,10); + hard = memtoll(v[j+1],NULL); + soft = memtoll(v[j+2],NULL); soft_seconds = strtoll(v[j+3],NULL,10); server.client_obuf_limits[class].hard_limit_bytes = hard; @@ -1088,220 +717,18 @@ void configSetCommand(client *c) { if (flags == -1) goto badfmt; server.notify_keyspace_events = flags; - } config_set_special_field_with_alias("slave-announce-ip", - "replica-announce-ip") - { - zfree(server.slave_announce_ip); - server.slave_announce_ip = ((char*)o->ptr)[0] ? zstrdup(o->ptr) : NULL; - - /* Boolean fields. - * config_set_bool_field(name,var). */ - } config_set_bool_field( - "rdbcompression", server.rdb_compression) { - } config_set_bool_field( - "repl-disable-tcp-nodelay",server.repl_disable_tcp_nodelay) { - } config_set_bool_field( - "repl-diskless-sync",server.repl_diskless_sync) { - } config_set_bool_field( - "cluster-require-full-coverage",server.cluster_require_full_coverage) { - } config_set_bool_field( - "cluster-slave-no-failover",server.cluster_slave_no_failover) { - } config_set_bool_field( - "cluster-replica-no-failover",server.cluster_slave_no_failover) { - } config_set_bool_field( - "aof-rewrite-incremental-fsync",server.aof_rewrite_incremental_fsync) { - } config_set_bool_field( - "rdb-save-incremental-fsync",server.rdb_save_incremental_fsync) { - } config_set_bool_field( - "aof-load-truncated",server.aof_load_truncated) { - } config_set_bool_field( - "aof-use-rdb-preamble",server.aof_use_rdb_preamble) { - } config_set_bool_field( - "slave-serve-stale-data",server.repl_serve_stale_data) { - } config_set_bool_field( - "replica-serve-stale-data",server.repl_serve_stale_data) { - } config_set_bool_field( - "slave-read-only",server.repl_slave_ro) { - } config_set_bool_field( - "replica-read-only",server.repl_slave_ro) { - } config_set_bool_field( - "slave-ignore-maxmemory",server.repl_slave_ignore_maxmemory) { - } config_set_bool_field( - "replica-ignore-maxmemory",server.repl_slave_ignore_maxmemory) { - } 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, - "-DISABLED 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( - "gopher-enabled",server.gopher_enabled) { - } config_set_bool_field( - "stop-writes-on-bgsave-error",server.stop_writes_on_bgsave_err) { - } config_set_bool_field( - "lazyfree-lazy-eviction",server.lazyfree_lazy_eviction) { - } config_set_bool_field( - "lazyfree-lazy-expire",server.lazyfree_lazy_expire) { - } config_set_bool_field( - "lazyfree-lazy-server-del",server.lazyfree_lazy_server_del) { - } config_set_bool_field( - "slave-lazy-flush",server.repl_slave_lazy_flush) { - } config_set_bool_field( - "replica-lazy-flush",server.repl_slave_lazy_flush) { - } config_set_bool_field( - "no-appendfsync-on-rewrite",server.aof_no_fsync_on_rewrite) { - } config_set_bool_field( - "dynamic-hz",server.dynamic_hz) { - /* Numerical fields. * config_set_numerical_field(name,var,min,max) */ - } config_set_numerical_field( - "tcp-keepalive",server.tcpkeepalive,0,INT_MAX) { - } config_set_numerical_field( - "maxmemory-samples",server.maxmemory_samples,1,INT_MAX) { - } config_set_numerical_field( - "lfu-log-factor",server.lfu_log_factor,0,INT_MAX) { - } config_set_numerical_field( - "lfu-decay-time",server.lfu_decay_time,0,INT_MAX) { - } config_set_numerical_field( - "timeout",server.maxidletime,0,INT_MAX) { - } config_set_numerical_field( - "active-defrag-threshold-lower",server.active_defrag_threshold_lower,0,1000) { - } config_set_numerical_field( - "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( - "active-defrag-max-scan-fields",server.active_defrag_max_scan_fields,1,LONG_MAX) { - } config_set_numerical_field( - "auto-aof-rewrite-percentage",server.aof_rewrite_perc,0,INT_MAX){ - } config_set_numerical_field( - "hash-max-ziplist-entries",server.hash_max_ziplist_entries,0,LONG_MAX) { - } config_set_numerical_field( - "hash-max-ziplist-value",server.hash_max_ziplist_value,0,LONG_MAX) { - } config_set_numerical_field( - "stream-node-max-bytes",server.stream_node_max_bytes,0,LONG_MAX) { - } config_set_numerical_field( - "stream-node-max-entries",server.stream_node_max_entries,0,LLONG_MAX) { - } config_set_numerical_field( - "list-max-ziplist-size",server.list_max_ziplist_size,INT_MIN,INT_MAX) { - } config_set_numerical_field( - "list-compress-depth",server.list_compress_depth,0,INT_MAX) { - } config_set_numerical_field( - "set-max-intset-entries",server.set_max_intset_entries,0,LONG_MAX) { - } config_set_numerical_field( - "zset-max-ziplist-entries",server.zset_max_ziplist_entries,0,LONG_MAX) { - } config_set_numerical_field( - "zset-max-ziplist-value",server.zset_max_ziplist_value,0,LONG_MAX) { - } config_set_numerical_field( - "hll-sparse-max-bytes",server.hll_sparse_max_bytes,0,LONG_MAX) { - } config_set_numerical_field( - "lua-time-limit",server.lua_time_limit,0,LONG_MAX) { - } config_set_numerical_field( - "slowlog-log-slower-than",server.slowlog_log_slower_than,-1,LLONG_MAX) { - } config_set_numerical_field( - "slowlog-max-len",ll,0,LONG_MAX) { - /* Cast to unsigned. */ - server.slowlog_max_len = (unsigned long)ll; - } config_set_numerical_field( - "latency-monitor-threshold",server.latency_monitor_threshold,0,LLONG_MAX){ - } config_set_numerical_field( - "repl-ping-slave-period",server.repl_ping_slave_period,1,INT_MAX) { - } config_set_numerical_field( - "repl-ping-replica-period",server.repl_ping_slave_period,1,INT_MAX) { - } config_set_numerical_field( - "repl-timeout",server.repl_timeout,1,INT_MAX) { - } config_set_numerical_field( - "repl-backlog-ttl",server.repl_backlog_time_limit,0,LONG_MAX) { - } config_set_numerical_field( - "repl-diskless-sync-delay",server.repl_diskless_sync_delay,0,INT_MAX) { - } config_set_numerical_field( - "slave-priority",server.slave_priority,0,INT_MAX) { - } config_set_numerical_field( - "replica-priority",server.slave_priority,0,INT_MAX) { - } config_set_numerical_field( - "slave-announce-port",server.slave_announce_port,0,65535) { - } config_set_numerical_field( - "replica-announce-port",server.slave_announce_port,0,65535) { - } config_set_numerical_field( - "min-slaves-to-write",server.repl_min_slaves_to_write,0,INT_MAX) { - refreshGoodSlavesCount(); - } config_set_numerical_field( - "min-replicas-to-write",server.repl_min_slaves_to_write,0,INT_MAX) { - refreshGoodSlavesCount(); - } config_set_numerical_field( - "min-slaves-max-lag",server.repl_min_slaves_max_lag,0,INT_MAX) { - refreshGoodSlavesCount(); - } config_set_numerical_field( - "min-replicas-max-lag",server.repl_min_slaves_max_lag,0,INT_MAX) { - refreshGoodSlavesCount(); - } config_set_numerical_field( - "cluster-node-timeout",server.cluster_node_timeout,0,LLONG_MAX) { - } config_set_numerical_field( - "cluster-announce-port",server.cluster_announce_port,0,65535) { - } config_set_numerical_field( - "cluster-announce-bus-port",server.cluster_announce_bus_port,0,65535) { - } config_set_numerical_field( - "cluster-migration-barrier",server.cluster_migration_barrier,0,INT_MAX){ - } config_set_numerical_field( - "cluster-slave-validity-factor",server.cluster_slave_validity_factor,0,INT_MAX) { - } config_set_numerical_field( - "cluster-replica-validity-factor",server.cluster_slave_validity_factor,0,INT_MAX) { - } config_set_numerical_field( - "hz",server.config_hz,0,INT_MAX) { - /* Hz is more an hint from the user, so we accept values out of range - * but cap them to reasonable values. */ - if (server.config_hz < CONFIG_MIN_HZ) server.config_hz = CONFIG_MIN_HZ; - if (server.config_hz > CONFIG_MAX_HZ) server.config_hz = CONFIG_MAX_HZ; } config_set_numerical_field( "watchdog-period",ll,0,INT_MAX) { if (ll) enableWatchdog(ll); else disableWatchdog(); - /* Memory fields. * config_set_memory_field(name,var) */ - } config_set_memory_field("maxmemory",server.maxmemory) { - if (server.maxmemory) { - if (server.maxmemory < zmalloc_used_memory()) { - serverLog(LL_WARNING,"WARNING: the new maxmemory value set via CONFIG SET is smaller than the current memory usage. This will result in key eviction and/or the inability to accept new write commands depending on the maxmemory-policy."); - } - freeMemoryIfNeededAndSafe(); - } - } config_set_memory_field( - "proto-max-bulk-len",server.proto_max_bulk_len) { } config_set_memory_field( "client-query-buffer-limit",server.client_max_querybuf_len) { - } 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) */ - } config_set_enum_field( - "loglevel",server.verbosity,loglevel_enum) { - } config_set_enum_field( - "maxmemory-policy",server.maxmemory_policy,maxmemory_policy_enum) { - } config_set_enum_field( - "appendfsync",server.aof_fsync,aof_fsync_enum) { - /* Everyhing else is an error... */ } config_set_else { addReplyErrorFormat(c,"Unsupported CONFIG parameter: %s", @@ -1314,9 +741,16 @@ void configSetCommand(client *c) { return; badfmt: /* Bad format errors */ - addReplyErrorFormat(c,"Invalid argument '%s' for CONFIG SET '%s'", - (char*)o->ptr, - (char*)c->argv[2]->ptr); + if (errstr) { + addReplyErrorFormat(c,"Invalid argument '%s' for CONFIG SET '%s' - %s", + (char*)o->ptr, + (char*)c->argv[2]->ptr, + errstr); + } else { + addReplyErrorFormat(c,"Invalid argument '%s' for CONFIG SET '%s'", + (char*)o->ptr, + (char*)c->argv[2]->ptr); + } } /*----------------------------------------------------------------------------- @@ -1348,13 +782,6 @@ badfmt: /* Bad format errors */ } \ } while(0); -#define config_get_enum_field(_name,_var,_enumvar) do { \ - if (stringmatch(pattern,_name,1)) { \ - addReplyBulkCString(c,_name); \ - addReplyBulkCString(c,configEnumGetNameOrUnknown(_enumvar,_var)); \ - matches++; \ - } \ -} while(0); void configGetCommand(client *c) { robj *o = c->argv[2]; @@ -1364,165 +791,29 @@ void configGetCommand(client *c) { int matches = 0; serverAssertWithInfo(c,o,sdsEncodedObject(o)); + /* Iterate the configs that are standard */ + for (standardConfig *config = configs; config->name != NULL; config++) { + if (stringmatch(pattern,config->name,1)) { + addReplyBulkCString(c,config->name); + config->interface.get(c,config->data); + matches++; + } + if (config->alias && stringmatch(pattern,config->alias,1)) { + addReplyBulkCString(c,config->alias); + config->interface.get(c,config->data); + matches++; + } + } + /* String values */ - config_get_string_field("dbfilename",server.rdb_filename); - config_get_string_field("masteruser",server.masteruser); - config_get_string_field("masterauth",server.masterauth); - config_get_string_field("cluster-announce-ip",server.cluster_announce_ip); - config_get_string_field("unixsocket",server.unixsocket); config_get_string_field("logfile",server.logfile); - config_get_string_field("aclfile",server.acl_filename); - config_get_string_field("pidfile",server.pidfile); - config_get_string_field("slave-announce-ip",server.slave_announce_ip); - config_get_string_field("replica-announce-ip",server.slave_announce_ip); /* Numerical values */ - config_get_numerical_field("maxmemory",server.maxmemory); - config_get_numerical_field("proto-max-bulk-len",server.proto_max_bulk_len); config_get_numerical_field("client-query-buffer-limit",server.client_max_querybuf_len); - 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("active-defrag-max-scan-fields",server.active_defrag_max_scan_fields); - config_get_numerical_field("auto-aof-rewrite-percentage", - server.aof_rewrite_perc); - config_get_numerical_field("auto-aof-rewrite-min-size", - server.aof_rewrite_min_size); - config_get_numerical_field("hash-max-ziplist-entries", - server.hash_max_ziplist_entries); - config_get_numerical_field("hash-max-ziplist-value", - server.hash_max_ziplist_value); - config_get_numerical_field("stream-node-max-bytes", - server.stream_node_max_bytes); - config_get_numerical_field("stream-node-max-entries", - server.stream_node_max_entries); - config_get_numerical_field("list-max-ziplist-size", - server.list_max_ziplist_size); - config_get_numerical_field("list-compress-depth", - server.list_compress_depth); - config_get_numerical_field("set-max-intset-entries", - server.set_max_intset_entries); - config_get_numerical_field("zset-max-ziplist-entries", - server.zset_max_ziplist_entries); - config_get_numerical_field("zset-max-ziplist-value", - server.zset_max_ziplist_value); - config_get_numerical_field("hll-sparse-max-bytes", - server.hll_sparse_max_bytes); - config_get_numerical_field("lua-time-limit",server.lua_time_limit); - config_get_numerical_field("slowlog-log-slower-than", - server.slowlog_log_slower_than); - config_get_numerical_field("latency-monitor-threshold", - server.latency_monitor_threshold); - config_get_numerical_field("slowlog-max-len", - server.slowlog_max_len); - config_get_numerical_field("port",server.port); - config_get_numerical_field("cluster-announce-port",server.cluster_announce_port); - config_get_numerical_field("cluster-announce-bus-port",server.cluster_announce_bus_port); - config_get_numerical_field("tcp-backlog",server.tcp_backlog); - config_get_numerical_field("databases",server.dbnum); - config_get_numerical_field("repl-ping-slave-period",server.repl_ping_slave_period); - config_get_numerical_field("repl-ping-replica-period",server.repl_ping_slave_period); - config_get_numerical_field("repl-timeout",server.repl_timeout); - config_get_numerical_field("repl-backlog-size",server.repl_backlog_size); - config_get_numerical_field("repl-backlog-ttl",server.repl_backlog_time_limit); - config_get_numerical_field("maxclients",server.maxclients); config_get_numerical_field("watchdog-period",server.watchdog_period); - config_get_numerical_field("slave-priority",server.slave_priority); - config_get_numerical_field("replica-priority",server.slave_priority); - config_get_numerical_field("slave-announce-port",server.slave_announce_port); - config_get_numerical_field("replica-announce-port",server.slave_announce_port); - config_get_numerical_field("min-slaves-to-write",server.repl_min_slaves_to_write); - config_get_numerical_field("min-replicas-to-write",server.repl_min_slaves_to_write); - config_get_numerical_field("min-slaves-max-lag",server.repl_min_slaves_max_lag); - config_get_numerical_field("min-replicas-max-lag",server.repl_min_slaves_max_lag); - config_get_numerical_field("hz",server.config_hz); - config_get_numerical_field("cluster-node-timeout",server.cluster_node_timeout); - config_get_numerical_field("cluster-migration-barrier",server.cluster_migration_barrier); - config_get_numerical_field("cluster-slave-validity-factor",server.cluster_slave_validity_factor); - config_get_numerical_field("cluster-replica-validity-factor",server.cluster_slave_validity_factor); - config_get_numerical_field("repl-diskless-sync-delay",server.repl_diskless_sync_delay); - config_get_numerical_field("tcp-keepalive",server.tcpkeepalive); - - /* Bool (yes/no) values */ - config_get_bool_field("cluster-require-full-coverage", - server.cluster_require_full_coverage); - config_get_bool_field("cluster-slave-no-failover", - server.cluster_slave_no_failover); - config_get_bool_field("cluster-replica-no-failover", - server.cluster_slave_no_failover); - config_get_bool_field("no-appendfsync-on-rewrite", - server.aof_no_fsync_on_rewrite); - config_get_bool_field("slave-serve-stale-data", - server.repl_serve_stale_data); - config_get_bool_field("replica-serve-stale-data", - server.repl_serve_stale_data); - config_get_bool_field("slave-read-only", - server.repl_slave_ro); - config_get_bool_field("replica-read-only", - server.repl_slave_ro); - config_get_bool_field("slave-ignore-maxmemory", - server.repl_slave_ignore_maxmemory); - config_get_bool_field("replica-ignore-maxmemory", - server.repl_slave_ignore_maxmemory); - config_get_bool_field("stop-writes-on-bgsave-error", - server.stop_writes_on_bgsave_err); - config_get_bool_field("daemonize", server.daemonize); - 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("gopher-enabled", server.gopher_enabled); - config_get_bool_field("repl-disable-tcp-nodelay", - server.repl_disable_tcp_nodelay); - config_get_bool_field("repl-diskless-sync", - server.repl_diskless_sync); - config_get_bool_field("aof-rewrite-incremental-fsync", - server.aof_rewrite_incremental_fsync); - config_get_bool_field("rdb-save-incremental-fsync", - server.rdb_save_incremental_fsync); - config_get_bool_field("aof-load-truncated", - server.aof_load_truncated); - config_get_bool_field("aof-use-rdb-preamble", - server.aof_use_rdb_preamble); - config_get_bool_field("lazyfree-lazy-eviction", - server.lazyfree_lazy_eviction); - config_get_bool_field("lazyfree-lazy-expire", - server.lazyfree_lazy_expire); - config_get_bool_field("lazyfree-lazy-server-del", - server.lazyfree_lazy_server_del); - config_get_bool_field("slave-lazy-flush", - server.repl_slave_lazy_flush); - config_get_bool_field("replica-lazy-flush", - server.repl_slave_lazy_flush); - config_get_bool_field("dynamic-hz", - server.dynamic_hz); - - /* Enum values */ - config_get_enum_field("maxmemory-policy", - server.maxmemory_policy,maxmemory_policy_enum); - config_get_enum_field("loglevel", - server.verbosity,loglevel_enum); - config_get_enum_field("supervised", - server.supervised_mode,supervised_mode_enum); - config_get_enum_field("appendfsync", - server.aof_fsync,aof_fsync_enum); - config_get_enum_field("syslog-facility", - server.syslog_facility,syslog_facility_enum); /* Everything we can't handle with macros follows. */ - if (stringmatch(pattern,"appendonly",1)) { - addReplyBulkCString(c,"appendonly"); - addReplyBulkCString(c,server.aof_state == AOF_OFF ? "no" : "yes"); - matches++; - } if (stringmatch(pattern,"dir",1)) { char buf[1024]; @@ -1591,12 +882,10 @@ void configGetCommand(client *c) { matches++; } if (stringmatch(pattern,"notify-keyspace-events",1)) { - robj *flagsobj = createObject(OBJ_STRING, - keyspaceEventsFlagsToString(server.notify_keyspace_events)); + sds flags = keyspaceEventsFlagsToString(server.notify_keyspace_events); addReplyBulkCString(c,"notify-keyspace-events"); - addReplyBulk(c,flagsobj); - decrRefCount(flagsobj); + addReplyBulkSds(c,flags); matches++; } if (stringmatch(pattern,"bind",1)) { @@ -1617,6 +906,7 @@ void configGetCommand(client *c) { } matches++; } + setDeferredMapLen(c,replylen,matches); } @@ -1700,12 +990,11 @@ void rewriteConfigMarkAsProcessed(struct rewriteConfigState *state, const char * * If the old file does not exist at all, an empty state is returned. */ struct rewriteConfigState *rewriteConfigReadOldFile(char *path) { FILE *fp = fopen(path,"r"); - struct rewriteConfigState *state = zmalloc(sizeof(*state)); - char buf[CONFIG_MAX_LINE+1]; - int linenum = -1; - if (fp == NULL && errno != ENOENT) return NULL; + char buf[CONFIG_MAX_LINE+1]; + int linenum = -1; + struct rewriteConfigState *state = zmalloc(sizeof(*state)); state->option_to_line = dictCreate(&optionToLineDictType,NULL); state->rewritten = dictCreate(&optionSetDictType,NULL); state->numlines = 0; @@ -1837,7 +1126,7 @@ int rewriteConfigFormatMemory(char *buf, size_t len, long long bytes) { } /* Rewrite a simple "option-name " configuration option. */ -void rewriteConfigBytesOption(struct rewriteConfigState *state, char *option, long long value, long long defvalue) { +void rewriteConfigBytesOption(struct rewriteConfigState *state, const char *option, long long value, long long defvalue) { char buf[64]; int force = value != defvalue; sds line; @@ -1848,7 +1137,7 @@ void rewriteConfigBytesOption(struct rewriteConfigState *state, char *option, lo } /* Rewrite a yes/no option. */ -void rewriteConfigYesNoOption(struct rewriteConfigState *state, char *option, int value, int defvalue) { +void rewriteConfigYesNoOption(struct rewriteConfigState *state, const char *option, int value, int defvalue) { int force = value != defvalue; sds line = sdscatprintf(sdsempty(),"%s %s",option, value ? "yes" : "no"); @@ -1857,7 +1146,7 @@ void rewriteConfigYesNoOption(struct rewriteConfigState *state, char *option, in } /* Rewrite a string option. */ -void rewriteConfigStringOption(struct rewriteConfigState *state, char *option, char *value, char *defvalue) { +void rewriteConfigStringOption(struct rewriteConfigState *state, const char *option, char *value, const char *defvalue) { int force = 1; sds line; @@ -1879,7 +1168,7 @@ void rewriteConfigStringOption(struct rewriteConfigState *state, char *option, c } /* Rewrite a numerical (long long range) option. */ -void rewriteConfigNumericalOption(struct rewriteConfigState *state, char *option, long long value, long long defvalue) { +void rewriteConfigNumericalOption(struct rewriteConfigState *state, const char *option, long long value, long long defvalue) { int force = value != defvalue; sds line = sdscatprintf(sdsempty(),"%s %lld",option,value); @@ -1897,7 +1186,7 @@ void rewriteConfigOctalOption(struct rewriteConfigState *state, char *option, in /* Rewrite an enumeration option. It takes as usually state and option name, * and in addition the enumeration array and the default value for the * option. */ -void rewriteConfigEnumOption(struct rewriteConfigState *state, char *option, int value, configEnum *ce, int defval) { +void rewriteConfigEnumOption(struct rewriteConfigState *state, const char *option, int value, configEnum *ce, int defval) { sds line; const char *name = configEnumGetNameOrUnknown(ce,value); int force = value != defval; @@ -1906,18 +1195,6 @@ void rewriteConfigEnumOption(struct rewriteConfigState *state, char *option, int rewriteConfigRewriteLine(state,option,line,force); } -/* Rewrite the syslog-facility option. */ -void rewriteConfigSyslogfacilityOption(struct rewriteConfigState *state) { - int value = server.syslog_facility; - int force = value != LOG_LOCAL0; - const char *name = NULL, *option = "syslog-facility"; - sds line; - - name = configEnumGetNameOrUnknown(syslog_facility_enum,value); - line = sdscatprintf(sdsempty(),"%s %s",option,name); - rewriteConfigRewriteLine(state,option,line,force); -} - /* Rewrite the save option. */ void rewriteConfigSaveOption(struct rewriteConfigState *state) { int j; @@ -2218,109 +1495,23 @@ int rewriteConfig(char *path) { /* Step 2: rewrite every single option, replacing or appending it inside * the rewrite state. */ - rewriteConfigYesNoOption(state,"daemonize",server.daemonize,0); - rewriteConfigStringOption(state,"pidfile",server.pidfile,CONFIG_DEFAULT_PID_FILE); - rewriteConfigNumericalOption(state,"port",server.port,CONFIG_DEFAULT_SERVER_PORT); - rewriteConfigNumericalOption(state,"cluster-announce-port",server.cluster_announce_port,CONFIG_DEFAULT_CLUSTER_ANNOUNCE_PORT); - rewriteConfigNumericalOption(state,"cluster-announce-bus-port",server.cluster_announce_bus_port,CONFIG_DEFAULT_CLUSTER_ANNOUNCE_BUS_PORT); - rewriteConfigNumericalOption(state,"tcp-backlog",server.tcp_backlog,CONFIG_DEFAULT_TCP_BACKLOG); + /* Iterate the configs that are standard */ + for (standardConfig *config = configs; config->name != NULL; config++) { + config->interface.rewrite(config->data, config->name, state); + } + rewriteConfigBindOption(state); - rewriteConfigStringOption(state,"unixsocket",server.unixsocket,NULL); rewriteConfigOctalOption(state,"unixsocketperm",server.unixsocketperm,CONFIG_DEFAULT_UNIX_SOCKET_PERM); - rewriteConfigNumericalOption(state,"timeout",server.maxidletime,CONFIG_DEFAULT_CLIENT_TIMEOUT); - rewriteConfigNumericalOption(state,"tcp-keepalive",server.tcpkeepalive,CONFIG_DEFAULT_TCP_KEEPALIVE); - rewriteConfigNumericalOption(state,"replica-announce-port",server.slave_announce_port,CONFIG_DEFAULT_SLAVE_ANNOUNCE_PORT); - rewriteConfigEnumOption(state,"loglevel",server.verbosity,loglevel_enum,CONFIG_DEFAULT_VERBOSITY); rewriteConfigStringOption(state,"logfile",server.logfile,CONFIG_DEFAULT_LOGFILE); - rewriteConfigStringOption(state,"aclfile",server.acl_filename,CONFIG_DEFAULT_ACL_FILENAME); - rewriteConfigYesNoOption(state,"syslog-enabled",server.syslog_enabled,CONFIG_DEFAULT_SYSLOG_ENABLED); - rewriteConfigStringOption(state,"syslog-ident",server.syslog_ident,CONFIG_DEFAULT_SYSLOG_IDENT); - rewriteConfigSyslogfacilityOption(state); rewriteConfigSaveOption(state); rewriteConfigUserOption(state); - rewriteConfigNumericalOption(state,"databases",server.dbnum,CONFIG_DEFAULT_DBNUM); - rewriteConfigYesNoOption(state,"stop-writes-on-bgsave-error",server.stop_writes_on_bgsave_err,CONFIG_DEFAULT_STOP_WRITES_ON_BGSAVE_ERROR); - rewriteConfigYesNoOption(state,"rdbcompression",server.rdb_compression,CONFIG_DEFAULT_RDB_COMPRESSION); - rewriteConfigYesNoOption(state,"rdbchecksum",server.rdb_checksum,CONFIG_DEFAULT_RDB_CHECKSUM); - rewriteConfigStringOption(state,"dbfilename",server.rdb_filename,CONFIG_DEFAULT_RDB_FILENAME); rewriteConfigDirOption(state); rewriteConfigSlaveofOption(state,"replicaof"); - rewriteConfigStringOption(state,"replica-announce-ip",server.slave_announce_ip,CONFIG_DEFAULT_SLAVE_ANNOUNCE_IP); - rewriteConfigStringOption(state,"masteruser",server.masteruser,NULL); - rewriteConfigStringOption(state,"masterauth",server.masterauth,NULL); - rewriteConfigStringOption(state,"cluster-announce-ip",server.cluster_announce_ip,NULL); - rewriteConfigYesNoOption(state,"replica-serve-stale-data",server.repl_serve_stale_data,CONFIG_DEFAULT_SLAVE_SERVE_STALE_DATA); - rewriteConfigYesNoOption(state,"replica-read-only",server.repl_slave_ro,CONFIG_DEFAULT_SLAVE_READ_ONLY); - rewriteConfigYesNoOption(state,"replica-ignore-maxmemory",server.repl_slave_ignore_maxmemory,CONFIG_DEFAULT_SLAVE_IGNORE_MAXMEMORY); - rewriteConfigNumericalOption(state,"repl-ping-replica-period",server.repl_ping_slave_period,CONFIG_DEFAULT_REPL_PING_SLAVE_PERIOD); - rewriteConfigNumericalOption(state,"repl-timeout",server.repl_timeout,CONFIG_DEFAULT_REPL_TIMEOUT); - rewriteConfigBytesOption(state,"repl-backlog-size",server.repl_backlog_size,CONFIG_DEFAULT_REPL_BACKLOG_SIZE); - rewriteConfigBytesOption(state,"repl-backlog-ttl",server.repl_backlog_time_limit,CONFIG_DEFAULT_REPL_BACKLOG_TIME_LIMIT); - rewriteConfigYesNoOption(state,"repl-disable-tcp-nodelay",server.repl_disable_tcp_nodelay,CONFIG_DEFAULT_REPL_DISABLE_TCP_NODELAY); - rewriteConfigYesNoOption(state,"repl-diskless-sync",server.repl_diskless_sync,CONFIG_DEFAULT_REPL_DISKLESS_SYNC); - rewriteConfigNumericalOption(state,"repl-diskless-sync-delay",server.repl_diskless_sync_delay,CONFIG_DEFAULT_REPL_DISKLESS_SYNC_DELAY); - rewriteConfigNumericalOption(state,"replica-priority",server.slave_priority,CONFIG_DEFAULT_SLAVE_PRIORITY); - rewriteConfigNumericalOption(state,"min-replicas-to-write",server.repl_min_slaves_to_write,CONFIG_DEFAULT_MIN_SLAVES_TO_WRITE); - rewriteConfigNumericalOption(state,"min-replicas-max-lag",server.repl_min_slaves_max_lag,CONFIG_DEFAULT_MIN_SLAVES_MAX_LAG); rewriteConfigRequirepassOption(state,"requirepass"); - rewriteConfigNumericalOption(state,"maxclients",server.maxclients,CONFIG_DEFAULT_MAX_CLIENTS); - rewriteConfigBytesOption(state,"maxmemory",server.maxmemory,CONFIG_DEFAULT_MAXMEMORY); - rewriteConfigBytesOption(state,"proto-max-bulk-len",server.proto_max_bulk_len,CONFIG_DEFAULT_PROTO_MAX_BULK_LEN); rewriteConfigBytesOption(state,"client-query-buffer-limit",server.client_max_querybuf_len,PROTO_MAX_QUERYBUF_LEN); - 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); - rewriteConfigNumericalOption(state,"active-defrag-max-scan-fields",server.active_defrag_max_scan_fields,CONFIG_DEFAULT_DEFRAG_MAX_SCAN_FIELDS); - 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); - rewriteConfigYesNoOption(state,"no-appendfsync-on-rewrite",server.aof_no_fsync_on_rewrite,CONFIG_DEFAULT_AOF_NO_FSYNC_ON_REWRITE); - rewriteConfigNumericalOption(state,"auto-aof-rewrite-percentage",server.aof_rewrite_perc,AOF_REWRITE_PERC); - rewriteConfigBytesOption(state,"auto-aof-rewrite-min-size",server.aof_rewrite_min_size,AOF_REWRITE_MIN_SIZE); - rewriteConfigNumericalOption(state,"lua-time-limit",server.lua_time_limit,LUA_SCRIPT_TIME_LIMIT); - rewriteConfigYesNoOption(state,"cluster-enabled",server.cluster_enabled,0); rewriteConfigStringOption(state,"cluster-config-file",server.cluster_configfile,CONFIG_DEFAULT_CLUSTER_CONFIG_FILE); - rewriteConfigYesNoOption(state,"cluster-require-full-coverage",server.cluster_require_full_coverage,CLUSTER_DEFAULT_REQUIRE_FULL_COVERAGE); - rewriteConfigYesNoOption(state,"cluster-replica-no-failover",server.cluster_slave_no_failover,CLUSTER_DEFAULT_SLAVE_NO_FAILOVER); - rewriteConfigNumericalOption(state,"cluster-node-timeout",server.cluster_node_timeout,CLUSTER_DEFAULT_NODE_TIMEOUT); - rewriteConfigNumericalOption(state,"cluster-migration-barrier",server.cluster_migration_barrier,CLUSTER_DEFAULT_MIGRATION_BARRIER); - rewriteConfigNumericalOption(state,"cluster-replica-validity-factor",server.cluster_slave_validity_factor,CLUSTER_DEFAULT_SLAVE_VALIDITY); - rewriteConfigNumericalOption(state,"slowlog-log-slower-than",server.slowlog_log_slower_than,CONFIG_DEFAULT_SLOWLOG_LOG_SLOWER_THAN); - rewriteConfigNumericalOption(state,"latency-monitor-threshold",server.latency_monitor_threshold,CONFIG_DEFAULT_LATENCY_MONITOR_THRESHOLD); - rewriteConfigNumericalOption(state,"slowlog-max-len",server.slowlog_max_len,CONFIG_DEFAULT_SLOWLOG_MAX_LEN); rewriteConfigNotifykeyspaceeventsOption(state); - rewriteConfigNumericalOption(state,"hash-max-ziplist-entries",server.hash_max_ziplist_entries,OBJ_HASH_MAX_ZIPLIST_ENTRIES); - rewriteConfigNumericalOption(state,"hash-max-ziplist-value",server.hash_max_ziplist_value,OBJ_HASH_MAX_ZIPLIST_VALUE); - rewriteConfigNumericalOption(state,"stream-node-max-bytes",server.stream_node_max_bytes,OBJ_STREAM_NODE_MAX_BYTES); - rewriteConfigNumericalOption(state,"stream-node-max-entries",server.stream_node_max_entries,OBJ_STREAM_NODE_MAX_ENTRIES); - rewriteConfigNumericalOption(state,"list-max-ziplist-size",server.list_max_ziplist_size,OBJ_LIST_MAX_ZIPLIST_SIZE); - rewriteConfigNumericalOption(state,"list-compress-depth",server.list_compress_depth,OBJ_LIST_COMPRESS_DEPTH); - rewriteConfigNumericalOption(state,"set-max-intset-entries",server.set_max_intset_entries,OBJ_SET_MAX_INTSET_ENTRIES); - rewriteConfigNumericalOption(state,"zset-max-ziplist-entries",server.zset_max_ziplist_entries,OBJ_ZSET_MAX_ZIPLIST_ENTRIES); - 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); - rewriteConfigYesNoOption(state,"gopher-enabled",server.gopher_enabled,CONFIG_DEFAULT_GOPHER_ENABLED); rewriteConfigClientoutputbufferlimitOption(state); - rewriteConfigNumericalOption(state,"hz",server.config_hz,CONFIG_DEFAULT_HZ); - rewriteConfigYesNoOption(state,"aof-rewrite-incremental-fsync",server.aof_rewrite_incremental_fsync,CONFIG_DEFAULT_AOF_REWRITE_INCREMENTAL_FSYNC); - rewriteConfigYesNoOption(state,"rdb-save-incremental-fsync",server.rdb_save_incremental_fsync,CONFIG_DEFAULT_RDB_SAVE_INCREMENTAL_FSYNC); - rewriteConfigYesNoOption(state,"aof-load-truncated",server.aof_load_truncated,CONFIG_DEFAULT_AOF_LOAD_TRUNCATED); - rewriteConfigYesNoOption(state,"aof-use-rdb-preamble",server.aof_use_rdb_preamble,CONFIG_DEFAULT_AOF_USE_RDB_PREAMBLE); - rewriteConfigEnumOption(state,"supervised",server.supervised_mode,supervised_mode_enum,SUPERVISED_NONE); - rewriteConfigYesNoOption(state,"lazyfree-lazy-eviction",server.lazyfree_lazy_eviction,CONFIG_DEFAULT_LAZYFREE_LAZY_EVICTION); - rewriteConfigYesNoOption(state,"lazyfree-lazy-expire",server.lazyfree_lazy_expire,CONFIG_DEFAULT_LAZYFREE_LAZY_EXPIRE); - rewriteConfigYesNoOption(state,"lazyfree-lazy-server-del",server.lazyfree_lazy_server_del,CONFIG_DEFAULT_LAZYFREE_LAZY_SERVER_DEL); - rewriteConfigYesNoOption(state,"replica-lazy-flush",server.repl_slave_lazy_flush,CONFIG_DEFAULT_SLAVE_LAZY_FLUSH); - rewriteConfigYesNoOption(state,"dynamic-hz",server.dynamic_hz,CONFIG_DEFAULT_DYNAMIC_HZ); /* Rewrite Sentinel config if in Sentinel mode. */ if (server.sentinel_mode) rewriteConfigSentinelOption(state); @@ -2340,6 +1531,695 @@ int rewriteConfig(char *path) { return retval; } +/*----------------------------------------------------------------------------- + * Configs that fit one of the major types and require no special handling + *----------------------------------------------------------------------------*/ +#define LOADBUF_SIZE 256 +static char loadbuf[LOADBUF_SIZE]; + +#define MODIFIABLE_CONFIG 1 +#define IMMUTABLE_CONFIG 0 + +#define embedCommonConfig(config_name, config_alias, is_modifiable) \ + .name = (config_name), \ + .alias = (config_alias), \ + .modifiable = (is_modifiable), + +#define embedConfigInterface(initfn, setfn, getfn, rewritefn) .interface = { \ + .init = (initfn), \ + .set = (setfn), \ + .get = (getfn), \ + .rewrite = (rewritefn) \ +}, + +/* What follows is the generic config types that are supported. To add a new + * config with one of these types, add it to the standardConfig table with + * the creation macro for each type. + * + * Each type contains the following: + * * A function defining how to load this type on startup. + * * A function defining how to update this type on CONFIG SET. + * * A function defining how to serialize this type on CONFIG SET. + * * A function defining how to rewrite this type on CONFIG REWRITE. + * * A Macro defining how to create this type. + */ + +/* Bool Configs */ +static void boolConfigInit(typeData data) { + *data.yesno.config = data.yesno.default_value; +} + +static int boolConfigSet(typeData data, sds value, int update, char **err) { + int yn = yesnotoi(value); + if (yn == -1) { + *err = "argument must be 'yes' or 'no'"; + return 0; + } + if (data.yesno.is_valid_fn && !data.yesno.is_valid_fn(yn, err)) + return 0; + int prev = *(data.yesno.config); + *(data.yesno.config) = yn; + if (update && data.yesno.update_fn && !data.yesno.update_fn(yn, prev, err)) { + *(data.yesno.config) = prev; + return 0; + } + return 1; +} + +static void boolConfigGet(client *c, typeData data) { + addReplyBulkCString(c, *data.yesno.config ? "yes" : "no"); +} + +static void boolConfigRewrite(typeData data, const char *name, struct rewriteConfigState *state) { + rewriteConfigYesNoOption(state, name,*(data.yesno.config), data.yesno.default_value); +} + +#define createBoolConfig(name, alias, modifiable, config_addr, default, is_valid, update) { \ + embedCommonConfig(name, alias, modifiable) \ + embedConfigInterface(boolConfigInit, boolConfigSet, boolConfigGet, boolConfigRewrite) \ + .data.yesno = { \ + .config = &(config_addr), \ + .default_value = (default), \ + .is_valid_fn = (is_valid), \ + .update_fn = (update), \ + } \ +} + +/* String Configs */ +static void stringConfigInit(typeData data) { + if (data.string.convert_empty_to_null) { + *data.string.config = data.string.default_value ? zstrdup(data.string.default_value) : NULL; + } else { + *data.string.config = zstrdup(data.string.default_value); + } +} + +static int stringConfigSet(typeData data, sds value, int update, char **err) { + if (data.string.is_valid_fn && !data.string.is_valid_fn(value, err)) + return 0; + char *prev = *data.string.config; + if (data.string.convert_empty_to_null) { + *data.string.config = value[0] ? zstrdup(value) : NULL; + } else { + *data.string.config = zstrdup(value); + } + if (update && data.string.update_fn && !data.string.update_fn(*data.string.config, prev, err)) { + zfree(*data.string.config); + *data.string.config = prev; + return 0; + } + zfree(prev); + return 1; +} + +static void stringConfigGet(client *c, typeData data) { + addReplyBulkCString(c, *data.string.config ? *data.string.config : ""); +} + +static void stringConfigRewrite(typeData data, const char *name, struct rewriteConfigState *state) { + rewriteConfigStringOption(state, name,*(data.string.config), data.string.default_value); +} + +#define ALLOW_EMPTY_STRING 0 +#define EMPTY_STRING_IS_NULL 1 + +#define createStringConfig(name, alias, modifiable, empty_to_null, config_addr, default, is_valid, update) { \ + embedCommonConfig(name, alias, modifiable) \ + embedConfigInterface(stringConfigInit, stringConfigSet, stringConfigGet, stringConfigRewrite) \ + .data.string = { \ + .config = &(config_addr), \ + .default_value = (default), \ + .is_valid_fn = (is_valid), \ + .update_fn = (update), \ + .convert_empty_to_null = (empty_to_null), \ + } \ +} + +/* Enum configs */ +static void enumConfigInit(typeData data) { + *data.enumd.config = data.enumd.default_value; +} + +static int enumConfigSet(typeData data, sds value, int update, char **err) { + int enumval = configEnumGetValue(data.enumd.enum_value, value); + if (enumval == INT_MIN) { + sds enumerr = sdsnew("argument must be one of the following: "); + configEnum *enumNode = data.enumd.enum_value; + while(enumNode->name != NULL) { + enumerr = sdscatlen(enumerr, enumNode->name, strlen(enumNode->name)); + enumerr = sdscatlen(enumerr, ", ", 2); + enumNode++; + } + + enumerr[sdslen(enumerr) - 2] = '\0'; + + /* Make sure we don't overrun the fixed buffer */ + enumerr[LOADBUF_SIZE - 1] = '\0'; + strncpy(loadbuf, enumerr, LOADBUF_SIZE); + + sdsfree(enumerr); + *err = loadbuf; + return 0; + } + if (data.enumd.is_valid_fn && !data.enumd.is_valid_fn(enumval, err)) + return 0; + int prev = *(data.enumd.config); + *(data.enumd.config) = enumval; + if (update && data.enumd.update_fn && !data.enumd.update_fn(enumval, prev, err)) { + *(data.enumd.config) = prev; + return 0; + } + return 1; +} + +static void enumConfigGet(client *c, typeData data) { + addReplyBulkCString(c, configEnumGetNameOrUnknown(data.enumd.enum_value,*data.enumd.config)); +} + +static void enumConfigRewrite(typeData data, const char *name, struct rewriteConfigState *state) { + rewriteConfigEnumOption(state, name,*(data.enumd.config), data.enumd.enum_value, data.enumd.default_value); +} + +#define createEnumConfig(name, alias, modifiable, enum, config_addr, default, is_valid, update) { \ + embedCommonConfig(name, alias, modifiable) \ + embedConfigInterface(enumConfigInit, enumConfigSet, enumConfigGet, enumConfigRewrite) \ + .data.enumd = { \ + .config = &(config_addr), \ + .default_value = (default), \ + .is_valid_fn = (is_valid), \ + .update_fn = (update), \ + .enum_value = (enum), \ + } \ +} + +/* Gets a 'long long val' and sets it into the union, using a macro to get + * compile time type check. */ +#define SET_NUMERIC_TYPE(val) \ + if (data.numeric.numeric_type == NUMERIC_TYPE_INT) { \ + *(data.numeric.config.i) = (int) val; \ + } else if (data.numeric.numeric_type == NUMERIC_TYPE_UINT) { \ + *(data.numeric.config.ui) = (unsigned int) val; \ + } else if (data.numeric.numeric_type == NUMERIC_TYPE_LONG) { \ + *(data.numeric.config.l) = (long) val; \ + } else if (data.numeric.numeric_type == NUMERIC_TYPE_ULONG) { \ + *(data.numeric.config.ul) = (unsigned long) val; \ + } else if (data.numeric.numeric_type == NUMERIC_TYPE_LONG_LONG) { \ + *(data.numeric.config.ll) = (long long) val; \ + } else if (data.numeric.numeric_type == NUMERIC_TYPE_ULONG_LONG) { \ + *(data.numeric.config.ull) = (unsigned long long) val; \ + } else if (data.numeric.numeric_type == NUMERIC_TYPE_SIZE_T) { \ + *(data.numeric.config.st) = (size_t) val; \ + } else if (data.numeric.numeric_type == NUMERIC_TYPE_SSIZE_T) { \ + *(data.numeric.config.sst) = (ssize_t) val; \ + } else if (data.numeric.numeric_type == NUMERIC_TYPE_OFF_T) { \ + *(data.numeric.config.ot) = (off_t) val; \ + } else if (data.numeric.numeric_type == NUMERIC_TYPE_TIME_T) { \ + *(data.numeric.config.tt) = (time_t) val; \ + } + +/* Gets a 'long long val' and sets it with the value from the union, using a + * macro to get compile time type check. */ +#define GET_NUMERIC_TYPE(val) \ + if (data.numeric.numeric_type == NUMERIC_TYPE_INT) { \ + val = *(data.numeric.config.i); \ + } else if (data.numeric.numeric_type == NUMERIC_TYPE_UINT) { \ + val = *(data.numeric.config.ui); \ + } else if (data.numeric.numeric_type == NUMERIC_TYPE_LONG) { \ + val = *(data.numeric.config.l); \ + } else if (data.numeric.numeric_type == NUMERIC_TYPE_ULONG) { \ + val = *(data.numeric.config.ul); \ + } else if (data.numeric.numeric_type == NUMERIC_TYPE_LONG_LONG) { \ + val = *(data.numeric.config.ll); \ + } else if (data.numeric.numeric_type == NUMERIC_TYPE_ULONG_LONG) { \ + val = *(data.numeric.config.ull); \ + } else if (data.numeric.numeric_type == NUMERIC_TYPE_SIZE_T) { \ + val = *(data.numeric.config.st); \ + } else if (data.numeric.numeric_type == NUMERIC_TYPE_SSIZE_T) { \ + val = *(data.numeric.config.sst); \ + } else if (data.numeric.numeric_type == NUMERIC_TYPE_OFF_T) { \ + val = *(data.numeric.config.ot); \ + } else if (data.numeric.numeric_type == NUMERIC_TYPE_TIME_T) { \ + val = *(data.numeric.config.tt); \ + } + +/* Numeric configs */ +static void numericConfigInit(typeData data) { + SET_NUMERIC_TYPE(data.numeric.default_value) +} + +static int numericBoundaryCheck(typeData data, long long ll, char **err) { + if (data.numeric.numeric_type == NUMERIC_TYPE_ULONG_LONG || + data.numeric.numeric_type == NUMERIC_TYPE_UINT || + data.numeric.numeric_type == NUMERIC_TYPE_SIZE_T) { + /* Boundary check for unsigned types */ + unsigned long long ull = ll; + unsigned long long upper_bound = data.numeric.upper_bound; + unsigned long long lower_bound = data.numeric.lower_bound; + if (ull > upper_bound || ull < lower_bound) { + snprintf(loadbuf, LOADBUF_SIZE, + "argument must be between %llu and %llu inclusive", + lower_bound, + upper_bound); + *err = loadbuf; + return 0; + } + } else { + /* Boundary check for signed types */ + if (ll > data.numeric.upper_bound || ll < data.numeric.lower_bound) { + snprintf(loadbuf, LOADBUF_SIZE, + "argument must be between %lld and %lld inclusive", + data.numeric.lower_bound, + data.numeric.upper_bound); + *err = loadbuf; + return 0; + } + } + return 1; +} + +static int numericConfigSet(typeData data, sds value, int update, char **err) { + long long ll, prev = 0; + if (data.numeric.is_memory) { + int memerr; + ll = memtoll(value, &memerr); + if (memerr || ll < 0) { + *err = "argument must be a memory value"; + return 0; + } + } else { + if (!string2ll(value, sdslen(value),&ll)) { + *err = "argument couldn't be parsed into an integer" ; + return 0; + } + } + + if (!numericBoundaryCheck(data, ll, err)) + return 0; + + if (data.numeric.is_valid_fn && !data.numeric.is_valid_fn(ll, err)) + return 0; + + GET_NUMERIC_TYPE(prev) + SET_NUMERIC_TYPE(ll) + + if (update && data.numeric.update_fn && !data.numeric.update_fn(ll, prev, err)) { + SET_NUMERIC_TYPE(prev) + return 0; + } + return 1; +} + +static void numericConfigGet(client *c, typeData data) { + char buf[128]; + long long value = 0; + + GET_NUMERIC_TYPE(value) + + ll2string(buf, sizeof(buf), value); + addReplyBulkCString(c, buf); +} + +static void numericConfigRewrite(typeData data, const char *name, struct rewriteConfigState *state) { + long long value = 0; + + GET_NUMERIC_TYPE(value) + + if (data.numeric.is_memory) { + rewriteConfigBytesOption(state, name, value, data.numeric.default_value); + } else { + rewriteConfigNumericalOption(state, name, value, data.numeric.default_value); + } +} + +#define INTEGER_CONFIG 0 +#define MEMORY_CONFIG 1 + +#define embedCommonNumericalConfig(name, alias, modifiable, lower, upper, config_addr, default, memory, is_valid, update) { \ + embedCommonConfig(name, alias, modifiable) \ + embedConfigInterface(numericConfigInit, numericConfigSet, numericConfigGet, numericConfigRewrite) \ + .data.numeric = { \ + .lower_bound = (lower), \ + .upper_bound = (upper), \ + .default_value = (default), \ + .is_valid_fn = (is_valid), \ + .update_fn = (update), \ + .is_memory = (memory), + +#define createIntConfig(name, alias, modifiable, lower, upper, config_addr, default, memory, is_valid, update) \ + embedCommonNumericalConfig(name, alias, modifiable, lower, upper, config_addr, default, memory, is_valid, update) \ + .numeric_type = NUMERIC_TYPE_INT, \ + .config.i = &(config_addr) \ + } \ +} + +#define createUIntConfig(name, alias, modifiable, lower, upper, config_addr, default, memory, is_valid, update) \ + embedCommonNumericalConfig(name, alias, modifiable, lower, upper, config_addr, default, memory, is_valid, update) \ + .numeric_type = NUMERIC_TYPE_UINT, \ + .config.ui = &(config_addr) \ + } \ +} + +#define createLongConfig(name, alias, modifiable, lower, upper, config_addr, default, memory, is_valid, update) \ + embedCommonNumericalConfig(name, alias, modifiable, lower, upper, config_addr, default, memory, is_valid, update) \ + .numeric_type = NUMERIC_TYPE_LONG, \ + .config.l = &(config_addr) \ + } \ +} + +#define createULongConfig(name, alias, modifiable, lower, upper, config_addr, default, memory, is_valid, update) \ + embedCommonNumericalConfig(name, alias, modifiable, lower, upper, config_addr, default, memory, is_valid, update) \ + .numeric_type = NUMERIC_TYPE_ULONG, \ + .config.ul = &(config_addr) \ + } \ +} + +#define createLongLongConfig(name, alias, modifiable, lower, upper, config_addr, default, memory, is_valid, update) \ + embedCommonNumericalConfig(name, alias, modifiable, lower, upper, config_addr, default, memory, is_valid, update) \ + .numeric_type = NUMERIC_TYPE_LONG_LONG, \ + .config.ll = &(config_addr) \ + } \ +} + +#define createULongLongConfig(name, alias, modifiable, lower, upper, config_addr, default, memory, is_valid, update) \ + embedCommonNumericalConfig(name, alias, modifiable, lower, upper, config_addr, default, memory, is_valid, update) \ + .numeric_type = NUMERIC_TYPE_ULONG_LONG, \ + .config.ull = &(config_addr) \ + } \ +} + +#define createSizeTConfig(name, alias, modifiable, lower, upper, config_addr, default, memory, is_valid, update) \ + embedCommonNumericalConfig(name, alias, modifiable, lower, upper, config_addr, default, memory, is_valid, update) \ + .numeric_type = NUMERIC_TYPE_SIZE_T, \ + .config.st = &(config_addr) \ + } \ +} + +#define createSSizeTConfig(name, alias, modifiable, lower, upper, config_addr, default, memory, is_valid, update) \ + embedCommonNumericalConfig(name, alias, modifiable, lower, upper, config_addr, default, memory, is_valid, update) \ + .numeric_type = NUMERIC_TYPE_SSIZE_T, \ + .config.sst = &(config_addr) \ + } \ +} + +#define createTimeTConfig(name, alias, modifiable, lower, upper, config_addr, default, memory, is_valid, update) \ + embedCommonNumericalConfig(name, alias, modifiable, lower, upper, config_addr, default, memory, is_valid, update) \ + .numeric_type = NUMERIC_TYPE_TIME_T, \ + .config.tt = &(config_addr) \ + } \ +} + +#define createOffTConfig(name, alias, modifiable, lower, upper, config_addr, default, memory, is_valid, update) \ + embedCommonNumericalConfig(name, alias, modifiable, lower, upper, config_addr, default, memory, is_valid, update) \ + .numeric_type = NUMERIC_TYPE_OFF_T, \ + .config.ot = &(config_addr) \ + } \ +} + +static int isValidActiveDefrag(int val, char **err) { +#ifndef HAVE_DEFRAG + if (val) { + *err = "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 0; + } +#else + UNUSED(val); + UNUSED(err); +#endif + return 1; +} + +static int isValidDBfilename(char *val, char **err) { + if (!pathIsBaseName(val)) { + *err = "dbfilename can't be a path, just a filename"; + return 0; + } + return 1; +} + +static int isValidAOFfilename(char *val, char **err) { + if (!pathIsBaseName(val)) { + *err = "appendfilename can't be a path, just a filename"; + return 0; + } + return 1; +} + +static int updateHZ(long long val, long long prev, char **err) { + UNUSED(prev); + UNUSED(err); + /* Hz is more an hint from the user, so we accept values out of range + * but cap them to reasonable values. */ + server.config_hz = val; + if (server.config_hz < CONFIG_MIN_HZ) server.config_hz = CONFIG_MIN_HZ; + if (server.config_hz > CONFIG_MAX_HZ) server.config_hz = CONFIG_MAX_HZ; + server.hz = server.config_hz; + return 1; +} + +static int updateJemallocBgThread(int val, int prev, char **err) { + UNUSED(prev); + UNUSED(err); + set_jemalloc_bg_thread(val); + return 1; +} + +static int updateReplBacklogSize(long long val, long long prev, char **err) { + /* resizeReplicationBacklog sets server.repl_backlog_size, and relies on + * being able to tell when the size changes, so restore prev becore calling it. */ + UNUSED(err); + server.repl_backlog_size = prev; + resizeReplicationBacklog(val); + return 1; +} + +static int updateMaxmemory(long long val, long long prev, char **err) { + UNUSED(prev); + UNUSED(err); + if (val) { + size_t used = zmalloc_used_memory()-freeMemoryGetNotCountedMemory(); + if ((unsigned long long)val < used) { + serverLog(LL_WARNING,"WARNING: the new maxmemory value set via CONFIG SET (%llu) is smaller than the current memory usage (%zu). This will result in key eviction and/or the inability to accept new write commands depending on the maxmemory-policy.", server.maxmemory, used); + } + freeMemoryIfNeededAndSafe(); + } + return 1; +} + +static int updateGoodSlaves(long long val, long long prev, char **err) { + UNUSED(val); + UNUSED(prev); + UNUSED(err); + refreshGoodSlavesCount(); + return 1; +} + +static int updateAppendonly(int val, int prev, char **err) { + UNUSED(prev); + if (val == 0 && server.aof_state != AOF_OFF) { + stopAppendOnly(); + } else if (val && server.aof_state == AOF_OFF) { + if (startAppendOnly() == C_ERR) { + *err = "Unable to turn on AOF. Check server logs."; + return 0; + } + } + return 1; +} + +static int updateMaxclients(long long val, long long prev, char **err) { + /* Try to check if the OS is capable of supporting so many FDs. */ + if (val > prev) { + adjustOpenFilesLimit(); + if (server.maxclients != val) { + static char msg[128]; + sprintf(msg, "The operating system is not able to handle the specified number of clients, try with %d", server.maxclients); + *err = msg; + if (server.maxclients > prev) { + server.maxclients = prev; + adjustOpenFilesLimit(); + } + return 0; + } + if ((unsigned int) aeGetSetSize(server.el) < + server.maxclients + CONFIG_FDSET_INCR) + { + if (aeResizeSetSize(server.el, + server.maxclients + CONFIG_FDSET_INCR) == AE_ERR) + { + *err = "The event loop API used by Redis is not able to handle the specified number of clients"; + return 0; + } + } + } + return 1; +} + +#ifdef USE_OPENSSL +static int updateTlsCfg(char *val, char *prev, char **err) { + UNUSED(val); + UNUSED(prev); + UNUSED(err); + if (tlsConfigure(&server.tls_ctx_config) == C_ERR) { + *err = "Unable to configure tls-cert-file. Check server logs."; + return 0; + } + return 1; +} +static int updateTlsCfgBool(int val, int prev, char **err) { + UNUSED(val); + UNUSED(prev); + return updateTlsCfg(NULL, NULL, err); +} +#endif /* USE_OPENSSL */ + +standardConfig configs[] = { + /* Bool configs */ + createBoolConfig("rdbchecksum", NULL, IMMUTABLE_CONFIG, server.rdb_checksum, 1, NULL, NULL), + createBoolConfig("daemonize", NULL, IMMUTABLE_CONFIG, server.daemonize, 0, NULL, NULL), + createBoolConfig("io-threads-do-reads", NULL, IMMUTABLE_CONFIG, server.io_threads_do_reads, 0,NULL, NULL), /* Read + parse from threads? */ + createBoolConfig("lua-replicate-commands", NULL, MODIFIABLE_CONFIG, server.lua_always_replicate_commands, 1, NULL, NULL), + createBoolConfig("always-show-logo", NULL, IMMUTABLE_CONFIG, server.always_show_logo, 0, NULL, NULL), + createBoolConfig("protected-mode", NULL, MODIFIABLE_CONFIG, server.protected_mode, 1, NULL, NULL), + createBoolConfig("rdbcompression", NULL, MODIFIABLE_CONFIG, server.rdb_compression, 1, NULL, NULL), + createBoolConfig("activerehashing", NULL, MODIFIABLE_CONFIG, server.activerehashing, 1, NULL, NULL), + createBoolConfig("stop-writes-on-bgsave-error", NULL, MODIFIABLE_CONFIG, server.stop_writes_on_bgsave_err, 1, NULL, NULL), + createBoolConfig("dynamic-hz", NULL, MODIFIABLE_CONFIG, server.dynamic_hz, 1, NULL, NULL), /* Adapt hz to # of clients.*/ + createBoolConfig("lazyfree-lazy-eviction", NULL, MODIFIABLE_CONFIG, server.lazyfree_lazy_eviction, 0, NULL, NULL), + createBoolConfig("lazyfree-lazy-expire", NULL, MODIFIABLE_CONFIG, server.lazyfree_lazy_expire, 0, NULL, NULL), + createBoolConfig("lazyfree-lazy-server-del", NULL, MODIFIABLE_CONFIG, server.lazyfree_lazy_server_del, 0, NULL, NULL), + createBoolConfig("repl-disable-tcp-nodelay", NULL, MODIFIABLE_CONFIG, server.repl_disable_tcp_nodelay, 0, NULL, NULL), + createBoolConfig("repl-diskless-sync", NULL, MODIFIABLE_CONFIG, server.repl_diskless_sync, 0, NULL, NULL), + createBoolConfig("gopher-enabled", NULL, MODIFIABLE_CONFIG, server.gopher_enabled, 0, NULL, NULL), + createBoolConfig("aof-rewrite-incremental-fsync", NULL, MODIFIABLE_CONFIG, server.aof_rewrite_incremental_fsync, 1, NULL, NULL), + createBoolConfig("no-appendfsync-on-rewrite", NULL, MODIFIABLE_CONFIG, server.aof_no_fsync_on_rewrite, 0, NULL, NULL), + createBoolConfig("cluster-require-full-coverage", NULL, MODIFIABLE_CONFIG, server.cluster_require_full_coverage, 1, NULL, NULL), + createBoolConfig("rdb-save-incremental-fsync", NULL, MODIFIABLE_CONFIG, server.rdb_save_incremental_fsync, 1, NULL, NULL), + createBoolConfig("aof-load-truncated", NULL, MODIFIABLE_CONFIG, server.aof_load_truncated, 1, NULL, NULL), + createBoolConfig("aof-use-rdb-preamble", NULL, MODIFIABLE_CONFIG, server.aof_use_rdb_preamble, 1, NULL, NULL), + createBoolConfig("cluster-replica-no-failover", "cluster-slave-no-failover", MODIFIABLE_CONFIG, server.cluster_slave_no_failover, 0, NULL, NULL), /* Failover by default. */ + createBoolConfig("replica-lazy-flush", "slave-lazy-flush", MODIFIABLE_CONFIG, server.repl_slave_lazy_flush, 0, NULL, NULL), + createBoolConfig("replica-serve-stale-data", "slave-serve-stale-data", MODIFIABLE_CONFIG, server.repl_serve_stale_data, 1, NULL, NULL), + createBoolConfig("replica-read-only", "slave-read-only", MODIFIABLE_CONFIG, server.repl_slave_ro, 1, NULL, NULL), + createBoolConfig("replica-ignore-maxmemory", "slave-ignore-maxmemory", MODIFIABLE_CONFIG, server.repl_slave_ignore_maxmemory, 1, NULL, NULL), + createBoolConfig("jemalloc-bg-thread", NULL, MODIFIABLE_CONFIG, server.jemalloc_bg_thread, 1, NULL, updateJemallocBgThread), + createBoolConfig("activedefrag", NULL, MODIFIABLE_CONFIG, server.active_defrag_enabled, 0, isValidActiveDefrag, NULL), + createBoolConfig("syslog-enabled", NULL, IMMUTABLE_CONFIG, server.syslog_enabled, 0, NULL, NULL), + createBoolConfig("cluster-enabled", NULL, IMMUTABLE_CONFIG, server.cluster_enabled, 0, NULL, NULL), + createBoolConfig("appendonly", NULL, MODIFIABLE_CONFIG, server.aof_enabled, 0, NULL, updateAppendonly), + createBoolConfig("cluster-allow-reads-when-down", NULL, MODIFIABLE_CONFIG, server.cluster_allow_reads_when_down, 0, NULL, NULL), + + + /* String Configs */ + createStringConfig("aclfile", NULL, IMMUTABLE_CONFIG, ALLOW_EMPTY_STRING, server.acl_filename, "", NULL, NULL), + createStringConfig("unixsocket", NULL, IMMUTABLE_CONFIG, EMPTY_STRING_IS_NULL, server.unixsocket, NULL, NULL, NULL), + createStringConfig("pidfile", NULL, IMMUTABLE_CONFIG, EMPTY_STRING_IS_NULL, server.pidfile, NULL, NULL, NULL), + createStringConfig("replica-announce-ip", "slave-announce-ip", MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.slave_announce_ip, NULL, NULL, NULL), + createStringConfig("masteruser", NULL, MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.masteruser, NULL, NULL, NULL), + createStringConfig("masterauth", NULL, MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.masterauth, NULL, NULL, NULL), + createStringConfig("cluster-announce-ip", NULL, MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.cluster_announce_ip, NULL, NULL, NULL), + createStringConfig("syslog-ident", NULL, IMMUTABLE_CONFIG, ALLOW_EMPTY_STRING, server.syslog_ident, "redis", NULL, NULL), + createStringConfig("dbfilename", NULL, MODIFIABLE_CONFIG, ALLOW_EMPTY_STRING, server.rdb_filename, "dump.rdb", isValidDBfilename, NULL), + createStringConfig("appendfilename", NULL, IMMUTABLE_CONFIG, ALLOW_EMPTY_STRING, server.aof_filename, "appendonly.aof", isValidAOFfilename, NULL), + + /* Enum Configs */ + createEnumConfig("supervised", NULL, IMMUTABLE_CONFIG, supervised_mode_enum, server.supervised_mode, SUPERVISED_NONE, NULL, NULL), + createEnumConfig("syslog-facility", NULL, IMMUTABLE_CONFIG, syslog_facility_enum, server.syslog_facility, LOG_LOCAL0, NULL, NULL), + createEnumConfig("repl-diskless-load", NULL, MODIFIABLE_CONFIG, repl_diskless_load_enum, server.repl_diskless_load, REPL_DISKLESS_LOAD_DISABLED, NULL, NULL), + createEnumConfig("loglevel", NULL, MODIFIABLE_CONFIG, loglevel_enum, server.verbosity, LL_NOTICE, NULL, NULL), + createEnumConfig("maxmemory-policy", NULL, MODIFIABLE_CONFIG, maxmemory_policy_enum, server.maxmemory_policy, MAXMEMORY_NO_EVICTION, NULL, NULL), + createEnumConfig("appendfsync", NULL, MODIFIABLE_CONFIG, aof_fsync_enum, server.aof_fsync, AOF_FSYNC_EVERYSEC, NULL, NULL), + + /* Integer configs */ + createIntConfig("databases", NULL, IMMUTABLE_CONFIG, 1, INT_MAX, server.dbnum, 16, INTEGER_CONFIG, NULL, NULL), + createIntConfig("port", NULL, IMMUTABLE_CONFIG, 0, 65535, server.port, 6379, INTEGER_CONFIG, NULL, NULL), /* TCP port. */ + createIntConfig("io-threads", NULL, IMMUTABLE_CONFIG, 1, 128, server.io_threads_num, 1, INTEGER_CONFIG, NULL, NULL), /* Single threaded by default */ + createIntConfig("auto-aof-rewrite-percentage", NULL, MODIFIABLE_CONFIG, 0, INT_MAX, server.aof_rewrite_perc, 100, INTEGER_CONFIG, NULL, NULL), + createIntConfig("cluster-replica-validity-factor", "cluster-slave-validity-factor", MODIFIABLE_CONFIG, 0, INT_MAX, server.cluster_slave_validity_factor, 10, INTEGER_CONFIG, NULL, NULL), /* Slave max data age factor. */ + createIntConfig("list-max-ziplist-size", NULL, MODIFIABLE_CONFIG, INT_MIN, INT_MAX, server.list_max_ziplist_size, -2, INTEGER_CONFIG, NULL, NULL), + createIntConfig("tcp-keepalive", NULL, MODIFIABLE_CONFIG, 0, INT_MAX, server.tcpkeepalive, 300, INTEGER_CONFIG, NULL, NULL), + createIntConfig("cluster-migration-barrier", NULL, MODIFIABLE_CONFIG, 0, INT_MAX, server.cluster_migration_barrier, 1, INTEGER_CONFIG, NULL, NULL), + createIntConfig("active-defrag-cycle-min", NULL, MODIFIABLE_CONFIG, 1, 99, server.active_defrag_cycle_min, 1, INTEGER_CONFIG, NULL, NULL), /* Default: 1% CPU min (at lower threshold) */ + createIntConfig("active-defrag-cycle-max", NULL, MODIFIABLE_CONFIG, 1, 99, server.active_defrag_cycle_max, 25, INTEGER_CONFIG, NULL, NULL), /* Default: 25% CPU max (at upper threshold) */ + createIntConfig("active-defrag-threshold-lower", NULL, MODIFIABLE_CONFIG, 0, 1000, server.active_defrag_threshold_lower, 10, INTEGER_CONFIG, NULL, NULL), /* Default: don't defrag when fragmentation is below 10% */ + createIntConfig("active-defrag-threshold-upper", NULL, MODIFIABLE_CONFIG, 0, 1000, server.active_defrag_threshold_upper, 100, INTEGER_CONFIG, NULL, NULL), /* Default: maximum defrag force at 100% fragmentation */ + createIntConfig("lfu-log-factor", NULL, MODIFIABLE_CONFIG, 0, INT_MAX, server.lfu_log_factor, 10, INTEGER_CONFIG, NULL, NULL), + createIntConfig("lfu-decay-time", NULL, MODIFIABLE_CONFIG, 0, INT_MAX, server.lfu_decay_time, 1, INTEGER_CONFIG, NULL, NULL), + createIntConfig("replica-priority", "slave-priority", MODIFIABLE_CONFIG, 0, INT_MAX, server.slave_priority, 100, INTEGER_CONFIG, NULL, NULL), + createIntConfig("repl-diskless-sync-delay", NULL, MODIFIABLE_CONFIG, 0, INT_MAX, server.repl_diskless_sync_delay, 5, INTEGER_CONFIG, NULL, NULL), + createIntConfig("maxmemory-samples", NULL, MODIFIABLE_CONFIG, 1, INT_MAX, server.maxmemory_samples, 5, INTEGER_CONFIG, NULL, NULL), + createIntConfig("timeout", NULL, MODIFIABLE_CONFIG, 0, INT_MAX, server.maxidletime, 0, INTEGER_CONFIG, NULL, NULL), /* Default client timeout: infinite */ + createIntConfig("replica-announce-port", "slave-announce-port", MODIFIABLE_CONFIG, 0, 65535, server.slave_announce_port, 0, INTEGER_CONFIG, NULL, NULL), + createIntConfig("tcp-backlog", NULL, IMMUTABLE_CONFIG, 0, INT_MAX, server.tcp_backlog, 511, INTEGER_CONFIG, NULL, NULL), /* TCP listen backlog. */ + createIntConfig("cluster-announce-bus-port", NULL, MODIFIABLE_CONFIG, 0, 65535, server.cluster_announce_bus_port, 0, INTEGER_CONFIG, NULL, NULL), /* Default: Use +10000 offset. */ + createIntConfig("cluster-announce-port", NULL, MODIFIABLE_CONFIG, 0, 65535, server.cluster_announce_port, 0, INTEGER_CONFIG, NULL, NULL), /* Use server.port */ + createIntConfig("repl-timeout", NULL, MODIFIABLE_CONFIG, 1, INT_MAX, server.repl_timeout, 60, INTEGER_CONFIG, NULL, NULL), + createIntConfig("repl-ping-replica-period", "repl-ping-slave-period", MODIFIABLE_CONFIG, 1, INT_MAX, server.repl_ping_slave_period, 10, INTEGER_CONFIG, NULL, NULL), + createIntConfig("list-compress-depth", NULL, MODIFIABLE_CONFIG, 0, INT_MAX, server.list_compress_depth, 0, INTEGER_CONFIG, NULL, NULL), + createIntConfig("rdb-key-save-delay", NULL, MODIFIABLE_CONFIG, 0, INT_MAX, server.rdb_key_save_delay, 0, INTEGER_CONFIG, NULL, NULL), + createIntConfig("key-load-delay", NULL, MODIFIABLE_CONFIG, 0, INT_MAX, server.key_load_delay, 0, INTEGER_CONFIG, NULL, NULL), + createIntConfig("active-expire-effort", NULL, MODIFIABLE_CONFIG, 1, 10, server.active_expire_effort, 1, INTEGER_CONFIG, NULL, NULL), /* From 1 to 10. */ + createIntConfig("hz", NULL, MODIFIABLE_CONFIG, 0, INT_MAX, server.config_hz, CONFIG_DEFAULT_HZ, INTEGER_CONFIG, NULL, updateHZ), + createIntConfig("min-replicas-to-write", "min-slaves-to-write", MODIFIABLE_CONFIG, 0, INT_MAX, server.repl_min_slaves_to_write, 0, INTEGER_CONFIG, NULL, updateGoodSlaves), + createIntConfig("min-replicas-max-lag", "min-slaves-max-lag", MODIFIABLE_CONFIG, 0, INT_MAX, server.repl_min_slaves_max_lag, 10, INTEGER_CONFIG, NULL, updateGoodSlaves), + + /* Unsigned int configs */ + createUIntConfig("maxclients", NULL, MODIFIABLE_CONFIG, 1, UINT_MAX, server.maxclients, 10000, INTEGER_CONFIG, NULL, updateMaxclients), + + /* Unsigned Long configs */ + createULongConfig("active-defrag-max-scan-fields", NULL, MODIFIABLE_CONFIG, 1, LONG_MAX, server.active_defrag_max_scan_fields, 1000, INTEGER_CONFIG, NULL, NULL), /* Default: keys with more than 1000 fields will be processed separately */ + createULongConfig("slowlog-max-len", NULL, MODIFIABLE_CONFIG, 0, LONG_MAX, server.slowlog_max_len, 128, INTEGER_CONFIG, NULL, NULL), + createULongConfig("acllog-max-len", NULL, MODIFIABLE_CONFIG, 0, LONG_MAX, server.acllog_max_len, 128, INTEGER_CONFIG, NULL, NULL), + + /* Long Long configs */ + createLongLongConfig("lua-time-limit", NULL, MODIFIABLE_CONFIG, 0, LONG_MAX, server.lua_time_limit, 5000, INTEGER_CONFIG, NULL, NULL),/* milliseconds */ + createLongLongConfig("cluster-node-timeout", NULL, MODIFIABLE_CONFIG, 0, LLONG_MAX, server.cluster_node_timeout, 15000, INTEGER_CONFIG, NULL, NULL), + createLongLongConfig("slowlog-log-slower-than", NULL, MODIFIABLE_CONFIG, -1, LLONG_MAX, server.slowlog_log_slower_than, 10000, INTEGER_CONFIG, NULL, NULL), + createLongLongConfig("latency-monitor-threshold", NULL, MODIFIABLE_CONFIG, 0, LLONG_MAX, server.latency_monitor_threshold, 0, INTEGER_CONFIG, NULL, NULL), + createLongLongConfig("proto-max-bulk-len", NULL, MODIFIABLE_CONFIG, 0, LLONG_MAX, server.proto_max_bulk_len, 512ll*1024*1024, MEMORY_CONFIG, NULL, NULL), /* Bulk request max size */ + createLongLongConfig("stream-node-max-entries", NULL, MODIFIABLE_CONFIG, 0, LLONG_MAX, server.stream_node_max_entries, 100, INTEGER_CONFIG, NULL, NULL), + createLongLongConfig("repl-backlog-size", NULL, MODIFIABLE_CONFIG, 1, LLONG_MAX, server.repl_backlog_size, 1024*1024, MEMORY_CONFIG, NULL, updateReplBacklogSize), /* Default: 1mb */ + + /* Unsigned Long Long configs */ + createULongLongConfig("maxmemory", NULL, MODIFIABLE_CONFIG, 0, ULLONG_MAX, server.maxmemory, 0, MEMORY_CONFIG, NULL, updateMaxmemory), + + /* Size_t configs */ + createSizeTConfig("hash-max-ziplist-entries", NULL, MODIFIABLE_CONFIG, 0, LONG_MAX, server.hash_max_ziplist_entries, 512, INTEGER_CONFIG, NULL, NULL), + createSizeTConfig("set-max-intset-entries", NULL, MODIFIABLE_CONFIG, 0, LONG_MAX, server.set_max_intset_entries, 512, INTEGER_CONFIG, NULL, NULL), + createSizeTConfig("zset-max-ziplist-entries", NULL, MODIFIABLE_CONFIG, 0, LONG_MAX, server.zset_max_ziplist_entries, 128, INTEGER_CONFIG, NULL, NULL), + createSizeTConfig("active-defrag-ignore-bytes", NULL, MODIFIABLE_CONFIG, 1, LLONG_MAX, server.active_defrag_ignore_bytes, 100<<20, MEMORY_CONFIG, NULL, NULL), /* Default: don't defrag if frag overhead is below 100mb */ + createSizeTConfig("hash-max-ziplist-value", NULL, MODIFIABLE_CONFIG, 0, LONG_MAX, server.hash_max_ziplist_value, 64, MEMORY_CONFIG, NULL, NULL), + createSizeTConfig("stream-node-max-bytes", NULL, MODIFIABLE_CONFIG, 0, LONG_MAX, server.stream_node_max_bytes, 4096, MEMORY_CONFIG, NULL, NULL), + createSizeTConfig("zset-max-ziplist-value", NULL, MODIFIABLE_CONFIG, 0, LONG_MAX, server.zset_max_ziplist_value, 64, MEMORY_CONFIG, NULL, NULL), + createSizeTConfig("hll-sparse-max-bytes", NULL, MODIFIABLE_CONFIG, 0, LONG_MAX, server.hll_sparse_max_bytes, 3000, MEMORY_CONFIG, NULL, NULL), + createSizeTConfig("tracking-table-max-keys", NULL, MODIFIABLE_CONFIG, 0, LONG_MAX, server.tracking_table_max_keys, 1000000, INTEGER_CONFIG, NULL, NULL), /* Default: 1 million keys max. */ + + /* Other configs */ + createTimeTConfig("repl-backlog-ttl", NULL, MODIFIABLE_CONFIG, 0, LONG_MAX, server.repl_backlog_time_limit, 60*60, INTEGER_CONFIG, NULL, NULL), /* Default: 1 hour */ + createOffTConfig("auto-aof-rewrite-min-size", NULL, MODIFIABLE_CONFIG, 0, LLONG_MAX, server.aof_rewrite_min_size, 64*1024*1024, MEMORY_CONFIG, NULL, NULL), + +#ifdef USE_OPENSSL + createIntConfig("tls-port", NULL, IMMUTABLE_CONFIG, 0, 65535, server.tls_port, 0, INTEGER_CONFIG, NULL, NULL), /* TCP port. */ + createBoolConfig("tls-cluster", NULL, MODIFIABLE_CONFIG, server.tls_cluster, 0, NULL, NULL), + createBoolConfig("tls-replication", NULL, MODIFIABLE_CONFIG, server.tls_replication, 0, NULL, NULL), + createBoolConfig("tls-auth-clients", NULL, MODIFIABLE_CONFIG, server.tls_auth_clients, 1, NULL, NULL), + createBoolConfig("tls-prefer-server-ciphers", NULL, MODIFIABLE_CONFIG, server.tls_ctx_config.prefer_server_ciphers, 0, NULL, updateTlsCfgBool), + createStringConfig("tls-cert-file", NULL, MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.tls_ctx_config.cert_file, NULL, NULL, updateTlsCfg), + createStringConfig("tls-key-file", NULL, MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.tls_ctx_config.key_file, NULL, NULL, updateTlsCfg), + createStringConfig("tls-dh-params-file", NULL, MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.tls_ctx_config.dh_params_file, NULL, NULL, updateTlsCfg), + createStringConfig("tls-ca-cert-file", NULL, MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.tls_ctx_config.ca_cert_file, NULL, NULL, updateTlsCfg), + createStringConfig("tls-ca-cert-dir", NULL, MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.tls_ctx_config.ca_cert_dir, NULL, NULL, updateTlsCfg), + createStringConfig("tls-protocols", NULL, MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.tls_ctx_config.protocols, NULL, NULL, updateTlsCfg), + createStringConfig("tls-ciphers", NULL, MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.tls_ctx_config.ciphers, NULL, NULL, updateTlsCfg), + createStringConfig("tls-ciphersuites", NULL, MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.tls_ctx_config.ciphersuites, NULL, NULL, updateTlsCfg), +#endif + + /* NULL Terminator */ + {NULL} +}; + /*----------------------------------------------------------------------------- * CONFIG command entry point *----------------------------------------------------------------------------*/ diff --git a/src/connection.c b/src/connection.c new file mode 100644 index 000000000..58d86c31b --- /dev/null +++ b/src/connection.c @@ -0,0 +1,407 @@ +/* + * Copyright (c) 2019, Redis Labs + * 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 "connhelpers.h" + +/* The connections module provides a lean abstraction of network connections + * to avoid direct socket and async event management across the Redis code base. + * + * It does NOT provide advanced connection features commonly found in similar + * libraries such as complete in/out buffer management, throttling, etc. These + * functions remain in networking.c. + * + * The primary goal is to allow transparent handling of TCP and TLS based + * connections. To do so, connections have the following properties: + * + * 1. A connection may live before its corresponding socket exists. This + * allows various context and configuration setting to be handled before + * establishing the actual connection. + * 2. The caller may register/unregister logical read/write handlers to be + * called when the connection has data to read from/can accept writes. + * These logical handlers may or may not correspond to actual AE events, + * depending on the implementation (for TCP they are; for TLS they aren't). + */ + +ConnectionType CT_Socket; + +/* When a connection is created we must know its type already, but the + * underlying socket may or may not exist: + * + * - For accepted connections, it exists as we do not model the listen/accept + * part; So caller calls connCreateSocket() followed by connAccept(). + * - For outgoing connections, the socket is created by the connection module + * itself; So caller calls connCreateSocket() followed by connConnect(), + * which registers a connect callback that fires on connected/error state + * (and after any transport level handshake was done). + * + * NOTE: An earlier version relied on connections being part of other structs + * and not independently allocated. This could lead to further optimizations + * like using container_of(), etc. However it was discontinued in favor of + * this approach for these reasons: + * + * 1. In some cases conns are created/handled outside the context of the + * containing struct, in which case it gets a bit awkward to copy them. + * 2. Future implementations may wish to allocate arbitrary data for the + * connection. + * 3. The container_of() approach is anyway risky because connections may + * be embedded in different structs, not just client. + */ + +connection *connCreateSocket() { + connection *conn = zcalloc(sizeof(connection)); + conn->type = &CT_Socket; + conn->fd = -1; + + return conn; +} + +/* Create a new socket-type connection that is already associated with + * an accepted connection. + * + * The socket is not read for I/O until connAccept() was called and + * invoked the connection-level accept handler. + */ +connection *connCreateAcceptedSocket(int fd) { + connection *conn = connCreateSocket(); + conn->fd = fd; + conn->state = CONN_STATE_ACCEPTING; + return conn; +} + +static int connSocketConnect(connection *conn, const char *addr, int port, const char *src_addr, + ConnectionCallbackFunc connect_handler) { + int fd = anetTcpNonBlockBestEffortBindConnect(NULL,addr,port,src_addr); + if (fd == -1) { + conn->state = CONN_STATE_ERROR; + conn->last_errno = errno; + return C_ERR; + } + + conn->fd = fd; + conn->state = CONN_STATE_CONNECTING; + + conn->conn_handler = connect_handler; + aeCreateFileEvent(server.el, conn->fd, AE_WRITABLE, + conn->type->ae_handler, conn); + + return C_OK; +} + +/* Returns true if a write handler is registered */ +int connHasWriteHandler(connection *conn) { + return conn->write_handler != NULL; +} + +/* Returns true if a read handler is registered */ +int connHasReadHandler(connection *conn) { + return conn->read_handler != NULL; +} + +/* Associate a private data pointer with the connection */ +void connSetPrivateData(connection *conn, void *data) { + conn->private_data = data; +} + +/* Get the associated private data pointer */ +void *connGetPrivateData(connection *conn) { + return conn->private_data; +} + +/* ------ Pure socket connections ------- */ + +/* A very incomplete list of implementation-specific calls. Much of the above shall + * move here as we implement additional connection types. + */ + +/* Close the connection and free resources. */ +static void connSocketClose(connection *conn) { + if (conn->fd != -1) { + aeDeleteFileEvent(server.el,conn->fd,AE_READABLE); + aeDeleteFileEvent(server.el,conn->fd,AE_WRITABLE); + close(conn->fd); + conn->fd = -1; + } + + /* If called from within a handler, schedule the close but + * keep the connection until the handler returns. + */ + if (conn->flags & CONN_FLAG_IN_HANDLER) { + conn->flags |= CONN_FLAG_CLOSE_SCHEDULED; + return; + } + + zfree(conn); +} + +static int connSocketWrite(connection *conn, const void *data, size_t data_len) { + int ret = write(conn->fd, data, data_len); + if (ret < 0 && errno != EAGAIN) { + conn->last_errno = errno; + conn->state = CONN_STATE_ERROR; + } + + return ret; +} + +static int connSocketRead(connection *conn, void *buf, size_t buf_len) { + int ret = read(conn->fd, buf, buf_len); + if (!ret) { + conn->state = CONN_STATE_CLOSED; + } else if (ret < 0 && errno != EAGAIN) { + conn->last_errno = errno; + conn->state = CONN_STATE_ERROR; + } + + return ret; +} + +static int connSocketAccept(connection *conn, ConnectionCallbackFunc accept_handler) { + if (conn->state != CONN_STATE_ACCEPTING) return C_ERR; + conn->state = CONN_STATE_CONNECTED; + if (!callHandler(conn, accept_handler)) return C_ERR; + return C_OK; +} + +/* Register a write handler, to be called when the connection is writable. + * If NULL, the existing handler is removed. + * + * The barrier flag indicates a write barrier is requested, resulting with + * CONN_FLAG_WRITE_BARRIER set. This will ensure that the write handler is + * always called before and not after the read handler in a single event + * loop. + */ +static int connSocketSetWriteHandler(connection *conn, ConnectionCallbackFunc func, int barrier) { + if (func == conn->write_handler) return C_OK; + + conn->write_handler = func; + if (barrier) + conn->flags |= CONN_FLAG_WRITE_BARRIER; + else + conn->flags &= ~CONN_FLAG_WRITE_BARRIER; + if (!conn->write_handler) + aeDeleteFileEvent(server.el,conn->fd,AE_WRITABLE); + else + if (aeCreateFileEvent(server.el,conn->fd,AE_WRITABLE, + conn->type->ae_handler,conn) == AE_ERR) return C_ERR; + return C_OK; +} + +/* Register a read handler, to be called when the connection is readable. + * If NULL, the existing handler is removed. + */ +static int connSocketSetReadHandler(connection *conn, ConnectionCallbackFunc func) { + if (func == conn->read_handler) return C_OK; + + conn->read_handler = func; + if (!conn->read_handler) + aeDeleteFileEvent(server.el,conn->fd,AE_READABLE); + else + if (aeCreateFileEvent(server.el,conn->fd, + AE_READABLE,conn->type->ae_handler,conn) == AE_ERR) return C_ERR; + return C_OK; +} + +static const char *connSocketGetLastError(connection *conn) { + return strerror(conn->last_errno); +} + +static void connSocketEventHandler(struct aeEventLoop *el, int fd, void *clientData, int mask) +{ + UNUSED(el); + UNUSED(fd); + connection *conn = clientData; + + if (conn->state == CONN_STATE_CONNECTING && + (mask & AE_WRITABLE) && conn->conn_handler) { + + if (connGetSocketError(conn)) { + conn->last_errno = errno; + conn->state = CONN_STATE_ERROR; + } else { + conn->state = CONN_STATE_CONNECTED; + } + + if (!conn->write_handler) aeDeleteFileEvent(server.el,conn->fd,AE_WRITABLE); + + if (!callHandler(conn, conn->conn_handler)) return; + conn->conn_handler = NULL; + } + + /* Normally we execute the readable event first, and the writable + * event later. This is useful as sometimes we may be able + * to serve the reply of a query immediately after processing the + * query. + * + * However if WRITE_BARRIER is set in the mask, our application is + * asking us to do the reverse: never fire the writable event + * after the readable. In such a case, we invert the calls. + * This is useful when, for instance, we want to do things + * in the beforeSleep() hook, like fsync'ing a file to disk, + * before replying to a client. */ + int invert = conn->flags & CONN_FLAG_WRITE_BARRIER; + + int call_write = (mask & AE_WRITABLE) && conn->write_handler; + int call_read = (mask & AE_READABLE) && conn->read_handler; + + /* Handle normal I/O flows */ + if (!invert && call_read) { + if (!callHandler(conn, conn->read_handler)) return; + } + /* Fire the writable event. */ + if (call_write) { + if (!callHandler(conn, conn->write_handler)) return; + } + /* If we have to invert the call, fire the readable event now + * after the writable one. */ + if (invert && call_read) { + if (!callHandler(conn, conn->read_handler)) return; + } +} + +static int connSocketBlockingConnect(connection *conn, const char *addr, int port, long long timeout) { + int fd = anetTcpNonBlockConnect(NULL,addr,port); + if (fd == -1) { + conn->state = CONN_STATE_ERROR; + conn->last_errno = errno; + return C_ERR; + } + + if ((aeWait(fd, AE_WRITABLE, timeout) & AE_WRITABLE) == 0) { + conn->state = CONN_STATE_ERROR; + conn->last_errno = ETIMEDOUT; + } + + conn->fd = fd; + conn->state = CONN_STATE_CONNECTED; + return C_OK; +} + +/* Connection-based versions of syncio.c functions. + * NOTE: This should ideally be refactored out in favor of pure async work. + */ + +static ssize_t connSocketSyncWrite(connection *conn, char *ptr, ssize_t size, long long timeout) { + return syncWrite(conn->fd, ptr, size, timeout); +} + +static ssize_t connSocketSyncRead(connection *conn, char *ptr, ssize_t size, long long timeout) { + return syncRead(conn->fd, ptr, size, timeout); +} + +static ssize_t connSocketSyncReadLine(connection *conn, char *ptr, ssize_t size, long long timeout) { + return syncReadLine(conn->fd, ptr, size, timeout); +} + + +ConnectionType CT_Socket = { + .ae_handler = connSocketEventHandler, + .close = connSocketClose, + .write = connSocketWrite, + .read = connSocketRead, + .accept = connSocketAccept, + .connect = connSocketConnect, + .set_write_handler = connSocketSetWriteHandler, + .set_read_handler = connSocketSetReadHandler, + .get_last_error = connSocketGetLastError, + .blocking_connect = connSocketBlockingConnect, + .sync_write = connSocketSyncWrite, + .sync_read = connSocketSyncRead, + .sync_readline = connSocketSyncReadLine +}; + + +int connGetSocketError(connection *conn) { + int sockerr = 0; + socklen_t errlen = sizeof(sockerr); + + if (getsockopt(conn->fd, SOL_SOCKET, SO_ERROR, &sockerr, &errlen) == -1) + sockerr = errno; + return sockerr; +} + +int connPeerToString(connection *conn, char *ip, size_t ip_len, int *port) { + return anetPeerToString(conn ? conn->fd : -1, ip, ip_len, port); +} + +int connFormatPeer(connection *conn, char *buf, size_t buf_len) { + return anetFormatPeer(conn ? conn->fd : -1, buf, buf_len); +} + +int connSockName(connection *conn, char *ip, size_t ip_len, int *port) { + return anetSockName(conn->fd, ip, ip_len, port); +} + +int connBlock(connection *conn) { + if (conn->fd == -1) return C_ERR; + return anetBlock(NULL, conn->fd); +} + +int connNonBlock(connection *conn) { + if (conn->fd == -1) return C_ERR; + return anetNonBlock(NULL, conn->fd); +} + +int connEnableTcpNoDelay(connection *conn) { + if (conn->fd == -1) return C_ERR; + return anetEnableTcpNoDelay(NULL, conn->fd); +} + +int connDisableTcpNoDelay(connection *conn) { + if (conn->fd == -1) return C_ERR; + return anetDisableTcpNoDelay(NULL, conn->fd); +} + +int connKeepAlive(connection *conn, int interval) { + if (conn->fd == -1) return C_ERR; + return anetKeepAlive(NULL, conn->fd, interval); +} + +int connSendTimeout(connection *conn, long long ms) { + return anetSendTimeout(NULL, conn->fd, ms); +} + +int connRecvTimeout(connection *conn, long long ms) { + return anetRecvTimeout(NULL, conn->fd, ms); +} + +int connGetState(connection *conn) { + return conn->state; +} + +/* Return a text that describes the connection, suitable for inclusion + * in CLIENT LIST and similar outputs. + * + * For sockets, we always return "fd=" to maintain compatibility. + */ +const char *connGetInfo(connection *conn, char *buf, size_t buf_len) { + snprintf(buf, buf_len-1, "fd=%i", conn->fd); + return buf; +} + diff --git a/src/connection.h b/src/connection.h new file mode 100644 index 000000000..97622f8d6 --- /dev/null +++ b/src/connection.h @@ -0,0 +1,220 @@ + +/* + * Copyright (c) 2019, Redis Labs + * 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 __REDIS_CONNECTION_H +#define __REDIS_CONNECTION_H + +#define CONN_INFO_LEN 32 + +struct aeEventLoop; +typedef struct connection connection; + +typedef enum { + CONN_STATE_NONE = 0, + CONN_STATE_CONNECTING, + CONN_STATE_ACCEPTING, + CONN_STATE_CONNECTED, + CONN_STATE_CLOSED, + CONN_STATE_ERROR +} ConnectionState; + +#define CONN_FLAG_IN_HANDLER (1<<0) /* A handler execution is in progress */ +#define CONN_FLAG_CLOSE_SCHEDULED (1<<1) /* Closed scheduled by a handler */ +#define CONN_FLAG_WRITE_BARRIER (1<<2) /* Write barrier requested */ + +typedef void (*ConnectionCallbackFunc)(struct connection *conn); + +typedef struct ConnectionType { + void (*ae_handler)(struct aeEventLoop *el, int fd, void *clientData, int mask); + int (*connect)(struct connection *conn, const char *addr, int port, const char *source_addr, ConnectionCallbackFunc connect_handler); + int (*write)(struct connection *conn, const void *data, size_t data_len); + int (*read)(struct connection *conn, void *buf, size_t buf_len); + void (*close)(struct connection *conn); + int (*accept)(struct connection *conn, ConnectionCallbackFunc accept_handler); + int (*set_write_handler)(struct connection *conn, ConnectionCallbackFunc handler, int barrier); + int (*set_read_handler)(struct connection *conn, ConnectionCallbackFunc handler); + const char *(*get_last_error)(struct connection *conn); + int (*blocking_connect)(struct connection *conn, const char *addr, int port, long long timeout); + ssize_t (*sync_write)(struct connection *conn, char *ptr, ssize_t size, long long timeout); + ssize_t (*sync_read)(struct connection *conn, char *ptr, ssize_t size, long long timeout); + ssize_t (*sync_readline)(struct connection *conn, char *ptr, ssize_t size, long long timeout); +} ConnectionType; + +struct connection { + ConnectionType *type; + ConnectionState state; + int flags; + int last_errno; + void *private_data; + ConnectionCallbackFunc conn_handler; + ConnectionCallbackFunc write_handler; + ConnectionCallbackFunc read_handler; + int fd; +}; + +/* The connection module does not deal with listening and accepting sockets, + * so we assume we have a socket when an incoming connection is created. + * + * The fd supplied should therefore be associated with an already accept()ed + * socket. + * + * connAccept() may directly call accept_handler(), or return and call it + * at a later time. This behavior is a bit awkward but aims to reduce the need + * to wait for the next event loop, if no additional handshake is required. + */ + +static inline int connAccept(connection *conn, ConnectionCallbackFunc accept_handler) { + return conn->type->accept(conn, accept_handler); +} + +/* Establish a connection. The connect_handler will be called when the connection + * is established, or if an error has occured. + * + * The connection handler will be responsible to set up any read/write handlers + * as needed. + * + * If C_ERR is returned, the operation failed and the connection handler shall + * not be expected. + */ +static inline int connConnect(connection *conn, const char *addr, int port, const char *src_addr, + ConnectionCallbackFunc connect_handler) { + return conn->type->connect(conn, addr, port, src_addr, connect_handler); +} + +/* Blocking connect. + * + * NOTE: This is implemented in order to simplify the transition to the abstract + * connections, but should probably be refactored out of cluster.c and replication.c, + * in favor of a pure async implementation. + */ +static inline int connBlockingConnect(connection *conn, const char *addr, int port, long long timeout) { + return conn->type->blocking_connect(conn, addr, port, timeout); +} + +/* Write to connection, behaves the same as write(2). + * + * Like write(2), a short write is possible. A -1 return indicates an error. + * + * The caller should NOT rely on errno. Testing for an EAGAIN-like condition, use + * connGetState() to see if the connection state is still CONN_STATE_CONNECTED. + */ +static inline int connWrite(connection *conn, const void *data, size_t data_len) { + return conn->type->write(conn, data, data_len); +} + +/* Read from the connection, behaves the same as read(2). + * + * Like read(2), a short read is possible. A return value of 0 will indicate the + * connection was closed, and -1 will indicate an error. + * + * The caller should NOT rely on errno. Testing for an EAGAIN-like condition, use + * connGetState() to see if the connection state is still CONN_STATE_CONNECTED. + */ +static inline int connRead(connection *conn, void *buf, size_t buf_len) { + return conn->type->read(conn, buf, buf_len); +} + +/* Register a write handler, to be called when the connection is writable. + * If NULL, the existing handler is removed. + */ +static inline int connSetWriteHandler(connection *conn, ConnectionCallbackFunc func) { + return conn->type->set_write_handler(conn, func, 0); +} + +/* Register a read handler, to be called when the connection is readable. + * If NULL, the existing handler is removed. + */ +static inline int connSetReadHandler(connection *conn, ConnectionCallbackFunc func) { + return conn->type->set_read_handler(conn, func); +} + +/* Set a write handler, and possibly enable a write barrier, this flag is + * cleared when write handler is changed or removed. + * With barroer enabled, we never fire the event if the read handler already + * fired in the same event loop iteration. Useful when you want to persist + * things to disk before sending replies, and want to do that in a group fashion. */ +static inline int connSetWriteHandlerWithBarrier(connection *conn, ConnectionCallbackFunc func, int barrier) { + return conn->type->set_write_handler(conn, func, barrier); +} + +static inline void connClose(connection *conn) { + conn->type->close(conn); +} + +/* Returns the last error encountered by the connection, as a string. If no error, + * a NULL is returned. + */ +static inline const char *connGetLastError(connection *conn) { + return conn->type->get_last_error(conn); +} + +static inline ssize_t connSyncWrite(connection *conn, char *ptr, ssize_t size, long long timeout) { + return conn->type->sync_write(conn, ptr, size, timeout); +} + +static inline ssize_t connSyncRead(connection *conn, char *ptr, ssize_t size, long long timeout) { + return conn->type->sync_read(conn, ptr, size, timeout); +} + +static inline ssize_t connSyncReadLine(connection *conn, char *ptr, ssize_t size, long long timeout) { + return conn->type->sync_readline(conn, ptr, size, timeout); +} + +connection *connCreateSocket(); +connection *connCreateAcceptedSocket(int fd); + +connection *connCreateTLS(); +connection *connCreateAcceptedTLS(int fd, int require_auth); + +void connSetPrivateData(connection *conn, void *data); +void *connGetPrivateData(connection *conn); +int connGetState(connection *conn); +int connHasWriteHandler(connection *conn); +int connHasReadHandler(connection *conn); +int connGetSocketError(connection *conn); + +/* anet-style wrappers to conns */ +int connBlock(connection *conn); +int connNonBlock(connection *conn); +int connEnableTcpNoDelay(connection *conn); +int connDisableTcpNoDelay(connection *conn); +int connKeepAlive(connection *conn, int interval); +int connSendTimeout(connection *conn, long long ms); +int connRecvTimeout(connection *conn, long long ms); +int connPeerToString(connection *conn, char *ip, size_t ip_len, int *port); +int connFormatPeer(connection *conn, char *buf, size_t buf_len); +int connSockName(connection *conn, char *ip, size_t ip_len, int *port); +const char *connGetInfo(connection *conn, char *buf, size_t buf_len); + +/* Helpers for tls special considerations */ +int tlsHasPendingData(); +void tlsProcessPendingData(); + +#endif /* __REDIS_CONNECTION_H */ diff --git a/src/connhelpers.h b/src/connhelpers.h new file mode 100644 index 000000000..f237c9b1d --- /dev/null +++ b/src/connhelpers.h @@ -0,0 +1,85 @@ + +/* + * Copyright (c) 2019, Redis Labs + * 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 __REDIS_CONNHELPERS_H +#define __REDIS_CONNHELPERS_H + +#include "connection.h" + +/* These are helper functions that are common to different connection + * implementations (currently sockets in connection.c and TLS in tls.c). + * + * Currently helpers implement the mechanisms for invoking connection + * handlers, tracking in-handler states and dealing with deferred + * destruction (if invoked by a handler). + */ + +/* Called whenever a handler is invoked on a connection and sets the + * CONN_FLAG_IN_HANDLER flag to indicate we're in a handler context. + * + * An attempt to close a connection while CONN_FLAG_IN_HANDLER is + * set will result with deferred close, i.e. setting the CONN_FLAG_CLOSE_SCHEDULED + * instead of destructing it. + */ +static inline void enterHandler(connection *conn) { + conn->flags |= CONN_FLAG_IN_HANDLER; +} + +/* Called whenever a handler returns. This unsets the CONN_FLAG_IN_HANDLER + * flag and performs actual close/destruction if a deferred close was + * scheduled by the handler. + */ +static inline int exitHandler(connection *conn) { + conn->flags &= ~CONN_FLAG_IN_HANDLER; + if (conn->flags & CONN_FLAG_CLOSE_SCHEDULED) { + connClose(conn); + return 0; + } + return 1; +} + +/* Helper for connection implementations to call handlers: + * 1. Mark the handler in use. + * 2. Execute the handler (if set). + * 3. Mark the handler as NOT in use and perform deferred close if was + * requested by the handler at any time. + */ +static inline int callHandler(connection *conn, ConnectionCallbackFunc handler) { + conn->flags |= CONN_FLAG_IN_HANDLER; + if (handler) handler(conn); + conn->flags &= ~CONN_FLAG_IN_HANDLER; + if (conn->flags & CONN_FLAG_CLOSE_SCHEDULED) { + connClose(conn); + return 0; + } + return 1; +} + +#endif /* __REDIS_CONNHELPERS_H */ diff --git a/src/crc16_slottable.h b/src/crc16_slottable.h new file mode 100644 index 000000000..652aea9e1 --- /dev/null +++ b/src/crc16_slottable.h @@ -0,0 +1,835 @@ +#ifndef _CRC16_TABLE_H__ +#define _CRC16_TABLE_H__ + +/* A table of the shortest possible alphanumeric string that is mapped by redis' crc16 + * to any given redis cluster slot. + * + * The array indexes are slot numbers, so that given a desired slot, this string is guaranteed + * to make redis cluster route a request to the shard holding this slot + */ + +const char *crc16_slot_table[] = { +"06S", "Qi", "5L5", "4Iu", "4gY", "460", "1Y7", "1LV", "0QG", "ru", "7Ok", "4ji", "4DE", "65n", "2JH", "I8", "F9", "SX", "7nF", "4KD", +"4eh", "6PK", "2ke", "1Ng", "0Sv", "4L", "491", "4hX", "4Ft", "5C4", "2Hy", "09R", "021", "0cX", "4Xv", "6mU", "6Cy", "42R", "0Mt", "nF", +"cv", "1Pe", "5kK", "6NI", "74L", "4UF", "0nh", "MZ", "2TJ", "0ai", "4ZG", "6od", "6AH", "40c", "0OE", "lw", "aG", "0Bu", "5iz", "6Lx", +"5R7", "4Ww", "0lY", "Ok", "5n3", "4ks", "8YE", "7g", "2KR", "1nP", "714", "64t", "69D", "4Ho", "07I", "Ps", "2hN", "1ML", "4fC", "7CA", +"avs", "4iB", "0Rl", "5V", "2Ic", "08H", "4Gn", "66E", "aUo", "b4e", "05x", "RB", "8f", "8VD", "4dr", "5a2", "4zp", "6OS", "bl", "355", +"0or", "1j2", "75V", "bno", "4Yl", "6lO", "Ap", "0bB", "0Ln", "2yM", "6Bc", "43H", "4xA", "6Mb", "22D", "14", "0mC", "Nq", "6cN", "4Vm", +"ban", "aDl", "CA", "14Z", "8GG", "mm", "549", "41y", "53t", "464", "1Y3", "1LR", "06W", "Qm", "5L1", "4Iq", "4DA", "65j", "2JL", "1oN", +"0QC", "6y", "7Oo", "4jm", "4el", "6PO", "9x", "1Nc", "04f", "2EM", "7nB", "bqs", "4Fp", "5C0", "d6F", "09V", "0Sr", "4H", "495", "bRo", +"aio", "42V", "0Mp", "nB", "025", "17u", "4Xr", "6mQ", "74H", "4UB", "0nl", "3Kn", "cr", "1Pa", "5kO", "6NM", "6AL", "40g", "0OA", "ls", +"2TN", "0am", "4ZC", "aEr", "5R3", "4Ws", "18t", "Oo", "aC", "0Bq", "bCl", "afn", "2KV", "1nT", "5Uz", "64p", "5n7", "4kw", "0PY", "7c", +"2hJ", "1MH", "4fG", "6Sd", "7mi", "4Hk", "07M", "Pw", "2Ig", "08L", "4Gj", "66A", "7LD", "4iF", "0Rh", "5R", "8b", "1Oy", "4dv", "5a6", +"7oX", "4JZ", "0qt", "RF", "0ov", "LD", "4A9", "4TX", "4zt", "6OW", "bh", "0AZ", "z9", "oX", "6Bg", "43L", "4Yh", "6lK", "At", "0bF", +"0mG", "Nu", "6cJ", "4Vi", "4xE", "6Mf", "2vH", "10", "8GC", "mi", "5p5", "4uu", "5Kx", "4N8", "CE", "1pV", "0QO", "6u", "7Oc", "4ja", +"4DM", "65f", "3Za", "I0", "0rS", "Qa", "68V", "b7F", "4gQ", "468", "dSo", "285", "274", "4D", "499", "4hP", "b8G", "67W", "0h3", "09Z", +"F1", "SP", "7nN", "4KL", "51I", "6PC", "9t", "1No", "21g", "1Pm", "5kC", "6NA", "74D", "4UN", "X3", "MR", "029", "0cP", "bbM", "79t", +"4c3", "42Z", "8Dd", "nN", "aO", "8Ke", "4yS", "4l2", "76u", "635", "0lQ", "Oc", "BS", "W2", "4ZO", "6ol", "7Qa", "40k", "0OM", "2zn", +"69L", "4Hg", "07A", "2Fj", "2hF", "k6", "4fK", "6Sh", "7Ny", "6K9", "0PU", "7o", "2KZ", "1nX", "4EW", "4P6", "7oT", "4JV", "05p", "RJ", +"8n", "1Ou", "4dz", "6QY", "7LH", "4iJ", "d7", "qV", "2Ik", "1li", "4Gf", "66M", "4Yd", "6lG", "Ax", "0bJ", "z5", "oT", "6Bk", "4wH", +"4zx", "aeI", "bd", "0AV", "0oz", "LH", "4A5", "4TT", "5Kt", "4N4", "CI", "14R", "0NW", "me", "541", "41q", "4xI", "6Mj", "22L", "u4", +"0mK", "Ny", "6cF", "4Ve", "4DI", "65b", "2JD", "I4", "0QK", "6q", "7Og", "4je", "4gU", "4r4", "2iX", "1LZ", "0rW", "Qe", "5L9", "4Iy", +"4Fx", "5C8", "0h7", "1mw", "0Sz", "pH", "7MV", "4hT", "4ed", "6PG", "9p", "1Nk", "F5", "ST", "7nJ", "4KH", "7pH", "4UJ", "X7", "MV", +"cz", "1Pi", "5kG", "6NE", "4c7", "4vV", "0Mx", "nJ", "0v5", "0cT", "4Xz", "6mY", "6bX", "5GZ", "0lU", "Og", "aK", "0By", "4yW", "4l6", +"6AD", "40o", "0OI", "2zj", "BW", "W6", "4ZK", "6oh", "2hB", "k2", "4fO", "6Sl", "69H", "4Hc", "07E", "2Fn", "d5e", "83m", "4ES", "4P2", +"a0F", "bQL", "0PQ", "7k", "8j", "1Oq", "50W", "hbv", "7oP", "4JR", "05t", "RN", "2Io", "08D", "4Gb", "66I", "7LL", "4iN", "d3", "5Z", +"z1", "oP", "6Bo", "43D", "5IA", "6lC", "2Wm", "0bN", "8ff", "LL", "4A1", "4TP", "cPn", "aeM", "0T3", "0AR", "0NS", "ma", "545", "41u", +"5Kp", "4N0", "CM", "14V", "0mO", "2Xl", "6cB", "4Va", "4xM", "6Mn", "22H", "18", "04s", "SI", "7nW", "4KU", "4ey", "6PZ", "9m", "1Nv", +"e4", "pU", "7MK", "4hI", "4Fe", "67N", "2Hh", "09C", "06B", "Qx", "68O", "4Id", "4gH", "6Rk", "2iE", "j5", "0QV", "6l", "5o8", "4jx", +"4DT", "4Q5", "2JY", "82j", "BJ", "0ax", "4ZV", "4O7", "552", "40r", "0OT", "lf", "aV", "t7", "4yJ", "6Li", "6bE", "4Wf", "0lH", "Oz", +"2Vj", "0cI", "4Xg", "6mD", "6Ch", "42C", "0Me", "nW", "cg", "1Pt", "5kZ", "6NX", "7pU", "4UW", "0ny", "MK", "7LQ", "4iS", "267", "5G", +"0i0", "08Y", "b9D", "66T", "7oM", "4JO", "G2", "RS", "8w", "1Ol", "4dc", "7Aa", "atS", "4kb", "0PL", "7v", "2KC", "H3", "4EN", "64e", +"69U", "b6E", "07X", "Pb", "dRl", "296", "4fR", "4s3", "4xP", "4m1", "22U", "8Jf", "0mR", "0x3", "77v", "626", "5Km", "6no", "CP", "V1", +"0NN", "3kL", "7Pb", "41h", "4za", "6OB", "20d", "0AO", "Y0", "LQ", "6an", "4TM", "bcN", "78w", "Aa", "0bS", "8Eg", "oM", "4b0", "43Y", +"51T", "azL", "9i", "1Nr", "04w", "SM", "7nS", "4KQ", "4Fa", "67J", "2Hl", "09G", "e0", "4Y", "7MO", "4hM", "4gL", "6Ro", "2iA", "j1", +"06F", "2Gm", "68K", "5YA", "4DP", "4Q1", "d4f", "82n", "0QR", "6h", "a1E", "bPO", "556", "40v", "0OP", "lb", "BN", "15U", "4ZR", "4O3", +"6bA", "4Wb", "0lL", "2Yo", "aR", "t3", "4yN", "6Lm", "6Cl", "42G", "0Ma", "nS", "2Vn", "0cM", "4Xc", "79i", "74Y", "4US", "8ge", "MO", +"cc", "1Pp", "bAL", "adN", "0i4", "1lt", "5WZ", "66P", "7LU", "4iW", "0Ry", "5C", "8s", "1Oh", "4dg", "6QD", "7oI", "4JK", "G6", "RW", +"2KG", "H7", "4EJ", "64a", "7Nd", "4kf", "0PH", "7r", "1X8", "1MY", "4fV", "4s7", "69Q", "4Hz", "0sT", "Pf", "0mV", "Nd", "5S8", "4Vx", +"4xT", "4m5", "22Q", "0Cz", "0NJ", "mx", "7Pf", "41l", "5Ki", "6nk", "CT", "V5", "Y4", "LU", "6aj", "4TI", "4ze", "6OF", "by", "0AK", +"2l9", "oI", "4b4", "4wU", "4Yy", "6lZ", "Ae", "0bW", "0So", "4U", "7MC", "4hA", "4Fm", "67F", "3XA", "09K", "0ps", "SA", "aTl", "b5f", +"4eq", "6PR", "9e", "8WG", "8XF", "6d", "5o0", "4jp", "707", "65w", "1z2", "1oS", "06J", "Qp", "68G", "4Il", "53i", "6Rc", "2iM", "1LO", +"23G", "07", "4yB", "6La", "6bM", "4Wn", "18i", "Or", "BB", "0ap", "c4D", "aEo", "5q2", "40z", "8FD", "ln", "co", "346", "5kR", "6NP", +"74U", "bol", "0nq", "MC", "2Vb", "0cA", "4Xo", "6mL", "7SA", "42K", "0Mm", "2xN", "7oE", "4JG", "05a", "2DJ", "2jf", "1Od", "4dk", "6QH", +"482", "5yz", "0Ru", "5O", "0i8", "08Q", "4Gw", "5B7", "5M6", "4Hv", "07P", "Pj", "1X4", "1MU", "4fZ", "473", "7Nh", "4kj", "0PD", "sv", +"2KK", "1nI", "4EF", "64m", "5Ke", "6ng", "CX", "V9", "0NF", "mt", "7Pj", "4uh", "4xX", "4m9", "1F6", "0Cv", "0mZ", "Nh", "5S4", "4Vt", +"4Yu", "6lV", "Ai", "16r", "0Lw", "oE", "4b8", "43Q", "4zi", "6OJ", "bu", "0AG", "Y8", "LY", "6af", "4TE", "4Fi", "67B", "2Hd", "09O", +"e8", "4Q", "7MG", "4hE", "4eu", "6PV", "9a", "1Nz", "0pw", "SE", "aTh", "4KY", "4DX", "4Q9", "1z6", "1oW", "0QZ", "rh", "5o4", "4jt", +"4gD", "6Rg", "2iI", "j9", "06N", "Qt", "68C", "4Ih", "6bI", "4Wj", "0lD", "Ov", "aZ", "03", "4yF", "6Le", "5q6", "4tv", "0OX", "lj", +"BF", "0at", "4ZZ", "6oy", "74Q", "5Ez", "0nu", "MG", "ck", "1Px", "5kV", "6NT", "6Cd", "42O", "0Mi", "2xJ", "2Vf", "0cE", "4Xk", "6mH", +"2jb", "8VY", "4do", "6QL", "7oA", "4JC", "05e", "2DN", "d7E", "08U", "4Gs", "5B3", "486", "bSl", "0Rq", "5K", "1X0", "1MQ", "52w", "477", +"5M2", "4Hr", "07T", "Pn", "2KO", "1nM", "4EB", "64i", "7Nl", "4kn", "8YX", "7z", "0NB", "mp", "7Pn", "41d", "5Ka", "6nc", "2UM", "14G", +"19w", "Nl", "5S0", "4Vp", "bBo", "agm", "1F2", "0Cr", "0Ls", "oA", "ahl", "43U", "4Yq", "6lR", "Am", "16v", "0oo", "2ZL", "6ab", "4TA", +"4zm", "6ON", "bq", "0AC", "2VY", "0cz", "4XT", "4M5", "570", "42p", "0MV", "nd", "cT", "v5", "5ki", "6Nk", "74n", "4Ud", "0nJ", "Mx", +"By", "0aK", "4Ze", "6oF", "6Aj", "40A", "y4", "lU", "ae", "0BW", "4yy", "581", "4B4", "4WU", "18R", "OI", "06q", "QK", "7lU", "4IW", +"53R", "6RX", "0I4", "1Lt", "g6", "rW", "7OI", "4jK", "4Dg", "65L", "2Jj", "1oh", "0pH", "Sz", "7nd", "4Kf", "4eJ", "6Pi", "2kG", "h7", +"0ST", "4n", "7Mx", "4hz", "4FV", "4S7", "1x8", "09p", "4zR", "4o3", "bN", "8Hd", "0oP", "Lb", "75t", "604", "4YN", "6lm", "AR", "T3", +"0LL", "2yo", "6BA", "43j", "4xc", "agR", "22f", "0CM", "0ma", "NS", "6cl", "4VO", "baL", "aDN", "Cc", "14x", "8Ge", "mO", "7PQ", "4uS", +"7NS", "4kQ", "245", "7E", "0k2", "1nr", "coo", "64V", "69f", "4HM", "E0", "PQ", "2hl", "1Mn", "4fa", "6SB", "7Lb", "5yA", "0RN", "5t", +"2IA", "J1", "4GL", "66g", "aUM", "b4G", "05Z", "0d3", "8D", "8Vf", "4dP", "459", "574", "42t", "0MR", "0X3", "dln", "17W", "4XP", "4M1", +"74j", "5EA", "0nN", "3KL", "cP", "29", "5km", "6No", "6An", "40E", "y0", "lQ", "2Tl", "0aO", "4Za", "6oB", "4B0", "4WQ", "18V", "OM", +"aa", "0BS", "bCN", "585", "53V", "axN", "0I0", "1Lp", "06u", "QO", "68x", "4IS", "4Dc", "65H", "2Jn", "1ol", "g2", "rS", "7OM", "4jO", +"4eN", "6Pm", "9Z", "h3", "04D", "2Eo", "aTS", "4Kb", "4FR", "4S3", "d6d", "09t", "0SP", "4j", "a3G", "bRM", "0oT", "Lf", "6aY", "4Tz", +"4zV", "4o7", "bJ", "0Ax", "0LH", "oz", "6BE", "43n", "4YJ", "6li", "AV", "T7", "0me", "NW", "6ch", "4VK", "4xg", "6MD", "22b", "0CI", +"0Ny", "mK", "7PU", "4uW", "5KZ", "6nX", "Cg", "1pt", "0k6", "1nv", "4Ey", "64R", "7NW", "4kU", "241", "7A", "2hh", "1Mj", "4fe", "6SF", +"69b", "4HI", "E4", "PU", "2IE", "J5", "4GH", "66c", "7Lf", "4id", "0RJ", "5p", "2jY", "8Vb", "4dT", "4q5", "5O8", "4Jx", "0qV", "Rd", +"21E", "25", "5ka", "6Nc", "74f", "4Ul", "0nB", "Mp", "1f2", "0cr", "bbo", "79V", "578", "42x", "395", "nl", "am", "364", "4yq", "589", +"76W", "bmn", "0ls", "OA", "Bq", "0aC", "4Zm", "6oN", "6Ab", "40I", "0Oo", "2zL", "0Qm", "6W", "7OA", "4jC", "4Do", "65D", "2Jb", "82Q", +"06y", "QC", "68t", "b7d", "4gs", "5b3", "dSM", "8UE", "8ZD", "4f", "5m2", "4hr", "725", "67u", "1x0", "09x", "04H", "Sr", "7nl", "4Kn", +"4eB", "6Pa", "9V", "1NM", "4YF", "6le", "AZ", "0bh", "0LD", "ov", "6BI", "43b", "4zZ", "6Oy", "bF", "0At", "0oX", "Lj", "5Q6", "4Tv", +"5KV", "6nT", "Ck", "14p", "0Nu", "mG", "7PY", "41S", "4xk", "6MH", "22n", "0CE", "0mi", "2XJ", "6cd", "4VG", "69n", "4HE", "E8", "PY", +"2hd", "1Mf", "4fi", "6SJ", "ath", "4kY", "0Pw", "7M", "2Kx", "1nz", "4Eu", "6pV", "5O4", "4Jt", "05R", "Rh", "8L", "1OW", "4dX", "451", +"7Lj", "4ih", "0RF", "qt", "2II", "J9", "4GD", "66o", "74b", "4Uh", "0nF", "Mt", "cX", "21", "5ke", "6Ng", "5s4", "4vt", "0MZ", "nh", +"1f6", "0cv", "4XX", "4M9", "4B8", "4WY", "0lw", "OE", "ai", "1Rz", "4yu", "6LV", "6Af", "40M", "y8", "lY", "Bu", "0aG", "4Zi", "6oJ", +"4Dk", "6qH", "2Jf", "1od", "0Qi", "6S", "7OE", "4jG", "4gw", "5b7", "0I8", "1Lx", "0ru", "QG", "68p", "5Yz", "4FZ", "67q", "1x4", "1mU", +"0SX", "4b", "5m6", "4hv", "4eF", "6Pe", "9R", "1NI", "04L", "Sv", "7nh", "4Kj", "8EX", "or", "6BM", "43f", "4YB", "6la", "2WO", "0bl", +"8fD", "Ln", "5Q2", "4Tr", "cPL", "aeo", "bB", "0Ap", "0Nq", "mC", "ajn", "41W", "5KR", "6nP", "Co", "14t", "0mm", "2XN", "77I", "4VC", +"4xo", "6ML", "22j", "0CA", "3xA", "1Mb", "4fm", "6SN", "69j", "4HA", "07g", "2FL", "d5G", "83O", "4Eq", "64Z", "a0d", "bQn", "0Ps", "7I", +"8H", "1OS", "50u", "455", "5O0", "4Jp", "05V", "Rl", "2IM", "08f", "5Wa", "66k", "7Ln", "4il", "0RB", "5x", "Bh", "0aZ", "4Zt", "6oW", +"4a9", "40P", "0Ov", "lD", "at", "0BF", "4yh", "6LK", "6bg", "4WD", "Z9", "OX", "2VH", "U8", "4XE", "6mf", "6CJ", "42a", "0MG", "nu", +"cE", "1PV", "5kx", "4n8", "5P5", "4Uu", "8gC", "Mi", "04Q", "Sk", "5N7", "4Kw", "51r", "442", "9O", "1NT", "0SE", "pw", "7Mi", "4hk", +"4FG", "67l", "2HJ", "09a", "3", "QZ", "68m", "4IF", "4gj", "6RI", "2ig", "1Le", "0Qt", "6N", "7OX", "4jZ", "4Dv", "5A6", "0j9", "1oy", +"4xr", "6MQ", "22w", "377", "0mp", "NB", "77T", "blm", "5KO", "6nM", "Cr", "14i", "0Nl", "3kn", "ajs", "41J", "4zC", "aer", "20F", "36", +"0oA", "Ls", "6aL", "4To", "bcl", "78U", "AC", "0bq", "386", "oo", "5r3", "4ws", "5l1", "4iq", "9Kf", "5e", "1y3", "1lR", "736", "66v", +"7oo", "4Jm", "05K", "Rq", "8U", "1ON", "4dA", "6Qb", "7NB", "bQs", "0Pn", "7T", "2Ka", "1nc", "4El", "64G", "69w", "b6g", "07z", "1v2", +"dRN", "8TF", "4fp", "5c0", "akm", "40T", "0Or", "1J2", "Bl", "15w", "4Zp", "6oS", "6bc", "5Ga", "0ln", "2YM", "ap", "0BB", "4yl", "6LO", +"6CN", "42e", "0MC", "nq", "2VL", "0co", "4XA", "6mb", "5P1", "4Uq", "8gG", "Mm", "cA", "1PR", "bAn", "adl", "51v", "446", "9K", "1NP", +"04U", "So", "5N3", "4Ks", "4FC", "67h", "2HN", "09e", "0SA", "ps", "7Mm", "4ho", "4gn", "6RM", "2ic", "1La", "7", "2GO", "68i", "4IB", +"4Dr", "5A2", "d4D", "82L", "0Qp", "6J", "a1g", "bPm", "0mt", "NF", "6cy", "4VZ", "4xv", "6MU", "0V9", "0CX", "0Nh", "mZ", "7PD", "41N", +"5KK", "6nI", "Cv", "14m", "0oE", "Lw", "6aH", "4Tk", "4zG", "6Od", "20B", "32", "0LY", "ok", "5r7", "4ww", "5Iz", "6lx", "AG", "0bu", +"1y7", "1lV", "4GY", "4R8", "5l5", "4iu", "1Bz", "5a", "8Q", "i8", "4dE", "6Qf", "7ok", "4Ji", "05O", "Ru", "2Ke", "1ng", "4Eh", "64C", +"7NF", "4kD", "f9", "7P", "2hy", "3m9", "4ft", "5c4", "69s", "4HX", "0sv", "PD", "23e", "0BN", "5iA", "6LC", "6bo", "4WL", "Z1", "OP", +"0t3", "0aR", "c4f", "aEM", "4a1", "40X", "8Ff", "lL", "cM", "8Ig", "5kp", "4n0", "74w", "617", "0nS", "Ma", "3Fa", "U0", "4XM", "6mn", +"6CB", "42i", "0MO", "2xl", "0SM", "4w", "7Ma", "4hc", "4FO", "67d", "2HB", "K2", "04Y", "Sc", "aTN", "b5D", "4eS", "4p2", "9G", "8We", +"256", "6F", "7OP", "4jR", "cnl", "65U", "0j1", "1oq", "D3", "QR", "68e", "4IN", "4gb", "6RA", "2io", "1Lm", "5KG", "6nE", "Cz", "14a", +"x7", "mV", "7PH", "41B", "4xz", "592", "0V5", "0CT", "0mx", "NJ", "4C7", "4VV", "4YW", "4L6", "AK", "0by", "0LU", "og", "563", "43s", +"4zK", "6Oh", "bW", "w6", "0oI", "2Zj", "6aD", "4Tg", "7og", "4Je", "05C", "Ry", "2jD", "i4", "4dI", "6Qj", "5l9", "4iy", "0RW", "5m", +"2IX", "08s", "4GU", "4R4", "7mV", "4HT", "07r", "PH", "0H7", "1Mw", "4fx", "5c8", "7NJ", "4kH", "f5", "sT", "2Ki", "1nk", "4Ed", "64O", +"6bk", "4WH", "Z5", "OT", "ax", "0BJ", "4yd", "6LG", "4a5", "4tT", "0Oz", "lH", "Bd", "0aV", "4Zx", "aEI", "5P9", "4Uy", "0nW", "Me", +"cI", "1PZ", "5kt", "4n4", "6CF", "42m", "0MK", "ny", "2VD", "U4", "4XI", "6mj", "4FK", "6sh", "2HF", "K6", "0SI", "4s", "7Me", "4hg", +"4eW", "4p6", "9C", "1NX", "0pU", "Sg", "7ny", "6k9", "4Dz", "65Q", "0j5", "1ou", "0Qx", "6B", "7OT", "4jV", "4gf", "6RE", "2ik", "1Li", +"D7", "QV", "68a", "4IJ", "x3", "mR", "7PL", "41F", "5KC", "6nA", "2Uo", "14e", "19U", "NN", "4C3", "4VR", "bBM", "596", "0V1", "0CP", +"0LQ", "oc", "567", "43w", "4YS", "4L2", "AO", "16T", "0oM", "2Zn", "75i", "4Tc", "4zO", "6Ol", "bS", "w2", "8Y", "i0", "4dM", "6Qn", +"7oc", "4Ja", "05G", "2Dl", "d7g", "08w", "4GQ", "4R0", "a2D", "bSN", "0RS", "5i", "0H3", "1Ms", "52U", "ayM", "7mR", "4HP", "07v", "PL", +"2Km", "1no", "5UA", "64K", "7NN", "4kL", "f1", "7X", "5nw", "4k7", "fJ", "0Ex", "0kT", "Hf", "6eY", "4Pz", "5Mk", "6hi", "EV", "P7", +"0HH", "kz", "6FE", "47n", "48o", "6ID", "26b", "0GI", "0ie", "JW", "6gh", "4RK", "5OZ", "6jX", "Gg", "0dU", "0Jy", "iK", "4d6", "4qW", +"4z4", "4oU", "1DZ", "3A", "Ye", "0zW", "4Ay", "5D9", "6yj", "4LI", "A4", "TU", "zy", "0YK", "4be", "6WF", "6XG", "4md", "0VJ", "1p", +"2ME", "N5", "4CH", "62c", "5K8", "4Nx", "0uV", "Vd", "xH", "8Rb", "5pu", "4u5", "D", "13W", "5Lq", "4I1", "534", "46t", "0IR", "28y", +"gP", "69", "5om", "6Jo", "6dC", "5AA", "0jN", "3OL", "2Pl", "0eO", "aT1", "6kB", "6En", "44E", "98", "hQ", "ea", "0FS", "49u", "abL", +"4F0", "4SQ", "8ag", "KM", "02u", "UO", "4X2", "4MS", "57V", "a8F", "0M0", "0XQ", "c2", "vS", "7KM", "4nO", "5PB", "61H", "2Nn", "1kl", +"00D", "2Ao", "6zA", "4Ob", "4aN", "6Tm", "yR", "l3", "0WP", "0j", "a7G", "58W", "4BR", "4W3", "ZN", "84l", "0kP", "Hb", "71t", "644", +"5ns", "4k3", "fN", "8Ld", "0HL", "29g", "6FA", "47j", "5Mo", "6hm", "ER", "P3", "0ia", "JS", "6gl", "4RO", "48k", "7Ya", "26f", "0GM", +"8Ce", "iO", "4d2", "4qS", "beL", "hYw", "Gc", "0dQ", "Ya", "0zS", "cko", "60V", "4z0", "4oQ", "205", "3E", "2ll", "0YO", "4ba", "6WB", +"6yn", "4LM", "A0", "TQ", "2MA", "N1", "4CL", "62g", "6XC", "59I", "0VN", "1t", "xL", "8Rf", "54y", "419", "aQM", "b0G", "01Z", "3PP", +"530", "46p", "0IV", "jd", "DH", "0gz", "5Lu", "4I5", "6dG", "4Qd", "0jJ", "Ix", "gT", "r5", "5oi", "6Jk", "6Ej", "44A", "0Kg", "hU", +"Fy", "0eK", "5ND", "6kF", "4F4", "4SU", "1xZ", "KI", "ee", "0FW", "49q", "5x9", "57R", "6VX", "0M4", "0XU", "02q", "UK", "4X6", "4MW", +"5PF", "61L", "2Nj", "1kh", "c6", "vW", "7KI", "4nK", "4aJ", "6Ti", "yV", "l7", "0tH", "Wz", "6zE", "4Of", "4BV", "4W7", "ZJ", "0yx", +"0WT", "0n", "6YY", "4lz", "5Mc", "6ha", "2SO", "0fl", "1Xa", "kr", "6FM", "47f", "bDm", "aao", "fB", "0Ep", "8bD", "Hn", "5U2", "4Pr", +"5OR", "5Z3", "Go", "10t", "0Jq", "iC", "ann", "45W", "48g", "6IL", "ds", "0GA", "0im", "3Lo", "73I", "4RC", "6yb", "4LA", "03g", "2BL", +"zq", "0YC", "4bm", "6WN", "a4d", "bUn", "0Ts", "3I", "Ym", "87O", "4Aq", "5D1", "5K0", "4Np", "01V", "Vl", "2nQ", "1KS", "54u", "415", +"6XO", "4ml", "0VB", "1x", "2MM", "0xn", "5Sa", "62k", "gX", "61", "5oe", "6Jg", "6dK", "4Qh", "0jF", "It", "L", "0gv", "5Ly", "4I9", +"5w4", "4rt", "0IZ", "jh", "ei", "1Vz", "5mT", "5x5", "4F8", "4SY", "0hw", "KE", "Fu", "0eG", "5NH", "6kJ", "6Ef", "44M", "90", "hY", +"0Ui", "2S", "7KE", "4nG", "5PJ", "6uH", "Xw", "1kd", "0vu", "UG", "6xx", "790", "4cw", "5f7", "0M8", "0XY", "0WX", "0b", "5i6", "4lv", +"4BZ", "63q", "ZF", "0yt", "00L", "Wv", "6zI", "4Oj", "4aF", "6Te", "yZ", "0Zh", "0HD", "kv", "6FI", "47b", "5Mg", "6he", "EZ", "0fh", +"0kX", "Hj", "5U6", "4Pv", "7N9", "6Ky", "fF", "0Et", "0Ju", "iG", "6Dx", "45S", "5OV", "5Z7", "Gk", "0dY", "0ii", "3Lk", "6gd", "4RG", +"48c", "6IH", "dw", "0GE", "zu", "0YG", "4bi", "6WJ", "6yf", "4LE", "A8", "TY", "Yi", "1jz", "4Au", "5D5", "4z8", "4oY", "0Tw", "3M", +"xD", "1KW", "54q", "411", "5K4", "4Nt", "01R", "Vh", "2MI", "N9", "4CD", "62o", "6XK", "4mh", "0VF", "ut", "6dO", "4Ql", "0jB", "Ip", +"25E", "65", "5oa", "6Jc", "538", "46x", "9Pg", "jl", "H", "0gr", "bfo", "aCm", "72W", "bin", "0hs", "KA", "em", "324", "49y", "5x1", +"6Eb", "44I", "94", "3nm", "Fq", "0eC", "5NL", "6kN", "5PN", "61D", "Xs", "86Q", "0Um", "2W", "7KA", "4nC", "4cs", "5f3", "39W", "8QE", +"02y", "UC", "aRn", "794", "765", "63u", "ZB", "0yp", "9Ne", "0f", "5i2", "4lr", "4aB", "6Ta", "2oO", "0Zl", "00H", "Wr", "6zM", "4On", +"5lW", "5y6", "dj", "0GX", "0it", "JF", "6gy", "4RZ", "5OK", "6jI", "Gv", "0dD", "83", "iZ", "6De", "45N", "5nf", "6Kd", "24B", "72", +"0kE", "Hw", "6eH", "4Pk", "5Mz", "6hx", "EG", "0fu", "0HY", "kk", "5v7", "4sw", "5h5", "4mu", "1Fz", "1a", "2MT", "0xw", "4CY", "4V8", +"7kk", "4Ni", "01O", "Vu", "xY", "m8", "54l", "6Uf", "6Zg", "4oD", "b9", "3P", "Yt", "0zF", "4Ah", "60C", "4Y9", "4LX", "0wv", "TD", +"zh", "0YZ", "4bt", "5g4", "Fl", "11w", "5NQ", "6kS", "aom", "44T", "0Kr", "1N2", "ep", "0FB", "49d", "6HO", "6fc", "5Ca", "0hn", "3Ml", +"U", "0go", "bfr", "6ib", "6GN", "46e", "0IC", "jq", "gA", "0Ds", "bEn", "hyU", "5T1", "4Qq", "8cG", "Im", "00U", "Wo", "5J3", "4Os", +"55v", "406", "yC", "0Zq", "0WA", "ts", "6YL", "4lo", "4BC", "63h", "2LN", "0ym", "02d", "2CO", "6xa", "4MB", "4cn", "6VM", "2mc", "1Ha", +"0Up", "2J", "a5g", "bTm", "5PS", "5E2", "Xn", "86L", "0ip", "JB", "73T", "bhm", "48z", "5y2", "dn", "337", "87", "3on", "6Da", "45J", +"5OO", "6jM", "Gr", "10i", "0kA", "Hs", "6eL", "4Po", "5nb", "aar", "24F", "76", "8AE", "ko", "5v3", "4ss", "bgl", "aBn", "EC", "0fq", +"2MP", "0xs", "776", "62v", "5h1", "4mq", "9Of", "1e", "2nL", "1KN", "54h", "6Ub", "7ko", "4Nm", "01K", "Vq", "Yp", "0zB", "4Al", "60G", +"6Zc", "bUs", "0Tn", "3T", "zl", "8PF", "4bp", "5g0", "aSm", "787", "03z", "1r2", "4e9", "44P", "0Kv", "hD", "Fh", "0eZ", "5NU", "6kW", +"6fg", "4SD", "0hj", "KX", "et", "0FF", "5mI", "6HK", "6GJ", "46a", "0IG", "ju", "Q", "Q8", "5Ld", "6if", "5T5", "4Qu", "1zz", "Ii", +"gE", "0Dw", "5ox", "4j8", "55r", "402", "yG", "0Zu", "00Q", "Wk", "5J7", "4Ow", "4BG", "63l", "2LJ", "0yi", "0WE", "tw", "6YH", "4lk", +"4cj", "6VI", "2mg", "0XD", "0vh", "UZ", "6xe", "4MF", "5PW", "5E6", "Xj", "1ky", "0Ut", "2N", "7KX", "4nZ", "5OC", "6jA", "2Qo", "0dL", +"1ZA", "iR", "6Dm", "45F", "48v", "acO", "db", "0GP", "94M", "JN", "4G3", "4RR", "5Mr", "4H2", "EO", "12T", "0HQ", "kc", "527", "47w", +"5nn", "6Kl", "fS", "s2", "0kM", "3NO", "71i", "4Pc", "7kc", "4Na", "01G", "3PM", "xQ", "m0", "54d", "6Un", "a6D", "59T", "0VS", "1i", +"197", "85o", "4CQ", "4V0", "4Y1", "4LP", "03v", "TL", "0L3", "0YR", "56U", "a9E", "6Zo", "4oL", "b1", "3X", "2Om", "0zN", "5QA", "60K", +"ex", "0FJ", "49l", "6HG", "6fk", "4SH", "0hf", "KT", "Fd", "0eV", "5NY", "aAI", "4e5", "4pT", "0Kz", "hH", "gI", "1TZ", "5ot", "4j4", +"5T9", "4Qy", "0jW", "Ie", "DU", "Q4", "5Lh", "6ij", "6GF", "46m", "0IK", "jy", "0WI", "0s", "6YD", "4lg", "4BK", "6wh", "ZW", "O6", +"0tU", "Wg", "6zX", "6o9", "4aW", "4t6", "yK", "0Zy", "0Ux", "2B", "7KT", "4nV", "bzI", "61Q", "Xf", "1ku", "02l", "UV", "6xi", "4MJ", +"4cf", "6VE", "2mk", "0XH", "0Jd", "iV", "6Di", "45B", "5OG", "6jE", "Gz", "0dH", "0ix", "JJ", "4G7", "4RV", "48r", "6IY", "df", "0GT", +"0HU", "kg", "523", "47s", "5Mv", "4H6", "EK", "0fy", "0kI", "3NK", "6eD", "4Pg", "5nj", "6Kh", "fW", "s6", "xU", "m4", "5ph", "6Uj", +"7kg", "4Ne", "01C", "Vy", "193", "1hZ", "4CU", "4V4", "5h9", "4my", "0VW", "1m", "zd", "0YV", "4bx", "5g8", "4Y5", "4LT", "03r", "TH", +"Yx", "0zJ", "4Ad", "60O", "6Zk", "4oH", "b5", "wT", "6fo", "4SL", "0hb", "KP", "27e", "0FN", "49h", "6HC", "4e1", "44X", "8Bf", "hL", +"0p3", "0eR", "bdO", "aAM", "70w", "657", "0jS", "Ia", "gM", "8Mg", "5op", "4j0", "6GB", "46i", "0IO", "28d", "Y", "Q0", "5Ll", "6in", +"4BO", "63d", "ZS", "O2", "0WM", "0w", "7Ia", "4lc", "4aS", "4t2", "yO", "8Se", "00Y", "Wc", "aPN", "b1D", "bzM", "61U", "Xb", "1kq", +"216", "2F", "7KP", "4nR", "4cb", "6VA", "2mo", "0XL", "02h", "UR", "6xm", "4MN", "5j7", "4ow", "0TY", "3c", "YG", "0zu", "5Qz", "60p", +"6yH", "4Lk", "03M", "Tw", "2lJ", "0Yi", "4bG", "6Wd", "6Xe", "4mF", "0Vh", "1R", "2Mg", "0xD", "4Cj", "62A", "7kX", "4NZ", "0ut", "VF", +"xj", "1Ky", "5pW", "5e6", "5nU", "6KW", "fh", "0EZ", "0kv", "HD", "4E9", "4PX", "5MI", "6hK", "Et", "0fF", "0Hj", "kX", "6Fg", "47L", +"48M", "6If", "dY", "50", "0iG", "Ju", "6gJ", "4Ri", "5Ox", "4J8", "GE", "0dw", "1Zz", "ii", "5t5", "4qu", "02W", "Um", "5H1", "4Mq", +"57t", "424", "2mP", "0Xs", "0UC", "2y", "7Ko", "4nm", "bzr", "61j", "2NL", "1kN", "00f", "2AM", "6zc", "bus", "4al", "6TO", "yp", "0ZB", +"0Wr", "0H", "a7e", "58u", "4Bp", "5G0", "Zl", "84N", "f", "13u", "5LS", "5Y2", "amo", "46V", "0Ip", "jB", "gr", "1Ta", "5oO", "6JM", +"6da", "4QB", "0jl", "3On", "2PN", "0em", "5Nb", "aAr", "6EL", "44g", "0KA", "hs", "eC", "0Fq", "49W", "abn", "5V3", "4Ss", "8aE", "Ko", +"YC", "0zq", "754", "60t", "5j3", "4os", "9Md", "3g", "2lN", "0Ym", "4bC", "7GA", "6yL", "4Lo", "03I", "Ts", "2Mc", "1ha", "4Cn", "62E", +"6Xa", "4mB", "0Vl", "1V", "xn", "8RD", "5pS", "5e2", "aQo", "b0e", "01x", "VB", "0kr", "1n2", "71V", "bjo", "5nQ", "6KS", "fl", "315", +"0Hn", "29E", "6Fc", "47H", "5MM", "6hO", "Ep", "0fB", "0iC", "Jq", "6gN", "4Rm", "48I", "6Ib", "26D", "54", "8CG", "im", "509", "45y", +"ben", "hYU", "GA", "0ds", "4cY", "420", "2mT", "0Xw", "02S", "Ui", "5H5", "4Mu", "5Pd", "61n", "XY", "M8", "0UG", "vu", "7Kk", "4ni", +"4ah", "6TK", "yt", "0ZF", "B9", "WX", "6zg", "4OD", "4Bt", "5G4", "Zh", "0yZ", "0Wv", "0L", "4y9", "4lX", "6Gy", "46R", "0It", "jF", +"b", "0gX", "5LW", "5Y6", "6de", "4QF", "0jh", "IZ", "gv", "0DD", "5oK", "6JI", "6EH", "44c", "0KE", "hw", "2PJ", "0ei", "5Nf", "6kd", +"5V7", "4Sw", "0hY", "Kk", "eG", "0Fu", "49S", "6Hx", "7ia", "4Lc", "03E", "2Bn", "zS", "o2", "4bO", "6Wl", "a4F", "bUL", "0TQ", "3k", +"YO", "87m", "4AS", "4T2", "7kP", "4NR", "01t", "VN", "xb", "1Kq", "54W", "hfv", "6Xm", "4mN", "1FA", "1Z", "2Mo", "0xL", "4Cb", "62I", +"5MA", "6hC", "2Sm", "0fN", "0Hb", "kP", "6Fo", "47D", "bDO", "aaM", "0P3", "0ER", "8bf", "HL", "4E1", "4PP", "5Op", "4J0", "GM", "10V", +"0JS", "ia", "505", "45u", "48E", "6In", "dQ", "58", "0iO", "3LM", "6gB", "4Ra", "0UK", "2q", "7Kg", "4ne", "5Ph", "61b", "XU", "M4", +"0vW", "Ue", "5H9", "4My", "4cU", "4v4", "2mX", "1HZ", "0Wz", "tH", "4y5", "4lT", "4Bx", "5G8", "Zd", "0yV", "B5", "WT", "6zk", "4OH", +"4ad", "6TG", "yx", "0ZJ", "gz", "0DH", "5oG", "6JE", "6di", "4QJ", "0jd", "IV", "n", "0gT", "680", "6iY", "4g7", "4rV", "0Ix", "jJ", +"eK", "0Fy", "5mv", "4h6", "6fX", "5CZ", "0hU", "Kg", "FW", "S6", "5Nj", "6kh", "6ED", "44o", "0KI", "3nK", "zW", "o6", "4bK", "6Wh", +"6yD", "4Lg", "03A", "2Bj", "YK", "0zy", "4AW", "4T6", "6ZX", "6O9", "0TU", "3o", "xf", "1Ku", "54S", "6UY", "7kT", "4NV", "01p", "VJ", +"2Mk", "0xH", "4Cf", "62M", "6Xi", "4mJ", "0Vd", "uV", "0Hf", "kT", "6Fk", "4sH", "5ME", "6hG", "Ex", "0fJ", "0kz", "HH", "4E5", "4PT", +"5nY", "aaI", "fd", "0EV", "0JW", "ie", "501", "45q", "5Ot", "4J4", "GI", "10R", "0iK", "Jy", "6gF", "4Re", "48A", "6Ij", "dU", "q4", +"5Pl", "61f", "XQ", "M0", "0UO", "2u", "7Kc", "4na", "4cQ", "428", "39u", "8Qg", "0vS", "Ua", "aRL", "b3F", "bxO", "63W", "0l3", "0yR", +"234", "0D", "4y1", "4lP", "55I", "6TC", "2om", "0ZN", "B1", "WP", "6zo", "4OL", "6dm", "4QN", "1zA", "IR", "25g", "0DL", "5oC", "6JA", +"4g3", "46Z", "9PE", "jN", "j", "0gP", "684", "aCO", "72u", "675", "0hQ", "Kc", "eO", "8Oe", "5mr", "4h2", "7Ua", "44k", "0KM", "3nO", +"FS", "S2", "5Nn", "6kl", "4x6", "4mW", "0Vy", "1C", "0m4", "0xU", "5SZ", "62P", "7kI", "4NK", "C6", "VW", "2nj", "1Kh", "54N", "6UD", +"6ZE", "4of", "0TH", "3r", "YV", "L7", "4AJ", "60a", "6yY", "4Lz", "0wT", "Tf", "zJ", "0Yx", "4bV", "4w7", "5lu", "4i5", "dH", "0Gz", +"0iV", "Jd", "5W8", "4Rx", "5Oi", "6jk", "GT", "R5", "0JJ", "ix", "6DG", "45l", "5nD", "6KF", "fy", "0EK", "0kg", "HU", "6ej", "4PI", +"5MX", "5X9", "Ee", "0fW", "1XZ", "kI", "4f4", "4sU", "00w", "WM", "4Z0", "4OQ", "55T", "hgu", "ya", "0ZS", "a0", "0Y", "6Yn", "4lM", +"4Ba", "63J", "2Ll", "0yO", "02F", "2Cm", "6xC", "aG0", "4cL", "6Vo", "2mA", "n1", "0UR", "2h", "a5E", "bTO", "5Pq", "4U1", "XL", "86n", +"FN", "11U", "5Ns", "4K3", "516", "44v", "0KP", "hb", "eR", "p3", "49F", "6Hm", "6fA", "4Sb", "0hL", "3MN", "w", "0gM", "5LB", "7ya", +"6Gl", "46G", "0Ia", "jS", "gc", "0DQ", "bEL", "hyw", "4D2", "4QS", "8ce", "IO", "0m0", "0xQ", "byL", "62T", "4x2", "4mS", "227", "1G", +"2nn", "1Kl", "54J", "7Ea", "7kM", "4NO", "C2", "VS", "YR", "L3", "4AN", "60e", "6ZA", "4ob", "0TL", "3v", "zN", "8Pd", "4bR", "4w3", +"aSO", "b2E", "03X", "Tb", "0iR", "3LP", "73v", "666", "48X", "4i1", "dL", "8Nf", "0JN", "3oL", "6DC", "45h", "5Om", "6jo", "GP", "R1", +"0kc", "HQ", "6en", "4PM", "a09", "6KB", "24d", "0EO", "8Ag", "kM", "4f0", "47Y", "697", "aBL", "Ea", "0fS", "4ay", "5d9", "ye", "0ZW", +"00s", "WI", "4Z4", "4OU", "4Be", "63N", "Zy", "0yK", "a4", "tU", "6Yj", "4lI", "4cH", "6Vk", "2mE", "n5", "02B", "Ux", "6xG", "4Md", +"5Pu", "4U5", "XH", "86j", "0UV", "2l", "5k8", "4nx", "512", "44r", "0KT", "hf", "FJ", "0ex", "5Nw", "4K7", "6fE", "4Sf", "0hH", "Kz", +"eV", "p7", "49B", "6Hi", "6Gh", "46C", "0Ie", "jW", "s", "0gI", "5LF", "6iD", "4D6", "4QW", "0jy", "IK", "gg", "0DU", "5oZ", "6JX", +"7kA", "4NC", "01e", "3Po", "xs", "8RY", "54F", "6UL", "a6f", "59v", "0Vq", "1K", "d3E", "85M", "4Cs", "5F3", "5I2", "4Lr", "03T", "Tn", +"zB", "0Yp", "56w", "437", "6ZM", "4on", "1Da", "3z", "2OO", "0zl", "4AB", "60i", "5Oa", "6jc", "2QM", "0dn", "0JB", "ip", "6DO", "45d", +"48T", "acm", "1B2", "0Gr", "94o", "Jl", "5W0", "4Rp", "5MP", "5X1", "Em", "12v", "0Hs", "kA", "all", "47U", "5nL", "6KN", "fq", "0EC", +"0ko", "3Nm", "6eb", "4PA", "a8", "0Q", "6Yf", "4lE", "4Bi", "63B", "Zu", "0yG", "0tw", "WE", "4Z8", "4OY", "4au", "5d5", "yi", "1Jz", +"0UZ", "vh", "5k4", "4nt", "5Py", "4U9", "XD", "1kW", "02N", "Ut", "6xK", "4Mh", "4cD", "6Vg", "2mI", "n9", "eZ", "43", "49N", "6He", +"6fI", "4Sj", "0hD", "Kv", "FF", "0et", "7n9", "6ky", "5u6", "4pv", "0KX", "hj", "gk", "0DY", "5oV", "5z7", "6dx", "5Az", "0ju", "IG", +"Dw", "0gE", "5LJ", "6iH", "6Gd", "46O", "0Ii", "28B", "xw", "1Kd", "54B", "6UH", "7kE", "4NG", "01a", "3Pk", "0m8", "0xY", "4Cw", "5F7", +"6Xx", "59r", "0Vu", "1O", "zF", "0Yt", "4bZ", "433", "5I6", "4Lv", "03P", "Tj", "YZ", "0zh", "4AF", "60m", "6ZI", "4oj", "0TD", "wv", +"0JF", "it", "6DK", "4qh", "5Oe", "6jg", "GX", "R9", "0iZ", "Jh", "5W4", "4Rt", "48P", "4i9", "dD", "0Gv", "0Hw", "kE", "4f8", "47Q", +"5MT", "5X5", "Ei", "12r", "0kk", "HY", "6ef", "4PE", "5nH", "6KJ", "fu", "0EG", "4Bm", "63F", "Zq", "0yC", "0Wo", "0U", "6Yb", "4lA", +"4aq", "5d1", "ym", "8SG", "0ts", "WA", "aPl", "b1f", "747", "61w", "2NQ", "1kS", "9Lg", "2d", "5k0", "4np", "57i", "6Vc", "2mM", "0Xn", +"02J", "Up", "6xO", "4Ml", "6fM", "4Sn", "1xa", "Kr", "27G", "47", "49J", "6Ha", "5u2", "44z", "8BD", "hn", "FB", "0ep", "bdm", "aAo", +"70U", "bkl", "0jq", "IC", "go", "306", "5oR", "5z3", "7WA", "46K", "0Im", "28F", "Ds", "0gA", "5LN", "6iL", "0cY", "020", "6mT", "4Xw", +"42S", "6Cx", "nG", "0Mu", "1Pd", "cw", "6NH", "5kJ", "4UG", "74M", "3Kk", "0ni", "0ah", "BZ", "6oe", "4ZF", "40b", "6AI", "lv", "0OD", +"0Bt", "aF", "6Ly", "4yZ", "4Wv", "5R6", "Oj", "0lX", "Qh", "06R", "4It", "5L4", "461", "4gX", "1LW", "1Y6", "rt", "0QF", "4jh", "7Oj", +"65o", "4DD", "I9", "2JI", "SY", "F8", "4KE", "7nG", "6PJ", "4ei", "1Nf", "2kd", "4M", "0Sw", "4hY", "490", "5C5", "4Fu", "09S", "2Hx", +"6OR", "4zq", "354", "bm", "LA", "0os", "bnn", "75W", "6lN", "4Ym", "0bC", "Aq", "2yL", "0Lo", "43I", "6Bb", "6Mc", "5ha", "15", "22E", +"Np", "0mB", "4Vl", "6cO", "aDm", "bao", "1pS", "1e2", "ml", "8GF", "41x", "548", "4kr", "5n2", "7f", "8YD", "1nQ", "2KS", "64u", "715", +"4Hn", "69E", "Pr", "07H", "1MM", "2hO", "6Sa", "4fB", "4iC", "7LA", "5W", "0Rm", "08I", "2Ib", "66D", "4Go", "b4d", "aUn", "RC", "05y", +"8VE", "8g", "5a3", "4ds", "42W", "ain", "nC", "0Mq", "17t", "024", "6mP", "4Xs", "4UC", "74I", "3Ko", "0nm", "8IY", "cs", "6NL", "5kN", +"40f", "6AM", "lr", "8FX", "0al", "2TO", "6oa", "4ZB", "4Wr", "5R2", "On", "18u", "0Bp", "aB", "afo", "bCm", "465", "53u", "1LS", "1Y2", +"Ql", "06V", "4Ip", "5L0", "65k", "5Ta", "1oO", "2JM", "6x", "0QB", "4jl", "7On", "6PN", "4em", "1Nb", "9y", "2EL", "04g", "4KA", "7nC", +"5C1", "4Fq", "09W", "d6G", "4I", "0Ss", "bRn", "494", "LE", "0ow", "4TY", "4A8", "6OV", "4zu", "1Qz", "bi", "oY", "z8", "43M", "6Bf", +"6lJ", "4Yi", "0bG", "Au", "Nt", "0mF", "4Vh", "6cK", "6Mg", "4xD", "11", "22A", "mh", "0NZ", "4ut", "5p4", "4N9", "5Ky", "1pW", "CD", +"1nU", "2KW", "64q", "4EZ", "4kv", "5n6", "7b", "0PX", "1MI", "2hK", "6Se", "4fF", "4Hj", "69A", "Pv", "07L", "08M", "2If", "6rH", "4Gk", +"4iG", "7LE", "5S", "0Ri", "1Ox", "8c", "5a7", "4dw", "5Zz", "7oY", "RG", "0qu", "1Pl", "21f", "adR", "5kB", "4UO", "74E", "MS", "X2", +"0cQ", "028", "79u", "bbL", "4vS", "4c2", "nO", "8De", "8Kd", "aN", "4l3", "4yR", "634", "76t", "Ob", "0lP", "W3", "BR", "6om", "4ZN", +"40j", "6AA", "2zo", "0OL", "6t", "0QN", "5zA", "7Ob", "65g", "4DL", "I1", "2JA", "0g3", "06Z", "b7G", "68W", "469", "4gP", "284", "dSn", +"4E", "275", "4hQ", "498", "67V", "b8F", "1mr", "0h2", "SQ", "F0", "4KM", "7nO", "6PB", "4ea", "1Nn", "9u", "6lF", "4Ye", "0bK", "Ay", +"oU", "z4", "43A", "6Bj", "6OZ", "4zy", "0AW", "be", "LI", "2O9", "4TU", "4A4", "4N5", "5Ku", "14S", "CH", "md", "0NV", "41p", "540", +"6Mk", "4xH", "u5", "22M", "Nx", "0mJ", "4Vd", "6cG", "4Hf", "69M", "Pz", "0sH", "k7", "2hG", "6Si", "4fJ", "4kz", "7Nx", "7n", "0PT", +"1nY", "dqh", "4P7", "4EV", "4JW", "7oU", "RK", "05q", "1Ot", "8o", "6QX", "50R", "4iK", "7LI", "qW", "d6", "08A", "2Ij", "66L", "4Gg", +"4UK", "74A", "MW", "X6", "1Ph", "21b", "6ND", "5kF", "4vW", "4c6", "nK", "0My", "0cU", "0v4", "6mX", "5HZ", "4Wz", "6bY", "Of", "0lT", +"0Bx", "aJ", "4l7", "4yV", "40n", "6AE", "lz", "0OH", "W7", "BV", "6oi", "4ZJ", "65c", "4DH", "I5", "2JE", "6p", "0QJ", "4jd", "7Of", +"4r5", "4gT", "280", "2iY", "Qd", "0rV", "4Ix", "5L8", "5C9", "4Fy", "1mv", "0h6", "4A", "1CZ", "4hU", "7MW", "6PF", "4ee", "1Nj", "9q", +"SU", "F4", "4KI", "7nK", "oQ", "z0", "43E", "6Bn", "6lB", "4Ya", "0bO", "2Wl", "LM", "8fg", "4TQ", "4A0", "aeL", "cPo", "0AS", "ba", +"3kP", "0NR", "41t", "544", "4N1", "5Kq", "14W", "CL", "2Xm", "0mN", "5FA", "6cC", "6Mo", "4xL", "19", "22I", "k3", "2hC", "6Sm", "4fN", +"4Hb", "69I", "2Fo", "07D", "83l", "d5d", "4P3", "4ER", "bQM", "a0G", "7j", "0PP", "1Op", "8k", "hbw", "50V", "4JS", "7oQ", "RO", "05u", +"08E", "2In", "66H", "4Gc", "4iO", "7LM", "qS", "d2", "0ay", "BK", "4O6", "4ZW", "40s", "553", "lg", "0OU", "t6", "aW", "6Lh", "4yK", +"4Wg", "6bD", "2Yj", "0lI", "0cH", "2Vk", "6mE", "4Xf", "42B", "6Ci", "nV", "0Md", "1Pu", "cf", "6NY", "bAI", "4UV", "7pT", "MJ", "0nx", +"SH", "04r", "4KT", "7nV", "azI", "4ex", "1Nw", "9l", "pT", "e5", "4hH", "7MJ", "67O", "4Fd", "09B", "2Hi", "Qy", "06C", "4Ie", "68N", +"6Rj", "4gI", "j4", "2iD", "6m", "0QW", "4jy", "5o9", "4Q4", "4DU", "1oZ", "2JX", "4m0", "4xQ", "8Jg", "22T", "Na", "0mS", "627", "77w", +"6nn", "5Kl", "V0", "CQ", "3kM", "0NO", "41i", "7Pc", "6OC", "5jA", "0AN", "20e", "LP", "Y1", "4TL", "6ao", "78v", "bcO", "0bR", "0w3", +"oL", "8Ef", "43X", "4b1", "4iR", "7LP", "5F", "266", "08X", "0i1", "66U", "b9E", "4JN", "7oL", "RR", "G3", "1Om", "8v", "6QA", "4db", +"4kc", "7Na", "7w", "0PM", "H2", "2KB", "64d", "4EO", "b6D", "69T", "Pc", "07Y", "297", "dRm", "4s2", "4fS", "40w", "557", "lc", "0OQ", +"15T", "BO", "4O2", "4ZS", "4Wc", "76i", "2Yn", "0lM", "t2", "aS", "6Ll", "4yO", "42F", "6Cm", "nR", "8Dx", "0cL", "2Vo", "6mA", "4Xb", +"4UR", "74X", "MN", "8gd", "1Pq", "cb", "adO", "bAM", "azM", "51U", "1Ns", "9h", "SL", "04v", "4KP", "7nR", "67K", "5VA", "09F", "2Hm", +"4X", "e1", "4hL", "7MN", "6Rn", "4gM", "j0", "3ya", "2Gl", "06G", "4Ia", "68J", "4Q0", "4DQ", "82o", "d4g", "6i", "0QS", "bPN", "a1D", +"Ne", "0mW", "4Vy", "5S9", "4m4", "4xU", "1SZ", "22P", "my", "0NK", "41m", "7Pg", "6nj", "5Kh", "V4", "CU", "LT", "Y5", "4TH", "6ak", +"6OG", "4zd", "0AJ", "bx", "oH", "0Lz", "4wT", "4b5", "78r", "4Yx", "0bV", "Ad", "1lu", "0i5", "66Q", "4Gz", "4iV", "7LT", "5B", "0Rx", +"1Oi", "8r", "6QE", "4df", "4JJ", "7oH", "RV", "G7", "H6", "2KF", "6ph", "4EK", "4kg", "7Ne", "7s", "0PI", "1MX", "1X9", "4s6", "4fW", +"5XZ", "69P", "Pg", "0sU", "06", "23F", "afr", "4yC", "4Wo", "6bL", "Os", "0lA", "0aq", "BC", "aEn", "c4E", "4ts", "5q3", "lo", "8FE", +"347", "cn", "6NQ", "5kS", "bom", "74T", "MB", "0np", "17i", "2Vc", "6mM", "4Xn", "42J", "6Ca", "2xO", "0Ml", "4T", "0Sn", "5xa", "7MB", +"67G", "4Fl", "09J", "2Ha", "1u2", "04z", "b5g", "aTm", "6PS", "4ep", "8WF", "9d", "6e", "8XG", "4jq", "5o1", "65v", "706", "1oR", "1z3", +"Qq", "06K", "4Im", "68F", "6Rb", "4gA", "1LN", "2iL", "6nf", "5Kd", "V8", "CY", "mu", "0NG", "41a", "7Pk", "4m8", "4xY", "0Cw", "1F7", +"Ni", "19r", "4Vu", "5S5", "6lW", "4Yt", "0bZ", "Ah", "oD", "0Lv", "43P", "4b9", "6OK", "4zh", "0AF", "bt", "LX", "Y9", "4TD", "6ag", +"4JF", "7oD", "RZ", "0qh", "1Oe", "2jg", "6QI", "4dj", "4iZ", "483", "5N", "0Rt", "08P", "0i9", "5B6", "4Gv", "4Hw", "5M7", "Pk", "07Q", +"1MT", "1X5", "472", "52r", "4kk", "7Ni", "sw", "0PE", "1nH", "2KJ", "64l", "4EG", "4Wk", "6bH", "Ow", "0lE", "02", "23B", "6Ld", "4yG", +"4tw", "5q7", "lk", "0OY", "0au", "BG", "6ox", "5Jz", "4UZ", "74P", "MF", "0nt", "1Py", "cj", "6NU", "5kW", "42N", "6Ce", "nZ", "0Mh", +"0cD", "2Vg", "6mI", "4Xj", "67C", "4Fh", "09N", "2He", "4P", "e9", "4hD", "7MF", "6PW", "4et", "3n9", "2ky", "SD", "0pv", "4KX", "7nZ", +"4Q8", "4DY", "1oV", "1z7", "6a", "1Az", "4ju", "5o5", "6Rf", "4gE", "j8", "2iH", "Qu", "06O", "4Ii", "68B", "mq", "0NC", "41e", "7Po", +"6nb", "bar", "14F", "2UL", "Nm", "19v", "4Vq", "5S1", "agl", "bBn", "0Cs", "1F3", "1I2", "0Lr", "43T", "ahm", "6lS", "4Yp", "16w", "Al", +"2ZM", "0on", "5Da", "6ac", "6OO", "4zl", "0AB", "bp", "1Oa", "8z", "6QM", "4dn", "4JB", "aUs", "2DO", "05d", "08T", "d7D", "5B2", "4Gr", +"bSm", "487", "5J", "0Rp", "1MP", "1X1", "476", "52v", "4Hs", "5M3", "Po", "07U", "1nL", "2KN", "64h", "4EC", "4ko", "7Nm", "ss", "0PA", +"QJ", "06p", "4IV", "7lT", "6RY", "4gz", "1Lu", "0I5", "rV", "g7", "4jJ", "7OH", "65M", "4Df", "1oi", "2Jk", "2Ej", "04A", "4Kg", "7ne", +"6Ph", "4eK", "h6", "2kF", "4o", "0SU", "5xZ", "7My", "4S6", "4FW", "09q", "1x9", "17R", "2VX", "4M4", "4XU", "42q", "571", "ne", "0MW", +"v4", "cU", "6Nj", "5kh", "4Ue", "74o", "My", "0nK", "0aJ", "Bx", "6oG", "4Zd", "4tH", "6Ak", "lT", "y5", "0BV", "ad", "580", "4yx", +"4WT", "4B5", "OH", "0lz", "4kP", "7NR", "7D", "244", "1ns", "0k3", "64W", "con", "4HL", "69g", "PP", "E1", "1Mo", "2hm", "6SC", "52I", +"4ia", "7Lc", "5u", "0RO", "J0", "3Ya", "66f", "4GM", "b4F", "aUL", "Ra", "0qS", "8Vg", "8E", "458", "4dQ", "4o2", "4zS", "8He", "bO", +"Lc", "0oQ", "605", "75u", "6ll", "4YO", "T2", "AS", "2yn", "0LM", "43k", "7Ra", "6MA", "4xb", "0CL", "22g", "NR", "19I", "4VN", "6cm", +"aDO", "baM", "14y", "Cb", "mN", "8Gd", "41Z", "7PP", "axO", "53W", "1Lq", "0I1", "QN", "06t", "4IR", "68y", "65I", "4Db", "1om", "2Jo", +"6Z", "g3", "4jN", "7OL", "6Pl", "4eO", "h2", "2kB", "2En", "04E", "4Kc", "7na", "4S2", "4FS", "09u", "d6e", "4k", "0SQ", "bRL", "a3F", +"42u", "575", "na", "0MS", "17V", "dlo", "4M0", "4XQ", "4Ua", "74k", "3KM", "0nO", "28", "cQ", "6Nn", "5kl", "40D", "6Ao", "lP", "y1", +"0aN", "2Tm", "6oC", "5JA", "4WP", "4B1", "OL", "18W", "0BR", "0W3", "584", "bCO", "1nw", "0k7", "64S", "4Ex", "4kT", "7NV", "sH", "0Pz", +"1Mk", "2hi", "6SG", "4fd", "4HH", "69c", "PT", "E5", "J4", "2ID", "66b", "4GI", "4ie", "7Lg", "5q", "0RK", "1OZ", "8A", "4q4", "4dU", +"4Jy", "5O9", "Re", "0qW", "Lg", "0oU", "5DZ", "6aX", "4o6", "4zW", "0Ay", "bK", "2yj", "0LI", "43o", "6BD", "6lh", "4YK", "T6", "AW", +"NV", "0md", "4VJ", "6ci", "6ME", "4xf", "0CH", "22c", "mJ", "0Nx", "4uV", "7PT", "6nY", "baI", "1pu", "Cf", "6V", "0Ql", "4jB", "aus", +"65E", "4Dn", "1oa", "2Jc", "QB", "06x", "b7e", "68u", "5b2", "4gr", "8UD", "dSL", "4g", "8ZE", "4hs", "5m3", "67t", "724", "09y", "1x1", +"Ss", "04I", "4Ko", "7nm", "azr", "4eC", "1NL", "9W", "24", "21D", "6Nb", "bAr", "4Um", "74g", "Mq", "0nC", "0cs", "1f3", "79W", "bbn", +"42y", "579", "nm", "394", "365", "al", "588", "4yp", "bmo", "76V", "1i2", "0lr", "0aB", "Bp", "6oO", "4Zl", "40H", "6Ac", "2zM", "0On", +"4HD", "69o", "PX", "E9", "1Mg", "2he", "6SK", "4fh", "4kX", "7NZ", "7L", "0Pv", "3N9", "2Ky", "6pW", "4Et", "4Ju", "5O5", "Ri", "05S", +"1OV", "8M", "450", "4dY", "4ii", "7Lk", "qu", "0RG", "J8", "2IH", "66n", "4GE", "6ld", "4YG", "0bi", "2WJ", "ow", "0LE", "43c", "6BH", +"6Ox", "5jz", "0Au", "bG", "Lk", "0oY", "4Tw", "5Q7", "6nU", "5KW", "14q", "Cj", "mF", "0Nt", "41R", "7PX", "6MI", "4xj", "0CD", "22o", +"NZ", "0mh", "4VF", "6ce", "65A", "4Dj", "1oe", "2Jg", "6R", "0Qh", "4jF", "7OD", "5b6", "4gv", "1Ly", "0I9", "QF", "0rt", "4IZ", "68q", +"67p", "5Vz", "1mT", "1x5", "4c", "0SY", "4hw", "5m7", "6Pd", "4eG", "1NH", "9S", "Sw", "04M", "4Kk", "7ni", "4Ui", "74c", "Mu", "0nG", +"20", "cY", "6Nf", "5kd", "4vu", "5s5", "ni", "390", "0cw", "1f7", "4M8", "4XY", "4WX", "4B9", "OD", "0lv", "0BZ", "ah", "6LW", "4yt", +"40L", "6Ag", "lX", "y9", "0aF", "Bt", "6oK", "4Zh", "1Mc", "2ha", "6SO", "4fl", "5Xa", "69k", "2FM", "07f", "83N", "d5F", "6pS", "4Ep", +"bQo", "a0e", "7H", "0Pr", "1OR", "8I", "454", "50t", "4Jq", "5O1", "Rm", "05W", "08g", "2IL", "66j", "4GA", "4im", "7Lo", "5y", "0RC", +"os", "0LA", "43g", "6BL", "78I", "4YC", "0bm", "2WN", "Lo", "8fE", "4Ts", "5Q3", "aen", "cPM", "0Aq", "bC", "mB", "0Np", "41V", "ajo", +"6nQ", "5KS", "14u", "Cn", "2XO", "0ml", "4VB", "6ca", "6MM", "4xn", "1Sa", "22k", "Sj", "04P", "4Kv", "5N6", "443", "4eZ", "1NU", "9N", +"pv", "0SD", "4hj", "7Mh", "67m", "4FF", "1mI", "2HK", "2GJ", "2", "4IG", "68l", "6RH", "4gk", "1Ld", "2if", "6O", "0Qu", "5zz", "7OY", +"5A7", "4Dw", "1ox", "0j8", "15r", "Bi", "6oV", "4Zu", "40Q", "4a8", "lE", "0Ow", "0BG", "au", "6LJ", "4yi", "4WE", "6bf", "OY", "Z8", +"U9", "2VI", "6mg", "4XD", "4vh", "6CK", "nt", "0MF", "1PW", "cD", "4n9", "5ky", "4Ut", "5P4", "Mh", "0nZ", "4ip", "5l0", "5d", "9Kg", +"08z", "1y2", "66w", "737", "4Jl", "7on", "Rp", "05J", "1OO", "8T", "6Qc", "50i", "4kA", "7NC", "7U", "0Po", "1nb", "dqS", "64F", "4Em", +"b6f", "69v", "PA", "0ss", "8TG", "dRO", "5c1", "4fq", "6MP", "4xs", "376", "22v", "NC", "0mq", "bll", "77U", "6nL", "5KN", "14h", "Cs", +"3ko", "0Nm", "41K", "7PA", "6Oa", "4zB", "37", "20G", "Lr", "8fX", "4Tn", "6aM", "78T", "bcm", "0bp", "AB", "on", "387", "43z", "5r2", +"447", "51w", "1NQ", "9J", "Sn", "04T", "4Kr", "5N2", "67i", "4FB", "09d", "2HO", "4z", "1Ca", "4hn", "7Ml", "6RL", "4go", "8UY", "2ib", +"2GN", "6", "4IC", "68h", "5A3", "4Ds", "82M", "d4E", "6K", "0Qq", "bPl", "a1f", "40U", "akl", "lA", "0Os", "15v", "Bm", "6oR", "4Zq", +"4WA", "6bb", "2YL", "0lo", "0BC", "aq", "6LN", "4ym", "42d", "6CO", "np", "0MB", "0cn", "2VM", "6mc", "5Ha", "4Up", "5P0", "Ml", "8gF", +"1PS", "1E2", "adm", "bAo", "1lW", "1y6", "4R9", "4GX", "4it", "5l4", "qh", "0RZ", "i9", "8P", "6Qg", "4dD", "4Jh", "7oj", "Rt", "05N", +"1nf", "2Kd", "64B", "4Ei", "4kE", "7NG", "7Q", "f8", "1Mz", "2hx", "5c5", "4fu", "4HY", "69r", "PE", "0sw", "NG", "0mu", "5Fz", "6cx", +"6MT", "4xw", "0CY", "0V8", "3kk", "0Ni", "41O", "7PE", "6nH", "5KJ", "14l", "Cw", "Lv", "0oD", "4Tj", "6aI", "6Oe", "4zF", "33", "bZ", +"oj", "0LX", "4wv", "5r6", "6ly", "4YZ", "0bt", "AF", "4v", "0SL", "4hb", "awS", "67e", "4FN", "K3", "2HC", "Sb", "04X", "b5E", "aTO", +"4p3", "4eR", "8Wd", "9F", "6G", "257", "4jS", "7OQ", "65T", "cnm", "1op", "0j0", "QS", "D2", "4IO", "68d", "7Ba", "4gc", "1Ll", "2in", +"0BO", "23d", "6LB", "4ya", "4WM", "6bn", "OQ", "Z0", "0aS", "Ba", "aEL", "c4g", "40Y", "4a0", "lM", "8Fg", "8If", "cL", "4n1", "5kq", +"616", "74v", "3KP", "0nR", "U1", "2VA", "6mo", "4XL", "42h", "6CC", "2xm", "0MN", "4Jd", "7of", "Rx", "05B", "i5", "2jE", "6Qk", "4dH", +"4ix", "5l8", "5l", "0RV", "08r", "2IY", "4R5", "4GT", "4HU", "7mW", "PI", "07s", "1Mv", "0H6", "5c9", "4fy", "4kI", "7NK", "sU", "f4", +"1nj", "2Kh", "64N", "4Ee", "6nD", "5KF", "1ph", "2Uj", "mW", "x6", "41C", "7PI", "593", "5hZ", "0CU", "0V4", "NK", "0my", "4VW", "4C6", +"4L7", "4YV", "0bx", "AJ", "of", "0LT", "43r", "562", "6Oi", "4zJ", "w7", "bV", "Lz", "0oH", "4Tf", "6aE", "67a", "4FJ", "K7", "2HG", +"4r", "0SH", "4hf", "7Md", "4p7", "4eV", "1NY", "9B", "Sf", "0pT", "4Kz", "7nx", "65P", "5TZ", "1ot", "0j4", "6C", "0Qy", "4jW", "7OU", +"6RD", "4gg", "1Lh", "2ij", "QW", "D6", "4IK", "7lI", "4WI", "6bj", "OU", "Z4", "0BK", "ay", "6LF", "4ye", "4tU", "4a4", "lI", "2o9", +"0aW", "Be", "6oZ", "4Zy", "4Ux", "5P8", "Md", "0nV", "8Ib", "cH", "4n5", "5ku", "42l", "6CG", "nx", "0MJ", "U5", "2VE", "6mk", "4XH", +"i1", "8X", "6Qo", "4dL", "5ZA", "7ob", "2Dm", "05F", "08v", "d7f", "4R1", "4GP", "bSO", "a2E", "5h", "0RR", "1Mr", "0H2", "ayL", "52T", +"4HQ", "69z", "PM", "07w", "1nn", "2Kl", "64J", "4Ea", "4kM", "7NO", "7Y", "f0", "mS", "x2", "41G", "7PM", "aDR", "5KB", "14d", "2Un", +"NO", "19T", "4VS", "4C2", "597", "bBL", "0CQ", "0V0", "ob", "0LP", "43v", "566", "4L3", "4YR", "16U", "AN", "2Zo", "0oL", "4Tb", "6aA", +"6Om", "4zN", "w3", "bR", "4oT", "4z5", "wH", "0Tz", "0zV", "Yd", "5D8", "4Ax", "4LH", "6yk", "TT", "A5", "0YJ", "zx", "6WG", "4bd", +"4me", "6XF", "1q", "0VK", "N4", "2MD", "62b", "4CI", "4Ny", "5K9", "Ve", "0uW", "1KZ", "xI", "4u4", "5pt", "4k6", "5nv", "0Ey", "fK", +"Hg", "0kU", "641", "6eX", "6hh", "5Mj", "P6", "EW", "29b", "0HI", "47o", "6FD", "6IE", "48n", "0GH", "dz", "JV", "0id", "4RJ", "6gi", +"6jY", "beI", "0dT", "Gf", "iJ", "0Jx", "4qV", "4d7", "UN", "02t", "4MR", "4X3", "a8G", "57W", "0XP", "0M1", "2Z", "c3", "4nN", "7KL", +"61I", "5PC", "1km", "2No", "2An", "00E", "4Oc", "7ja", "6Tl", "4aO", "l2", "yS", "0k", "0WQ", "58V", "a7F", "4W2", "4BS", "84m", "ZO", +"13V", "E", "4I0", "5Lp", "46u", "535", "ja", "0IS", "68", "gQ", "6Jn", "5ol", "4Qa", "6dB", "3OM", "0jO", "0eN", "2Pm", "6kC", "5NA", +"44D", "6Eo", "hP", "99", "0FR", "0S3", "abM", "49t", "4SP", "4F1", "KL", "8af", "0zR", "0o3", "60W", "ckn", "4oP", "4z1", "3D", "204", +"0YN", "2lm", "6WC", "56I", "4LL", "6yo", "TP", "A1", "N0", "903", "62f", "4CM", "4ma", "6XB", "1u", "0VO", "8Rg", "xM", "418", "54x", +"b0F", "aQL", "Va", "0uS", "Hc", "0kQ", "645", "71u", "4k2", "5nr", "8Le", "fO", "29f", "0HM", "47k", "7Va", "6hl", "5Mn", "P2", "ES", +"JR", "1yA", "4RN", "6gm", "6IA", "48j", "0GL", "26g", "iN", "8Cd", "45Z", "4d3", "hYv", "beM", "0dP", "Gb", "6VY", "4cz", "0XT", "0M5", +"UJ", "02p", "4MV", "4X7", "61M", "5PG", "1ki", "Xz", "vV", "c7", "4nJ", "7KH", "6Th", "4aK", "l6", "yW", "2Aj", "00A", "4Og", "6zD", +"4W6", "4BW", "0yy", "ZK", "0o", "0WU", "58R", "6YX", "46q", "531", "je", "0IW", "13R", "A", "4I4", "5Lt", "4Qe", "6dF", "Iy", "0jK", +"r4", "gU", "6Jj", "5oh", "4pH", "6Ek", "hT", "0Kf", "0eJ", "Fx", "6kG", "5NE", "4ST", "4F5", "KH", "0hz", "0FV", "ed", "5x8", "49p", +"bvs", "6yc", "2BM", "03f", "0YB", "zp", "6WO", "4bl", "bUo", "a4e", "3H", "0Tr", "87N", "Yl", "5D0", "4Ap", "4Nq", "5K1", "Vm", "01W", +"1KR", "xA", "414", "54t", "4mm", "6XN", "1y", "0VC", "0xo", "2ML", "62j", "4CA", "7xA", "5Mb", "0fm", "2SN", "ks", "0HA", "47g", "6FL", +"aan", "bDl", "0Eq", "fC", "Ho", "8bE", "4Ps", "5U3", "5Z2", "5OS", "10u", "Gn", "iB", "0Jp", "45V", "ano", "6IM", "48f", "1Wa", "dr", +"3Ln", "0il", "4RB", "6ga", "2R", "0Uh", "4nF", "7KD", "61A", "5PK", "1ke", "Xv", "UF", "0vt", "4MZ", "6xy", "5f6", "4cv", "0XX", "0M9", +"0c", "0WY", "4lw", "5i7", "63p", "5Rz", "0yu", "ZG", "Ww", "00M", "4Ok", "6zH", "6Td", "4aG", "0Zi", "2oJ", "60", "gY", "6Jf", "5od", +"4Qi", "6dJ", "Iu", "0jG", "0gw", "M", "4I8", "5Lx", "4ru", "5w5", "ji", "1Yz", "0FZ", "eh", "5x4", "5mU", "4SX", "4F9", "KD", "0hv", +"0eF", "Ft", "6kK", "5NI", "44L", "6Eg", "hX", "91", "0YF", "zt", "6WK", "4bh", "4LD", "6yg", "TX", "A9", "0zZ", "Yh", "5D4", "4At", +"4oX", "4z9", "3L", "0Tv", "1KV", "xE", "410", "54p", "4Nu", "5K5", "Vi", "01S", "N8", "2MH", "62n", "4CE", "4mi", "6XJ", "uu", "0VG", +"kw", "0HE", "47c", "6FH", "6hd", "5Mf", "0fi", "2SJ", "Hk", "0kY", "4Pw", "5U7", "6Kx", "5nz", "0Eu", "fG", "iF", "0Jt", "45R", "6Dy", +"5Z6", "5OW", "0dX", "Gj", "JZ", "0ih", "4RF", "6ge", "6II", "48b", "0GD", "dv", "61E", "5PO", "1ka", "Xr", "2V", "0Ul", "4nB", "aqs", +"5f2", "4cr", "8QD", "39V", "UB", "02x", "795", "aRo", "63t", "764", "0yq", "ZC", "0g", "9Nd", "4ls", "5i3", "7DA", "4aC", "0Zm", "2oN", +"Ws", "00I", "4Oo", "6zL", "4Qm", "6dN", "Iq", "0jC", "64", "25D", "6Jb", "bEr", "46y", "539", "jm", "9Pf", "0gs", "I", "aCl", "bfn", +"bio", "72V", "1m2", "0hr", "325", "el", "5x0", "49x", "44H", "6Ec", "3nl", "95", "0eB", "Fp", "6kO", "5NM", "4mt", "5h4", "uh", "0VZ", +"0xv", "2MU", "4V9", "4CX", "4Nh", "7kj", "Vt", "01N", "m9", "xX", "6Ug", "54m", "4oE", "6Zf", "3Q", "b8", "0zG", "Yu", "60B", "4Ai", +"4LY", "4Y8", "TE", "0ww", "1Iz", "zi", "5g5", "4bu", "5y7", "5lV", "0GY", "dk", "JG", "0iu", "5Bz", "6gx", "6jH", "5OJ", "0dE", "Gw", +"3ok", "82", "45O", "6Dd", "6Ke", "5ng", "73", "fZ", "Hv", "0kD", "4Pj", "6eI", "6hy", "7m9", "0ft", "EF", "kj", "0HX", "4sv", "5v6", +"Wn", "00T", "4Or", "5J2", "407", "55w", "0Zp", "yB", "0z", "1Ga", "4ln", "6YM", "63i", "4BB", "0yl", "2LO", "2CN", "02e", "4MC", "7hA", +"6VL", "4co", "0XA", "2mb", "2K", "0Uq", "bTl", "a5f", "5E3", "5PR", "86M", "Xo", "11v", "Fm", "6kR", "5NP", "44U", "aol", "hA", "0Ks", +"0FC", "eq", "6HN", "49e", "4SA", "6fb", "3Mm", "0ho", "0gn", "T", "6ic", "5La", "46d", "6GO", "jp", "0IB", "0Dr", "1A2", "hyT", "bEo", +"4Qp", "5T0", "Il", "8cF", "0xr", "2MQ", "62w", "777", "4mp", "5h0", "1d", "9Og", "1KO", "2nM", "6Uc", "54i", "4Nl", "7kn", "Vp", "01J", +"0zC", "Yq", "60F", "4Am", "4oA", "6Zb", "3U", "0To", "8PG", "zm", "5g1", "4bq", "786", "aSl", "TA", "0ws", "JC", "0iq", "bhl", "73U", +"5y3", "5lR", "336", "do", "3oo", "86", "45K", "7TA", "6jL", "5ON", "0dA", "Gs", "Hr", "8bX", "4Pn", "6eM", "6Ka", "5nc", "77", "24G", +"kn", "8AD", "47z", "5v2", "aBo", "bgm", "0fp", "EB", "403", "4aZ", "0Zt", "yF", "Wj", "00P", "4Ov", "5J6", "63m", "4BF", "0yh", "ZZ", +"tv", "0WD", "4lj", "6YI", "6VH", "4ck", "0XE", "2mf", "2CJ", "02a", "4MG", "6xd", "5E7", "5PV", "1kx", "Xk", "2O", "0Uu", "bTh", "7KY", +"44Q", "4e8", "hE", "0Kw", "11r", "Fi", "6kV", "5NT", "4SE", "6ff", "KY", "0hk", "0FG", "eu", "6HJ", "49a", "4rh", "6GK", "jt", "0IF", +"Q9", "P", "6ig", "5Le", "4Qt", "5T4", "Ih", "0jZ", "0Dv", "gD", "4j9", "5oy", "aD0", "7kb", "3PL", "01F", "m1", "xP", "6Uo", "54e", +"59U", "a6E", "1h", "0VR", "85n", "196", "4V1", "4CP", "4LQ", "4Y0", "TM", "03w", "0YS", "za", "a9D", "56T", "4oM", "6Zn", "3Y", "b0", +"0zO", "2Ol", "60J", "4Aa", "7za", "5OB", "0dM", "2Qn", "iS", "0Ja", "45G", "6Dl", "acN", "48w", "0GQ", "dc", "JO", "94L", "4RS", "4G2", +"4H3", "5Ms", "12U", "EN", "kb", "0HP", "47v", "526", "6Km", "5no", "s3", "fR", "3NN", "0kL", "4Pb", "6eA", "0r", "0WH", "4lf", "6YE", +"63a", "4BJ", "O7", "ZV", "Wf", "0tT", "4Oz", "6zY", "4t7", "4aV", "0Zx", "yJ", "2C", "0Uy", "4nW", "7KU", "61P", "5PZ", "1kt", "Xg", +"UW", "02m", "4MK", "6xh", "6VD", "4cg", "0XI", "2mj", "0FK", "ey", "6HF", "49m", "4SI", "6fj", "KU", "0hg", "0eW", "Fe", "6kZ", "5NX", +"4pU", "4e4", "hI", "2k9", "0Dz", "gH", "4j5", "5ou", "4Qx", "5T8", "Id", "0jV", "Q5", "DT", "6ik", "5Li", "46l", "6GG", "jx", "0IJ", +"m5", "xT", "6Uk", "54a", "4Nd", "7kf", "Vx", "01B", "0xz", "192", "4V5", "4CT", "4mx", "5h8", "1l", "0VV", "0YW", "ze", "5g9", "4by", +"4LU", "4Y4", "TI", "03s", "0zK", "Yy", "60N", "4Ae", "4oI", "6Zj", "wU", "b4", "iW", "0Je", "45C", "6Dh", "6jD", "5OF", "0dI", "2Qj", +"JK", "0iy", "4RW", "4G6", "6IX", "48s", "0GU", "dg", "kf", "0HT", "47r", "522", "4H7", "5Mw", "0fx", "EJ", "Hz", "0kH", "4Pf", "6eE", +"6Ki", "5nk", "s7", "fV", "63e", "4BN", "O3", "ZR", "0v", "0WL", "4lb", "6YA", "4t3", "4aR", "8Sd", "yN", "Wb", "00X", "b1E", "aPO", +"61T", "bzL", "1kp", "Xc", "2G", "217", "4nS", "7KQ", "7Fa", "4cc", "0XM", "2mn", "US", "02i", "4MO", "6xl", "4SM", "6fn", "KQ", "0hc", +"0FO", "27d", "6HB", "49i", "44Y", "4e0", "hM", "8Bg", "0eS", "Fa", "aAL", "bdN", "656", "70v", "3OP", "0jR", "8Mf", "gL", "4j1", "5oq", +"46h", "6GC", "28e", "0IN", "Q1", "X", "6io", "5Lm", "6KV", "5nT", "1Uz", "fi", "HE", "0kw", "4PY", "4E8", "6hJ", "5MH", "0fG", "Eu", +"kY", "0Hk", "47M", "6Ff", "6Ig", "48L", "51", "dX", "Jt", "0iF", "4Rh", "6gK", "4J9", "5Oy", "0dv", "GD", "ih", "0JZ", "4qt", "5t4", +"4ov", "5j6", "3b", "0TX", "0zt", "YF", "60q", "4AZ", "4Lj", "6yI", "Tv", "03L", "0Yh", "zZ", "6We", "4bF", "4mG", "6Xd", "1S", "0Vi", +"0xE", "2Mf", "6vH", "4Ck", "bth", "7kY", "VG", "0uu", "1Kx", "xk", "5e7", "5pV", "13t", "g", "5Y3", "5LR", "46W", "amn", "jC", "0Iq", +"0DA", "gs", "6JL", "5oN", "4QC", "70I", "3Oo", "0jm", "0el", "2PO", "6ka", "5Nc", "44f", "6EM", "hr", "8BX", "0Fp", "eB", "abo", "49V", +"4Sr", "5V2", "Kn", "8aD", "Ul", "02V", "4Mp", "5H0", "425", "57u", "0Xr", "2mQ", "2x", "0UB", "4nl", "7Kn", "61k", "5Pa", "1kO", "2NM", +"2AL", "00g", "4OA", "6zb", "6TN", "4am", "0ZC", "yq", "0I", "0Ws", "58t", "a7d", "5G1", "4Bq", "84O", "Zm", "HA", "0ks", "bjn", "71W", +"6KR", "5nP", "314", "fm", "29D", "0Ho", "47I", "6Fb", "6hN", "5ML", "0fC", "Eq", "Jp", "0iB", "4Rl", "6gO", "6Ic", "48H", "55", "26E", +"il", "8CF", "45x", "508", "hYT", "beo", "0dr", "1a2", "0zp", "YB", "60u", "755", "4or", "5j2", "3f", "9Me", "0Yl", "2lO", "6Wa", "4bB", +"4Ln", "6yM", "Tr", "03H", "0xA", "2Mb", "62D", "4Co", "4mC", "7HA", "1W", "0Vm", "8RE", "xo", "5e3", "54Z", "b0d", "aQn", "VC", "01y", +"46S", "6Gx", "jG", "0Iu", "0gY", "c", "5Y7", "5LV", "4QG", "6dd", "3Ok", "0ji", "0DE", "gw", "6JH", "5oJ", "44b", "6EI", "hv", "0KD", +"0eh", "FZ", "6ke", "5Ng", "4Sv", "5V6", "Kj", "0hX", "0Ft", "eF", "6Hy", "49R", "421", "4cX", "0Xv", "2mU", "Uh", "02R", "4Mt", "5H4", +"61o", "5Pe", "M9", "XX", "vt", "0UF", "4nh", "7Kj", "6TJ", "4ai", "0ZG", "yu", "WY", "B8", "4OE", "6zf", "5G5", "4Bu", "1iz", "Zi", +"0M", "0Ww", "4lY", "4y8", "6hB", "aW1", "0fO", "2Sl", "kQ", "0Hc", "47E", "6Fn", "aaL", "bDN", "0ES", "fa", "HM", "8bg", "4PQ", "4E0", +"4J1", "5Oq", "10W", "GL", "3oP", "0JR", "45t", "504", "6Io", "48D", "59", "dP", "3LL", "0iN", "5BA", "6gC", "4Lb", "6yA", "2Bo", "03D", +"o3", "zR", "6Wm", "4bN", "bUM", "a4G", "3j", "0TP", "87l", "YN", "4T3", "4AR", "4NS", "7kQ", "VO", "01u", "1Kp", "xc", "hfw", "54V", +"4mO", "6Xl", "uS", "0Va", "0xM", "2Mn", "62H", "4Cc", "0DI", "25b", "6JD", "5oF", "4QK", "6dh", "IW", "0je", "0gU", "o", "6iX", "5LZ", +"4rW", "4g6", "jK", "0Iy", "0Fx", "eJ", "4h7", "5mw", "4Sz", "6fY", "Kf", "0hT", "S7", "FV", "6ki", "5Nk", "44n", "6EE", "hz", "0KH", +"2p", "0UJ", "4nd", "7Kf", "61c", "5Pi", "M5", "XT", "Ud", "0vV", "4Mx", "5H8", "4v5", "4cT", "0Xz", "2mY", "0A", "1GZ", "4lU", "4y4", +"5G9", "4By", "0yW", "Ze", "WU", "B4", "4OI", "6zj", "6TF", "4ae", "0ZK", "yy", "kU", "0Hg", "47A", "6Fj", "6hF", "5MD", "0fK", "Ey", +"HI", "2K9", "4PU", "4E4", "6KZ", "5nX", "0EW", "fe", "id", "0JV", "45p", "500", "4J5", "5Ou", "0dz", "GH", "Jx", "0iJ", "4Rd", "6gG", +"6Ik", "5li", "q5", "dT", "o7", "zV", "6Wi", "4bJ", "4Lf", "6yE", "Tz", "0wH", "0zx", "YJ", "4T7", "4AV", "4oz", "6ZY", "3n", "0TT", +"1Kt", "xg", "6UX", "54R", "4NW", "7kU", "VK", "01q", "0xI", "2Mj", "62L", "4Cg", "4mK", "6Xh", "uW", "0Ve", "4QO", "6dl", "IS", "0ja", +"0DM", "25f", "7Za", "5oB", "4rS", "4g2", "jO", "9PD", "0gQ", "k", "aCN", "685", "674", "72t", "Kb", "0hP", "8Od", "eN", "4h3", "49Z", +"44j", "6EA", "3nN", "0KL", "S3", "FR", "6km", "5No", "61g", "5Pm", "M1", "XP", "2t", "0UN", "ad0", "7Kb", "429", "4cP", "8Qf", "39t", +"0c3", "02Z", "b3G", "aRM", "63V", "bxN", "0yS", "Za", "0E", "235", "4lQ", "4y0", "6TB", "4aa", "0ZO", "2ol", "WQ", "B0", "4OM", "6zn", +"4i4", "5lt", "1WZ", "dI", "Je", "0iW", "4Ry", "5W9", "6jj", "5Oh", "R4", "GU", "iy", "0JK", "45m", "6DF", "6KG", "5nE", "0EJ", "fx", +"HT", "0kf", "4PH", "6ek", "5X8", "5MY", "0fV", "Ed", "kH", "0Hz", "4sT", "4f5", "4mV", "4x7", "1B", "0Vx", "0xT", "0m5", "62Q", "4Cz", +"4NJ", "7kH", "VV", "C7", "1Ki", "xz", "6UE", "54O", "4og", "6ZD", "3s", "0TI", "L6", "YW", "6th", "4AK", "6l9", "6yX", "Tg", "0wU", +"0Yy", "zK", "4w6", "4bW", "11T", "FO", "4K2", "5Nr", "44w", "517", "hc", "0KQ", "p2", "eS", "6Hl", "49G", "4Sc", "72i", "3MO", "0hM", +"0gL", "v", "6iA", "5LC", "46F", "6Gm", "jR", "1YA", "0DP", "gb", "hyv", "bEM", "4QR", "4D3", "IN", "8cd", "WL", "00v", "4OP", "4Z1", +"hgt", "55U", "0ZR", "0O3", "0X", "a1", "4lL", "6Yo", "63K", "5RA", "0yN", "2Lm", "2Cl", "02G", "4Ma", "6xB", "6Vn", "4cM", "n0", "39i", +"2i", "0US", "bTN", "a5D", "4U0", "5Pp", "86o", "XM", "Ja", "0iS", "667", "73w", "4i0", "48Y", "8Ng", "dM", "3oM", "0JO", "45i", "6DB", +"6jn", "5Ol", "R0", "GQ", "HP", "0kb", "4PL", "6eo", "6KC", "5nA", "0EN", "24e", "kL", "8Af", "47X", "4f1", "aBM", "696", "0fR", "0s3", +"0xP", "0m1", "62U", "byM", "4mR", "4x3", "1F", "226", "1Km", "2no", "6UA", "54K", "4NN", "7kL", "VR", "C3", "L2", "YS", "60d", "4AO", +"4oc", "7Ja", "3w", "0TM", "8Pe", "zO", "4w2", "4bS", "b2D", "aSN", "Tc", "03Y", "44s", "513", "hg", "0KU", "0ey", "FK", "4K6", "5Nv", +"4Sg", "6fD", "3MK", "0hI", "p6", "eW", "6Hh", "49C", "46B", "6Gi", "jV", "0Id", "0gH", "r", "6iE", "5LG", "4QV", "4D7", "IJ", "0jx", +"0DT", "gf", "6JY", "bEI", "5d8", "4ax", "0ZV", "yd", "WH", "00r", "4OT", "4Z5", "63O", "4Bd", "0yJ", "Zx", "tT", "a5", "4lH", "6Yk", +"6Vj", "4cI", "n4", "2mD", "Uy", "02C", "4Me", "6xF", "4U4", "5Pt", "1kZ", "XI", "2m", "0UW", "4ny", "5k9", "6jb", "ber", "0do", "2QL", +"iq", "0JC", "45e", "6DN", "acl", "48U", "0Gs", "dA", "Jm", "94n", "4Rq", "5W1", "5X0", "5MQ", "12w", "El", "1M2", "0Hr", "47T", "alm", +"6KO", "5nM", "0EB", "fp", "3Nl", "0kn", "bjs", "6ec", "4NB", "aQs", "3Pn", "01d", "1Ka", "xr", "6UM", "54G", "59w", "a6g", "1J", "0Vp", +"85L", "d3D", "5F2", "4Cr", "4Ls", "5I3", "To", "03U", "0Yq", "zC", "436", "56v", "4oo", "6ZL", "ws", "0TA", "0zm", "2ON", "60h", "4AC", +"42", "27B", "6Hd", "49O", "4Sk", "6fH", "Kw", "0hE", "0eu", "FG", "6kx", "5Nz", "4pw", "5u7", "hk", "0KY", "0DX", "gj", "5z6", "5oW", +"4QZ", "6dy", "IF", "0jt", "0gD", "Dv", "6iI", "5LK", "46N", "6Ge", "jZ", "0Ih", "0P", "a9", "4lD", "6Yg", "63C", "4Bh", "0yF", "Zt", +"WD", "0tv", "4OX", "4Z9", "5d4", "4at", "0ZZ", "yh", "2a", "1Ez", "4nu", "5k5", "4U8", "5Px", "1kV", "XE", "Uu", "02O", "4Mi", "6xJ", +"6Vf", "4cE", "n8", "2mH", "iu", "0JG", "45a", "6DJ", "6jf", "5Od", "R8", "GY", "Ji", "1yz", "4Ru", "5W5", "4i8", "48Q", "0Gw", "dE", +"kD", "0Hv", "47P", "4f9", "5X4", "5MU", "0fZ", "Eh", "HX", "0kj", "4PD", "6eg", "6KK", "5nI", "0EF", "ft", "1Ke", "xv", "6UI", "54C", +"4NF", "7kD", "VZ", "0uh", "0xX", "0m9", "5F6", "4Cv", "4mZ", "6Xy", "1N", "0Vt", "0Yu", "zG", "432", "56r", "4Lw", "5I7", "Tk", "03Q", +"0zi", "2OJ", "60l", "4AG", "4ok", "6ZH", "ww", "0TE", "4So", "6fL", "Ks", "0hA", "46", "27F", "7XA", "49K", "4ps", "5u3", "ho", "8BE", +"0eq", "FC", "aAn", "bdl", "bkm", "70T", "IB", "0jp", "307", "gn", "5z2", "5oS", "46J", "6Ga", "28G", "0Il", "13i", "z", "6iM", "5LO", +"63G", "4Bl", "0yB", "Zp", "0T", "0Wn", "58i", "6Yc", "5d0", "4ap", "8SF", "yl", "1q2", "00z", "b1g", "aPm", "61v", "746", "1kR", "XA", +"2e", "9Lf", "4nq", "5k1", "6Vb", "4cA", "0Xo", "2mL", "Uq", "02K", "4Mm", "6xN", "8YG", "7e", "5n1", "4kq", "716", "64v", "2KP", "1nR", +"07K", "Pq", "69F", "4Hm", "4fA", "6Sb", "2hL", "1MN", "0Rn", "5T", "7LB", "5ya", "4Gl", "66G", "2Ia", "08J", "05z", "1t2", "aUm", "b4g", +"4dp", "5a0", "8d", "8VF", "bn", "357", "4zr", "6OQ", "75T", "bnm", "0op", "LB", "Ar", "16i", "4Yn", "6lM", "6Ba", "43J", "0Ll", "2yO", +"22F", "16", "4xC", "agr", "6cL", "4Vo", "0mA", "Ns", "CC", "14X", "bal", "aDn", "5p3", "4us", "8GE", "mo", "5L7", "4Iw", "06Q", "Qk", +"1Y5", "1LT", "53r", "462", "7Oi", "4jk", "0QE", "rw", "2JJ", "1oH", "4DG", "65l", "7nD", "4KF", "0ph", "SZ", "2kg", "1Ne", "4ej", "6PI", +"493", "4hZ", "0St", "4N", "0h9", "09P", "4Fv", "5C6", "4Xt", "6mW", "023", "0cZ", "0Mv", "nD", "4c9", "42P", "5kI", "6NK", "ct", "1Pg", +"X9", "MX", "74N", "4UD", "4ZE", "6of", "BY", "W8", "0OG", "lu", "6AJ", "40a", "4yY", "4l8", "aE", "0Bw", "18r", "Oi", "5R5", "4Wu", +"4EY", "4P8", "2KT", "1nV", "8YC", "7a", "5n5", "4ku", "4fE", "6Sf", "2hH", "k8", "07O", "Pu", "69B", "4Hi", "4Gh", "66C", "2Ie", "08N", +"d9", "5P", "7LF", "4iD", "4dt", "5a4", "2jy", "3o9", "0qv", "RD", "7oZ", "4JX", "6ay", "4TZ", "0ot", "LF", "bj", "0AX", "4zv", "6OU", +"6Be", "43N", "0Lh", "oZ", "Av", "0bD", "4Yj", "6lI", "6cH", "4Vk", "0mE", "Nw", "22B", "12", "4xG", "6Md", "5p7", "4uw", "0NY", "mk", +"CG", "1pT", "5Kz", "6nx", "1Y1", "1LP", "53v", "466", "5L3", "4Is", "06U", "Qo", "2JN", "1oL", "4DC", "65h", "7Om", "4jo", "0QA", "rs", +"9z", "1Na", "4en", "6PM", "aTs", "4KB", "04d", "2EO", "d6D", "09T", "4Fr", "5C2", "497", "bRm", "0Sp", "4J", "0Mr", "1H2", "aim", "42T", +"4Xp", "6mS", "027", "17w", "0nn", "3Kl", "74J", "5Ea", "5kM", "6NO", "cp", "1Pc", "0OC", "lq", "6AN", "40e", "4ZA", "6ob", "2TL", "0ao", +"18v", "Om", "5R1", "4Wq", "bCn", "afl", "aA", "0Bs", "07C", "Py", "69N", "4He", "4fI", "6Sj", "2hD", "k4", "0PW", "7m", "5n9", "4ky", +"4EU", "4P4", "2KX", "1nZ", "05r", "RH", "7oV", "4JT", "4dx", "5a8", "8l", "1Ow", "d5", "qT", "7LJ", "4iH", "4Gd", "66O", "2Ii", "08B", +"Az", "0bH", "4Yf", "6lE", "6Bi", "43B", "z7", "oV", "bf", "0AT", "4zz", "6OY", "4A7", "4TV", "0ox", "LJ", "CK", "14P", "5Kv", "4N6", +"543", "41s", "0NU", "mg", "22N", "u6", "4xK", "6Mh", "6cD", "4Vg", "0mI", "2Xj", "7Oa", "4jc", "0QM", "6w", "2JB", "I2", "4DO", "65d", +"68T", "b7D", "06Y", "Qc", "dSm", "287", "4gS", "4r2", "7MP", "4hR", "276", "4F", "0h1", "09X", "b8E", "67U", "7nL", "4KN", "F3", "SR", +"9v", "1Nm", "4eb", "6PA", "5kA", "6NC", "21e", "1Po", "X1", "MP", "74F", "4UL", "bbO", "79v", "0v3", "0cR", "8Df", "nL", "4c1", "42X", +"4yQ", "4l0", "aM", "8Kg", "0lS", "Oa", "76w", "637", "4ZM", "6on", "BQ", "W0", "0OO", "2zl", "6AB", "40i", "4fM", "6Sn", "3xa", "k0", +"07G", "2Fl", "69J", "4Ha", "4EQ", "4P0", "d5g", "83o", "0PS", "7i", "a0D", "bQN", "50U", "hbt", "8h", "1Os", "05v", "RL", "7oR", "4JP", +"5WA", "66K", "2Im", "08F", "d1", "5X", "7LN", "4iL", "6Bm", "43F", "z3", "oR", "2Wo", "0bL", "4Yb", "6lA", "4A3", "4TR", "8fd", "LN", +"bb", "0AP", "cPl", "aeO", "547", "41w", "0NQ", "mc", "CO", "14T", "5Kr", "4N2", "77i", "4Vc", "0mM", "2Xn", "22J", "u2", "4xO", "6Ml", +"2JF", "I6", "4DK", "6qh", "7Oe", "4jg", "0QI", "6s", "1Y9", "1LX", "4gW", "4r6", "68P", "5YZ", "0rU", "Qg", "0h5", "1mu", "4Fz", "67Q", +"7MT", "4hV", "0Sx", "4B", "9r", "1Ni", "4ef", "6PE", "7nH", "4KJ", "F7", "SV", "X5", "MT", "74B", "4UH", "5kE", "6NG", "cx", "1Pk", +"0Mz", "nH", "4c5", "4vT", "4Xx", "79r", "0v7", "0cV", "0lW", "Oe", "5R9", "4Wy", "4yU", "4l4", "aI", "1RZ", "0OK", "ly", "6AF", "40m", +"4ZI", "6oj", "BU", "W4", "265", "5E", "488", "4iQ", "b9F", "66V", "0i2", "1lr", "G0", "RQ", "7oO", "4JM", "4da", "6QB", "8u", "1On", +"0PN", "7t", "7Nb", "aa0", "4EL", "64g", "2KA", "H1", "07Z", "0f3", "69W", "b6G", "4fP", "479", "dRn", "294", "22W", "8Jd", "4xR", "4m3", +"77t", "624", "0mP", "Nb", "CR", "V3", "5Ko", "6nm", "ajS", "41j", "0NL", "3kN", "20f", "0AM", "4zc", "aeR", "6al", "4TO", "Y2", "LS", +"Ac", "0bQ", "bcL", "78u", "4b2", "4wS", "8Ee", "oO", "7nU", "4KW", "04q", "SK", "9o", "1Nt", "51R", "6PX", "7MI", "4hK", "e6", "pW", +"2Hj", "09A", "4Fg", "67L", "68M", "4If", "0rH", "Qz", "2iG", "j7", "4gJ", "6Ri", "7Ox", "4jz", "0QT", "6n", "1z8", "1oY", "4DV", "4Q7", +"4ZT", "4O5", "BH", "0az", "0OV", "ld", "550", "40p", "4yH", "6Lk", "aT", "t5", "0lJ", "Ox", "6bG", "4Wd", "4Xe", "6mF", "2Vh", "0cK", +"0Mg", "nU", "6Cj", "42A", "5kX", "6NZ", "ce", "1Pv", "2N9", "MI", "7pW", "4UU", "4Gy", "5B9", "0i6", "1lv", "1BZ", "5A", "7LW", "4iU", +"4de", "6QF", "8q", "1Oj", "G4", "RU", "7oK", "4JI", "4EH", "64c", "2KE", "H5", "0PJ", "7p", "7Nf", "4kd", "4fT", "4s5", "2hY", "290", +"0sV", "Pd", "5M8", "4Hx", "6cY", "4Vz", "0mT", "Nf", "1F8", "0Cx", "4xV", "4m7", "7Pd", "41n", "0NH", "mz", "CV", "V7", "5Kk", "6ni", +"6ah", "4TK", "Y6", "LW", "20b", "0AI", "4zg", "6OD", "4b6", "4wW", "0Ly", "oK", "Ag", "0bU", "5IZ", "6lX", "9k", "1Np", "51V", "azN", +"7nQ", "4KS", "04u", "SO", "2Hn", "09E", "4Fc", "67H", "7MM", "4hO", "e2", "pS", "2iC", "j3", "4gN", "6Rm", "68I", "4Ib", "06D", "2Go", +"d4d", "82l", "4DR", "4Q3", "a1G", "bPM", "0QP", "6j", "0OR", "0Z3", "554", "40t", "4ZP", "4O1", "BL", "15W", "0lN", "2Ym", "6bC", "5GA", +"4yL", "6Lo", "aP", "09", "0Mc", "nQ", "6Cn", "42E", "4Xa", "6mB", "2Vl", "0cO", "8gg", "MM", "7pS", "4UQ", "bAN", "adL", "ca", "1Pr", +"G8", "RY", "7oG", "4JE", "4di", "6QJ", "2jd", "1Of", "0Rw", "5M", "480", "4iY", "4Gu", "5B5", "2Ix", "08S", "07R", "Ph", "5M4", "4Ht", +"4fX", "471", "1X6", "1MW", "0PF", "st", "7Nj", "4kh", "4ED", "64o", "2KI", "H9", "CZ", "14A", "5Kg", "6ne", "7Ph", "41b", "0ND", "mv", +"1F4", "0Ct", "4xZ", "6My", "5S6", "4Vv", "0mX", "Nj", "Ak", "0bY", "4Yw", "6lT", "6Bx", "43S", "0Lu", "oG", "bw", "0AE", "4zk", "6OH", +"6ad", "4TG", "0oi", "2ZJ", "7MA", "4hC", "0Sm", "4W", "2Hb", "09I", "4Fo", "67D", "aTn", "b5d", "04y", "SC", "9g", "8WE", "4es", "6PP", +"5o2", "4jr", "8XD", "6f", "1z0", "1oQ", "705", "65u", "68E", "4In", "06H", "Qr", "2iO", "1LM", "4gB", "6Ra", "5ia", "6Lc", "23E", "05", +"0lB", "Op", "6bO", "4Wl", "c4F", "aEm", "1d2", "0ar", "8FF", "ll", "558", "40x", "5kP", "6NR", "cm", "344", "0ns", "MA", "74W", "bon", +"4Xm", "6mN", "3FA", "0cC", "0Mo", "2xL", "6Cb", "42I", "4dm", "6QN", "8y", "1Ob", "05g", "2DL", "7oC", "4JA", "4Gq", "5B1", "d7G", "08W", +"0Rs", "5I", "484", "bSn", "52u", "475", "1X2", "1MS", "07V", "Pl", "5M0", "4Hp", "5Ua", "64k", "2KM", "1nO", "0PB", "7x", "7Nn", "4kl", +"7Pl", "41f", "8GX", "mr", "2UO", "14E", "5Kc", "6na", "5S2", "4Vr", "19u", "Nn", "1F0", "0Cp", "bBm", "ago", "ahn", "43W", "0Lq", "oC", +"Ao", "16t", "4Ys", "6lP", "75I", "4TC", "0om", "2ZN", "bs", "0AA", "4zo", "6OL", "2Hf", "09M", "4Fk", "6sH", "7ME", "4hG", "0Si", "4S", +"9c", "1Nx", "4ew", "6PT", "7nY", "bqh", "0pu", "SG", "1z4", "1oU", "4DZ", "65q", "5o6", "4jv", "0QX", "6b", "2iK", "1LI", "4gF", "6Re", +"68A", "4Ij", "06L", "Qv", "0lF", "Ot", "6bK", "4Wh", "4yD", "6Lg", "aX", "01", "0OZ", "lh", "5q4", "4tt", "4ZX", "4O9", "BD", "0av", +"0nw", "ME", "74S", "4UY", "5kT", "6NV", "ci", "1Pz", "0Mk", "nY", "6Cf", "42M", "4Xi", "6mJ", "2Vd", "0cG", "bL", "8Hf", "4zP", "4o1", +"75v", "606", "0oR", "0z3", "AP", "T1", "4YL", "6lo", "6BC", "43h", "0LN", "2ym", "22d", "0CO", "4xa", "6MB", "6cn", "4VM", "0mc", "NQ", +"Ca", "14z", "baN", "aDL", "7PS", "41Y", "8Gg", "mM", "247", "7G", "7NQ", "4kS", "com", "64T", "0k0", "1np", "E2", "PS", "69d", "4HO", +"4fc", "7Ca", "2hn", "1Ml", "0RL", "5v", "avS", "4ib", "4GN", "66e", "2IC", "J3", "05X", "Rb", "aUO", "b4E", "4dR", "4q3", "8F", "8Vd", +"4XV", "4M7", "1f8", "0cx", "0MT", "nf", "572", "42r", "5kk", "6Ni", "cV", "v7", "0nH", "Mz", "74l", "4Uf", "4Zg", "6oD", "2Tj", "0aI", +"y6", "lW", "6Ah", "40C", "5iZ", "583", "ag", "0BU", "0ly", "OK", "4B6", "4WW", "7lW", "4IU", "06s", "QI", "0I6", "1Lv", "4gy", "5b9", +"7OK", "4jI", "g4", "rU", "2Jh", "1oj", "4De", "65N", "7nf", "4Kd", "04B", "Sx", "2kE", "h5", "4eH", "6Pk", "5m8", "4hx", "0SV", "4l", +"2HY", "09r", "4FT", "4S5", "5Q8", "4Tx", "0oV", "Ld", "bH", "0Az", "4zT", "4o5", "6BG", "43l", "0LJ", "ox", "AT", "T5", "4YH", "6lk", +"6cj", "4VI", "0mg", "NU", "2vh", "0CK", "4xe", "6MF", "7PW", "4uU", "2n9", "mI", "Ce", "1pv", "5KX", "6nZ", "5UZ", "64P", "0k4", "1nt", +"0Py", "7C", "7NU", "4kW", "4fg", "6SD", "2hj", "1Mh", "E6", "PW", "7mI", "4HK", "4GJ", "66a", "2IG", "J7", "0RH", "5r", "7Ld", "4if", +"4dV", "4q7", "8B", "1OY", "0qT", "Rf", "7ox", "4Jz", "0MP", "nb", "576", "42v", "4XR", "4M3", "dll", "17U", "0nL", "3KN", "74h", "4Ub", +"5ko", "6Nm", "cR", "v3", "y2", "lS", "6Al", "40G", "4Zc", "aER", "2Tn", "0aM", "18T", "OO", "4B2", "4WS", "bCL", "587", "ac", "0BQ", +"0I2", "1Lr", "53T", "axL", "68z", "4IQ", "06w", "QM", "2Jl", "1on", "4Da", "65J", "7OO", "4jM", "g0", "6Y", "9X", "h1", "4eL", "6Po", +"7nb", "aA0", "04F", "2Em", "d6f", "09v", "4FP", "4S1", "a3E", "bRO", "0SR", "4h", "AX", "T9", "4YD", "6lg", "6BK", "4wh", "0LF", "ot", +"bD", "0Av", "4zX", "4o9", "5Q4", "4Tt", "0oZ", "Lh", "Ci", "14r", "5KT", "6nV", "ajh", "41Q", "0Nw", "mE", "22l", "0CG", "4xi", "6MJ", +"6cf", "4VE", "0mk", "NY", "07a", "2FJ", "69l", "4HG", "4fk", "6SH", "2hf", "1Md", "0Pu", "7O", "7NY", "bQh", "4Ew", "6pT", "0k8", "1nx", +"05P", "Rj", "5O6", "4Jv", "4dZ", "453", "8N", "1OU", "0RD", "qv", "7Lh", "4ij", "4GF", "66m", "2IK", "1lI", "5kc", "6Na", "21G", "27", +"8gX", "Mr", "74d", "4Un", "bbm", "79T", "1f0", "0cp", "397", "nn", "5s2", "42z", "4ys", "6LP", "ao", "366", "0lq", "OC", "76U", "bml", +"4Zo", "6oL", "Bs", "0aA", "0Om", "2zN", "7QA", "40K", "7OC", "4jA", "0Qo", "6U", "3ZA", "1ob", "4Dm", "65F", "68v", "b7f", "0rs", "QA", +"dSO", "8UG", "4gq", "5b1", "5m0", "4hp", "8ZF", "4d", "1x2", "09z", "727", "67w", "7nn", "4Kl", "04J", "Sp", "9T", "1NO", "51i", "6Pc", +"6BO", "43d", "0LB", "op", "2WM", "0bn", "5Ia", "6lc", "5Q0", "4Tp", "8fF", "Ll", "1D2", "0Ar", "cPN", "aem", "ajl", "41U", "0Ns", "mA", +"Cm", "14v", "5KP", "6nR", "6cb", "4VA", "0mo", "2XL", "22h", "0CC", "4xm", "6MN", "4fo", "6SL", "2hb", "8TY", "07e", "2FN", "69h", "4HC", +"4Es", "64X", "d5E", "83M", "0Pq", "7K", "a0f", "bQl", "50w", "457", "8J", "1OQ", "05T", "Rn", "5O2", "4Jr", "4GB", "66i", "2IO", "08d", +"1Ba", "5z", "7Ll", "4in", "0nD", "Mv", "7ph", "4Uj", "5kg", "6Ne", "cZ", "23", "0MX", "nj", "5s6", "4vv", "4XZ", "6my", "1f4", "0ct", +"0lu", "OG", "6bx", "5Gz", "4yw", "6LT", "ak", "0BY", "0Oi", "2zJ", "6Ad", "40O", "4Zk", "6oH", "Bw", "0aE", "2Jd", "1of", "4Di", "65B", +"7OG", "4jE", "g8", "6Q", "2ix", "1Lz", "4gu", "5b5", "68r", "4IY", "0rw", "QE", "1x6", "1mW", "4FX", "4S9", "5m4", "4ht", "0SZ", "ph", +"9P", "h9", "4eD", "6Pg", "7nj", "4Kh", "04N", "St", "22u", "375", "4xp", "598", "77V", "blo", "0mr", "1h2", "Cp", "14k", "5KM", "6nO", +"7PB", "41H", "0Nn", "3kl", "20D", "34", "4zA", "6Ob", "6aN", "4Tm", "0oC", "Lq", "AA", "0bs", "bcn", "78W", "569", "43y", "384", "om", +"9Kd", "5g", "5l3", "4is", "734", "66t", "1y1", "08y", "05I", "Rs", "7om", "4Jo", "4dC", "7AA", "8W", "1OL", "0Pl", "7V", "ats", "4kB", +"4En", "64E", "2Kc", "1na", "07x", "PB", "69u", "b6e", "4fr", "5c2", "dRL", "8TD", "4Zv", "6oU", "Bj", "0aX", "0Ot", "lF", "6Ay", "40R", +"4yj", "6LI", "av", "0BD", "0lh", "OZ", "6be", "4WF", "4XG", "6md", "2VJ", "0ci", "0ME", "nw", "6CH", "42c", "5kz", "6Nx", "cG", "1PT", +"0nY", "Mk", "5P7", "4Uw", "5N5", "4Ku", "04S", "Si", "9M", "1NV", "4eY", "440", "7Mk", "4hi", "0SG", "pu", "2HH", "K8", "4FE", "67n", +"68o", "4ID", "1", "QX", "2ie", "1Lg", "4gh", "6RK", "7OZ", "4jX", "0Qv", "6L", "2Jy", "3O9", "4Dt", "5A4", "4C9", "4VX", "0mv", "ND", +"22q", "0CZ", "4xt", "6MW", "7PF", "41L", "x9", "mX", "Ct", "14o", "5KI", "6nK", "6aJ", "4Ti", "0oG", "Lu", "bY", "30", "4zE", "6Of", +"5r5", "4wu", "380", "oi", "AE", "0bw", "4YY", "4L8", "5Wz", "66p", "1y5", "1lT", "0RY", "5c", "5l7", "4iw", "4dG", "6Qd", "8S", "1OH", +"05M", "Rw", "7oi", "4Jk", "4Ej", "64A", "2Kg", "1ne", "0Ph", "7R", "7ND", "4kF", "4fv", "5c6", "0H9", "1My", "0st", "PF", "69q", "4HZ", +"0Op", "lB", "ako", "40V", "4Zr", "6oQ", "Bn", "15u", "0ll", "2YO", "6ba", "4WB", "4yn", "6LM", "ar", "1Ra", "0MA", "ns", "6CL", "42g", +"4XC", "79I", "2VN", "0cm", "8gE", "Mo", "5P3", "4Us", "bAl", "adn", "cC", "1PP", "9I", "1NR", "51t", "444", "5N1", "4Kq", "04W", "Sm", +"2HL", "09g", "4FA", "67j", "7Mo", "4hm", "0SC", "4y", "2ia", "1Lc", "4gl", "6RO", "68k", "5Ya", "5", "2GM", "d4F", "82N", "4Dp", "5A0", +"a1e", "bPo", "0Qr", "6H", "Cx", "14c", "5KE", "6nG", "7PJ", "4uH", "x5", "mT", "0V7", "0CV", "4xx", "590", "4C5", "4VT", "0mz", "NH", +"AI", "16R", "4YU", "4L4", "561", "43q", "0LW", "oe", "bU", "w4", "4zI", "6Oj", "6aF", "4Te", "0oK", "Ly", "05A", "2Dj", "7oe", "4Jg", +"4dK", "6Qh", "2jF", "i6", "0RU", "5o", "7Ly", "5yZ", "4GW", "4R6", "1y9", "08q", "07p", "PJ", "7mT", "4HV", "4fz", "6SY", "0H5", "1Mu", +"f7", "sV", "7NH", "4kJ", "4Ef", "64M", "2Kk", "1ni", "4yb", "6LA", "23g", "0BL", "Z3", "OR", "6bm", "4WN", "c4d", "aEO", "Bb", "0aP", +"8Fd", "lN", "4a3", "40Z", "5kr", "4n2", "cO", "8Ie", "0nQ", "Mc", "74u", "615", "4XO", "6ml", "2VB", "U2", "0MM", "2xn", "7Sa", "42k", +"7Mc", "4ha", "0SO", "4u", "3Xa", "K0", "4FM", "67f", "aTL", "b5F", "0pS", "Sa", "9E", "8Wg", "4eQ", "448", "7OR", "4jP", "254", "6D", +"0j3", "1os", "cnn", "65W", "68g", "4IL", "9", "QP", "2im", "1Lo", "53I", "6RC", "7PN", "41D", "x1", "mP", "2Um", "14g", "5KA", "6nC", +"4C1", "4VP", "19W", "NL", "0V3", "0CR", "bBO", "594", "565", "43u", "0LS", "oa", "AM", "16V", "4YQ", "4L0", "6aB", "4Ta", "0oO", "2Zl", +"bQ", "38", "4zM", "6On", "4dO", "6Ql", "2jB", "i2", "05E", "2Dn", "7oa", "4Jc", "4GS", "4R2", "d7e", "08u", "0RQ", "5k", "a2F", "bSL", +"52W", "ayO", "0H1", "1Mq", "07t", "PN", "69y", "4HR", "4Eb", "64I", "2Ko", "1nm", "f3", "7Z", "7NL", "4kN", "Z7", "OV", "6bi", "4WJ", +"4yf", "6LE", "az", "0BH", "0Ox", "lJ", "4a7", "4tV", "4Zz", "6oY", "Bf", "0aT", "0nU", "Mg", "74q", "5EZ", "5kv", "4n6", "cK", "1PX", +"0MI", "2xj", "6CD", "42o", "4XK", "6mh", "2VF", "U6", "2HD", "K4", "4FI", "67b", "7Mg", "4he", "0SK", "4q", "9A", "1NZ", "4eU", "4p4", +"5N9", "4Ky", "0pW", "Se", "0j7", "1ow", "4Dx", "5A8", "7OV", "4jT", "0Qz", "rH", "2ii", "1Lk", "4gd", "6RG", "68c", "4IH", "D5", "QT", +"5Ls", "4I3", "F", "13U", "0IP", "jb", "536", "46v", "5oo", "6Jm", "gR", "r3", "0jL", "3ON", "6dA", "4Qb", "5NB", "aAR", "2Pn", "0eM", +"0Ka", "hS", "6El", "44G", "49w", "abN", "ec", "0FQ", "8ae", "KO", "4F2", "4SS", "4X0", "4MQ", "02w", "UM", "0M2", "0XS", "57T", "a8D", +"7KO", "4nM", "c0", "2Y", "2Nl", "1kn", "aJ1", "61J", "6zC", "aE0", "00F", "2Am", "yP", "l1", "4aL", "6To", "a7E", "58U", "0WR", "0h", +"ZL", "84n", "4BP", "4W1", "fH", "0Ez", "5nu", "4k5", "5U8", "4Px", "0kV", "Hd", "ET", "P5", "5Mi", "6hk", "6FG", "47l", "0HJ", "kx", +"dy", "0GK", "48m", "6IF", "6gj", "4RI", "0ig", "JU", "Ge", "0dW", "5OX", "5Z9", "4d4", "4qU", "1ZZ", "iI", "0Ty", "3C", "4z6", "4oW", +"5QZ", "60P", "Yg", "0zU", "A6", "TW", "6yh", "4LK", "4bg", "6WD", "2lj", "0YI", "0VH", "1r", "6XE", "4mf", "4CJ", "62a", "2MG", "N7", +"0uT", "Vf", "7kx", "4Nz", "5pw", "4u7", "xJ", "1KY", "0IT", "jf", "532", "46r", "5Lw", "4I7", "B", "0gx", "0jH", "Iz", "6dE", "4Qf", +"5ok", "6Ji", "gV", "r7", "0Ke", "hW", "6Eh", "44C", "5NF", "6kD", "2Pj", "0eI", "0hy", "KK", "4F6", "4SW", "49s", "6HX", "eg", "0FU", +"0M6", "0XW", "4cy", "5f9", "4X4", "4MU", "02s", "UI", "Xy", "1kj", "5PD", "61N", "7KK", "4nI", "c4", "vU", "yT", "l5", "4aH", "6Tk", +"6zG", "4Od", "00B", "Wx", "ZH", "0yz", "4BT", "4W5", "5i8", "4lx", "0WV", "0l", "71v", "646", "0kR", "3NP", "fL", "8Lf", "5nq", "4k1", +"6FC", "47h", "0HN", "29e", "EP", "P1", "5Mm", "6ho", "6gn", "4RM", "0ic", "JQ", "26d", "0GO", "48i", "6IB", "4d0", "45Y", "8Cg", "iM", +"Ga", "0dS", "beN", "hYu", "ckm", "60T", "Yc", "0zQ", "207", "3G", "4z2", "4oS", "4bc", "7Ga", "2ln", "0YM", "A2", "TS", "6yl", "4LO", +"4CN", "62e", "2MC", "N3", "0VL", "1v", "6XA", "4mb", "5ps", "4u3", "xN", "8Rd", "01X", "Vb", "aQO", "b0E", "5og", "6Je", "gZ", "63", +"0jD", "Iv", "6dI", "4Qj", "7l9", "6iy", "N", "0gt", "0IX", "jj", "5w6", "4rv", "5mV", "5x7", "ek", "0FY", "0hu", "KG", "6fx", "5Cz", +"5NJ", "6kH", "Fw", "0eE", "92", "3nk", "6Ed", "44O", "7KG", "4nE", "c8", "2Q", "Xu", "1kf", "5PH", "61B", "4X8", "4MY", "0vw", "UE", +"2mx", "1Hz", "4cu", "5f5", "5i4", "4lt", "0WZ", "th", "ZD", "0yv", "4BX", "4W9", "6zK", "4Oh", "00N", "Wt", "yX", "l9", "4aD", "6Tg", +"2SM", "0fn", "5Ma", "6hc", "6FO", "47d", "0HB", "kp", "24Y", "0Er", "bDo", "aam", "5U0", "4Pp", "8bF", "Hl", "Gm", "10v", "5OP", "5Z1", +"anl", "45U", "0Js", "iA", "dq", "0GC", "48e", "6IN", "6gb", "4RA", "0io", "3Lm", "03e", "2BN", "7iA", "4LC", "4bo", "6WL", "zs", "0YA", +"0Tq", "3K", "a4f", "bUl", "4As", "5D3", "Yo", "87M", "01T", "Vn", "5K2", "4Nr", "54w", "417", "xB", "1KQ", "1Fa", "1z", "6XM", "4mn", +"4CB", "62i", "2MO", "0xl", "1za", "Ir", "6dM", "4Qn", "5oc", "6Ja", "25G", "67", "9Pe", "jn", "5w2", "46z", "bfm", "aCo", "J", "0gp", +"0hq", "KC", "72U", "bil", "5mR", "5x3", "eo", "326", "96", "3no", "7UA", "44K", "5NN", "6kL", "Fs", "0eA", "Xq", "1kb", "5PL", "61F", +"7KC", "4nA", "0Uo", "2U", "39U", "8QG", "4cq", "5f1", "aRl", "796", "0vs", "UA", "2LQ", "0yr", "767", "63w", "5i0", "4lp", "9Ng", "0d", +"2oM", "0Zn", "55i", "6Tc", "6zO", "4Ol", "00J", "Wp", "6FK", "4sh", "0HF", "kt", "EX", "P9", "5Me", "6hg", "5U4", "4Pt", "0kZ", "Hh", +"fD", "0Ev", "5ny", "4k9", "4d8", "45Q", "0Jw", "iE", "Gi", "10r", "5OT", "5Z5", "6gf", "4RE", "0ik", "JY", "du", "0GG", "48a", "6IJ", +"4bk", "6WH", "zw", "0YE", "03a", "2BJ", "6yd", "4LG", "4Aw", "5D7", "Yk", "0zY", "0Tu", "3O", "6Zx", "bUh", "54s", "413", "xF", "1KU", +"01P", "Vj", "5K6", "4Nv", "4CF", "62m", "2MK", "0xh", "0VD", "uv", "6XI", "4mj", "5NS", "6kQ", "Fn", "11u", "0Kp", "hB", "aoo", "44V", +"49f", "6HM", "er", "1Va", "0hl", "3Mn", "6fa", "4SB", "5Lb", "7yA", "W", "0gm", "0IA", "js", "6GL", "46g", "bEl", "hyW", "gC", "0Dq", +"8cE", "Io", "5T3", "4Qs", "5J1", "4Oq", "00W", "Wm", "yA", "0Zs", "55t", "404", "6YN", "4lm", "0WC", "0y", "2LL", "0yo", "4BA", "63j", +"6xc", "bws", "02f", "2CM", "2ma", "0XB", "4cl", "6VO", "a5e", "bTo", "0Ur", "2H", "Xl", "86N", "5PQ", "5E0", "dh", "0GZ", "5lU", "5y4", +"4G9", "4RX", "0iv", "JD", "Gt", "0dF", "5OI", "6jK", "6Dg", "45L", "81", "iX", "fY", "70", "5nd", "6Kf", "6eJ", "4Pi", "0kG", "Hu", +"EE", "0fw", "5Mx", "4H8", "5v5", "4su", "1Xz", "ki", "0VY", "1c", "5h7", "4mw", "5Sz", "62p", "2MV", "0xu", "01M", "Vw", "7ki", "4Nk", +"54n", "6Ud", "2nJ", "1KH", "0Th", "3R", "6Ze", "4oF", "4Aj", "60A", "Yv", "0zD", "0wt", "TF", "6yy", "4LZ", "4bv", "5g6", "zj", "0YX", +"0Kt", "hF", "6Ey", "44R", "5NW", "6kU", "Fj", "0eX", "0hh", "KZ", "6fe", "4SF", "49b", "6HI", "ev", "0FD", "0IE", "jw", "6GH", "46c", +"5Lf", "6id", "S", "0gi", "0jY", "Ik", "5T7", "4Qw", "5oz", "6Jx", "gG", "0Du", "yE", "0Zw", "4aY", "400", "5J5", "4Ou", "00S", "Wi", +"ZY", "O8", "4BE", "63n", "6YJ", "4li", "0WG", "tu", "2me", "0XF", "4ch", "6VK", "6xg", "4MD", "02b", "UX", "Xh", "3K9", "5PU", "5E4", +"7KZ", "4nX", "0Uv", "2L", "73V", "bho", "0ir", "1l2", "dl", "335", "48x", "5y0", "6Dc", "45H", "85", "3ol", "Gp", "0dB", "5OM", "6jO", +"6eN", "4Pm", "0kC", "Hq", "24D", "74", "bDr", "6Kb", "529", "47y", "8AG", "km", "EA", "0fs", "bgn", "aBl", "774", "62t", "199", "0xq", +"9Od", "1g", "5h3", "4ms", "54j", "7EA", "2nN", "1KL", "01I", "Vs", "7km", "4No", "4An", "60E", "Yr", "1ja", "0Tl", "3V", "6Za", "4oB", +"4br", "5g2", "zn", "8PD", "03x", "TB", "aSo", "785", "49n", "6HE", "ez", "0FH", "0hd", "KV", "6fi", "4SJ", "bdI", "6kY", "Ff", "0eT", +"0Kx", "hJ", "4e7", "4pV", "5ov", "4j6", "gK", "0Dy", "0jU", "Ig", "6dX", "5AZ", "5Lj", "6ih", "DW", "Q6", "0II", "28b", "6GD", "46o", +"6YF", "4le", "0WK", "0q", "ZU", "O4", "4BI", "63b", "5J9", "4Oy", "0tW", "We", "yI", "1JZ", "4aU", "4t4", "7KV", "4nT", "0Uz", "vH", +"Xd", "1kw", "5PY", "5E8", "6xk", "4MH", "02n", "UT", "2mi", "0XJ", "4cd", "6VG", "2Qm", "0dN", "5OA", "6jC", "6Do", "45D", "89", "iP", +"0R3", "0GR", "48t", "acM", "4G1", "4RP", "94O", "JL", "EM", "12V", "5Mp", "4H0", "525", "47u", "0HS", "ka", "fQ", "78", "5nl", "6Kn", +"6eB", "4Pa", "0kO", "3NM", "01E", "3PO", "7ka", "4Nc", "54f", "6Ul", "xS", "m2", "0VQ", "1k", "a6F", "59V", "4CS", "4V2", "195", "85m", +"03t", "TN", "4Y3", "4LR", "56W", "a9G", "zb", "0YP", "b3", "3Z", "6Zm", "4oN", "4Ab", "60I", "2Oo", "0zL", "1xA", "KR", "6fm", "4SN", +"49j", "6HA", "27g", "0FL", "8Bd", "hN", "4e3", "44Z", "bdM", "aAO", "Fb", "0eP", "0jQ", "Ic", "70u", "655", "5or", "4j2", "gO", "8Me", +"0IM", "28f", "7Wa", "46k", "5Ln", "6il", "DS", "Q2", "ZQ", "O0", "4BM", "63f", "6YB", "4la", "0WO", "0u", "yM", "8Sg", "4aQ", "408", +"aPL", "b1F", "0tS", "Wa", "0n3", "1ks", "bzO", "61W", "7KR", "4nP", "214", "2D", "2mm", "0XN", "57I", "6VC", "6xo", "4ML", "02j", "UP", +"6Dk", "4qH", "0Jf", "iT", "Gx", "0dJ", "5OE", "6jG", "4G5", "4RT", "0iz", "JH", "dd", "0GV", "48p", "5y8", "521", "47q", "0HW", "ke", +"EI", "12R", "5Mt", "4H4", "6eF", "4Pe", "0kK", "Hy", "fU", "s4", "5nh", "6Kj", "54b", "6Uh", "xW", "m6", "01A", "3PK", "7ke", "4Ng", +"4CW", "4V6", "191", "0xy", "0VU", "1o", "6XX", "59R", "4bz", "6WY", "zf", "0YT", "03p", "TJ", "4Y7", "4LV", "4Af", "60M", "Yz", "0zH", +"b7", "wV", "6Zi", "4oJ", "5H3", "4Ms", "02U", "Uo", "2mR", "0Xq", "57v", "426", "7Km", "4no", "0UA", "vs", "2NN", "1kL", "5Pb", "61h", +"6za", "4OB", "00d", "2AO", "yr", "1Ja", "4an", "6TM", "a7g", "58w", "0Wp", "0J", "Zn", "84L", "4Br", "5G2", "5LQ", "5Y0", "d", "13w", +"0Ir", "1L2", "amm", "46T", "5oM", "6JO", "gp", "0DB", "0jn", "3Ol", "6dc", "5Aa", "bdr", "6kb", "2PL", "0eo", "0KC", "hq", "6EN", "44e", +"49U", "abl", "eA", "0Fs", "8aG", "Km", "5V1", "4Sq", "1Dz", "3a", "5j5", "4ou", "4AY", "4T8", "YE", "0zw", "03O", "Tu", "6yJ", "4Li", +"4bE", "6Wf", "zY", "o8", "0Vj", "1P", "6Xg", "4mD", "4Ch", "62C", "2Me", "0xF", "0uv", "VD", "7kZ", "4NX", "5pU", "5e4", "xh", "3k9", +"fj", "0EX", "5nW", "6KU", "6ey", "4PZ", "0kt", "HF", "Ev", "0fD", "5MK", "6hI", "6Fe", "47N", "0Hh", "kZ", "26B", "52", "48O", "6Id", +"6gH", "4Rk", "0iE", "Jw", "GG", "0du", "5Oz", "6jx", "5t7", "4qw", "0JY", "ik", "2mV", "0Xu", "57r", "422", "5H7", "4Mw", "02Q", "Uk", +"2NJ", "1kH", "5Pf", "61l", "7Ki", "4nk", "0UE", "vw", "yv", "0ZD", "4aj", "6TI", "6ze", "4OF", "0th", "WZ", "Zj", "0yX", "4Bv", "5G6", +"6Yy", "4lZ", "0Wt", "0N", "0Iv", "jD", "4g9", "46P", "5LU", "5Y4", "Dh", "0gZ", "0jj", "IX", "6dg", "4QD", "5oI", "6JK", "gt", "0DF", +"0KG", "hu", "6EJ", "44a", "5Nd", "6kf", "FY", "S8", "1xz", "Ki", "5V5", "4Su", "49Q", "4h8", "eE", "0Fw", "756", "60v", "YA", "0zs", +"9Mf", "3e", "5j1", "4oq", "4bA", "6Wb", "2lL", "0Yo", "03K", "Tq", "6yN", "4Lm", "4Cl", "62G", "2Ma", "0xB", "0Vn", "1T", "6Xc", "59i", +"54Y", "5e0", "xl", "8RF", "01z", "1p2", "aQm", "b0g", "71T", "bjm", "0kp", "HB", "fn", "317", "5nS", "6KQ", "6Fa", "47J", "0Hl", "29G", +"Er", "12i", "5MO", "6hM", "6gL", "4Ro", "0iA", "Js", "26F", "56", "48K", "7YA", "5t3", "4qs", "8CE", "io", "GC", "0dq", "bel", "hYW", +"7Ke", "4ng", "0UI", "2s", "XW", "M6", "5Pj", "6uh", "6xX", "6m9", "0vU", "Ug", "2mZ", "0Xy", "4cW", "4v6", "4y7", "4lV", "0Wx", "0B", +"Zf", "0yT", "4Bz", "63Q", "6zi", "4OJ", "B7", "WV", "yz", "0ZH", "4af", "6TE", "5oE", "6JG", "gx", "0DJ", "0jf", "IT", "6dk", "4QH", +"5LY", "5Y8", "l", "0gV", "0Iz", "jH", "4g5", "4rT", "5mt", "4h4", "eI", "1VZ", "0hW", "Ke", "5V9", "4Sy", "5Nh", "6kj", "FU", "S4", +"0KK", "hy", "6EF", "44m", "03G", "2Bl", "6yB", "4La", "4bM", "6Wn", "zQ", "o0", "0TS", "3i", "a4D", "bUN", "4AQ", "4T0", "YM", "87o", +"01v", "VL", "7kR", "4NP", "54U", "hft", "0N3", "1Ks", "0Vb", "1X", "6Xo", "4mL", "5SA", "62K", "2Mm", "0xN", "2So", "0fL", "5MC", "6hA", +"6Fm", "47F", "1XA", "kR", "fb", "0EP", "bDM", "aaO", "4E3", "4PR", "8bd", "HN", "GO", "10T", "5Or", "4J2", "507", "45w", "0JQ", "ic", +"dS", "q2", "48G", "6Il", "73i", "4Rc", "0iM", "3LO", "XS", "M2", "5Pn", "61d", "7Ka", "4nc", "0UM", "2w", "39w", "8Qe", "4cS", "4v2", +"aRN", "b3D", "02Y", "Uc", "Zb", "0yP", "bxM", "63U", "4y3", "4lR", "236", "0F", "2oo", "0ZL", "4ab", "6TA", "6zm", "4ON", "B3", "WR", +"0jb", "IP", "6do", "4QL", "5oA", "6JC", "25e", "0DN", "9PG", "jL", "4g1", "46X", "686", "aCM", "h", "0gR", "0hS", "Ka", "72w", "677", +"49Y", "4h0", "eM", "8Og", "0KO", "3nM", "6EB", "44i", "5Nl", "6kn", "FQ", "S0", "4bI", "6Wj", "zU", "o4", "03C", "Ty", "6yF", "4Le", +"4AU", "4T4", "YI", "1jZ", "0TW", "3m", "5j9", "4oy", "54Q", "5e8", "xd", "1Kw", "01r", "VH", "7kV", "4NT", "4Cd", "62O", "2Mi", "0xJ", +"0Vf", "uT", "6Xk", "4mH", "6Fi", "47B", "0Hd", "kV", "Ez", "0fH", "5MG", "6hE", "4E7", "4PV", "0kx", "HJ", "ff", "0ET", "bDI", "6KY", +"503", "45s", "0JU", "ig", "GK", "0dy", "5Ov", "4J6", "6gD", "4Rg", "0iI", "3LK", "dW", "q6", "48C", "6Ih", "4Z2", "4OS", "00u", "WO", +"yc", "0ZQ", "55V", "hgw", "6Yl", "4lO", "a2", "tS", "2Ln", "0yM", "4Bc", "63H", "6xA", "4Mb", "02D", "2Co", "2mC", "n3", "4cN", "6Vm", +"a5G", "bTM", "0UP", "2j", "XN", "86l", "5Ps", "4U3", "5Nq", "4K1", "FL", "11W", "0KR", "3nP", "514", "44t", "49D", "6Ho", "eP", "49", +"0hN", "3ML", "6fC", "5CA", "aV1", "6iB", "u", "0gO", "0Ic", "jQ", "6Gn", "46E", "bEN", "hyu", "ga", "0DS", "8cg", "IM", "4D0", "4QQ", +"1FZ", "1A", "4x4", "4mU", "4Cy", "5F9", "0m6", "0xW", "C4", "VU", "7kK", "4NI", "54L", "6UF", "xy", "1Kj", "0TJ", "3p", "6ZG", "4od", +"4AH", "60c", "YT", "L5", "0wV", "Td", "5I8", "4Lx", "4bT", "4w5", "zH", "0Yz", "dJ", "0Gx", "5lw", "4i7", "6gY", "4Rz", "0iT", "Jf", +"GV", "R7", "5Ok", "6ji", "6DE", "45n", "0JH", "iz", "24b", "0EI", "5nF", "6KD", "6eh", "4PK", "0ke", "HW", "Eg", "0fU", "5MZ", "6hX", +"4f6", "4sW", "0Hy", "kK", "yg", "0ZU", "55R", "6TX", "4Z6", "4OW", "00q", "WK", "2Lj", "0yI", "4Bg", "63L", "6Yh", "4lK", "a6", "tW", +"2mG", "n7", "4cJ", "6Vi", "6xE", "4Mf", "0vH", "Uz", "XJ", "1kY", "5Pw", "4U7", "7Kx", "4nz", "0UT", "2n", "0KV", "hd", "510", "44p", +"5Nu", "4K5", "FH", "0ez", "0hJ", "Kx", "6fG", "4Sd", "5mi", "6Hk", "eT", "p5", "0Ig", "jU", "6Gj", "46A", "5LD", "6iF", "q", "0gK", +"1zZ", "II", "4D4", "4QU", "5oX", "5z9", "ge", "0DW", "byN", "62V", "0m2", "0xS", "225", "1E", "4x0", "4mQ", "54H", "6UB", "2nl", "1Kn", +"C0", "VQ", "7kO", "4NM", "4AL", "60g", "YP", "L1", "0TN", "3t", "6ZC", "ae0", "4bP", "439", "zL", "8Pf", "03Z", "0b3", "aSM", "b2G", +"73t", "664", "0iP", "Jb", "dN", "8Nd", "48Z", "4i3", "6DA", "45j", "0JL", "3oN", "GR", "R3", "5Oo", "6jm", "6el", "4PO", "0ka", "HS", +"24f", "0EM", "5nB", "aaR", "4f2", "4sS", "8Ae", "kO", "Ec", "0fQ", "695", "aBN", "6Yd", "4lG", "0Wi", "0S", "Zw", "0yE", "4Bk", "6wH", +"6zx", "buh", "0tu", "WG", "yk", "0ZY", "4aw", "5d7", "5k6", "4nv", "0UX", "2b", "XF", "1kU", "741", "61q", "6xI", "4Mj", "02L", "Uv", +"2mK", "0Xh", "4cF", "6Ve", "49L", "6Hg", "eX", "41", "0hF", "Kt", "6fK", "4Sh", "5Ny", "4K9", "FD", "0ev", "0KZ", "hh", "5u4", "4pt", +"5oT", "5z5", "gi", "1Tz", "0jw", "IE", "4D8", "4QY", "5LH", "6iJ", "Du", "0gG", "0Ik", "jY", "6Gf", "46M", "01g", "3Pm", "7kC", "4NA", +"54D", "6UN", "xq", "1Kb", "0Vs", "1I", "a6d", "59t", "4Cq", "5F1", "d3G", "85O", "03V", "Tl", "5I0", "4Lp", "56u", "435", "2lQ", "0Yr", +"0TB", "3x", "6ZO", "4ol", "5Qa", "60k", "2OM", "0zn", "2QO", "0dl", "5Oc", "6ja", "6DM", "45f", "1Za", "ir", "dB", "0Gp", "48V", "aco", +"5W2", "4Rr", "94m", "Jn", "Eo", "12t", "5MR", "5X3", "aln", "47W", "0Hq", "kC", "fs", "0EA", "5nN", "6KL", "71I", "4PC", "0km", "3No", +"Zs", "0yA", "4Bo", "63D", "7IA", "4lC", "0Wm", "0W", "yo", "8SE", "4as", "5d3", "aPn", "b1d", "00y", "WC", "XB", "1kQ", "745", "61u", +"5k2", "4nr", "9Le", "2f", "2mO", "0Xl", "4cB", "6Va", "6xM", "4Mn", "02H", "Ur", "0hB", "Kp", "6fO", "4Sl", "49H", "6Hc", "27E", "45", +"8BF", "hl", "518", "44x", "bdo", "aAm", "2PQ", "0er", "0js", "IA", "70W", "bkn", "5oP", "5z1", "gm", "304", "0Io", "28D", "6Gb", "46I", +"5LL", "6iN", "y", "0gC", "5pH", "6UJ", "xu", "1Kf", "C8", "VY", "7kG", "4NE", "4Cu", "5F5", "2Mx", "1hz", "0Vw", "1M", "4x8", "4mY", +"4bX", "431", "zD", "0Yv", "03R", "Th", "5I4", "4Lt", "4AD", "60o", "YX", "L9", "0TF", "wt", "6ZK", "4oh", "6DI", "45b", "0JD", "iv", +"GZ", "0dh", "5Og", "6je", "5W6", "4Rv", "0iX", "Jj", "dF", "0Gt", "48R", "6Iy", "6Fx", "47S", "0Hu", "kG", "Ek", "0fY", "5MV", "5X7", +"6ed", "4PG", "0ki", "3Nk", "fw", "0EE", "5nJ", "6KH", "356", "bo", "6OP", "4zs", "bnl", "75U", "LC", "0oq", "0bA", "As", "6lL", "4Yo", +"43K", "7RA", "2yN", "0Lm", "17", "22G", "6Ma", "4xB", "4Vn", "6cM", "Nr", "19i", "14Y", "CB", "aDo", "bam", "41z", "5p2", "mn", "8GD", +"7d", "8YF", "4kp", "5n0", "64w", "717", "1nS", "2KQ", "Pp", "07J", "4Hl", "69G", "6Sc", "52i", "1MO", "2hM", "5U", "0Ro", "4iA", "7LC", +"66F", "4Gm", "08K", "3YA", "RA", "0qs", "b4f", "aUl", "5a1", "4dq", "8VG", "8e", "6mV", "4Xu", "17r", "022", "nE", "0Mw", "42Q", "4c8", +"6NJ", "5kH", "1Pf", "cu", "MY", "X8", "4UE", "74O", "6og", "4ZD", "W9", "BX", "lt", "0OF", "4th", "6AK", "4l9", "4yX", "0Bv", "aD", +"Oh", "0lZ", "4Wt", "5R4", "4Iv", "5L6", "Qj", "06P", "1LU", "1Y4", "463", "4gZ", "4jj", "7Oh", "rv", "0QD", "1oI", "2JK", "65m", "4DF", +"4KG", "7nE", "2EJ", "04a", "1Nd", "2kf", "6PH", "4ek", "5xz", "492", "4O", "0Su", "09Q", "0h8", "5C7", "4Fw", "5Dz", "6ax", "LG", "0ou", +"0AY", "bk", "6OT", "4zw", "43O", "6Bd", "2yJ", "0Li", "0bE", "Aw", "6lH", "4Yk", "4Vj", "6cI", "Nv", "0mD", "13", "22C", "6Me", "4xF", +"4uv", "5p6", "mj", "0NX", "1pU", "CF", "6ny", "7k9", "4P9", "4EX", "1nW", "2KU", "sh", "0PZ", "4kt", "5n4", "6Sg", "4fD", "k9", "2hI", +"Pt", "07N", "4Hh", "69C", "66B", "4Gi", "08O", "2Id", "5Q", "d8", "4iE", "7LG", "5a5", "4du", "1Oz", "8a", "RE", "0qw", "4JY", "aUh", +"nA", "0Ms", "42U", "ail", "6mR", "4Xq", "17v", "026", "3Km", "0no", "4UA", "74K", "6NN", "5kL", "1Pb", "cq", "lp", "0OB", "40d", "6AO", +"6oc", "5Ja", "0an", "2TM", "Ol", "18w", "4Wp", "5R0", "afm", "bCo", "0Br", "1G2", "1LQ", "1Y0", "467", "53w", "4Ir", "5L2", "Qn", "06T", +"1oM", "2JO", "65i", "4DB", "4jn", "7Ol", "6z", "1Aa", "8WY", "2kb", "6PL", "4eo", "4KC", "7nA", "2EN", "04e", "09U", "d6E", "5C3", "4Fs", +"bRl", "496", "4K", "0Sq", "0bI", "2Wj", "6lD", "4Yg", "43C", "6Bh", "oW", "z6", "0AU", "bg", "6OX", "5jZ", "4TW", "4A6", "LK", "0oy", +"14Q", "CJ", "4N7", "5Kw", "41r", "542", "mf", "0NT", "u7", "22O", "6Mi", "4xJ", "4Vf", "6cE", "Nz", "0mH", "Px", "07B", "4Hd", "69O", +"6Sk", "4fH", "k5", "2hE", "7l", "0PV", "4kx", "5n8", "4P5", "4ET", "83j", "2KY", "RI", "05s", "4JU", "7oW", "5a9", "4dy", "1Ov", "8m", +"qU", "d4", "4iI", "7LK", "66N", "4Ge", "08C", "2Ih", "6NB", "a59", "1Pn", "21d", "MQ", "X0", "4UM", "74G", "79w", "bbN", "0cS", "0v2", +"nM", "8Dg", "42Y", "4c0", "4l1", "4yP", "8Kf", "aL", "0y3", "0lR", "636", "76v", "6oo", "4ZL", "W1", "BP", "2zm", "0ON", "40h", "6AC", +"4jb", "auS", "6v", "0QL", "I3", "2JC", "65e", "4DN", "b7E", "68U", "Qb", "06X", "286", "dSl", "4r3", "4gR", "4hS", "7MQ", "4G", "277", +"09Y", "0h0", "67T", "b8D", "4KO", "7nM", "SS", "F2", "1Nl", "9w", "azR", "4ec", "43G", "6Bl", "oS", "z2", "0bM", "2Wn", "78i", "4Yc", +"4TS", "4A2", "LO", "8fe", "0AQ", "bc", "aeN", "cPm", "41v", "546", "mb", "0NP", "14U", "CN", "4N3", "5Ks", "4Vb", "6cA", "2Xo", "0mL", +"u3", "22K", "6Mm", "4xN", "6So", "4fL", "k1", "2hA", "2Fm", "07F", "5XA", "69K", "4P1", "4EP", "83n", "d5f", "7h", "0PR", "bQO", "a0E", +"hbu", "50T", "1Or", "8i", "RM", "05w", "4JQ", "7oS", "66J", "4Ga", "08G", "2Il", "5Y", "d0", "4iM", "7LO", "MU", "X4", "4UI", "74C", +"6NF", "5kD", "1Pj", "cy", "nI", "2m9", "4vU", "4c4", "6mZ", "4Xy", "0cW", "0v6", "Od", "0lV", "4Wx", "5R8", "4l5", "4yT", "0Bz", "aH", +"lx", "0OJ", "40l", "6AG", "6ok", "4ZH", "W5", "BT", "I7", "2JG", "65a", "4DJ", "4jf", "7Od", "6r", "0QH", "1LY", "1Y8", "4r7", "4gV", +"4Iz", "68Q", "Qf", "0rT", "1mt", "0h4", "67P", "5VZ", "4hW", "7MU", "4C", "0Sy", "1Nh", "9s", "6PD", "4eg", "4KK", "7nI", "SW", "F6", +"8Je", "22V", "4m2", "4xS", "625", "77u", "Nc", "0mQ", "V2", "CS", "6nl", "5Kn", "41k", "7Pa", "3kO", "0NM", "0AL", "20g", "6OA", "4zb", +"4TN", "6am", "LR", "Y3", "0bP", "Ab", "78t", "bcM", "43Z", "4b3", "oN", "8Ed", "5D", "264", "4iP", "489", "66W", "b9G", "08Z", "0i3", +"RP", "G1", "4JL", "7oN", "6QC", "50I", "1Oo", "8t", "7u", "0PO", "4ka", "7Nc", "64f", "4EM", "H0", "963", "Pa", "0sS", "b6F", "69V", +"478", "4fQ", "295", "dRo", "4O4", "4ZU", "15R", "BI", "le", "0OW", "40q", "551", "6Lj", "4yI", "t4", "aU", "Oy", "0lK", "4We", "6bF", +"6mG", "4Xd", "0cJ", "2Vi", "nT", "0Mf", "4vH", "6Ck", "adI", "5kY", "1Pw", "cd", "MH", "0nz", "4UT", "7pV", "4KV", "7nT", "SJ", "04p", +"1Nu", "9n", "6PY", "4ez", "4hJ", "7MH", "pV", "e7", "1mi", "2Hk", "67M", "4Ff", "4Ig", "68L", "2Gj", "06A", "j6", "2iF", "6Rh", "4gK", +"5zZ", "7Oy", "6o", "0QU", "1oX", "1z9", "4Q6", "4DW", "5FZ", "6cX", "Ng", "0mU", "0Cy", "1F9", "4m6", "4xW", "41o", "7Pe", "3kK", "0NI", +"V6", "CW", "6nh", "5Kj", "4TJ", "6ai", "LV", "Y7", "0AH", "bz", "6OE", "4zf", "4wV", "4b7", "oJ", "0Lx", "0bT", "Af", "6lY", "4Yz", +"5B8", "4Gx", "1lw", "0i7", "qH", "0Rz", "4iT", "7LV", "6QG", "4dd", "1Ok", "8p", "RT", "G5", "4JH", "7oJ", "64b", "4EI", "H4", "2KD", +"7q", "0PK", "4ke", "7Ng", "4s4", "4fU", "1MZ", "2hX", "Pe", "0sW", "4Hy", "5M9", "la", "0OS", "40u", "555", "4O0", "4ZQ", "15V", "BM", +"2Yl", "0lO", "4Wa", "6bB", "6Ln", "4yM", "08", "aQ", "nP", "0Mb", "42D", "6Co", "6mC", "5HA", "0cN", "2Vm", "ML", "8gf", "4UP", "74Z", +"adM", "bAO", "1Ps", "0U3", "1Nq", "9j", "azO", "51W", "4KR", "7nP", "SN", "04t", "09D", "2Ho", "67I", "4Fb", "4hN", "7ML", "4Z", "e3", +"j2", "2iB", "6Rl", "4gO", "4Ic", "68H", "2Gn", "06E", "82m", "d4e", "4Q2", "4DS", "bPL", "a1F", "6k", "0QQ", "1pH", "2UJ", "6nd", "5Kf", +"41c", "7Pi", "mw", "0NE", "0Cu", "1F5", "6Mx", "5hz", "4Vw", "5S7", "Nk", "0mY", "0bX", "Aj", "6lU", "4Yv", "43R", "6By", "oF", "0Lt", +"0AD", "bv", "6OI", "4zj", "4TF", "6ae", "LZ", "0oh", "RX", "G9", "4JD", "7oF", "6QK", "4dh", "1Og", "2je", "5L", "0Rv", "4iX", "481", +"5B4", "4Gt", "08R", "2Iy", "Pi", "07S", "4Hu", "5M5", "470", "4fY", "1MV", "1X7", "su", "0PG", "4ki", "7Nk", "64n", "4EE", "H8", "2KH", +"6Lb", "4yA", "04", "23D", "Oq", "0lC", "4Wm", "6bN", "aEl", "c4G", "0as", "BA", "lm", "8FG", "40y", "559", "6NS", "5kQ", "345", "cl", +"1k2", "0nr", "boo", "74V", "6mO", "4Xl", "0cB", "2Va", "2xM", "0Mn", "42H", "6Cc", "4hB", "aws", "4V", "0Sl", "09H", "2Hc", "67E", "4Fn", +"b5e", "aTo", "SB", "04x", "8WD", "9f", "6PQ", "4er", "4js", "5o3", "6g", "8XE", "1oP", "1z1", "65t", "704", "4Io", "68D", "Qs", "06I", +"1LL", "2iN", "7BA", "4gC", "41g", "7Pm", "ms", "0NA", "14D", "2UN", "aDr", "5Kb", "4Vs", "5S3", "No", "19t", "0Cq", "1F1", "agn", "bBl", +"43V", "aho", "oB", "0Lp", "16u", "An", "6lQ", "4Yr", "4TB", "6aa", "2ZO", "0ol", "1Qa", "br", "6OM", "4zn", "6QO", "4dl", "1Oc", "8x", +"2DM", "05f", "5Za", "7oB", "5B0", "4Gp", "08V", "d7F", "5H", "0Rr", "bSo", "485", "474", "52t", "1MR", "1X3", "Pm", "07W", "4Hq", "5M1", +"64j", "4EA", "1nN", "2KL", "7y", "0PC", "4km", "7No", "Ou", "0lG", "4Wi", "6bJ", "6Lf", "4yE", "00", "aY", "li", "8FC", "4tu", "5q5", +"4O8", "4ZY", "0aw", "BE", "MD", "0nv", "4UX", "74R", "6NW", "5kU", "341", "ch", "nX", "0Mj", "42L", "6Cg", "6mK", "4Xh", "0cF", "2Ve", +"09L", "2Hg", "67A", "4Fj", "4hF", "7MD", "4R", "0Sh", "1Ny", "9b", "6PU", "4ev", "4KZ", "7nX", "SF", "0pt", "1oT", "1z5", "65p", "5Tz", +"4jw", "5o7", "6c", "0QY", "1LH", "2iJ", "6Rd", "4gG", "4Ik", "7li", "Qw", "06M", "7F", "246", "4kR", "7NP", "64U", "col", "1nq", "0k1", +"PR", "E3", "4HN", "69e", "6SA", "4fb", "1Mm", "2ho", "5w", "0RM", "4ic", "7La", "66d", "4GO", "J2", "2IB", "Rc", "05Y", "b4D", "aUN", +"4q2", "4dS", "8Ve", "8G", "8Hg", "bM", "4o0", "4zQ", "607", "75w", "La", "0oS", "T0", "AQ", "6ln", "4YM", "43i", "6BB", "2yl", "0LO", +"0CN", "22e", "6MC", "5hA", "4VL", "6co", "NP", "0mb", "1ps", "0u3", "aDM", "baO", "41X", "7PR", "mL", "8Gf", "4IT", "7lV", "QH", "06r", +"1Lw", "0I7", "5b8", "4gx", "4jH", "7OJ", "rT", "g5", "1ok", "2Ji", "65O", "4Dd", "4Ke", "7ng", "Sy", "04C", "h4", "2kD", "6Pj", "4eI", +"4hy", "5m9", "4m", "0SW", "09s", "2HX", "4S4", "4FU", "4M6", "4XW", "0cy", "1f9", "ng", "0MU", "42s", "573", "6Nh", "5kj", "v6", "cW", +"3KK", "0nI", "4Ug", "74m", "6oE", "4Zf", "0aH", "Bz", "lV", "y7", "40B", "6Ai", "582", "4yz", "0BT", "af", "OJ", "0lx", "4WV", "4B7", +"64Q", "4Ez", "1nu", "0k5", "7B", "0Px", "4kV", "7NT", "6SE", "4ff", "1Mi", "2hk", "PV", "E7", "4HJ", "69a", "6rh", "4GK", "J6", "2IF", +"5s", "0RI", "4ig", "7Le", "4q6", "4dW", "1OX", "8C", "Rg", "0qU", "5ZZ", "7oy", "4Ty", "5Q9", "Le", "0oW", "1QZ", "bI", "4o4", "4zU", +"43m", "6BF", "oy", "0LK", "T4", "AU", "6lj", "4YI", "4VH", "6ck", "NT", "0mf", "0CJ", "22a", "6MG", "4xd", "4uT", "7PV", "mH", "0Nz", +"1pw", "Cd", "aDI", "5KY", "1Ls", "0I3", "axM", "53U", "4IP", "7lR", "QL", "06v", "1oo", "2Jm", "65K", "5TA", "4jL", "7ON", "6X", "g1", +"h0", "9Y", "6Pn", "4eM", "4Ka", "7nc", "2El", "04G", "09w", "d6g", "4S0", "4FQ", "bRN", "a3D", "4i", "0SS", "nc", "0MQ", "42w", "577", +"4M2", "4XS", "17T", "dlm", "3KO", "0nM", "4Uc", "74i", "6Nl", "5kn", "v2", "cS", "lR", "y3", "40F", "6Am", "6oA", "4Zb", "0aL", "2To", +"ON", "18U", "4WR", "4B3", "586", "bCM", "0BP", "ab", "PZ", "0sh", "4HF", "69m", "6SI", "4fj", "1Me", "2hg", "7N", "0Pt", "4kZ", "7NX", +"6pU", "4Ev", "1ny", "0k9", "Rk", "05Q", "4Jw", "5O7", "452", "50r", "1OT", "8O", "qw", "0RE", "4ik", "7Li", "66l", "4GG", "08a", "2IJ", +"T8", "AY", "6lf", "4YE", "43a", "6BJ", "ou", "0LG", "0Aw", "bE", "4o8", "4zY", "4Tu", "5Q5", "Li", "8fC", "14s", "Ch", "6nW", "5KU", +"41P", "7PZ", "mD", "0Nv", "0CF", "22m", "6MK", "4xh", "4VD", "6cg", "NX", "0mj", "5za", "7OB", "6T", "0Qn", "1oc", "2Ja", "65G", "4Dl", +"b7g", "68w", "1w2", "06z", "8UF", "dSN", "5b0", "4gp", "4hq", "5m1", "4e", "8ZG", "1mR", "1x3", "67v", "726", "4Km", "7no", "Sq", "04K", +"1NN", "9U", "6Pb", "4eA", "adr", "5kb", "26", "21F", "Ms", "0nA", "4Uo", "74e", "79U", "bbl", "0cq", "1f1", "no", "396", "4vs", "5s3", +"6LQ", "4yr", "367", "an", "OB", "0lp", "bmm", "76T", "6oM", "4Zn", "15i", "Br", "2zO", "0Ol", "40J", "6Aa", "6SM", "4fn", "1Ma", "2hc", +"2FO", "07d", "4HB", "69i", "64Y", "4Er", "83L", "d5D", "7J", "0Pp", "bQm", "a0g", "456", "50v", "1OP", "8K", "Ro", "05U", "4Js", "5O3", +"66h", "4GC", "08e", "2IN", "qs", "0RA", "4io", "7Lm", "43e", "6BN", "oq", "0LC", "0bo", "2WL", "6lb", "4YA", "4Tq", "5Q1", "Lm", "8fG", +"0As", "bA", "ael", "cPO", "41T", "ajm", "1K2", "0Nr", "14w", "Cl", "6nS", "5KQ", "5Fa", "6cc", "2XM", "0mn", "0CB", "22i", "6MO", "4xl", +"1og", "2Je", "65C", "4Dh", "4jD", "7OF", "6P", "g9", "3l9", "2iy", "5b4", "4gt", "4IX", "68s", "QD", "0rv", "1mV", "1x7", "4S8", "4FY", +"4hu", "5m5", "4a", "1Cz", "h8", "9Q", "6Pf", "4eE", "4Ki", "7nk", "Su", "04O", "Mw", "0nE", "4Uk", "74a", "6Nd", "5kf", "22", "21B", +"nk", "0MY", "4vw", "5s7", "6mx", "5Hz", "0cu", "1f5", "OF", "0lt", "4WZ", "6by", "6LU", "4yv", "0BX", "aj", "lZ", "0Oh", "40N", "6Ae", +"6oI", "4Zj", "0aD", "Bv", "5f", "9Ke", "4ir", "5l2", "66u", "735", "08x", "1y0", "Rr", "05H", "4Jn", "7ol", "6Qa", "4dB", "1OM", "8V", +"7W", "0Pm", "4kC", "7NA", "64D", "4Eo", "83Q", "2Kb", "PC", "07y", "b6d", "69t", "5c3", "4fs", "8TE", "dRM", "374", "22t", "599", "4xq", +"bln", "77W", "NA", "0ms", "14j", "Cq", "6nN", "5KL", "41I", "7PC", "3km", "0No", "35", "20E", "6Oc", "5ja", "4Tl", "6aO", "Lp", "0oB", +"0br", "1g2", "78V", "bco", "43x", "568", "ol", "385", "4Kt", "5N4", "Sh", "04R", "1NW", "9L", "441", "4eX", "4hh", "7Mj", "pt", "0SF", +"K9", "2HI", "67o", "4FD", "4IE", "68n", "QY", "0", "1Lf", "2id", "6RJ", "4gi", "4jY", "auh", "6M", "0Qw", "1oz", "2Jx", "5A5", "4Du", +"6oT", "4Zw", "0aY", "Bk", "lG", "0Ou", "40S", "6Ax", "6LH", "4yk", "0BE", "aw", "2YJ", "0li", "4WG", "6bd", "6me", "4XF", "0ch", "2VK", +"nv", "0MD", "42b", "6CI", "6Ny", "7K9", "1PU", "cF", "Mj", "0nX", "4Uv", "5P6", "66q", "4GZ", "1lU", "1y4", "5b", "0RX", "4iv", "5l6", +"6Qe", "4dF", "1OI", "8R", "Rv", "05L", "4Jj", "7oh", "6pH", "4Ek", "1nd", "2Kf", "7S", "0Pi", "4kG", "7NE", "5c7", "4fw", "1Mx", "0H8", +"PG", "0su", "5Xz", "69p", "4VY", "4C8", "NE", "0mw", "1Sz", "22p", "6MV", "4xu", "41M", "7PG", "mY", "x8", "14n", "Cu", "6nJ", "5KH", +"4Th", "6aK", "Lt", "0oF", "31", "bX", "6Og", "4zD", "4wt", "5r4", "oh", "0LZ", "0bv", "AD", "4L9", "4YX", "1NS", "9H", "445", "51u", +"4Kp", "5N0", "Sl", "04V", "09f", "2HM", "67k", "5Va", "4hl", "7Mn", "4x", "0SB", "1Lb", "3yA", "6RN", "4gm", "4IA", "68j", "2GL", "4", +"82O", "d4G", "5A1", "4Dq", "bPn", "a1d", "6I", "0Qs", "lC", "0Oq", "40W", "akn", "6oP", "4Zs", "15t", "Bo", "2YN", "0lm", "4WC", "76I", +"6LL", "4yo", "0BA", "as", "nr", "8DX", "42f", "6CM", "6ma", "4XB", "0cl", "2VO", "Mn", "8gD", "4Ur", "5P2", "ado", "bAm", "1PQ", "cB", +"Rz", "0qH", "4Jf", "7od", "6Qi", "4dJ", "i7", "2jG", "5n", "0RT", "4iz", "7Lx", "4R7", "4GV", "08p", "1y8", "PK", "07q", "4HW", "7mU", +"6SX", "52R", "1Mt", "0H4", "sW", "f6", "4kK", "7NI", "64L", "4Eg", "1nh", "2Kj", "14b", "Cy", "6nF", "5KD", "41A", "7PK", "mU", "x4", +"0CW", "0V6", "591", "4xy", "4VU", "4C4", "NI", "19R", "0bz", "AH", "4L5", "4YT", "43p", "560", "od", "0LV", "w5", "bT", "6Ok", "4zH", +"4Td", "6aG", "Lx", "0oJ", "5xA", "7Mb", "4t", "0SN", "K1", "2HA", "67g", "4FL", "b5G", "aTM", "0e3", "04Z", "8Wf", "9D", "449", "4eP", +"4jQ", "7OS", "6E", "255", "1or", "0j2", "65V", "cno", "4IM", "68f", "QQ", "8", "1Ln", "2il", "6RB", "4ga", "afR", "4yc", "0BM", "23f", +"OS", "Z2", "4WO", "6bl", "aEN", "c4e", "0aQ", "Bc", "lO", "8Fe", "4tS", "4a2", "4n3", "5ks", "8Id", "cN", "Mb", "0nP", "614", "74t", +"6mm", "4XN", "U3", "2VC", "2xo", "0ML", "42j", "6CA", "6Qm", "4dN", "i3", "8Z", "2Do", "05D", "4Jb", "aUS", "4R3", "4GR", "08t", "d7d", +"5j", "0RP", "bSM", "a2G", "ayN", "52V", "1Mp", "0H0", "PO", "07u", "4HS", "69x", "64H", "4Ec", "1nl", "2Kn", "sS", "f2", "4kO", "7NM", +"41E", "7PO", "mQ", "x0", "14f", "2Ul", "6nB", "aQ1", "4VQ", "4C0", "NM", "19V", "0CS", "0V2", "595", "bBN", "43t", "564", "0Y3", "0LR", +"16W", "AL", "4L1", "4YP", "5DA", "6aC", "2Zm", "0oN", "39", "bP", "6Oo", "4zL", "K5", "2HE", "67c", "4FH", "4hd", "7Mf", "4p", "0SJ", +"8Wb", "2kY", "4p5", "4eT", "4Kx", "5N8", "Sd", "0pV", "1ov", "0j6", "5A9", "4Dy", "4jU", "7OW", "6A", "1AZ", "1Lj", "2ih", "6RF", "4ge", +"4II", "68b", "QU", "D4", "OW", "Z6", "4WK", "6bh", "6LD", "4yg", "0BI", "23b", "lK", "0Oy", "4tW", "4a6", "6oX", "5JZ", "0aU", "Bg", +"Mf", "0nT", "4Uz", "74p", "4n7", "5kw", "1PY", "cJ", "nz", "0MH", "42n", "6CE", "6mi", "4XJ", "U7", "2VG", "4MP", "4X1", "UL", "02v", +"0XR", "0M3", "a8E", "57U", "4nL", "7KN", "2X", "c1", "1ko", "2Nm", "61K", "5PA", "4Oa", "6zB", "2Al", "00G", "l0", "yQ", "6Tn", "4aM", +"58T", "a7D", "0i", "0WS", "84o", "ZM", "4W0", "4BQ", "4I2", "5Lr", "13T", "G", "jc", "0IQ", "46w", "537", "6Jl", "5on", "r2", "gS", +"3OO", "0jM", "4Qc", "70i", "6kA", "5NC", "0eL", "2Po", "hR", "8Bx", "44F", "6Em", "abO", "49v", "0FP", "eb", "KN", "8ad", "4SR", "4F3", +"3B", "0Tx", "4oV", "4z7", "60Q", "4Az", "0zT", "Yf", "TV", "A7", "4LJ", "6yi", "6WE", "4bf", "0YH", "zz", "1s", "0VI", "4mg", "6XD", +"6vh", "4CK", "N6", "2MF", "Vg", "0uU", "6n9", "7ky", "4u6", "5pv", "1KX", "xK", "1UZ", "fI", "4k4", "5nt", "4Py", "5U9", "He", "0kW", +"P4", "EU", "6hj", "5Mh", "47m", "6FF", "ky", "0HK", "0GJ", "dx", "6IG", "48l", "4RH", "6gk", "JT", "0if", "0dV", "Gd", "5Z8", "5OY", +"4qT", "4d5", "iH", "0Jz", "0XV", "0M7", "5f8", "4cx", "4MT", "4X5", "UH", "02r", "1kk", "Xx", "61O", "5PE", "4nH", "7KJ", "vT", "c5", +"l4", "yU", "6Tj", "4aI", "4Oe", "6zF", "Wy", "00C", "1iZ", "ZI", "4W4", "4BU", "4ly", "5i9", "0m", "0WW", "jg", "0IU", "46s", "533", +"4I6", "5Lv", "0gy", "C", "3OK", "0jI", "4Qg", "6dD", "6Jh", "5oj", "r6", "gW", "hV", "0Kd", "44B", "6Ei", "6kE", "5NG", "0eH", "Fz", +"KJ", "0hx", "4SV", "4F7", "6HY", "49r", "0FT", "ef", "60U", "ckl", "0zP", "Yb", "3F", "206", "4oR", "4z3", "6WA", "4bb", "0YL", "2lo", +"TR", "A3", "4LN", "6ym", "62d", "4CO", "N2", "2MB", "1w", "0VM", "4mc", "7Ha", "4u2", "54z", "8Re", "xO", "Vc", "01Y", "b0D", "aQN", +"647", "71w", "Ha", "0kS", "8Lg", "fM", "4k0", "5np", "47i", "6FB", "29d", "0HO", "P0", "EQ", "6hn", "5Ml", "4RL", "6go", "JP", "0ib", +"0GN", "26e", "6IC", "48h", "45X", "4d1", "iL", "8Cf", "0dR", "0q3", "hYt", "beO", "4nD", "7KF", "2P", "c9", "1kg", "Xt", "61C", "5PI", +"4MX", "4X9", "UD", "0vv", "0XZ", "2my", "5f4", "4ct", "4lu", "5i5", "0a", "1Gz", "0yw", "ZE", "4W8", "4BY", "4Oi", "6zJ", "Wu", "00O", +"l8", "yY", "6Tf", "4aE", "6Jd", "5of", "62", "25B", "Iw", "0jE", "4Qk", "6dH", "6ix", "5Lz", "0gu", "O", "jk", "0IY", "4rw", "5w7", +"5x6", "5mW", "0FX", "ej", "KF", "0ht", "4SZ", "6fy", "6kI", "5NK", "0eD", "Fv", "hZ", "93", "44N", "6Ee", "2BO", "03d", "4LB", "6ya", +"6WM", "4bn", "1Ia", "zr", "3J", "0Tp", "bUm", "a4g", "5D2", "4Ar", "87L", "Yn", "Vo", "01U", "4Ns", "5K3", "416", "54v", "1KP", "xC", +"us", "0VA", "4mo", "6XL", "62h", "4CC", "0xm", "2MN", "0fo", "2SL", "6hb", "bgr", "47e", "6FN", "kq", "0HC", "0Es", "fA", "aal", "bDn", +"4Pq", "5U1", "Hm", "8bG", "10w", "Gl", "5Z0", "5OQ", "45T", "anm", "1O2", "0Jr", "0GB", "dp", "6IO", "48d", "5Ba", "6gc", "3Ll", "0in", +"1kc", "Xp", "61G", "5PM", "bTs", "7KB", "2T", "0Un", "8QF", "39T", "5f0", "4cp", "797", "aRm", "1s2", "02z", "0ys", "ZA", "63v", "766", +"4lq", "5i1", "0e", "9Nf", "0Zo", "2oL", "6Tb", "4aA", "4Om", "6zN", "Wq", "00K", "Is", "0jA", "4Qo", "6dL", "7ZA", "5ob", "66", "25F", +"jo", "9Pd", "4rs", "5w3", "aCn", "bfl", "0gq", "K", "KB", "0hp", "bim", "72T", "5x2", "49z", "327", "en", "3nn", "97", "44J", "6Ea", +"6kM", "5NO", "11i", "Fr", "6WI", "4bj", "0YD", "zv", "TZ", "0wh", "4LF", "6ye", "5D6", "4Av", "0zX", "Yj", "3N", "0Tt", "4oZ", "6Zy", +"412", "54r", "1KT", "xG", "Vk", "01Q", "4Nw", "5K7", "62l", "4CG", "0xi", "2MJ", "uw", "0VE", "4mk", "6XH", "47a", "6FJ", "ku", "0HG", +"P8", "EY", "6hf", "5Md", "4Pu", "5U5", "Hi", "8bC", "0Ew", "fE", "4k8", "5nx", "45P", "4d9", "iD", "0Jv", "0dZ", "Gh", "5Z4", "5OU", +"4RD", "6gg", "JX", "0ij", "0GF", "dt", "6IK", "5lI", "4Op", "5J0", "Wl", "00V", "0Zr", "2oQ", "405", "55u", "4ll", "6YO", "0x", "0WB", +"0yn", "2LM", "63k", "5Ra", "4MA", "6xb", "2CL", "02g", "0XC", "39I", "6VN", "4cm", "bTn", "a5d", "2I", "0Us", "86O", "Xm", "5E1", "5PP", +"6kP", "5NR", "11t", "Fo", "hC", "0Kq", "44W", "aon", "6HL", "49g", "0FA", "es", "3Mo", "0hm", "4SC", "72I", "6ia", "5Lc", "0gl", "V", +"jr", "1Ya", "46f", "6GM", "hyV", "bEm", "0Dp", "gB", "In", "8cD", "4Qr", "5T2", "1b", "0VX", "4mv", "5h6", "62q", "4CZ", "0xt", "2MW", +"Vv", "01L", "4Nj", "7kh", "6Ue", "54o", "1KI", "xZ", "3S", "0Ti", "4oG", "6Zd", "6tH", "4Ak", "0zE", "Yw", "TG", "0wu", "780", "6yx", +"5g7", "4bw", "0YY", "zk", "1Wz", "di", "5y5", "5lT", "4RY", "4G8", "JE", "0iw", "0dG", "Gu", "6jJ", "5OH", "45M", "6Df", "iY", "80", +"71", "fX", "6Kg", "5ne", "4Ph", "6eK", "Ht", "0kF", "0fv", "ED", "4H9", "5My", "4st", "5v4", "kh", "0HZ", "0Zv", "yD", "401", "4aX", +"4Ot", "5J4", "Wh", "00R", "O9", "ZX", "63o", "4BD", "4lh", "6YK", "tt", "0WF", "0XG", "2md", "6VJ", "4ci", "4ME", "6xf", "UY", "02c", +"1kz", "Xi", "5E5", "5PT", "4nY", "aqh", "2M", "0Uw", "hG", "0Ku", "44S", "6Ex", "6kT", "5NV", "0eY", "Fk", "3Mk", "0hi", "4SG", "6fd", +"6HH", "49c", "0FE", "ew", "jv", "0ID", "46b", "6GI", "6ie", "5Lg", "0gh", "R", "Ij", "0jX", "4Qv", "5T6", "6Jy", "7O9", "0Dt", "gF", +"62u", "775", "0xp", "198", "1f", "9Oe", "4mr", "5h2", "6Ua", "54k", "1KM", "2nO", "Vr", "01H", "4Nn", "7kl", "60D", "4Ao", "0zA", "Ys", +"3W", "0Tm", "4oC", "7JA", "5g3", "4bs", "8PE", "zo", "TC", "03y", "784", "aSn", "bhn", "73W", "JA", "0is", "334", "dm", "5y1", "48y", +"45I", "6Db", "3om", "84", "0dC", "Gq", "6jN", "5OL", "4Pl", "6eO", "Hp", "0kB", "75", "24E", "6Kc", "5na", "47x", "528", "kl", "8AF", +"0fr", "1c2", "aBm", "bgo", "4ld", "6YG", "0p", "0WJ", "O5", "ZT", "63c", "4BH", "4Ox", "5J8", "Wd", "0tV", "0Zz", "yH", "4t5", "4aT", +"4nU", "7KW", "2A", "1EZ", "1kv", "Xe", "5E9", "5PX", "4MI", "6xj", "UU", "02o", "0XK", "2mh", "6VF", "4ce", "6HD", "49o", "0FI", "27b", +"KW", "0he", "4SK", "6fh", "6kX", "5NZ", "0eU", "Fg", "hK", "0Ky", "4pW", "4e6", "4j7", "5ow", "0Dx", "gJ", "If", "0jT", "4Qz", "6dY", +"6ii", "5Lk", "Q7", "DV", "jz", "0IH", "46n", "6GE", "3PN", "01D", "4Nb", "aQS", "6Um", "54g", "m3", "xR", "1j", "0VP", "59W", "a6G", +"4V3", "4CR", "85l", "194", "TO", "03u", "4LS", "4Y2", "a9F", "56V", "0YQ", "zc", "wS", "b2", "4oO", "6Zl", "60H", "4Ac", "0zM", "2On", +"0dO", "2Ql", "6jB", "aU1", "45E", "6Dn", "iQ", "88", "0GS", "da", "acL", "48u", "4RQ", "4G0", "JM", "94N", "12W", "EL", "4H1", "5Mq", +"47t", "524", "29y", "0HR", "79", "fP", "6Ko", "5nm", "aZ0", "6eC", "3NL", "0kN", "O1", "ZP", "63g", "4BL", "58I", "6YC", "0t", "0WN", +"8Sf", "yL", "409", "4aP", "b1G", "aPM", "0a3", "00Z", "1kr", "Xa", "61V", "bzN", "4nQ", "7KS", "2E", "215", "0XO", "2ml", "6VB", "4ca", +"4MM", "6xn", "UQ", "02k", "KS", "0ha", "4SO", "6fl", "7Xa", "49k", "0FM", "27f", "hO", "8Be", "4pS", "4e2", "aAN", "bdL", "0eQ", "Fc", +"Ib", "0jP", "654", "70t", "4j3", "5os", "8Md", "gN", "28g", "0IL", "46j", "6GA", "6im", "5Lo", "Q3", "Z", "6Ui", "54c", "m7", "xV", +"Vz", "0uH", "4Nf", "7kd", "4V7", "4CV", "0xx", "190", "1n", "0VT", "4mz", "6XY", "6WX", "56R", "0YU", "zg", "TK", "03q", "4LW", "4Y6", +"60L", "4Ag", "0zI", "2Oj", "wW", "b6", "4oK", "6Zh", "45A", "6Dj", "iU", "0Jg", "0dK", "Gy", "6jF", "5OD", "4RU", "4G4", "JI", "1yZ", +"0GW", "de", "5y9", "48q", "47p", "520", "kd", "0HV", "0fz", "EH", "4H5", "5Mu", "4Pd", "6eG", "Hx", "0kJ", "s5", "fT", "6Kk", "5ni", +"5Y1", "5LP", "13v", "e", "jA", "0Is", "46U", "aml", "6JN", "5oL", "0DC", "gq", "3Om", "0jo", "4QA", "6db", "6kc", "5Na", "0en", "2PM", +"hp", "0KB", "44d", "6EO", "abm", "49T", "0Fr", "1C2", "Kl", "8aF", "4Sp", "5V0", "4Mr", "5H2", "Un", "02T", "0Xp", "2mS", "427", "57w", +"4nn", "7Kl", "2z", "1Ea", "1kM", "2NO", "61i", "5Pc", "4OC", "7jA", "2AN", "00e", "0ZA", "ys", "6TL", "4ao", "58v", "a7f", "0K", "0Wq", +"84M", "Zo", "5G3", "4Bs", "0EY", "fk", "6KT", "5nV", "bjh", "6ex", "HG", "0ku", "0fE", "Ew", "6hH", "5MJ", "47O", "6Fd", "29B", "0Hi", +"53", "dZ", "6Ie", "48N", "4Rj", "6gI", "Jv", "0iD", "0dt", "GF", "6jy", "7o9", "4qv", "5t6", "ij", "0JX", "wh", "0TZ", "4ot", "5j4", +"4T9", "4AX", "0zv", "YD", "Tt", "03N", "4Lh", "6yK", "6Wg", "4bD", "o9", "zX", "1Q", "0Vk", "4mE", "6Xf", "62B", "4Ci", "0xG", "2Md", +"VE", "0uw", "4NY", "aQh", "5e5", "5pT", "1Kz", "xi", "jE", "0Iw", "46Q", "4g8", "5Y5", "5LT", "13r", "a", "IY", "0jk", "4QE", "6df", +"6JJ", "5oH", "0DG", "gu", "ht", "0KF", "4ph", "6EK", "6kg", "5Ne", "S9", "FX", "Kh", "0hZ", "4St", "5V4", "4h9", "49P", "0Fv", "eD", +"0Xt", "2mW", "423", "4cZ", "4Mv", "5H6", "Uj", "02P", "1kI", "XZ", "61m", "5Pg", "4nj", "7Kh", "vv", "0UD", "0ZE", "yw", "6TH", "4ak", +"4OG", "6zd", "2AJ", "00a", "0yY", "Zk", "5G7", "4Bw", "58r", "6Yx", "0O", "0Wu", "bjl", "71U", "HC", "0kq", "316", "fo", "6KP", "5nR", +"47K", "7VA", "29F", "0Hm", "0fA", "Es", "6hL", "5MN", "4Rn", "6gM", "Jr", "1ya", "57", "26G", "6Ia", "48J", "45z", "5t2", "in", "8CD", +"0dp", "GB", "hYV", "bem", "60w", "757", "0zr", "2OQ", "3d", "9Mg", "4op", "5j0", "6Wc", "56i", "0Yn", "2lM", "Tp", "03J", "4Ll", "6yO", +"62F", "4Cm", "0xC", "dwS", "1U", "0Vo", "4mA", "6Xb", "5e1", "54X", "8RG", "xm", "VA", "0us", "b0f", "aQl", "6JF", "5oD", "0DK", "gy", +"IU", "0jg", "4QI", "6dj", "5Y9", "5LX", "0gW", "m", "jI", "1YZ", "4rU", "4g4", "4h5", "5mu", "0Fz", "eH", "Kd", "0hV", "4Sx", "5V8", +"6kk", "5Ni", "S5", "FT", "hx", "0KJ", "44l", "6EG", "4nf", "7Kd", "2r", "0UH", "M7", "XV", "61a", "5Pk", "4Mz", "6xY", "Uf", "0vT", +"0Xx", "39r", "4v7", "4cV", "4lW", "4y6", "0C", "0Wy", "0yU", "Zg", "63P", "5RZ", "4OK", "6zh", "WW", "B6", "0ZI", "2oj", "6TD", "4ag", +"0fM", "2Sn", "7xa", "5MB", "47G", "6Fl", "kS", "0Ha", "0EQ", "fc", "aaN", "bDL", "4PS", "4E2", "HO", "8be", "10U", "GN", "4J3", "5Os", +"45v", "506", "ib", "0JP", "q3", "dR", "6Im", "48F", "4Rb", "6gA", "3LN", "0iL", "2Bm", "03F", "aF0", "6yC", "6Wo", "4bL", "o1", "zP", +"3h", "0TR", "bUO", "a4E", "4T1", "4AP", "87n", "YL", "VM", "01w", "4NQ", "7kS", "hfu", "54T", "1Kr", "xa", "1Y", "0Vc", "4mM", "6Xn", +"62J", "4Ca", "0xO", "2Ml", "IQ", "0jc", "4QM", "6dn", "6JB", "a19", "0DO", "25d", "jM", "9PF", "46Y", "4g0", "aCL", "687", "0gS", "i", +"3MP", "0hR", "676", "72v", "4h1", "49X", "8Of", "eL", "3nL", "0KN", "44h", "6EC", "6ko", "5Nm", "S1", "FP", "M3", "XR", "61e", "5Po", +"4nb", "aqS", "2v", "0UL", "8Qd", "39v", "4v3", "4cR", "b3E", "aRO", "Ub", "02X", "0yQ", "Zc", "63T", "bxL", "4lS", "4y2", "0G", "237", +"0ZM", "2on", "7Da", "4ac", "4OO", "6zl", "WS", "B2", "47C", "6Fh", "kW", "0He", "0fI", "2Sj", "6hD", "5MF", "4PW", "4E6", "HK", "0ky", +"0EU", "fg", "6KX", "5nZ", "45r", "502", "if", "0JT", "0dx", "GJ", "4J7", "5Ow", "4Rf", "6gE", "Jz", "0iH", "q7", "dV", "6Ii", "48B", +"6Wk", "4bH", "o5", "zT", "Tx", "03B", "4Ld", "6yG", "4T5", "4AT", "0zz", "YH", "3l", "0TV", "4ox", "5j8", "5e9", "54P", "1Kv", "xe", +"VI", "01s", "4NU", "7kW", "62N", "4Ce", "0xK", "2Mh", "uU", "0Vg", "4mI", "6Xj", "4K0", "5Np", "11V", "FM", "ha", "0KS", "44u", "515", +"6Hn", "49E", "48", "eQ", "3MM", "0hO", "4Sa", "6fB", "6iC", "5LA", "0gN", "t", "jP", "0Ib", "46D", "6Go", "hyt", "bEO", "0DR", "0Q3", +"IL", "8cf", "4QP", "4D1", "4OR", "4Z3", "WN", "00t", "0ZP", "yb", "hgv", "55W", "4lN", "6Ym", "0Z", "a3", "0yL", "2Lo", "63I", "4Bb", +"4Mc", "7ha", "2Cn", "02E", "n2", "2mB", "6Vl", "4cO", "bTL", "a5F", "2k", "0UQ", "86m", "XO", "4U2", "5Pr", "0Gy", "dK", "4i6", "5lv", +"5BZ", "6gX", "Jg", "0iU", "R6", "GW", "6jh", "5Oj", "45o", "6DD", "3oK", "0JI", "0EH", "fz", "6KE", "5nG", "4PJ", "6ei", "HV", "0kd", +"0fT", "Ef", "6hY", "690", "4sV", "4f7", "kJ", "0Hx", "uH", "0Vz", "4mT", "4x5", "5F8", "4Cx", "0xV", "0m7", "VT", "C5", "4NH", "7kJ", +"6UG", "54M", "1Kk", "xx", "3q", "0TK", "4oe", "6ZF", "60b", "4AI", "L4", "YU", "Te", "0wW", "4Ly", "5I9", "4w4", "4bU", "1IZ", "zI", +"he", "0KW", "44q", "511", "4K4", "5Nt", "11R", "FI", "Ky", "0hK", "4Se", "6fF", "6Hj", "49A", "p4", "eU", "jT", "0If", "4rH", "6Gk", +"6iG", "5LE", "0gJ", "p", "IH", "0jz", "4QT", "4D5", "5z8", "5oY", "0DV", "gd", "0ZT", "yf", "6TY", "4az", "4OV", "4Z7", "WJ", "00p", +"0yH", "Zz", "63M", "4Bf", "4lJ", "6Yi", "tV", "a7", "n6", "2mF", "6Vh", "4cK", "4Mg", "6xD", "2Cj", "02A", "1kX", "XK", "4U6", "5Pv", +"6N9", "7Ky", "2o", "0UU", "665", "73u", "Jc", "0iQ", "8Ne", "dO", "4i2", "5lr", "45k", "7Ta", "3oO", "0JM", "R2", "GS", "6jl", "5On", +"4PN", "6em", "HR", "8bx", "0EL", "24g", "6KA", "5nC", "47Z", "4f3", "kN", "8Ad", "0fP", "Eb", "aBO", "694", "62W", "byO", "0xR", "0m3", +"1D", "224", "4mP", "4x1", "6UC", "54I", "1Ko", "2nm", "VP", "C1", "4NL", "7kN", "60f", "4AM", "L0", "YQ", "3u", "0TO", "4oa", "6ZB", +"438", "4bQ", "8Pg", "zM", "Ta", "0wS", "b2F", "aSL", "6Hf", "49M", "40", "eY", "Ku", "0hG", "4Si", "6fJ", "4K8", "5Nx", "0ew", "FE", +"hi", "8BC", "4pu", "5u5", "5z4", "5oU", "0DZ", "gh", "ID", "0jv", "4QX", "4D9", "6iK", "5LI", "0gF", "Dt", "jX", "0Ij", "46L", "6Gg", +"4lF", "6Ye", "0R", "0Wh", "0yD", "Zv", "63A", "4Bj", "4OZ", "6zy", "WF", "0tt", "0ZX", "yj", "5d6", "4av", "4nw", "5k7", "2c", "0UY", +"1kT", "XG", "61p", "5Pz", "4Mk", "6xH", "Uw", "02M", "0Xi", "2mJ", "6Vd", "4cG", "0dm", "2QN", "7zA", "5Ob", "45g", "6DL", "is", "0JA", +"0Gq", "dC", "acn", "48W", "4Rs", "5W3", "Jo", "94l", "12u", "En", "5X2", "5MS", "47V", "alo", "kB", "0Hp", "1Ua", "fr", "6KM", "5nO", +"4PB", "6ea", "3Nn", "0kl", "3Pl", "01f", "bts", "7kB", "6UO", "54E", "1Kc", "xp", "1H", "0Vr", "59u", "a6e", "5F0", "4Cp", "85N", "d3F", +"Tm", "03W", "4Lq", "5I1", "434", "56t", "0Ys", "zA", "3y", "0TC", "4om", "6ZN", "60j", "4AA", "0zo", "2OL", "Kq", "0hC", "4Sm", "6fN", +"6Hb", "49I", "44", "27D", "hm", "8BG", "44y", "519", "aAl", "bdn", "0es", "FA", "1o2", "0jr", "bko", "70V", "5z0", "5oQ", "305", "gl", +"28E", "0In", "46H", "6Gc", "6iO", "5LM", "0gB", "x", "1ia", "Zr", "63E", "4Bn", "4lB", "6Ya", "0V", "0Wl", "8SD", "yn", "5d2", "4ar", +"b1e", "aPo", "WB", "00x", "1kP", "XC", "61t", "744", "4ns", "5k3", "2g", "9Ld", "0Xm", "2mN", "7FA", "4cC", "4Mo", "6xL", "Us", "02I", +"45c", "6DH", "iw", "0JE", "0di", "2QJ", "6jd", "5Of", "4Rw", "5W7", "Jk", "0iY", "0Gu", "dG", "6Ix", "48S", "47R", "6Fy", "kF", "0Ht", +"0fX", "Ej", "5X6", "5MW", "4PF", "6ee", "HZ", "0kh", "0ED", "fv", "6KI", "5nK", "6UK", "54A", "1Kg", "xt", "VX", "C9", "4ND", "7kF", +"5F4", "4Ct", "0xZ", "2My", "1L", "0Vv", "4mX", "4x9", "430", "4bY", "0Yw", "zE", "Ti", "03S", "4Lu", "5I5", "60n", "4AE", "L8", "YY", +"wu", "0TG", "4oi", "6ZJ" }; + + +#endif + diff --git a/src/db.c b/src/db.c index 7950d5074..8a0242d9e 100644 --- a/src/db.c +++ b/src/db.c @@ -60,10 +60,7 @@ robj *lookupKey(redisDb *db, robj *key, int flags) { /* Update the access time for the ageing algorithm. * Don't do it if we have a saving child, as this will trigger * a copy on write madness. */ - if (server.rdb_child_pid == -1 && - server.aof_child_pid == -1 && - !(flags & LOOKUP_NOTOUCH)) - { + if (!hasActiveChildProcess() && !(flags & LOOKUP_NOTOUCH)){ if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) { updateLFU(val); } else { @@ -83,6 +80,7 @@ robj *lookupKey(redisDb *db, robj *key, int flags) { * 1. A key gets expired if it reached it's TTL. * 2. The key last access time is updated. * 3. The global keys hits/misses stats are updated (reported in INFO). + * 4. If keyspace notifications are enabled, a "keymiss" notification is fired. * * This API should not be used when we write to the key after obtaining * the object linked to the key, but only for read only operations. @@ -106,6 +104,7 @@ robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) { * to return NULL ASAP. */ if (server.masterhost == NULL) { server.stat_keyspace_misses++; + notifyKeyspaceEvent(NOTIFY_KEY_MISS, "keymiss", key, db->id); return NULL; } @@ -127,12 +126,15 @@ robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) { server.current_client->cmd->flags & CMD_READONLY) { server.stat_keyspace_misses++; + notifyKeyspaceEvent(NOTIFY_KEY_MISS, "keymiss", key, db->id); return NULL; } } val = lookupKey(db,key,flags); - if (val == NULL) + if (val == NULL) { server.stat_keyspace_misses++; + notifyKeyspaceEvent(NOTIFY_KEY_MISS, "keymiss", key, db->id); + } else server.stat_keyspace_hits++; return val; @@ -149,9 +151,13 @@ robj *lookupKeyRead(redisDb *db, robj *key) { * * Returns the linked value object if the key exists or NULL if the key * does not exist in the specified DB. */ -robj *lookupKeyWrite(redisDb *db, robj *key) { +robj *lookupKeyWriteWithFlags(redisDb *db, robj *key, int flags) { expireIfNeeded(db,key); - return lookupKey(db,key,LOOKUP_NONE); + return lookupKey(db,key,flags); +} + +robj *lookupKeyWrite(redisDb *db, robj *key) { + return lookupKeyWriteWithFlags(db, key, LOOKUP_NONE); } robj *lookupKeyReadOrReply(client *c, robj *key, robj *reply) { @@ -210,20 +216,28 @@ 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), + * unless 'keepttl' is true. * * All the new keys in the database should be created via this interface. */ -void setKey(redisDb *db, robj *key, robj *val) { +void genericSetKey(redisDb *db, robj *key, robj *val, int keepttl) { if (lookupKeyWrite(db,key) == NULL) { dbAdd(db,key,val); } else { dbOverwrite(db,key,val); } incrRefCount(val); - removeExpire(db,key); + if (!keepttl) removeExpire(db,key); signalModifiedKey(db,key); } +/* Common case for genericSetKey() where the TTL is not retained. */ +void setKey(redisDb *db, robj *key, robj *val) { + genericSetKey(db,key,val,0); +} + +/* Return true if the specified key exists in the specified database. + * LRU/LFU info is not updated in any way. */ int dbExists(redisDb *db, robj *key) { return dictFind(db->dict,key->ptr) != NULL; } @@ -333,14 +347,19 @@ robj *dbUnshareStringValue(redisDb *db, robj *key, robj *o) { * DB number if we want to flush only a single Redis database number. * * Flags are be EMPTYDB_NO_FLAGS if no special flags are specified or - * EMPTYDB_ASYNC if we want the memory to be freed in a different thread + * 1. EMPTYDB_ASYNC if we want the memory to be freed in a different thread. + * 2. EMPTYDB_BACKUP if we want to empty the backup dictionaries created by + * disklessLoadMakeBackups. In that case we only free memory and avoid + * firing module events. * and the function to return ASAP. * * On success the fuction returns the number of keys removed from the * database(s). Otherwise -1 is returned in the specific case the * DB number is out of range, and errno is set to EINVAL. */ -long long emptyDb(int dbnum, int flags, void(callback)(void*)) { +long long emptyDbGeneric(redisDb *dbarray, int dbnum, int flags, void(callback)(void*)) { int async = (flags & EMPTYDB_ASYNC); + int backup = (flags & EMPTYDB_BACKUP); /* Just free the memory, nothing else */ + RedisModuleFlushInfoV1 fi = {REDISMODULE_FLUSHINFO_VERSION,!async,dbnum}; long long removed = 0; if (dbnum < -1 || dbnum >= server.dbnum) { @@ -348,6 +367,19 @@ long long emptyDb(int dbnum, int flags, void(callback)(void*)) { return -1; } + /* Pre-flush actions */ + if (!backup) { + /* Fire the flushdb modules event. */ + moduleFireServerEvent(REDISMODULE_EVENT_FLUSHDB, + REDISMODULE_SUBEVENT_FLUSHDB_START, + &fi); + + /* Make sure the WATCHed keys are affected by the FLUSH* commands. + * Note that we need to call the function while the keys are still + * there. */ + signalFlushedDb(dbnum); + } + int startdb, enddb; if (dbnum == -1) { startdb = 0; @@ -357,25 +389,40 @@ long long emptyDb(int dbnum, int flags, void(callback)(void*)) { } for (int j = startdb; j <= enddb; j++) { - removed += dictSize(server.db[j].dict); + removed += dictSize(dbarray[j].dict); if (async) { - emptyDbAsync(&server.db[j]); + emptyDbAsync(&dbarray[j]); } else { - dictEmpty(server.db[j].dict,callback); - dictEmpty(server.db[j].expires,callback); + dictEmpty(dbarray[j].dict,callback); + dictEmpty(dbarray[j].expires,callback); } } - if (server.cluster_enabled) { - if (async) { - slotToKeyFlushAsync(); - } else { - slotToKeyFlush(); + + /* Post-flush actions */ + if (!backup) { + if (server.cluster_enabled) { + if (async) { + slotToKeyFlushAsync(); + } else { + slotToKeyFlush(); + } } + if (dbnum == -1) flushSlaveKeysWithExpireList(); + + /* Also fire the end event. Note that this event will fire almost + * immediately after the start event if the flush is asynchronous. */ + moduleFireServerEvent(REDISMODULE_EVENT_FLUSHDB, + REDISMODULE_SUBEVENT_FLUSHDB_END, + &fi); } - if (dbnum == -1) flushSlaveKeysWithExpireList(); + return removed; } +long long emptyDb(int dbnum, int flags, void(callback)(void*)) { + return emptyDbGeneric(server.db, dbnum, flags, callback); +} + int selectDb(client *c, int id) { if (id < 0 || id >= server.dbnum) return C_ERR; @@ -383,6 +430,15 @@ int selectDb(client *c, int id) { return C_OK; } +long long dbTotalServerKeyCount() { + long long total = 0; + int j; + for (j = 0; j < server.dbnum; j++) { + total += dictSize(server.db[j].dict); + } + return total; +} + /*----------------------------------------------------------------------------- * Hooks for key space changes. * @@ -394,10 +450,12 @@ int selectDb(client *c, int id) { void signalModifiedKey(redisDb *db, robj *key) { touchWatchedKey(db,key); + trackingInvalidateKey(key); } void signalFlushedDb(int dbid) { touchWatchedKeysOnFlush(dbid); + trackingInvalidateKeysOnFlush(dbid); } /*----------------------------------------------------------------------------- @@ -426,28 +484,9 @@ int getFlushCommandFlags(client *c, int *flags) { return C_OK; } -/* FLUSHDB [ASYNC] - * - * Flushes the currently SELECTed Redis DB. */ -void flushdbCommand(client *c) { - int flags; - - if (getFlushCommandFlags(c,&flags) == C_ERR) return; - signalFlushedDb(c->db->id); - server.dirty += emptyDb(c->db->id,flags,NULL); - addReply(c,shared.ok); -} - -/* FLUSHALL [ASYNC] - * - * Flushes the whole server data set. */ -void flushallCommand(client *c) { - int flags; - - if (getFlushCommandFlags(c,&flags) == C_ERR) return; - signalFlushedDb(-1); +/* Flushes the whole server data set. */ +void flushAllDataAndResetRDB(int flags) { server.dirty += emptyDb(-1,flags,NULL); - addReply(c,shared.ok); if (server.rdb_child_pid != -1) killRDBChild(); if (server.saveparamslen > 0) { /* Normally rdbSave() will reset dirty, but we don't want this here @@ -459,6 +498,41 @@ void flushallCommand(client *c) { server.dirty = saved_dirty; } server.dirty++; +#if defined(USE_JEMALLOC) + /* jemalloc 5 doesn't release pages back to the OS when there's no traffic. + * for large databases, flushdb blocks for long anyway, so a bit more won't + * harm and this way the flush and purge will be synchroneus. */ + if (!(flags & EMPTYDB_ASYNC)) + jemalloc_purge(); +#endif +} + +/* FLUSHDB [ASYNC] + * + * Flushes the currently SELECTed Redis DB. */ +void flushdbCommand(client *c) { + int flags; + + if (getFlushCommandFlags(c,&flags) == C_ERR) return; + server.dirty += emptyDb(c->db->id,flags,NULL); + addReply(c,shared.ok); +#if defined(USE_JEMALLOC) + /* jemalloc 5 doesn't release pages back to the OS when there's no traffic. + * for large databases, flushdb blocks for long anyway, so a bit more won't + * harm and this way the flush and purge will be synchroneus. */ + if (!(flags & EMPTYDB_ASYNC)) + jemalloc_purge(); +#endif +} + +/* FLUSHALL [ASYNC] + * + * Flushes the whole server data set. */ +void flushallCommand(client *c) { + int flags; + if (getFlushCommandFlags(c,&flags) == C_ERR) return; + flushAllDataAndResetRDB(flags); + addReply(c,shared.ok); } /* This command implements DEL and LAZYDEL. */ @@ -539,7 +613,7 @@ void keysCommand(client *c) { void *replylen = addReplyDeferredLen(c); di = dictGetSafeIterator(c->db->dict); - allkeys = (pattern[0] == '*' && pattern[1] == '\0'); + allkeys = (pattern[0] == '*' && plen == 1); while((de = dictNext(di)) != NULL) { sds key = dictGetKey(de); robj *keyobj; @@ -608,7 +682,7 @@ int parseScanCursorOrReply(client *c, robj *o, unsigned long *cursor) { } /* This command implements SCAN, HSCAN and SSCAN commands. - * If object 'o' is passed, then it must be a Hash or Set object, otherwise + * If object 'o' is passed, then it must be a Hash, Set or Zset object, otherwise * if 'o' is NULL the command will operate on the dictionary associated with * the current database. * @@ -624,6 +698,7 @@ void scanGenericCommand(client *c, robj *o, unsigned long cursor) { listNode *node, *nextnode; long count = 10; sds pat = NULL; + sds typename = NULL; int patlen = 0, use_pattern = 0; dict *ht; @@ -660,6 +735,10 @@ void scanGenericCommand(client *c, robj *o, unsigned long cursor) { use_pattern = !(pat[0] == '*' && patlen == 1); i += 2; + } else if (!strcasecmp(c->argv[i]->ptr, "type") && o == NULL && j >= 2) { + /* SCAN for a particular type only applies to the db dict */ + typename = c->argv[i+1]->ptr; + i+= 2; } else { addReply(c,shared.syntaxerr); goto cleanup; @@ -754,6 +833,13 @@ void scanGenericCommand(client *c, robj *o, unsigned long cursor) { } } + /* Filter an element if it isn't the type we want. */ + if (!filter && o == NULL && typename){ + robj* typecheck = lookupKeyReadWithFlags(c->db, kobj, LOOKUP_NOTOUCH); + char* type = getObjectTypeName(typecheck); + if (strcasecmp((char*) typename, type)) filter = 1; + } + /* Filter element if it is an expired key. */ if (!filter && o == NULL && expireIfNeeded(c->db, kobj)) filter = 1; @@ -810,11 +896,8 @@ void lastsaveCommand(client *c) { addReplyLongLong(c,server.lastsave); } -void typeCommand(client *c) { - robj *o; - char *type; - - o = lookupKeyReadWithFlags(c->db,c->argv[1],LOOKUP_NOTOUCH); +char* getObjectTypeName(robj *o) { + char* type; if (o == NULL) { type = "none"; } else { @@ -832,7 +915,13 @@ void typeCommand(client *c) { default: type = "unknown"; break; } } - addReplyStatus(c,type); + return type; +} + +void typeCommand(client *c) { + robj *o; + o = lookupKeyReadWithFlags(c->db,c->argv[1],LOOKUP_NOTOUCH); + addReplyStatus(c, getObjectTypeName(o)); } void shutdownCommand(client *c) { @@ -964,6 +1053,13 @@ void moveCommand(client *c) { /* OK! key moved, free the entry in the source DB */ dbDelete(src,c->argv[1]); + signalModifiedKey(src,c->argv[1]); + signalModifiedKey(dst,c->argv[1]); + notifyKeyspaceEvent(NOTIFY_GENERIC, + "move_from",c->argv[1],src->id); + notifyKeyspaceEvent(NOTIFY_GENERIC, + "move_to",c->argv[1],dst->id); + server.dirty++; addReply(c,shared.cone); } @@ -994,7 +1090,7 @@ void scanDatabaseForReadyLists(redisDb *db) { * * 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) { +int dbSwapDatabases(long id1, long id2) { if (id1 < 0 || id1 >= server.dbnum || id2 < 0 || id2 >= server.dbnum) return C_ERR; if (id1 == id2) return C_OK; @@ -1007,10 +1103,12 @@ int dbSwapDatabases(int id1, int id2) { db1->dict = db2->dict; db1->expires = db2->expires; db1->avg_ttl = db2->avg_ttl; + db1->expires_cursor = db2->expires_cursor; db2->dict = aux.dict; db2->expires = aux.expires; db2->avg_ttl = aux.avg_ttl; + db2->expires_cursor = aux.expires_cursor; /* Now we need to handle clients blocked on lists: as an effect * of swapping the two DBs, a client that was waiting for list @@ -1126,6 +1224,7 @@ void propagateExpire(redisDb *db, robj *key, int lazy) { /* Check if the key is expired. */ int keyIsExpired(redisDb *db, robj *key) { mstime_t when = getExpire(db,key); + mstime_t now; if (when < 0) return 0; /* No expire for this key */ @@ -1137,8 +1236,26 @@ int keyIsExpired(redisDb *db, robj *key) { * only the first time it is accessed and not in the middle of the * script execution, making propagation to slaves / AOF consistent. * See issue #1525 on Github for more information. */ - mstime_t now = server.lua_caller ? server.lua_time_start : mstime(); + if (server.lua_caller) { + now = server.lua_time_start; + } + /* If we are in the middle of a command execution, we still want to use + * a reference time that does not change: in that case we just use the + * cached time, that we update before each call in the call() function. + * This way we avoid that commands such as RPOPLPUSH or similar, that + * may re-open the same key multiple times, can invalidate an already + * open object in a next call, if the next call will see the key expired, + * while the first did not. */ + else if (server.fixed_time_expire > 0) { + now = server.mstime; + } + /* For the other cases, we want to use the most fresh time we have. */ + else { + now = mstime(); + } + /* The key expired if the current (virtual or real) time is greater + * than the expire time of the key. */ return now > when; } @@ -1179,8 +1296,10 @@ int expireIfNeeded(redisDb *db, robj *key) { propagateExpire(db,key,server.lazyfree_lazy_expire); notifyKeyspaceEvent(NOTIFY_EXPIRED, "expired",key,db->id); - return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) : - dbSyncDelete(db,key); + int retval = server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) : + dbSyncDelete(db,key); + if (retval) signalModifiedKey(db,key); + return retval; } /* ----------------------------------------------------------------------------- @@ -1416,6 +1535,22 @@ int *georadiusGetKeys(struct redisCommand *cmd, robj **argv, int argc, int *numk return keys; } +/* Helper function to extract keys from memory command. + * MEMORY USAGE */ +int *memoryGetKeys(struct redisCommand *cmd, robj **argv, int argc, int *numkeys) { + int *keys; + UNUSED(cmd); + + if (argc >= 3 && !strcasecmp(argv[1]->ptr,"usage")) { + keys = zmalloc(sizeof(int) * 1); + keys[0] = 2; + *numkeys = 1; + return keys; + } + *numkeys = 0; + return NULL; +} + /* XREAD [BLOCK ] [COUNT ] [GROUP ] * STREAMS key_1 key_2 ... key_N ID_1 ID_2 ... ID_N */ int *xreadGetKeys(struct redisCommand *cmd, robj **argv, int argc, int *numkeys) { diff --git a/src/debug.c b/src/debug.c index 0c6b5630c..dd96ad416 100644 --- a/src/debug.c +++ b/src/debug.c @@ -297,6 +297,56 @@ void computeDatasetDigest(unsigned char *final) { } } +#ifdef USE_JEMALLOC +void mallctl_int(client *c, robj **argv, int argc) { + int ret; + /* start with the biggest size (int64), and if that fails, try smaller sizes (int32, bool) */ + int64_t old = 0, val; + if (argc > 1) { + long long ll; + if (getLongLongFromObjectOrReply(c, argv[1], &ll, NULL) != C_OK) + return; + val = ll; + } + size_t sz = sizeof(old); + while (sz > 0) { + if ((ret=je_mallctl(argv[0]->ptr, &old, &sz, argc > 1? &val: NULL, argc > 1?sz: 0))) { + if (ret==EINVAL) { + /* size might be wrong, try a smaller one */ + sz /= 2; +#if BYTE_ORDER == BIG_ENDIAN + val <<= 8*sz; +#endif + continue; + } + addReplyErrorFormat(c,"%s", strerror(ret)); + return; + } else { +#if BYTE_ORDER == BIG_ENDIAN + old >>= 64 - 8*sz; +#endif + addReplyLongLong(c, old); + return; + } + } + addReplyErrorFormat(c,"%s", strerror(EINVAL)); +} + +void mallctl_string(client *c, robj **argv, int argc) { + int ret; + char *old; + size_t sz = sizeof(old); + /* for strings, it seems we need to first get the old value, before overriding it. */ + if ((ret=je_mallctl(argv[0]->ptr, &old, &sz, NULL, 0))) { + addReplyErrorFormat(c,"%s", strerror(ret)); + return; + } + addReplyBulkCString(c, old); + if(argc > 1) + je_mallctl(argv[0]->ptr, NULL, 0, &argv[1]->ptr, sizeof(char*)); +} +#endif + void debugCommand(client *c) { if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"help")) { const char *help[] = { @@ -305,6 +355,7 @@ void debugCommand(client *c) { "CRASH-AND-RECOVER -- Hard crash and restart after delay.", "DIGEST -- Output a hex signature representing the current DB content.", "DIGEST-VALUE ... -- Output a hex signature of the values of all the specified keys.", +"DEBUG PROTOCOL [string|integer|double|bignum|null|array|set|map|attrib|push|verbatim|true|false]", "ERROR -- Return a Redis protocol error with as message. Useful for clients unit tests to simulate Redis errors.", "LOG -- write message to the server log.", "HTSTATS -- Return hash table statistics of the specified Redis database.", @@ -312,6 +363,7 @@ void debugCommand(client *c) { "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 -- Show low level info about key and associated value.", +"OOM -- Crash the server simulating an out-of-memory error.", "PANIC -- Crash the server simulating a panic.", "POPULATE [prefix] [size] -- Create string keys named key:. If a prefix is specified is used instead of the 'key' prefix.", "RELOAD -- Save the RDB on disk and reload it back in memory.", @@ -319,10 +371,15 @@ void debugCommand(client *c) { "SDSLEN -- 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.", +"AOF-FLUSH-SLEEP -- Server will sleep before flushing the AOF, this is used for testing", "SLEEP -- Stop the server for . Decimals allowed.", "STRUCTSIZE -- Return the size of different Redis core C structures.", "ZIPLIST -- Show low level info about the ziplist encoding.", "STRINGMATCH-TEST -- Run a fuzz tester against the stringmatchlen() function.", +#ifdef USE_JEMALLOC +"MALLCTL [] -- Get or set a malloc tunning integer.", +"MALLCTL-STR [] -- Get or set a malloc tunning string.", +#endif NULL }; addReplyHelp(c, help); @@ -362,7 +419,7 @@ NULL } emptyDb(-1,EMPTYDB_NO_FLAGS,NULL); protectClient(c); - int ret = rdbLoad(server.rdb_filename,NULL); + int ret = rdbLoad(server.rdb_filename,NULL,RDBFLAGS_NONE); unprotectClient(c); if (ret != C_OK) { addReplyError(c,"Error trying to load the RDB dump"); @@ -531,7 +588,7 @@ NULL } } else if (!strcasecmp(c->argv[1]->ptr,"protocol") && c->argc == 3) { /* DEBUG PROTOCOL [string|integer|double|bignum|null|array|set|map| - * attrib|push|verbatim|true|false|state|err|bloberr] */ + * attrib|push|verbatim|true|false] */ char *name = c->argv[2]->ptr; if (!strcasecmp(name,"string")) { addReplyBulkCString(c,"Hello World"); @@ -579,7 +636,7 @@ NULL } else if (!strcasecmp(name,"verbatim")) { addReplyVerbatim(c,"This is a verbatim\nstring",25,"txt"); } else { - addReplyError(c,"Wrong protocol type name. Please use one of the following: string|integer|double|bignum|null|array|set|map|attrib|push|verbatim|true|false|state|err|bloberr"); + addReplyError(c,"Wrong protocol type name. Please use one of the following: string|integer|double|bignum|null|array|set|map|attrib|push|verbatim|true|false"); } } else if (!strcasecmp(c->argv[1]->ptr,"sleep") && c->argc == 3) { double dtime = strtod(c->argv[2]->ptr,NULL); @@ -595,6 +652,11 @@ NULL { server.active_expire_enabled = atoi(c->argv[2]->ptr); addReply(c,shared.ok); + } else if (!strcasecmp(c->argv[1]->ptr,"aof-flush-sleep") && + c->argc == 3) + { + server.aof_flush_sleep = atoi(c->argv[2]->ptr); + addReply(c,shared.ok); } else if (!strcasecmp(c->argv[1]->ptr,"lua-always-replicate-commands") && c->argc == 3) { @@ -638,7 +700,8 @@ NULL dictGetStats(buf,sizeof(buf),server.db[dbid].expires); stats = sdscat(stats,buf); - addReplyBulkSds(c,stats); + addReplyVerbatim(c,stats,sdslen(stats),"txt"); + sdsfree(stats); } else if (!strcasecmp(c->argv[1]->ptr,"htstats-key") && c->argc == 3) { robj *o; dict *ht = NULL; @@ -665,7 +728,7 @@ NULL } else { char buf[4096]; dictGetStats(buf,sizeof(buf),ht); - addReplyBulkCString(c,buf); + addReplyVerbatim(c,buf,strlen(buf),"txt"); } } 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"); @@ -676,6 +739,14 @@ NULL { stringmatchlen_fuzz_test(); addReplyStatus(c,"Apparently Redis did not crash: test passed"); +#ifdef USE_JEMALLOC + } else if(!strcasecmp(c->argv[1]->ptr,"mallctl") && c->argc >= 3) { + mallctl_int(c, c->argv+2, c->argc-2); + return; + } else if(!strcasecmp(c->argv[1]->ptr,"mallctl-str") && c->argc >= 3) { + mallctl_string(c, c->argv+2, c->argc-2); + return; +#endif } else { addReplySubcommandSyntaxError(c); return; @@ -699,11 +770,12 @@ void _serverAssert(const char *estr, const char *file, int line) { void _serverAssertPrintClientInfo(const client *c) { int j; + char conninfo[CONN_INFO_LEN]; bugReportStart(); serverLog(LL_WARNING,"=== ASSERTION FAILED CLIENT CONTEXT ==="); - serverLog(LL_WARNING,"client->flags = %d", c->flags); - serverLog(LL_WARNING,"client->fd = %d", c->fd); + serverLog(LL_WARNING,"client->flags = %llu", (unsigned long long) c->flags); + serverLog(LL_WARNING,"client->conn = %s", connGetInfo(c->conn, conninfo, sizeof(conninfo))); serverLog(LL_WARNING,"client->argc = %d", c->argc); for (j=0; j < c->argc; j++) { char buf[128]; @@ -1110,6 +1182,33 @@ void logRegisters(ucontext_t *uc) { (unsigned long) uc->uc_mcontext.mc_cs ); logStackContent((void**)uc->uc_mcontext.mc_rsp); +#elif defined(__aarch64__) /* Linux AArch64 */ + serverLog(LL_WARNING, + "\n" + "X18:%016lx X19:%016lx\nX20:%016lx X21:%016lx\n" + "X22:%016lx X23:%016lx\nX24:%016lx X25:%016lx\n" + "X26:%016lx X27:%016lx\nX28:%016lx X29:%016lx\n" + "X30:%016lx\n" + "pc:%016lx sp:%016lx\npstate:%016lx fault_address:%016lx\n", + (unsigned long) uc->uc_mcontext.regs[18], + (unsigned long) uc->uc_mcontext.regs[19], + (unsigned long) uc->uc_mcontext.regs[20], + (unsigned long) uc->uc_mcontext.regs[21], + (unsigned long) uc->uc_mcontext.regs[22], + (unsigned long) uc->uc_mcontext.regs[23], + (unsigned long) uc->uc_mcontext.regs[24], + (unsigned long) uc->uc_mcontext.regs[25], + (unsigned long) uc->uc_mcontext.regs[26], + (unsigned long) uc->uc_mcontext.regs[27], + (unsigned long) uc->uc_mcontext.regs[28], + (unsigned long) uc->uc_mcontext.regs[29], + (unsigned long) uc->uc_mcontext.regs[30], + (unsigned long) uc->uc_mcontext.pc, + (unsigned long) uc->uc_mcontext.sp, + (unsigned long) uc->uc_mcontext.pstate, + (unsigned long) uc->uc_mcontext.fault_address + ); + logStackContent((void**)uc->uc_mcontext.sp); #else serverLog(LL_WARNING, " Dumping of registers not supported for this OS/arch"); @@ -1337,6 +1436,12 @@ void sigsegvHandler(int sig, siginfo_t *info, void *secret) { /* Log dump of processor registers */ logRegisters(uc); + /* Log Modules INFO */ + serverLogRaw(LL_WARNING|LL_RAW, "\n------ MODULES INFO OUTPUT ------\n"); + infostring = modulesCollectInfo(sdsempty(), NULL, 1, 0); + serverLogRaw(LL_WARNING|LL_RAW, infostring); + sdsfree(infostring); + #if defined(HAVE_PROC_MAPS) /* Test memory */ serverLogRaw(LL_WARNING|LL_RAW, "\n------ FAST MEMORY TEST ------\n"); diff --git a/src/defrag.c b/src/defrag.c index d67b6e253..e729297a5 100644 --- a/src/defrag.c +++ b/src/defrag.c @@ -5,8 +5,8 @@ * 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 + * Copyright (c) 2020, Oran Agra + * Copyright (c) 2020, Redis Labs, Inc * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -47,7 +47,7 @@ int je_get_defrag_hint(void* ptr, int *bin_util, int *run_util); /* forward declarations*/ void defragDictBucketCallback(void *privdata, dictEntry **bucketref); -dictEntry* replaceSateliteDictKeyPtrAndOrDefragDictEntry(dict *d, sds oldkey, sds newkey, unsigned int hash, long *defragged); +dictEntry* replaceSateliteDictKeyPtrAndOrDefragDictEntry(dict *d, sds oldkey, sds newkey, uint64_t hash, long *defragged); /* Defrag helper for generic allocations. * @@ -355,7 +355,7 @@ long activeDefragSdsListAndDict(list *l, dict *d, int dict_val_type) { sdsele = ln->value; if ((newsds = activeDefragSds(sdsele))) { /* When defragging an sds value, we need to update the dict key */ - unsigned int hash = dictGetHash(d, sdsele); + uint64_t hash = dictGetHash(d, sdsele); replaceSateliteDictKeyPtrAndOrDefragDictEntry(d, sdsele, newsds, hash, &defragged); ln->value = newsds; defragged++; @@ -374,7 +374,7 @@ long activeDefragSdsListAndDict(list *l, dict *d, int dict_val_type) { if ((newele = activeDefragStringOb(ele, &defragged))) de->v.val = newele, defragged++; } else if (dict_val_type == DEFRAG_SDS_DICT_VAL_VOID_PTR) { - void *newptr, *ptr = ln->value; + void *newptr, *ptr = dictGetVal(de); if ((newptr = activeDefragAlloc(ptr))) ln->value = newptr, defragged++; } @@ -392,7 +392,7 @@ long activeDefragSdsListAndDict(list *l, dict *d, int dict_val_type) { * 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, long *defragged) { +dictEntry* replaceSateliteDictKeyPtrAndOrDefragDictEntry(dict *d, sds oldkey, sds newkey, uint64_t hash, long *defragged) { dictEntry **deref = dictFindEntryRefByPtrAndHash(d, oldkey, hash); if (deref) { dictEntry *de = *deref; @@ -408,25 +408,32 @@ dictEntry* replaceSateliteDictKeyPtrAndOrDefragDictEntry(dict *d, sds oldkey, sd return NULL; } -long activeDefragQuickListNodes(quicklist *ql) { - quicklistNode *node = ql->head, *newnode; +long activeDefragQuickListNode(quicklist *ql, quicklistNode **node_ref) { + quicklistNode *newnode, *node = *node_ref; long defragged = 0; unsigned char *newzl; + 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_ref = node = newnode; + defragged++; + } + if ((newzl = activeDefragAlloc(node->zl))) + defragged++, node->zl = newzl; + return defragged; +} + +long activeDefragQuickListNodes(quicklist *ql) { + quicklistNode *node = ql->head; + long defragged = 0; 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; + defragged += activeDefragQuickListNode(ql, &node); node = node->next; } return defragged; @@ -440,12 +447,48 @@ void defragLater(redisDb *db, dictEntry *kde) { listAddNodeTail(db->defrag_later, key); } -long scanLaterList(robj *ob) { +/* returns 0 if no more work needs to be been done, and 1 if time is up and more work is needed. */ +long scanLaterList(robj *ob, unsigned long *cursor, long long endtime, long long *defragged) { quicklist *ql = ob->ptr; + quicklistNode *node; + long iterations = 0; + int bookmark_failed = 0; if (ob->type != OBJ_LIST || ob->encoding != OBJ_ENCODING_QUICKLIST) return 0; - server.stat_active_defrag_scanned+=ql->len; - return activeDefragQuickListNodes(ql); + + if (*cursor == 0) { + /* if cursor is 0, we start new iteration */ + node = ql->head; + } else { + node = quicklistBookmarkFind(ql, "_AD"); + if (!node) { + /* if the bookmark was deleted, it means we reached the end. */ + *cursor = 0; + return 0; + } + node = node->next; + } + + (*cursor)++; + while (node) { + (*defragged) += activeDefragQuickListNode(ql, &node); + server.stat_active_defrag_scanned++; + if (++iterations > 128 && !bookmark_failed) { + if (ustime() > endtime) { + if (!quicklistBookmarkCreate(&ql, "_AD", node)) { + bookmark_failed = 1; + } else { + ob->ptr = ql; /* bookmark creation may have re-allocated the quicklist */ + return 1; + } + } + iterations = 0; + } + node = node->next; + } + quicklistBookmarkDelete(ql, "_AD"); + *cursor = 0; + return bookmark_failed? 1: 0; } typedef struct { @@ -638,7 +681,8 @@ int scanLaterStraemListpacks(robj *ob, unsigned long *cursor, long long endtime, void *newdata = activeDefragAlloc(ri.data); if (newdata) raxSetData(ri.node, ri.data=newdata), (*defragged)++; - if (++iterations > 16) { + server.stat_active_defrag_scanned++; + if (++iterations > 128) { if (ustime() > endtime) { serverAssert(ri.key_len==sizeof(last)); memcpy(last,ri.key,ri.key_len); @@ -900,8 +944,7 @@ int defragLaterItem(dictEntry *de, unsigned long *cursor, long long endtime) { if (de) { robj *ob = dictGetVal(de); if (ob->type == OBJ_LIST) { - server.stat_active_defrag_hits += scanLaterList(ob); - *cursor = 0; /* list has no scan, we must finish it in one go */ + return scanLaterList(ob, cursor, endtime, &server.stat_active_defrag_hits); } else if (ob->type == OBJ_SET) { server.stat_active_defrag_hits += scanLaterSet(ob, cursor); } else if (ob->type == OBJ_ZSET) { @@ -919,10 +962,12 @@ int defragLaterItem(dictEntry *de, unsigned long *cursor, long long endtime) { return 0; } +/* static variables serving defragLaterStep to continue scanning a key from were we stopped last time. */ +static sds defrag_later_current_key = NULL; +static unsigned long defrag_later_cursor = 0; + /* returns 0 if no more work needs to be been done, and 1 if time is up and more work is needed. */ int defragLaterStep(redisDb *db, long long endtime) { - static sds current_key = NULL; - static unsigned long cursor = 0; unsigned int iterations = 0; unsigned long long prev_defragged = server.stat_active_defrag_hits; unsigned long long prev_scanned = server.stat_active_defrag_scanned; @@ -930,16 +975,15 @@ int defragLaterStep(redisDb *db, long long endtime) { do { /* if we're not continuing a scan from the last call or loop, start a new one */ - if (!cursor) { + if (!defrag_later_cursor) { listNode *head = listFirst(db->defrag_later); /* Move on to next key */ - if (current_key) { - serverAssert(current_key == head->value); - sdsfree(head->value); + if (defrag_later_current_key) { + serverAssert(defrag_later_current_key == head->value); listDelNode(db->defrag_later, head); - cursor = 0; - current_key = NULL; + defrag_later_cursor = 0; + defrag_later_current_key = NULL; } /* stop if we reached the last one. */ @@ -948,23 +992,18 @@ int defragLaterStep(redisDb *db, long long endtime) { return 0; /* start a new key */ - current_key = head->value; - cursor = 0; + defrag_later_current_key = head->value; + defrag_later_cursor = 0; } /* each time we enter this function we need to fetch the key from the dict again (if it still exists) */ - dictEntry *de = dictFind(db->dict, current_key); + dictEntry *de = dictFind(db->dict, defrag_later_current_key); key_defragged = server.stat_active_defrag_hits; do { int quit = 0; - if (defragLaterItem(de, &cursor, endtime)) + if (defragLaterItem(de, &defrag_later_cursor, endtime)) quit = 1; /* time is up, we didn't finish all the work */ - /* Don't start a new BIG key in this loop, this is because the - * next key can be a list, and scanLaterList must be done in once cycle */ - if (!cursor) - quit = 1; - /* Once in 16 scan iterations, 512 pointer reallocations, or 64 fields * (if we have a lot of pointers in one hash bucket, or rehashing), * check if we reached the time limit. */ @@ -982,7 +1021,7 @@ int defragLaterStep(redisDb *db, long long endtime) { prev_defragged = server.stat_active_defrag_hits; prev_scanned = server.stat_active_defrag_scanned; } - } while(cursor); + } while(defrag_later_cursor); if(key_defragged != server.stat_active_defrag_hits) server.stat_active_defrag_key_hits++; else @@ -1039,7 +1078,22 @@ void activeDefragCycle(void) { mstime_t latency; int quit = 0; - if (server.aof_child_pid!=-1 || server.rdb_child_pid!=-1) + if (!server.active_defrag_enabled) { + if (server.active_defrag_running) { + /* if active defrag was disabled mid-run, start from fresh next time. */ + server.active_defrag_running = 0; + if (db) + listEmpty(db->defrag_later); + defrag_later_current_key = NULL; + defrag_later_cursor = 0; + current_db = -1; + cursor = 0; + db = NULL; + } + return; + } + + if (hasActiveChildProcess()) return; /* Defragging memory while there's a fork will just do damage. */ /* Once a second, check if we the fragmentation justfies starting a scan diff --git a/src/evict.c b/src/evict.c index 773916ce8..71260c040 100644 --- a/src/evict.c +++ b/src/evict.c @@ -78,7 +78,7 @@ unsigned int getLRUClock(void) { unsigned int LRU_CLOCK(void) { unsigned int lruclock; if (1000/server.hz <= LRU_CLOCK_RESOLUTION) { - atomicGet(server.lruclock,lruclock); + lruclock = server.lruclock; } else { lruclock = getLRUClock(); } @@ -444,6 +444,7 @@ int getMaxmemoryState(size_t *total, size_t *logical, size_t *tofree, float *lev * Otehrwise if we are over the memory limit, but not enough memory * was freed to return back under the limit, the function returns C_ERR. */ int freeMemoryIfNeeded(void) { + int keys_freed = 0; /* By default replicas should ignore maxmemory * and just be masters exact copies. */ if (server.masterhost && server.repl_slave_ignore_maxmemory) return C_OK; @@ -467,7 +468,7 @@ int freeMemoryIfNeeded(void) { latencyStartMonitor(latency); while (mem_freed < mem_tofree) { - int j, k, i, keys_freed = 0; + int j, k, i; static unsigned int next_db = 0; sds bestkey = NULL; int bestdbid; @@ -598,9 +599,7 @@ int freeMemoryIfNeeded(void) { mem_freed = mem_tofree; } } - } - - if (!keys_freed) { + } else { latencyEndMonitor(latency); latencyAddSampleIfNeeded("eviction-cycle",latency); goto cant_free; /* nothing to free... */ diff --git a/src/expire.c b/src/expire.c index 0b92ee3fe..5aff72ee0 100644 --- a/src/expire.c +++ b/src/expire.c @@ -64,6 +64,7 @@ int activeExpireCycleTryExpire(redisDb *db, dictEntry *de, long long now) { dbSyncDelete(db,keyobj); notifyKeyspaceEvent(NOTIFY_EXPIRED, "expired",keyobj,db->id); + trackingInvalidateKey(keyobj); decrRefCount(keyobj); server.stat_expiredkeys++; return 1; @@ -77,24 +78,63 @@ int activeExpireCycleTryExpire(redisDb *db, dictEntry *de, long long now) { * it will get more aggressive to avoid that too much memory is used by * keys that can be removed from the keyspace. * - * No more than CRON_DBS_PER_CALL databases are tested at every - * iteration. + * Every expire cycle tests multiple databases: the next call will start + * again from the next db, with the exception of exists for time limit: in that + * case we restart again from the last database we were processing. Anyway + * no more than CRON_DBS_PER_CALL databases are tested at every iteration. * - * This kind of call is used when Redis detects that timelimit_exit is - * true, so there is more work to do, and we do it more incrementally from - * the beforeSleep() function of the event loop. + * The function can perform more or less work, depending on the "type" + * argument. It can execute a "fast cycle" or a "slow cycle". The slow + * cycle is the main way we collect expired cycles: this happens with + * the "server.hz" frequency (usually 10 hertz). * - * Expire cycle type: + * However the slow cycle can exit for timeout, since it used too much time. + * For this reason the function is also invoked to perform a fast cycle + * at every event loop cycle, in the beforeSleep() function. The fast cycle + * will try to perform less work, but will do it much more often. + * + * The following are the details of the two expire cycles and their stop + * conditions: * * If type is ACTIVE_EXPIRE_CYCLE_FAST the function will try to run a * "fast" expire cycle that takes no longer than EXPIRE_FAST_CYCLE_DURATION * microseconds, and is not repeated again before the same amount of time. + * The cycle will also refuse to run at all if the latest slow cycle did not + * terminate because of a time limit condition. * * 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 ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC define. */ + * as specified by the ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC define. In the + * fast cycle, the check of every database is interrupted once the number + * of already expired keys in the database is estimated to be lower than + * a given percentage, in order to avoid doing too much work to gain too + * little memory. + * + * The configured expire "effort" will modify the baseline parameters in + * order to do more work in both the fast and slow expire cycles. + */ + +#define ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP 20 /* Keys for each DB loop. */ +#define ACTIVE_EXPIRE_CYCLE_FAST_DURATION 1000 /* Microseconds. */ +#define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25 /* Max % of CPU to use. */ +#define ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE 10 /* % of stale keys after which + we do extra efforts. */ void activeExpireCycle(int type) { + /* Adjust the running parameters according to the configured expire + * effort. The default effort is 1, and the maximum configurable effort + * is 10. */ + unsigned long + effort = server.active_expire_effort-1, /* Rescale from 0 to 9. */ + config_keys_per_loop = ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP + + ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP/4*effort, + config_cycle_fast_duration = ACTIVE_EXPIRE_CYCLE_FAST_DURATION + + ACTIVE_EXPIRE_CYCLE_FAST_DURATION/4*effort, + config_cycle_slow_time_perc = ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC + + 2*effort, + config_cycle_acceptable_stale = ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE- + effort; + /* This function has some global state in order to continue the work * incrementally across calls. */ static unsigned int current_db = 0; /* Last DB tested. */ @@ -112,10 +152,16 @@ void activeExpireCycle(int type) { if (type == ACTIVE_EXPIRE_CYCLE_FAST) { /* Don't start a fast cycle if the previous cycle did not exit - * for time limit. Also don't repeat a fast cycle for the same period + * for time limit, unless the percentage of estimated stale keys is + * too high. Also never repeat a fast cycle for the same period * as the fast cycle total duration itself. */ - if (!timelimit_exit) return; - if (start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return; + if (!timelimit_exit && + server.stat_expired_stale_perc < config_cycle_acceptable_stale) + return; + + if (start < last_fast_cycle + (long long)config_cycle_fast_duration*2) + return; + last_fast_cycle = start; } @@ -129,16 +175,16 @@ void activeExpireCycle(int type) { if (dbs_per_call > server.dbnum || timelimit_exit) dbs_per_call = server.dbnum; - /* We can use at max ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC percentage of CPU time - * per iteration. Since this function gets called with a frequency of + /* We can use at max 'config_cycle_slow_time_perc' percentage of CPU + * time per iteration. Since this function gets called with a frequency of * server.hz times per second, the following is the max amount of * microseconds we can spend in this function. */ - timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100; + timelimit = config_cycle_slow_time_perc*1000000/server.hz/100; timelimit_exit = 0; if (timelimit <= 0) timelimit = 1; if (type == ACTIVE_EXPIRE_CYCLE_FAST) - timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; /* in microseconds. */ + timelimit = config_cycle_fast_duration; /* in microseconds. */ /* Accumulate some global stats as we expire keys, to have some idea * about the number of keys that are already logically expired, but still @@ -147,7 +193,9 @@ void activeExpireCycle(int type) { long total_expired = 0; for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) { - int expired; + /* Expired and checked in a single loop. */ + unsigned long expired, sampled; + redisDb *db = server.db+(current_db % server.dbnum); /* Increment the DB now so we are sure if we run out of time @@ -155,8 +203,10 @@ void activeExpireCycle(int type) { * distribute the time evenly across DBs. */ current_db++; - /* Continue to expire if at the end of the cycle more than 25% - * of the keys were expired. */ + /* Continue to expire if at the end of the cycle there are still + * a big percentage of keys to expire, compared to the number of keys + * we scanned. The percentage, stored in config_cycle_acceptable_stale + * is not fixed, but depends on the Redis configured "expire effort". */ do { unsigned long num, slots; long long now, ttl_sum; @@ -171,8 +221,8 @@ void activeExpireCycle(int type) { slots = dictSlots(db->expires); now = mstime(); - /* When there are less than 1% filled slots getting random - * keys is expensive, so stop here waiting for better times... + /* When there are less than 1% filled slots, sampling the key + * space is expensive, so stop here waiting for better times... * The dictionary will be resized asap. */ if (num && slots > DICT_HT_INITIAL_SIZE && (num*100/slots < 1)) break; @@ -180,27 +230,58 @@ void activeExpireCycle(int type) { /* The main collection cycle. Sample random keys among keys * with an expire set, checking for expired ones. */ expired = 0; + sampled = 0; ttl_sum = 0; ttl_samples = 0; - if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP) - num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP; + if (num > config_keys_per_loop) + num = config_keys_per_loop; - while (num--) { - dictEntry *de; - long long ttl; + /* Here we access the low level representation of the hash table + * for speed concerns: this makes this code coupled with dict.c, + * but it hardly changed in ten years. + * + * Note that certain places of the hash table may be empty, + * so we want also a stop condition about the number of + * buckets that we scanned. However scanning for free buckets + * is very fast: we are in the cache line scanning a sequential + * array of NULL pointers, so we can scan a lot more buckets + * than keys in the same time. */ + long max_buckets = num*20; + long checked_buckets = 0; - if ((de = dictGetRandomKey(db->expires)) == NULL) break; - ttl = dictGetSignedIntegerVal(de)-now; - if (activeExpireCycleTryExpire(db,de,now)) expired++; - if (ttl > 0) { - /* We want the average TTL of keys yet not expired. */ - ttl_sum += ttl; - ttl_samples++; + while (sampled < num && checked_buckets < max_buckets) { + for (int table = 0; table < 2; table++) { + if (table == 1 && !dictIsRehashing(db->expires)) break; + + unsigned long idx = db->expires_cursor; + idx &= db->expires->ht[table].sizemask; + dictEntry *de = db->expires->ht[table].table[idx]; + long long ttl; + + /* Scan the current bucket of the current table. */ + checked_buckets++; + while(de) { + /* Get the next entry now since this entry may get + * deleted. */ + dictEntry *e = de; + de = de->next; + + ttl = dictGetSignedIntegerVal(e)-now; + if (activeExpireCycleTryExpire(db,e,now)) expired++; + if (ttl > 0) { + /* We want the average TTL of keys yet + * not expired. */ + ttl_sum += ttl; + ttl_samples++; + } + sampled++; + } } - total_sampled++; + db->expires_cursor++; } total_expired += expired; + total_sampled += sampled; /* Update the average TTL stats for this database. */ if (ttl_samples) { @@ -224,12 +305,15 @@ void activeExpireCycle(int type) { break; } } - /* 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); + /* We don't repeat the cycle for the current database if there are + * an acceptable amount of stale keys (logically expired but yet + * not reclaimed). */ + } while (sampled == 0 || + (expired*100/sampled) > config_cycle_acceptable_stale); } elapsed = ustime()-start; + server.stat_expire_cycle_time_used += elapsed; latencyAddSampleIfNeeded("expire-cycle",elapsed/1000); /* Update our estimate of keys existing but yet to be expired. diff --git a/src/geo.c b/src/geo.c index 91a0421f5..f7920a2e2 100644 --- a/src/geo.c +++ b/src/geo.c @@ -466,7 +466,7 @@ void georadiusGeneric(client *c, int flags) { /* Look up the requested zset */ robj *zobj = NULL; - if ((zobj = lookupKeyReadOrReply(c, key, shared.null[c->resp])) == NULL || + if ((zobj = lookupKeyReadOrReply(c, key, shared.emptyarray)) == NULL || checkType(c, zobj, OBJ_ZSET)) { return; } @@ -566,7 +566,7 @@ void georadiusGeneric(client *c, int flags) { /* If no matching results, the user gets an empty reply. */ if (ga->used == 0 && storekey == NULL) { - addReplyNull(c); + addReply(c,shared.emptyarray); geoArrayFree(ga); return; } @@ -659,7 +659,7 @@ void georadiusGeneric(client *c, int flags) { zsetConvertToZiplistIfNeeded(zobj,maxelelen); setKey(c->db,storekey,zobj); decrRefCount(zobj); - notifyKeyspaceEvent(NOTIFY_LIST,"georadiusstore",storekey, + notifyKeyspaceEvent(NOTIFY_ZSET,"georadiusstore",storekey, c->db->id); server.dirty += returned_items; } else if (dbDelete(c->db,storekey)) { @@ -737,7 +737,15 @@ void geohashCommand(client *c) { char buf[12]; int i; for (i = 0; i < 11; i++) { - int idx = (hash.bits >> (52-((i+1)*5))) & 0x1f; + int idx; + if (i == 10) { + /* We have just 52 bits, but the API used to output + * an 11 bytes geohash. For compatibility we assume + * zero. */ + idx = 0; + } else { + idx = (hash.bits >> (52-((i+1)*5))) & 0x1f; + } buf[i] = geoalphabet[idx]; } buf[11] = '\0'; diff --git a/src/hyperloglog.c b/src/hyperloglog.c index fc21ea006..facd99743 100644 --- a/src/hyperloglog.c +++ b/src/hyperloglog.c @@ -614,6 +614,7 @@ int hllSparseToDense(robj *o) { } else { runlen = HLL_SPARSE_VAL_LEN(p); regval = HLL_SPARSE_VAL_VALUE(p); + if ((runlen + idx) > HLL_REGISTERS) break; /* Overflow. */ while(runlen--) { HLL_DENSE_SET_REGISTER(hdr->registers,idx,regval); idx++; @@ -699,7 +700,7 @@ int hllSparseSet(robj *o, long index, uint8_t count) { p += oplen; first += span; } - if (span == 0) return -1; /* Invalid format. */ + if (span == 0 || p >= end) return -1; /* Invalid format. */ next = HLL_SPARSE_IS_XZERO(p) ? p+2 : p+1; if (next >= end) next = NULL; @@ -1013,7 +1014,12 @@ uint64_t hllCount(struct hllhdr *hdr, int *invalid) { double m = HLL_REGISTERS; double E; int j; - int reghisto[HLL_Q+2] = {0}; + /* Note that reghisto size could be just HLL_Q+2, becuase HLL_Q+1 is + * the maximum frequency of the "000...1" sequence the hash function is + * able to return. However it is slow to check for sanity of the + * input: instead we history array at a safe size: overflows will + * just write data to wrong, but correctly allocated, places. */ + int reghisto[64] = {0}; /* Compute register histogram */ if (hdr->encoding == HLL_DENSE) { @@ -1088,6 +1094,7 @@ int hllMerge(uint8_t *max, robj *hll) { } else { runlen = HLL_SPARSE_VAL_LEN(p); regval = HLL_SPARSE_VAL_VALUE(p); + if ((runlen + i) > HLL_REGISTERS) break; /* Overflow. */ while(runlen--) { if (regval > max[i]) max[i] = regval; i++; @@ -1235,7 +1242,7 @@ void pfcountCommand(client *c) { if (o == NULL) continue; /* Assume empty HLL for non existing var.*/ if (isHLLObjectOrReply(c,o) != C_OK) return; - /* Merge with this HLL with our 'max' HHL by setting max[i] + /* Merge with this HLL with our 'max' HLL by setting max[i] * to MAX(max[i],hll[i]). */ if (hllMerge(registers,o) == C_ERR) { addReplySds(c,sdsnew(invalid_hll_err)); @@ -1322,7 +1329,7 @@ void pfmergeCommand(client *c) { hdr = o->ptr; if (hdr->encoding == HLL_DENSE) use_dense = 1; - /* Merge with this HLL with our 'max' HHL by setting max[i] + /* Merge with this HLL with our 'max' HLL by setting max[i] * to MAX(max[i],hll[i]). */ if (hllMerge(max,o) == C_ERR) { addReplySds(c,sdsnew(invalid_hll_err)); @@ -1528,6 +1535,7 @@ void pfdebugCommand(client *c) { sds decoded = sdsempty(); if (hdr->encoding != HLL_SPARSE) { + sdsfree(decoded); addReplyError(c,"HLL encoding is not sparse"); return; } diff --git a/src/latency.c b/src/latency.c index 33aa1245b..74ced72a5 100644 --- a/src/latency.c +++ b/src/latency.c @@ -95,7 +95,7 @@ void latencyMonitorInit(void) { * This function is usually called via latencyAddSampleIfNeeded(), that * is a macro that only adds the sample if the latency is higher than * server.latency_monitor_threshold. */ -void latencyAddSample(char *event, mstime_t latency) { +void latencyAddSample(const char *event, mstime_t latency) { struct latencyTimeSeries *ts = dictFetchValue(server.latency_events,event); time_t now = time(NULL); int prev; @@ -599,7 +599,7 @@ NULL event = dictGetKey(de); graph = latencyCommandGenSparkeline(event,ts); - addReplyBulkCString(c,graph); + addReplyVerbatim(c,graph,sdslen(graph),"txt"); sdsfree(graph); } else if (!strcasecmp(c->argv[1]->ptr,"latest") && c->argc == 2) { /* LATENCY LATEST */ @@ -608,7 +608,7 @@ NULL /* LATENCY DOCTOR */ sds report = createLatencyReport(); - addReplyBulkCBuffer(c,report,sdslen(report)); + addReplyVerbatim(c,report,sdslen(report),"txt"); sdsfree(report); } else if (!strcasecmp(c->argv[1]->ptr,"reset") && c->argc >= 2) { /* LATENCY RESET */ diff --git a/src/latency.h b/src/latency.h index 0fe26e0e4..76640cfce 100644 --- a/src/latency.h +++ b/src/latency.h @@ -62,7 +62,7 @@ struct latencyStats { }; void latencyMonitorInit(void); -void latencyAddSample(char *event, mstime_t latency); +void latencyAddSample(const char *event, mstime_t latency); int THPIsEnabled(void); /* Latency monitoring macros. */ diff --git a/src/localtime.c b/src/localtime.c index 3f59a3331..e2ac81f98 100644 --- a/src/localtime.c +++ b/src/localtime.c @@ -52,8 +52,8 @@ static int is_leap_year(time_t year) { if (year % 4) return 0; /* A year not divisible by 4 is not leap. */ else if (year % 100) return 1; /* If div by 4 and not 100 is surely leap. */ - else if (year % 400) return 0; /* If div by 100 *and* 400 is not leap. */ - else return 1; /* If div by 100 and not by 400 is leap. */ + else if (year % 400) return 0; /* If div by 100 *and* not by 400 is not leap. */ + else return 1; /* If div by 100 and 400 is leap. */ } void nolocks_localtime(struct tm *tmp, time_t t, time_t tz, int dst) { diff --git a/src/lolwut.c b/src/lolwut.c index 19cbcf642..0e1552ba0 100644 --- a/src/lolwut.c +++ b/src/lolwut.c @@ -34,8 +34,11 @@ */ #include "server.h" +#include "lolwut.h" +#include void lolwut5Command(client *c); +void lolwut6Command(client *c); /* The default target for LOLWUT if no matching version was found. * This is what unstable versions of Redis will display. */ @@ -43,14 +46,143 @@ void lolwutUnstableCommand(client *c) { sds rendered = sdsnew("Redis ver. "); rendered = sdscat(rendered,REDIS_VERSION); rendered = sdscatlen(rendered,"\n",1); - addReplyBulkSds(c,rendered); + addReplyVerbatim(c,rendered,sdslen(rendered),"txt"); + sdsfree(rendered); } +/* LOLWUT [VERSION ] [... version specific arguments ...] */ void lolwutCommand(client *c) { char *v = REDIS_VERSION; - if ((v[0] == '5' && v[1] == '.') || + char verstr[64]; + + if (c->argc >= 3 && !strcasecmp(c->argv[1]->ptr,"version")) { + long ver; + if (getLongFromObjectOrReply(c,c->argv[2],&ver,NULL) != C_OK) return; + snprintf(verstr,sizeof(verstr),"%u.0.0",(unsigned int)ver); + v = verstr; + + /* Adjust argv/argc to filter the "VERSION ..." option, since the + * specific LOLWUT version implementations don't know about it + * and expect their arguments. */ + c->argv += 2; + c->argc -= 2; + } + + if ((v[0] == '5' && v[1] == '.' && v[2] != '9') || (v[0] == '4' && v[1] == '.' && v[2] == '9')) lolwut5Command(c); + else if ((v[0] == '6' && v[1] == '.' && v[2] != '9') || + (v[0] == '5' && v[1] == '.' && v[2] == '9')) + lolwut6Command(c); else lolwutUnstableCommand(c); + + /* Fix back argc/argv in case of VERSION argument. */ + if (v == verstr) { + c->argv -= 2; + c->argc += 2; + } +} + +/* ========================== LOLWUT Canvase =============================== + * Many LOWUT versions will likely print some computer art to the screen. + * This is the case with LOLWUT 5 and LOLWUT 6, so here there is a generic + * canvas implementation that can be reused. */ + +/* Allocate and return a new canvas of the specified size. */ +lwCanvas *lwCreateCanvas(int width, int height, int bgcolor) { + lwCanvas *canvas = zmalloc(sizeof(*canvas)); + canvas->width = width; + canvas->height = height; + canvas->pixels = zmalloc(width*height); + memset(canvas->pixels,bgcolor,width*height); + return canvas; +} + +/* Free the canvas created by lwCreateCanvas(). */ +void lwFreeCanvas(lwCanvas *canvas) { + zfree(canvas->pixels); + zfree(canvas); +} + +/* Set a pixel to the specified color. Color is 0 or 1, where zero means no + * dot will be displyed, and 1 means dot will be displayed. + * Coordinates are arranged so that left-top corner is 0,0. You can write + * out of the size of the canvas without issues. */ +void lwDrawPixel(lwCanvas *canvas, int x, int y, int color) { + if (x < 0 || x >= canvas->width || + y < 0 || y >= canvas->height) return; + canvas->pixels[x+y*canvas->width] = color; +} + +/* Return the value of the specified pixel on the canvas. */ +int lwGetPixel(lwCanvas *canvas, int x, int y) { + if (x < 0 || x >= canvas->width || + y < 0 || y >= canvas->height) return 0; + return canvas->pixels[x+y*canvas->width]; +} + +/* Draw a line from x1,y1 to x2,y2 using the Bresenham algorithm. */ +void lwDrawLine(lwCanvas *canvas, int x1, int y1, int x2, int y2, int color) { + int dx = abs(x2-x1); + int dy = abs(y2-y1); + int sx = (x1 < x2) ? 1 : -1; + int sy = (y1 < y2) ? 1 : -1; + int err = dx-dy, e2; + + while(1) { + lwDrawPixel(canvas,x1,y1,color); + if (x1 == x2 && y1 == y2) break; + e2 = err*2; + if (e2 > -dy) { + err -= dy; + x1 += sx; + } + if (e2 < dx) { + err += dx; + y1 += sy; + } + } +} + +/* Draw a square centered at the specified x,y coordinates, with the specified + * rotation angle and size. In order to write a rotated square, we use the + * trivial fact that the parametric equation: + * + * x = sin(k) + * y = cos(k) + * + * Describes a circle for values going from 0 to 2*PI. So basically if we start + * at 45 degrees, that is k = PI/4, with the first point, and then we find + * the other three points incrementing K by PI/2 (90 degrees), we'll have the + * points of the square. In order to rotate the square, we just start with + * k = PI/4 + rotation_angle, and we are done. + * + * Of course the vanilla equations above will describe the square inside a + * circle of radius 1, so in order to draw larger squares we'll have to + * multiply the obtained coordinates, and then translate them. However this + * is much simpler than implementing the abstract concept of 2D shape and then + * performing the rotation/translation transformation, so for LOLWUT it's + * a good approach. */ +void lwDrawSquare(lwCanvas *canvas, int x, int y, float size, float angle, int color) { + int px[4], py[4]; + + /* Adjust the desired size according to the fact that the square inscribed + * into a circle of radius 1 has the side of length SQRT(2). This way + * size becomes a simple multiplication factor we can use with our + * coordinates to magnify them. */ + size /= 1.4142135623; + size = round(size); + + /* Compute the four points. */ + float k = M_PI/4 + angle; + for (int j = 0; j < 4; j++) { + px[j] = round(sin(k) * size + x); + py[j] = round(cos(k) * size + y); + k += M_PI/2; + } + + /* Draw the square. */ + for (int j = 0; j < 4; j++) + lwDrawLine(canvas,px[j],py[j],px[(j+1)%4],py[(j+1)%4],color); } diff --git a/src/lolwut.h b/src/lolwut.h new file mode 100644 index 000000000..38c0de423 --- /dev/null +++ b/src/lolwut.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2018-2019, Salvatore Sanfilippo + * 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. + */ + +/* This structure represents our canvas. Drawing functions will take a pointer + * to a canvas to write to it. Later the canvas can be rendered to a string + * suitable to be printed on the screen, using unicode Braille characters. */ + +/* This represents a very simple generic canvas in order to draw stuff. + * It's up to each LOLWUT versions to translate what they draw to the + * screen, depending on the result to accomplish. */ +typedef struct lwCanvas { + int width; + int height; + char *pixels; +} lwCanvas; + +/* Drawing functions implemented inside lolwut.c. */ +lwCanvas *lwCreateCanvas(int width, int height, int bgcolor); +void lwFreeCanvas(lwCanvas *canvas); +void lwDrawPixel(lwCanvas *canvas, int x, int y, int color); +int lwGetPixel(lwCanvas *canvas, int x, int y); +void lwDrawLine(lwCanvas *canvas, int x1, int y1, int x2, int y2, int color); +void lwDrawSquare(lwCanvas *canvas, int x, int y, float size, float angle, int color); diff --git a/src/lolwut5.c b/src/lolwut5.c index 8408b378d..5a9348800 100644 --- a/src/lolwut5.c +++ b/src/lolwut5.c @@ -34,17 +34,9 @@ */ #include "server.h" +#include "lolwut.h" #include -/* This structure represents our canvas. Drawing functions will take a pointer - * to a canvas to write to it. Later the canvas can be rendered to a string - * suitable to be printed on the screen, using unicode Braille characters. */ -typedef struct lwCanvas { - int width; - int height; - char *pixels; -} lwCanvas; - /* Translate a group of 8 pixels (2x4 vertical rectangle) to the corresponding * braille character. The byte should correspond to the pixels arranged as * follows, where 0 is the least significant bit, and 7 the most significant @@ -69,104 +61,6 @@ void lwTranslatePixelsGroup(int byte, char *output) { output[2] = 0x80 | (code & 0x3F); /* 10-xxxxxx */ } -/* Allocate and return a new canvas of the specified size. */ -lwCanvas *lwCreateCanvas(int width, int height) { - lwCanvas *canvas = zmalloc(sizeof(*canvas)); - canvas->width = width; - canvas->height = height; - canvas->pixels = zmalloc(width*height); - memset(canvas->pixels,0,width*height); - return canvas; -} - -/* Free the canvas created by lwCreateCanvas(). */ -void lwFreeCanvas(lwCanvas *canvas) { - zfree(canvas->pixels); - zfree(canvas); -} - -/* Set a pixel to the specified color. Color is 0 or 1, where zero means no - * dot will be displyed, and 1 means dot will be displayed. - * Coordinates are arranged so that left-top corner is 0,0. You can write - * out of the size of the canvas without issues. */ -void lwDrawPixel(lwCanvas *canvas, int x, int y, int color) { - if (x < 0 || x >= canvas->width || - y < 0 || y >= canvas->height) return; - canvas->pixels[x+y*canvas->width] = color; -} - -/* Return the value of the specified pixel on the canvas. */ -int lwGetPixel(lwCanvas *canvas, int x, int y) { - if (x < 0 || x >= canvas->width || - y < 0 || y >= canvas->height) return 0; - return canvas->pixels[x+y*canvas->width]; -} - -/* Draw a line from x1,y1 to x2,y2 using the Bresenham algorithm. */ -void lwDrawLine(lwCanvas *canvas, int x1, int y1, int x2, int y2, int color) { - int dx = abs(x2-x1); - int dy = abs(y2-y1); - int sx = (x1 < x2) ? 1 : -1; - int sy = (y1 < y2) ? 1 : -1; - int err = dx-dy, e2; - - while(1) { - lwDrawPixel(canvas,x1,y1,color); - if (x1 == x2 && y1 == y2) break; - e2 = err*2; - if (e2 > -dy) { - err -= dy; - x1 += sx; - } - if (e2 < dx) { - err += dx; - y1 += sy; - } - } -} - -/* Draw a square centered at the specified x,y coordinates, with the specified - * rotation angle and size. In order to write a rotated square, we use the - * trivial fact that the parametric equation: - * - * x = sin(k) - * y = cos(k) - * - * Describes a circle for values going from 0 to 2*PI. So basically if we start - * at 45 degrees, that is k = PI/4, with the first point, and then we find - * the other three points incrementing K by PI/2 (90 degrees), we'll have the - * points of the square. In order to rotate the square, we just start with - * k = PI/4 + rotation_angle, and we are done. - * - * Of course the vanilla equations above will describe the square inside a - * circle of radius 1, so in order to draw larger squares we'll have to - * multiply the obtained coordinates, and then translate them. However this - * is much simpler than implementing the abstract concept of 2D shape and then - * performing the rotation/translation transformation, so for LOLWUT it's - * a good approach. */ -void lwDrawSquare(lwCanvas *canvas, int x, int y, float size, float angle) { - int px[4], py[4]; - - /* Adjust the desired size according to the fact that the square inscribed - * into a circle of radius 1 has the side of length SQRT(2). This way - * size becomes a simple multiplication factor we can use with our - * coordinates to magnify them. */ - size /= 1.4142135623; - size = round(size); - - /* Compute the four points. */ - float k = M_PI/4 + angle; - for (int j = 0; j < 4; j++) { - px[j] = round(sin(k) * size + x); - py[j] = round(cos(k) * size + y); - k += M_PI/2; - } - - /* Draw the square. */ - for (int j = 0; j < 4; j++) - lwDrawLine(canvas,px[j],py[j],px[(j+1)%4],py[(j+1)%4],1); -} - /* Schotter, the output of LOLWUT of Redis 5, is a computer graphic art piece * generated by Georg Nees in the 60s. It explores the relationship between * caos and order. @@ -180,7 +74,7 @@ lwCanvas *lwDrawSchotter(int console_cols, int squares_per_row, int squares_per_ int padding = canvas_width > 4 ? 2 : 0; float square_side = (float)(canvas_width-padding*2) / squares_per_row; int canvas_height = square_side * squares_per_col + padding*2; - lwCanvas *canvas = lwCreateCanvas(canvas_width, canvas_height); + lwCanvas *canvas = lwCreateCanvas(canvas_width, canvas_height, 0); for (int y = 0; y < squares_per_col; y++) { for (int x = 0; x < squares_per_row; x++) { @@ -200,7 +94,7 @@ lwCanvas *lwDrawSchotter(int console_cols, int squares_per_row, int squares_per_ sx += r2*square_side/3; sy += r3*square_side/3; } - lwDrawSquare(canvas,sx,sy,square_side,angle); + lwDrawSquare(canvas,sx,sy,square_side,angle,1); } } @@ -212,7 +106,7 @@ lwCanvas *lwDrawSchotter(int console_cols, int squares_per_row, int squares_per_ * logical canvas. The actual returned string will require a terminal that is * width/2 large and height/4 tall in order to hold the whole image without * overflowing or scrolling, since each Barille character is 2x4. */ -sds lwRenderCanvas(lwCanvas *canvas) { +static sds renderCanvas(lwCanvas *canvas) { sds text = sdsempty(); for (int y = 0; y < canvas->height; y += 4) { for (int x = 0; x < canvas->width; x += 2) { @@ -272,11 +166,12 @@ void lolwut5Command(client *c) { /* Generate some computer art and reply. */ lwCanvas *canvas = lwDrawSchotter(cols,squares_per_row,squares_per_col); - sds rendered = lwRenderCanvas(canvas); + sds rendered = renderCanvas(canvas); rendered = sdscat(rendered, "\nGeorg Nees - schotter, plotter on paper, 1968. Redis ver. "); rendered = sdscat(rendered,REDIS_VERSION); rendered = sdscatlen(rendered,"\n",1); - addReplyBulkSds(c,rendered); + addReplyVerbatim(c,rendered,sdslen(rendered),"txt"); + sdsfree(rendered); lwFreeCanvas(canvas); } diff --git a/src/lolwut6.c b/src/lolwut6.c new file mode 100644 index 000000000..471bf66c8 --- /dev/null +++ b/src/lolwut6.c @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2019, Salvatore Sanfilippo + * 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. + * + * ---------------------------------------------------------------------------- + * + * This file implements the LOLWUT command. The command should do something + * fun and interesting, and should be replaced by a new implementation at + * each new version of Redis. + * + * Thanks to Michele Hiki Falcone for the original image that ispired + * the image, part of his game, Plaguemon. + * + * Thanks to the Shhh computer art collective for the help in tuning the + * output to have a better artistic effect. + */ + +#include "server.h" +#include "lolwut.h" + +/* Render the canvas using the four gray levels of the standard color + * terminal: they match very well to the grayscale display of the gameboy. */ +static sds renderCanvas(lwCanvas *canvas) { + sds text = sdsempty(); + for (int y = 0; y < canvas->height; y++) { + for (int x = 0; x < canvas->width; x++) { + int color = lwGetPixel(canvas,x,y); + char *ce; /* Color escape sequence. */ + + /* Note that we set both the foreground and background color. + * This way we are able to get a more consistent result among + * different terminals implementations. */ + switch(color) { + case 0: ce = "0;30;40m"; break; /* Black */ + case 1: ce = "0;90;100m"; break; /* Gray 1 */ + case 2: ce = "0;37;47m"; break; /* Gray 2 */ + case 3: ce = "0;97;107m"; break; /* White */ + default: ce = "0;30;40m"; break; /* Just for safety. */ + } + text = sdscatprintf(text,"\033[%s \033[0m",ce); + } + if (y != canvas->height-1) text = sdscatlen(text,"\n",1); + } + return text; +} + +/* Draw a skyscraper on the canvas, according to the parameters in the + * 'skyscraper' structure. Window colors are random and are always one + * of the two grays. */ +struct skyscraper { + int xoff; /* X offset. */ + int width; /* Pixels width. */ + int height; /* Pixels height. */ + int windows; /* Draw windows if true. */ + int color; /* Color of the skyscraper. */ +}; + +void generateSkyscraper(lwCanvas *canvas, struct skyscraper *si) { + int starty = canvas->height-1; + int endy = starty - si->height + 1; + for (int y = starty; y >= endy; y--) { + for (int x = si->xoff; x < si->xoff+si->width; x++) { + /* The roof is four pixels less wide. */ + if (y == endy && (x <= si->xoff+1 || x >= si->xoff+si->width-2)) + continue; + int color = si->color; + /* Alter the color if this is a place where we want to + * draw a window. We check that we are in the inner part of the + * skyscraper, so that windows are far from the borders. */ + if (si->windows && + x > si->xoff+1 && + x < si->xoff+si->width-2 && + y > endy+1 && + y < starty-1) + { + /* Calculate the x,y position relative to the start of + * the window area. */ + int relx = x - (si->xoff+1); + int rely = y - (endy+1); + + /* Note that we want the windows to be two pixels wide + * but just one pixel tall, because terminal "pixels" + * (characters) are not square. */ + if (relx/2 % 2 && rely % 2) { + do { + color = 1 + rand() % 2; + } while (color == si->color); + /* Except we want adjacent pixels creating the same + * window to be the same color. */ + if (relx % 2) color = lwGetPixel(canvas,x-1,y); + } + } + lwDrawPixel(canvas,x,y,color); + } + } +} + +/* Generate a skyline inspired by the parallax backgrounds of 8 bit games. */ +void generateSkyline(lwCanvas *canvas) { + struct skyscraper si; + + /* First draw the background skyscraper without windows, using the + * two different grays. We use two passes to make sure that the lighter + * ones are always in the background. */ + for (int color = 2; color >= 1; color--) { + si.color = color; + for (int offset = -10; offset < canvas->width;) { + offset += rand() % 8; + si.xoff = offset; + si.width = 10 + rand()%9; + if (color == 2) + si.height = canvas->height/2 + rand()%canvas->height/2; + else + si.height = canvas->height/2 + rand()%canvas->height/3; + si.windows = 0; + generateSkyscraper(canvas, &si); + if (color == 2) + offset += si.width/2; + else + offset += si.width+1; + } + } + + /* Now draw the foreground skyscraper with the windows. */ + si.color = 0; + for (int offset = -10; offset < canvas->width;) { + offset += rand() % 8; + si.xoff = offset; + si.width = 5 + rand()%14; + if (si.width % 4) si.width += (si.width % 3); + si.height = canvas->height/3 + rand()%canvas->height/2; + si.windows = 1; + generateSkyscraper(canvas, &si); + offset += si.width+5; + } +} + +/* The LOLWUT 6 command: + * + * LOLWUT [columns] [rows] + * + * By default the command uses 80 columns, 40 squares per row + * per column. + */ +void lolwut6Command(client *c) { + long cols = 80; + long rows = 20; + + /* Parse the optional arguments if any. */ + if (c->argc > 1 && + getLongFromObjectOrReply(c,c->argv[1],&cols,NULL) != C_OK) + return; + + if (c->argc > 2 && + getLongFromObjectOrReply(c,c->argv[2],&rows,NULL) != C_OK) + return; + + /* Limits. We want LOLWUT to be always reasonably fast and cheap to execute + * so we have maximum number of columns, rows, and output resulution. */ + if (cols < 1) cols = 1; + if (cols > 1000) cols = 1000; + if (rows < 1) rows = 1; + if (rows > 1000) rows = 1000; + + /* Generate the city skyline and reply. */ + lwCanvas *canvas = lwCreateCanvas(cols,rows,3); + generateSkyline(canvas); + sds rendered = renderCanvas(canvas); + rendered = sdscat(rendered, + "\nDedicated to the 8 bit game developers of past and present.\n" + "Original 8 bit image from Plaguemon by hikikomori. Redis ver. "); + rendered = sdscat(rendered,REDIS_VERSION); + rendered = sdscatlen(rendered,"\n",1); + addReplyVerbatim(c,rendered,sdslen(rendered),"txt"); + sdsfree(rendered); + lwFreeCanvas(canvas); +} diff --git a/src/mkreleasehdr.sh b/src/mkreleasehdr.sh index e6d558b17..236c26c2b 100755 --- a/src/mkreleasehdr.sh +++ b/src/mkreleasehdr.sh @@ -3,7 +3,7 @@ GIT_SHA1=`(git show-ref --head --hash=8 2> /dev/null || echo 00000000) | head -n GIT_DIRTY=`git diff --no-ext-diff 2> /dev/null | wc -l` BUILD_ID=`uname -n`"-"`date +%s` if [ -n "$SOURCE_DATE_EPOCH" ]; then - BUILD_ID=$(date -u -d "@$SOURCE_DATE_EPOCH" +%s 2>/dev/null || date -u -r "$SOURCE_DATE_EPOCH" +%s 2>/dev/null || date -u %s) + BUILD_ID=$(date -u -d "@$SOURCE_DATE_EPOCH" +%s 2>/dev/null || date -u -r "$SOURCE_DATE_EPOCH" +%s 2>/dev/null || date -u +%s) fi test -f release.h || touch release.h (cat release.h | grep SHA1 | grep $GIT_SHA1) && \ diff --git a/src/module.c b/src/module.c index 81982ba76..aae15d74b 100644 --- a/src/module.c +++ b/src/module.c @@ -29,10 +29,10 @@ #include "server.h" #include "cluster.h" +#include "rdb.h" #include - -#define REDISMODULE_CORE 1 -#include "redismodule.h" +#include +#include /* -------------------------------------------------------------------------- * Private data structures used by the modules system. Those are data @@ -40,6 +40,17 @@ * pointers that have an API the module can call with them) * -------------------------------------------------------------------------- */ +typedef struct RedisModuleInfoCtx { + struct RedisModule *module; + const char *requested_section; + sds info; /* info string we collected so far */ + int sections; /* number of sections we collected so far */ + int in_section; /* indication if we're in an active section or not */ + int in_dict_field; /* indication that we're curreintly appending to a dict */ +} RedisModuleInfoCtx; + +typedef void (*RedisModuleInfoFunc)(RedisModuleInfoCtx *ctx, int for_crash_report); + /* This structure represents a module inside the system. */ struct RedisModule { void *handle; /* Module dlopen() handle. */ @@ -47,9 +58,27 @@ struct RedisModule { int ver; /* Module version. We use just progressive integers. */ int apiver; /* Module API version as requested during initialization.*/ list *types; /* Module data types. */ + list *usedby; /* List of modules using APIs from this one. */ + list *using; /* List of modules we use some APIs of. */ + list *filters; /* List of filters the module has registered. */ + int in_call; /* RM_Call() nesting level */ + int in_hook; /* Hooks callback nesting level for this module (0 or 1). */ + int options; /* Module options and capabilities. */ + int blocked_clients; /* Count of RedisModuleBlockedClient in this module. */ + RedisModuleInfoFunc info_cb; /* Callback for module to add INFO fields. */ }; typedef struct RedisModule RedisModule; +/* This represents a shared API. Shared APIs will be used to populate + * the server.sharedapi dictionary, mapping names of APIs exported by + * modules for other modules to use, to their structure specifying the + * function pointer that can be called. */ +struct RedisModuleSharedAPI { + void *func; + RedisModule *module; +}; +typedef struct RedisModuleSharedAPI RedisModuleSharedAPI; + static dict *modules; /* Hash table of modules. SDS -> RedisModule ptr.*/ /* Entries in the context->amqueue array, representing objects to free @@ -65,6 +94,7 @@ struct AutoMemEntry { #define REDISMODULE_AM_REPLY 2 #define REDISMODULE_AM_FREED 3 /* Explicitly freed by user already. */ #define REDISMODULE_AM_DICT 4 +#define REDISMODULE_AM_INFO 5 /* The pool allocator block. Redis Modules can allocate memory via this special * allocator that will automatically release it all once the callback returns. @@ -112,16 +142,23 @@ struct RedisModuleCtx { void **postponed_arrays; /* To set with RM_ReplySetArrayLength(). */ int postponed_arrays_count; /* Number of entries in postponed_arrays. */ void *blocked_privdata; /* Privdata set when unblocking a client. */ + RedisModuleString *blocked_ready_key; /* Key ready when the reply callback + gets called for clients blocked + on keys. */ /* Used if there is the REDISMODULE_CTX_KEYS_POS_REQUEST flag set. */ int *keys_pos; int keys_count; struct RedisModulePoolAllocBlock *pa_head; + redisOpArray saved_oparray; /* When propagating commands in a callback + we reallocate the "also propagate" op + array. Here we save the old one to + restore it later. */ }; typedef struct RedisModuleCtx RedisModuleCtx; -#define REDISMODULE_CTX_INIT {(void*)(unsigned long)&RM_GetApi, NULL, NULL, NULL, NULL, 0, 0, 0, NULL, 0, NULL, NULL, 0, NULL} +#define REDISMODULE_CTX_INIT {(void*)(unsigned long)&RM_GetApi, NULL, NULL, NULL, NULL, 0, 0, 0, NULL, 0, NULL, NULL, NULL, 0, NULL, {0}} #define REDISMODULE_CTX_MULTI_EMITTED (1<<0) #define REDISMODULE_CTX_AUTO_MEMORY (1<<1) #define REDISMODULE_CTX_KEYS_POS_REQUEST (1<<2) @@ -129,6 +166,7 @@ typedef struct RedisModuleCtx RedisModuleCtx; #define REDISMODULE_CTX_BLOCKED_TIMEOUT (1<<4) #define REDISMODULE_CTX_THREAD_SAFE (1<<5) #define REDISMODULE_CTX_BLOCKED_DISCONNECTED (1<<6) +#define REDISMODULE_CTX_MODULE_COMMAND_CALL (1<<7) /* This represents a Redis key opened with RM_OpenKey(). */ struct RedisModuleKey { @@ -212,6 +250,8 @@ typedef struct RedisModuleBlockedClient { client *reply_client; /* Fake client used to accumulate replies in thread safe contexts. */ int dbid; /* Database number selected by the original client. */ + int blocked_on_keys; /* If blocked via RM_BlockClientOnKeys(). */ + int unblocked; /* Already on the moduleUnblocked list. */ } RedisModuleBlockedClient; static pthread_mutex_t moduleUnblockedClientsMutex = PTHREAD_MUTEX_INITIALIZER; @@ -258,6 +298,78 @@ typedef struct RedisModuleDictIter { raxIterator ri; } RedisModuleDictIter; +typedef struct RedisModuleCommandFilterCtx { + RedisModuleString **argv; + int argc; +} RedisModuleCommandFilterCtx; + +typedef void (*RedisModuleCommandFilterFunc) (RedisModuleCommandFilterCtx *filter); + +typedef struct RedisModuleCommandFilter { + /* The module that registered the filter */ + RedisModule *module; + /* Filter callback function */ + RedisModuleCommandFilterFunc callback; + /* REDISMODULE_CMDFILTER_* flags */ + int flags; +} RedisModuleCommandFilter; + +/* Registered filters */ +static list *moduleCommandFilters; + +typedef void (*RedisModuleForkDoneHandler) (int exitcode, int bysignal, void *user_data); + +static struct RedisModuleForkInfo { + RedisModuleForkDoneHandler done_handler; + void* done_handler_user_data; +} moduleForkInfo = {0}; + +typedef struct RedisModuleServerInfoData { + rax *rax; /* parsed info data. */ +} RedisModuleServerInfoData; + +/* Flags for moduleCreateArgvFromUserFormat(). */ +#define REDISMODULE_ARGV_REPLICATE (1<<0) +#define REDISMODULE_ARGV_NO_AOF (1<<1) +#define REDISMODULE_ARGV_NO_REPLICAS (1<<2) + +/* Determine whether Redis should signalModifiedKey implicitly. + * In case 'ctx' has no 'module' member (and therefore no module->options), + * we assume default behavior, that is, Redis signals. + * (see RM_GetThreadSafeContext) */ +#define SHOULD_SIGNAL_MODIFIED_KEYS(ctx) \ + ctx->module? !(ctx->module->options & REDISMODULE_OPTION_NO_IMPLICIT_SIGNAL_MODIFIED) : 1 + +/* Server events hooks data structures and defines: this modules API + * allow modules to subscribe to certain events in Redis, such as + * the start and end of an RDB or AOF save, the change of role in replication, + * and similar other events. */ + +typedef struct RedisModuleEventListener { + RedisModule *module; + RedisModuleEvent event; + RedisModuleEventCallback callback; +} RedisModuleEventListener; + +list *RedisModule_EventListeners; /* Global list of all the active events. */ +unsigned long long ModulesInHooks = 0; /* Total number of modules in hooks + callbacks right now. */ + +/* Data structures related to the redis module users */ + +/* This callback type is called by moduleNotifyUserChanged() every time + * a user authenticated via the module API is associated with a different + * user or gets disconnected. */ +typedef void (*RedisModuleUserChangedFunc) (uint64_t client_id, void *privdata); + +/* This is the object returned by RM_CreateModuleUser(). The module API is + * able to create users, set ACLs to such users, and later authenticate + * clients using such newly created users. */ +typedef struct RedisModuleUser { + user *user; /* Reference to the real redis user */ +} RedisModuleUser; + + /* -------------------------------------------------------------------------- * Prototypes * -------------------------------------------------------------------------- */ @@ -270,6 +382,7 @@ void moduleReplicateMultiIfNeeded(RedisModuleCtx *ctx); void RM_ZsetRangeStop(RedisModuleKey *kp); static void zsetKeyReset(RedisModuleKey *key); void RM_FreeDict(RedisModuleCtx *ctx, RedisModuleDict *d); +void RM_FreeServerInfo(RedisModuleCtx *ctx, RedisModuleServerInfoData *data); /* -------------------------------------------------------------------------- * Heap allocation raw functions @@ -427,7 +540,8 @@ int moduleDelKeyIfEmpty(RedisModuleKey *key) { case OBJ_LIST: isempty = listTypeLength(o) == 0; break; case OBJ_SET: isempty = setTypeSize(o) == 0; break; case OBJ_ZSET: isempty = zsetLength(o) == 0; break; - case OBJ_HASH : isempty = hashTypeLength(o) == 0; break; + case OBJ_HASH: isempty = hashTypeLength(o) == 0; break; + case OBJ_STREAM: isempty = streamLength(o) == 0; break; default: isempty = 0; } @@ -463,8 +577,44 @@ int RM_GetApi(const char *funcname, void **targetPtrPtr) { return REDISMODULE_OK; } +/* Helper function for when a command callback is called, in order to handle + * details needed to correctly replicate commands. */ +void moduleHandlePropagationAfterCommandCallback(RedisModuleCtx *ctx) { + client *c = ctx->client; + + /* We don't need to do anything here if the context was never used + * in order to propagate commands. */ + if (!(ctx->flags & REDISMODULE_CTX_MULTI_EMITTED)) return; + + if (c->flags & CLIENT_LUA) return; + + /* Handle the replication of the final EXEC, since whatever a command + * emits is always wrapped around MULTI/EXEC. */ + alsoPropagate(server.execCommand,c->db->id,&shared.exec,1, + PROPAGATE_AOF|PROPAGATE_REPL); + + /* If this is not a module command context (but is instead a simple + * callback context), we have to handle directly the "also propagate" + * array and emit it. In a module command call this will be handled + * directly by call(). */ + if (!(ctx->flags & REDISMODULE_CTX_MODULE_COMMAND_CALL) && + server.also_propagate.numops) + { + for (int j = 0; j < server.also_propagate.numops; j++) { + redisOp *rop = &server.also_propagate.ops[j]; + int target = rop->target; + if (target) + propagate(rop->cmd,rop->dbid,rop->argv,rop->argc,target); + } + redisOpArrayFree(&server.also_propagate); + /* Restore the previous oparray in case of nexted use of the API. */ + server.also_propagate = ctx->saved_oparray; + } +} + /* Free the context after the user function was called. */ void moduleFreeContext(RedisModuleCtx *ctx) { + moduleHandlePropagationAfterCommandCallback(ctx); autoMemoryCollect(ctx); poolAllocRelease(ctx); if (ctx->postponed_arrays) { @@ -480,35 +630,33 @@ void moduleFreeContext(RedisModuleCtx *ctx) { if (ctx->flags & REDISMODULE_CTX_THREAD_SAFE) freeClient(ctx->client); } -/* Helper function for when a command callback is called, in order to handle - * details needed to correctly replicate commands. */ -void moduleHandlePropagationAfterCommandCallback(RedisModuleCtx *ctx) { - client *c = ctx->client; - - if (c->flags & CLIENT_LUA) return; - - /* Handle the replication of the final EXEC, since whatever a command - * emits is always wrapped around MULTI/EXEC. */ - if (ctx->flags & REDISMODULE_CTX_MULTI_EMITTED) { - robj *propargv[1]; - propargv[0] = createStringObject("EXEC",4); - alsoPropagate(server.execCommand,c->db->id,propargv,1, - PROPAGATE_AOF|PROPAGATE_REPL); - decrRefCount(propargv[0]); - } -} - /* This Redis command binds the normal Redis command invocation with commands * exported by modules. */ void RedisModuleCommandDispatcher(client *c) { RedisModuleCommandProxy *cp = (void*)(unsigned long)c->cmd->getkeys_proc; RedisModuleCtx ctx = REDISMODULE_CTX_INIT; + ctx.flags |= REDISMODULE_CTX_MODULE_COMMAND_CALL; ctx.module = cp->module; ctx.client = c; cp->func(&ctx,(void**)c->argv,c->argc); - moduleHandlePropagationAfterCommandCallback(&ctx); moduleFreeContext(&ctx); + + /* In some cases processMultibulkBuffer uses sdsMakeRoomFor to + * expand the query buffer, and in order to avoid a big object copy + * the query buffer SDS may be used directly as the SDS string backing + * the client argument vectors: sometimes this will result in the SDS + * string having unused space at the end. Later if a module takes ownership + * of the RedisString, such space will be wasted forever. Inside the + * Redis core this is not a problem because tryObjectEncoding() is called + * before storing strings in the key space. Here we need to do it + * for the module. */ + for (int i = 0; i < c->argc; i++) { + /* Only do the work if the module took ownership of the object: + * in that case the refcount is no longer 1. */ + if (c->argv[i]->refcount > 1) + trimStringObjectIfNeeded(c->argv[i]); + } } /* This function returns the list of keys, with the same interface as the @@ -566,9 +714,9 @@ void RM_KeyAtPos(RedisModuleCtx *ctx, int pos) { * flags into the command flags used by the Redis core. * * It returns the set of flags, or -1 if unknown flags are found. */ -int commandFlagsFromString(char *s) { +int64_t commandFlagsFromString(char *s) { int count, j; - int flags = 0; + int64_t flags = 0; sds *tokens = sdssplitlen(s,strlen(s)," ",1,&count); for (j = 0; j < count; j++) { char *t = tokens[j]; @@ -582,7 +730,9 @@ int commandFlagsFromString(char *s) { else if (!strcasecmp(t,"random")) flags |= CMD_RANDOM; else if (!strcasecmp(t,"allow-stale")) flags |= CMD_STALE; else if (!strcasecmp(t,"no-monitor")) flags |= CMD_SKIP_MONITOR; + else if (!strcasecmp(t,"no-slowlog")) flags |= CMD_SKIP_SLOWLOG; else if (!strcasecmp(t,"fast")) flags |= CMD_FAST; + else if (!strcasecmp(t,"no-auth")) flags |= CMD_NO_AUTH; else if (!strcasecmp(t,"getkeys-api")) flags |= CMD_MODULE_GETKEYS; else if (!strcasecmp(t,"no-cluster")) flags |= CMD_MODULE_NO_CLUSTER; else break; @@ -632,6 +782,8 @@ int commandFlagsFromString(char *s) { * this means. * * **"no-monitor"**: Don't propagate the command on monitor. Use this if * the command has sensible data among the arguments. + * * **"no-slowlog"**: Don't log this command in the slowlog. Use this if + * the command has sensible data among the arguments. * * **"fast"**: The command time complexity is not greater * than O(log(N)) where N is the size of the collection or * anything else representing the normal scalability @@ -644,9 +796,12 @@ int commandFlagsFromString(char *s) { * example, is unable to report the position of the * keys, programmatically creates key names, or any * other reason. + * * **"no-auth"**: This command can be run by an un-authenticated client. + * Normally this is used by a command that is used + * to authenticate a client. */ int RM_CreateCommand(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc cmdfunc, const char *strflags, int firstkey, int lastkey, int keystep) { - int flags = strflags ? commandFlagsFromString((char*)strflags) : 0; + int64_t flags = strflags ? commandFlagsFromString((char*)strflags) : 0; if (flags == -1) return REDISMODULE_ERR; if ((flags & CMD_MODULE_NO_CLUSTER) && server.cluster_enabled) return REDISMODULE_ERR; @@ -701,6 +856,13 @@ void RM_SetModuleAttribs(RedisModuleCtx *ctx, const char *name, int ver, int api module->ver = ver; module->apiver = apiver; module->types = listCreate(); + module->usedby = listCreate(); + module->using = listCreate(); + module->filters = listCreate(); + module->in_call = 0; + module->in_hook = 0; + module->options = 0; + module->info_cb = 0; ctx->module = module; } @@ -718,6 +880,26 @@ long long RM_Milliseconds(void) { return mstime(); } +/* Set flags defining capabilities or behavior bit flags. + * + * REDISMODULE_OPTIONS_HANDLE_IO_ERRORS: + * Generally, modules don't need to bother with this, as the process will just + * terminate if a read error happens, however, setting this flag would allow + * repl-diskless-load to work if enabled. + * The module should use RedisModule_IsIOError after reads, before using the + * data that was read, and in case of error, propagate it upwards, and also be + * able to release the partially populated value and all it's allocations. */ +void RM_SetModuleOptions(RedisModuleCtx *ctx, int options) { + ctx->module->options = options; +} + +/* Signals that the key is modified from user's perspective (i.e. invalidate WATCH + * and client side caching). */ +int RM_SignalModifiedKey(RedisModuleCtx *ctx, RedisModuleString *keyname) { + signalModifiedKey(ctx->client->db,keyname); + return REDISMODULE_OK; +} + /* -------------------------------------------------------------------------- * Automatic memory management for modules * -------------------------------------------------------------------------- */ @@ -793,6 +975,7 @@ void autoMemoryCollect(RedisModuleCtx *ctx) { case REDISMODULE_AM_REPLY: RM_FreeCallReply(ptr); break; case REDISMODULE_AM_KEY: RM_CloseKey(ptr); break; case REDISMODULE_AM_DICT: RM_FreeDict(NULL,ptr); break; + case REDISMODULE_AM_INFO: RM_FreeServerInfo(NULL,ptr); break; } } ctx->flags |= REDISMODULE_CTX_AUTO_MEMORY; @@ -859,6 +1042,21 @@ RedisModuleString *RM_CreateStringFromLongLong(RedisModuleCtx *ctx, long long ll return RM_CreateString(ctx,buf,len); } +/* Like RedisModule_CreatString(), but creates a string starting from a long + * double. + * + * The returned string must be released with RedisModule_FreeString() or by + * enabling automatic memory management. + * + * The passed context 'ctx' may be NULL if necessary, see the + * RedisModule_CreateString() documentation for more info. */ +RedisModuleString *RM_CreateStringFromLongDouble(RedisModuleCtx *ctx, long double ld, int humanfriendly) { + char buf[MAX_LONG_DOUBLE_CHARS]; + size_t len = ld2string(buf,sizeof(buf),ld, + (humanfriendly ? LD_STR_HUMAN : LD_STR_AUTO)); + return RM_CreateString(ctx,buf,len); +} + /* Like RedisModule_CreatString(), but creates a string starting from another * RedisModuleString. * @@ -963,6 +1161,14 @@ int RM_StringToDouble(const RedisModuleString *str, double *d) { return (retval == C_OK) ? REDISMODULE_OK : REDISMODULE_ERR; } +/* Convert the string into a long double, storing it at `*ld`. + * Returns REDISMODULE_OK on success or REDISMODULE_ERR if the string is + * not a valid string representation of a double value. */ +int RM_StringToLongDouble(const RedisModuleString *str, long double *ld) { + int retval = string2ld(str->ptr,sdslen(str->ptr),ld); + return retval ? REDISMODULE_OK : REDISMODULE_ERR; +} + /* Compare two string objects, returning -1, 0 or 1 respectively if * a < b, a == b, a > b. Strings are compared byte by byte as two * binary blobs without any encoding care / collation attempt. */ @@ -1072,10 +1278,9 @@ int RM_ReplyWithLongLong(RedisModuleCtx *ctx, long long ll) { int replyWithStatus(RedisModuleCtx *ctx, const char *msg, char *prefix) { client *c = moduleGetReplyClient(ctx); if (c == NULL) return REDISMODULE_OK; - sds strmsg = sdsnewlen(prefix,1); - strmsg = sdscat(strmsg,msg); - strmsg = sdscatlen(strmsg,"\r\n",2); - addReplySds(c,strmsg); + addReplyProto(c,prefix,strlen(prefix)); + addReplyProto(c,msg,strlen(msg)); + addReplyProto(c,"\r\n",2); return REDISMODULE_OK; } @@ -1132,6 +1337,27 @@ int RM_ReplyWithArray(RedisModuleCtx *ctx, long len) { return REDISMODULE_OK; } +/* Reply to the client with a null array, simply null in RESP3 + * null array in RESP2. + * + * The function always returns REDISMODULE_OK. */ +int RM_ReplyWithNullArray(RedisModuleCtx *ctx) { + client *c = moduleGetReplyClient(ctx); + if (c == NULL) return REDISMODULE_OK; + addReplyNullArray(c); + return REDISMODULE_OK; +} + +/* Reply to the client with an empty array. + * + * The function always returns REDISMODULE_OK. */ +int RM_ReplyWithEmptyArray(RedisModuleCtx *ctx) { + client *c = moduleGetReplyClient(ctx); + if (c == NULL) return REDISMODULE_OK; + addReply(c,shared.emptyarray); + return REDISMODULE_OK; +} + /* When RedisModule_ReplyWithArray() is used with the argument * REDISMODULE_POSTPONED_ARRAY_LEN, because we don't know beforehand the number * of items we are going to output as elements of the array, this function @@ -1189,6 +1415,17 @@ int RM_ReplyWithStringBuffer(RedisModuleCtx *ctx, const char *buf, size_t len) { return REDISMODULE_OK; } +/* Reply with a bulk string, taking in input a C buffer pointer that is + * assumed to be null-terminated. + * + * The function always returns REDISMODULE_OK. */ +int RM_ReplyWithCString(RedisModuleCtx *ctx, const char *buf) { + client *c = moduleGetReplyClient(ctx); + if (c == NULL) return REDISMODULE_OK; + addReplyBulkCString(c,(char*)buf); + return REDISMODULE_OK; +} + /* Reply with a bulk string, taking in input a RedisModuleString object. * * The function always returns REDISMODULE_OK. */ @@ -1199,8 +1436,28 @@ int RM_ReplyWithString(RedisModuleCtx *ctx, RedisModuleString *str) { return REDISMODULE_OK; } -/* Reply to the client with a NULL. In the RESP protocol a NULL is encoded - * as the string "$-1\r\n". +/* Reply with an empty string. + * + * The function always returns REDISMODULE_OK. */ +int RM_ReplyWithEmptyString(RedisModuleCtx *ctx) { + client *c = moduleGetReplyClient(ctx); + if (c == NULL) return REDISMODULE_OK; + addReply(c,shared.emptybulk); + return REDISMODULE_OK; +} + +/* Reply with a binary safe string, which should not be escaped or filtered + * taking in input a C buffer pointer and length. + * + * The function always returns REDISMODULE_OK. */ +int RM_ReplyWithVerbatimString(RedisModuleCtx *ctx, const char *buf, size_t len) { + client *c = moduleGetReplyClient(ctx); + if (c == NULL) return REDISMODULE_OK; + addReplyVerbatim(c, buf, len, "txt"); + return REDISMODULE_OK; +} + +/* Reply to the client with a NULL. * * The function always returns REDISMODULE_OK. */ int RM_ReplyWithNull(RedisModuleCtx *ctx) { @@ -1237,6 +1494,21 @@ int RM_ReplyWithDouble(RedisModuleCtx *ctx, double d) { return REDISMODULE_OK; } +/* Send a string reply obtained converting the long double 'ld' into a bulk + * string. This function is basically equivalent to converting a long double + * into a string into a C buffer, and then calling the function + * RedisModule_ReplyWithStringBuffer() with the buffer and length. + * The double string uses human readable formatting (see + * `addReplyHumanLongDouble` in networking.c). + * + * The function always returns REDISMODULE_OK. */ +int RM_ReplyWithLongDouble(RedisModuleCtx *ctx, long double ld) { + client *c = moduleGetReplyClient(ctx); + if (c == NULL) return REDISMODULE_OK; + addReplyHumanLongDouble(c, ld); + return REDISMODULE_OK; +} + /* -------------------------------------------------------------------------- * Commands replication API * -------------------------------------------------------------------------- */ @@ -1251,9 +1523,16 @@ void moduleReplicateMultiIfNeeded(RedisModuleCtx *ctx) { /* If we already emitted MULTI return ASAP. */ if (ctx->flags & REDISMODULE_CTX_MULTI_EMITTED) return; /* If this is a thread safe context, we do not want to wrap commands - * executed into MUTLI/EXEC, they are executed as single commands + * executed into MULTI/EXEC, they are executed as single commands * from an external client in essence. */ if (ctx->flags & REDISMODULE_CTX_THREAD_SAFE) return; + /* If this is a callback context, and not a module command execution + * context, we have to setup the op array for the "also propagate" API + * so that RM_Replicate() will work. */ + if (!(ctx->flags & REDISMODULE_CTX_MODULE_COMMAND_CALL)) { + ctx->saved_oparray = server.also_propagate; + redisOpArrayInit(&server.also_propagate); + } execCommandPropagateMulti(ctx->client); ctx->flags |= REDISMODULE_CTX_MULTI_EMITTED; } @@ -1275,6 +1554,24 @@ void moduleReplicateMultiIfNeeded(RedisModuleCtx *ctx) { * * Please refer to RedisModule_Call() for more information. * + * Using the special "A" and "R" modifiers, the caller can exclude either + * the AOF or the replicas from the propagation of the specified command. + * Otherwise, by default, the command will be propagated in both channels. + * + * ## Note about calling this function from a thread safe context: + * + * Normally when you call this function from the callback implementing a + * module command, or any other callback provided by the Redis Module API, + * Redis will accumulate all the calls to this function in the context of + * the callback, and will propagate all the commands wrapped in a MULTI/EXEC + * transaction. However when calling this function from a threaded safe context + * that can live an undefined amount of time, and can be locked/unlocked in + * at will, the behavior is different: MULTI/EXEC wrapper is not emitted + * and the command specified is inserted in the AOF and replication stream + * immediately. + * + * ## Return value + * * The command returns REDISMODULE_ERR if the format specifiers are invalid * or the command name does not belong to a known command. */ int RM_Replicate(RedisModuleCtx *ctx, const char *cmdname, const char *fmt, ...) { @@ -1292,10 +1589,23 @@ int RM_Replicate(RedisModuleCtx *ctx, const char *cmdname, const char *fmt, ...) va_end(ap); if (argv == NULL) return REDISMODULE_ERR; - /* Replicate! */ - moduleReplicateMultiIfNeeded(ctx); - alsoPropagate(cmd,ctx->client->db->id,argv,argc, - PROPAGATE_AOF|PROPAGATE_REPL); + /* Select the propagation target. Usually is AOF + replicas, however + * the caller can exclude one or the other using the "A" or "R" + * modifiers. */ + int target = 0; + if (!(flags & REDISMODULE_ARGV_NO_AOF)) target |= PROPAGATE_AOF; + if (!(flags & REDISMODULE_ARGV_NO_REPLICAS)) target |= PROPAGATE_REPL; + + /* Replicate! When we are in a threaded context, we want to just insert + * the replicated command ASAP, since it is not clear when the context + * will stop being used, so accumulating stuff does not make much sense, + * nor we could easily use the alsoPropagate() API from threads. */ + if (ctx->flags & REDISMODULE_CTX_THREAD_SAFE) { + propagate(cmd,ctx->client->db->id,argv,argc,target); + } else { + moduleReplicateMultiIfNeeded(ctx); + alsoPropagate(cmd,ctx->client->db->id,argv,argc,target); + } /* Release the argv. */ for (j = 0; j < argc; j++) decrRefCount(argv[j]); @@ -1337,12 +1647,133 @@ int RM_ReplicateVerbatim(RedisModuleCtx *ctx) { * are guaranteed to get IDs greater than any past ID previously seen. * * Valid IDs are from 1 to 2^64-1. If 0 is returned it means there is no way - * to fetch the ID in the context the function was currently called. */ + * to fetch the ID in the context the function was currently called. + * + * After obtaining the ID, it is possible to check if the command execution + * is actually happening in the context of AOF loading, using this macro: + * + * if (RedisModule_IsAOFClient(RedisModule_GetClientId(ctx)) { + * // Handle it differently. + * } + */ unsigned long long RM_GetClientId(RedisModuleCtx *ctx) { if (ctx->client == NULL) return 0; return ctx->client->id; } +/* This is an helper for RM_GetClientInfoById() and other functions: given + * a client, it populates the client info structure with the appropriate + * fields depending on the version provided. If the version is not valid + * then REDISMODULE_ERR is returned. Otherwise the function returns + * REDISMODULE_OK and the structure pointed by 'ci' gets populated. */ + +int modulePopulateClientInfoStructure(void *ci, client *client, int structver) { + if (structver != 1) return REDISMODULE_ERR; + + RedisModuleClientInfoV1 *ci1 = ci; + memset(ci1,0,sizeof(*ci1)); + ci1->version = structver; + if (client->flags & CLIENT_MULTI) + ci1->flags |= REDISMODULE_CLIENTINFO_FLAG_MULTI; + if (client->flags & CLIENT_PUBSUB) + ci1->flags |= REDISMODULE_CLIENTINFO_FLAG_PUBSUB; + if (client->flags & CLIENT_UNIX_SOCKET) + ci1->flags |= REDISMODULE_CLIENTINFO_FLAG_UNIXSOCKET; + if (client->flags & CLIENT_TRACKING) + ci1->flags |= REDISMODULE_CLIENTINFO_FLAG_TRACKING; + if (client->flags & CLIENT_BLOCKED) + ci1->flags |= REDISMODULE_CLIENTINFO_FLAG_BLOCKED; + + int port; + connPeerToString(client->conn,ci1->addr,sizeof(ci1->addr),&port); + ci1->port = port; + ci1->db = client->db->id; + ci1->id = client->id; + return REDISMODULE_OK; +} + +/* This is an helper for moduleFireServerEvent() and other functions: + * It populates the replication info structure with the appropriate + * fields depending on the version provided. If the version is not valid + * then REDISMODULE_ERR is returned. Otherwise the function returns + * REDISMODULE_OK and the structure pointed by 'ri' gets populated. */ +int modulePopulateReplicationInfoStructure(void *ri, int structver) { + if (structver != 1) return REDISMODULE_ERR; + + RedisModuleReplicationInfoV1 *ri1 = ri; + memset(ri1,0,sizeof(*ri1)); + ri1->version = structver; + ri1->master = server.masterhost==NULL; + ri1->masterhost = server.masterhost? server.masterhost: ""; + ri1->masterport = server.masterport; + ri1->replid1 = server.replid; + ri1->replid2 = server.replid2; + ri1->repl1_offset = server.master_repl_offset; + ri1->repl2_offset = server.second_replid_offset; + return REDISMODULE_OK; +} + +/* Return information about the client with the specified ID (that was + * previously obtained via the RedisModule_GetClientId() API). If the + * client exists, REDISMODULE_OK is returned, otherwise REDISMODULE_ERR + * is returned. + * + * When the client exist and the `ci` pointer is not NULL, but points to + * a structure of type RedisModuleClientInfo, previously initialized with + * the correct REDISMODULE_CLIENTINFO_INITIALIZER, the structure is populated + * with the following fields: + * + * uint64_t flags; // REDISMODULE_CLIENTINFO_FLAG_* + * uint64_t id; // Client ID + * char addr[46]; // IPv4 or IPv6 address. + * uint16_t port; // TCP port. + * uint16_t db; // Selected DB. + * + * Note: the client ID is useless in the context of this call, since we + * already know, however the same structure could be used in other + * contexts where we don't know the client ID, yet the same structure + * is returned. + * + * With flags having the following meaning: + * + * REDISMODULE_CLIENTINFO_FLAG_SSL Client using SSL connection. + * REDISMODULE_CLIENTINFO_FLAG_PUBSUB Client in Pub/Sub mode. + * REDISMODULE_CLIENTINFO_FLAG_BLOCKED Client blocked in command. + * REDISMODULE_CLIENTINFO_FLAG_TRACKING Client with keys tracking on. + * REDISMODULE_CLIENTINFO_FLAG_UNIXSOCKET Client using unix domain socket. + * REDISMODULE_CLIENTINFO_FLAG_MULTI Client in MULTI state. + * + * However passing NULL is a way to just check if the client exists in case + * we are not interested in any additional information. + * + * This is the correct usage when we want the client info structure + * returned: + * + * RedisModuleClientInfo ci = REDISMODULE_CLIENTINFO_INITIALIZER; + * int retval = RedisModule_GetClientInfoById(&ci,client_id); + * if (retval == REDISMODULE_OK) { + * printf("Address: %s\n", ci.addr); + * } + */ +int RM_GetClientInfoById(void *ci, uint64_t id) { + client *client = lookupClientByID(id); + if (client == NULL) return REDISMODULE_ERR; + if (ci == NULL) return REDISMODULE_OK; + + /* Fill the info structure if passed. */ + uint64_t structver = ((uint64_t*)ci)[0]; + return modulePopulateClientInfoStructure(ci,client,structver); +} + +/* Publish a message to subscribers (see PUBLISH command). */ +int RM_PublishMessage(RedisModuleCtx *ctx, RedisModuleString *channel, RedisModuleString *message) { + UNUSED(ctx); + int receivers = pubsubPublishMessage(channel, message); + if (server.cluster_enabled) + clusterPropagatePublish(channel, message); + return receivers; +} + /* Return the currently selected DB. */ int RM_GetSelectedDb(RedisModuleCtx *ctx) { return ctx->client->db->id; @@ -1359,6 +1790,9 @@ int RM_GetSelectedDb(RedisModuleCtx *ctx) { * * * REDISMODULE_CTX_FLAGS_MULTI: The command is running inside a transaction * + * * REDISMODULE_CTX_FLAGS_REPLICATED: The command was sent over the replication + * link by the MASTER + * * * REDISMODULE_CTX_FLAGS_MASTER: The Redis instance is a master * * * REDISMODULE_CTX_FLAGS_SLAVE: The Redis instance is a slave @@ -1381,6 +1815,23 @@ int RM_GetSelectedDb(RedisModuleCtx *ctx) { * * * REDISMODULE_CTX_FLAGS_OOM_WARNING: Less than 25% of memory remains before * reaching the maxmemory level. + * + * * REDISMODULE_CTX_FLAGS_LOADING: Server is loading RDB/AOF + * + * * REDISMODULE_CTX_FLAGS_REPLICA_IS_STALE: No active link with the master. + * + * * REDISMODULE_CTX_FLAGS_REPLICA_IS_CONNECTING: The replica is trying to + * connect with the master. + * + * * REDISMODULE_CTX_FLAGS_REPLICA_IS_TRANSFERRING: Master -> Replica RDB + * transfer is in progress. + * + * * REDISMODULE_CTX_FLAGS_REPLICA_IS_ONLINE: The replica has an active link + * with its master. This is the + * contrary of STALE state. + * + * * REDISMODULE_CTX_FLAGS_ACTIVE_CHILD: There is currently some background + * process active (RDB, AUX or module). */ int RM_GetContextFlags(RedisModuleCtx *ctx) { @@ -1391,11 +1842,23 @@ int RM_GetContextFlags(RedisModuleCtx *ctx) { flags |= REDISMODULE_CTX_FLAGS_LUA; if (ctx->client->flags & CLIENT_MULTI) flags |= REDISMODULE_CTX_FLAGS_MULTI; + /* Module command recieved from MASTER, is replicated. */ + if (ctx->client->flags & CLIENT_MASTER) + flags |= REDISMODULE_CTX_FLAGS_REPLICATED; + } + + /* For DIRTY flags, we need the blocked client if used */ + client *c = ctx->blocked_client ? ctx->blocked_client->client : ctx->client; + if (c && (c->flags & (CLIENT_DIRTY_CAS|CLIENT_DIRTY_EXEC))) { + flags |= REDISMODULE_CTX_FLAGS_MULTI_DIRTY; } if (server.cluster_enabled) flags |= REDISMODULE_CTX_FLAGS_CLUSTER; + if (server.loading) + flags |= REDISMODULE_CTX_FLAGS_LOADING; + /* Maxmemory and eviction policy */ if (server.maxmemory > 0) { flags |= REDISMODULE_CTX_FLAGS_MAXMEMORY; @@ -1417,6 +1880,20 @@ int RM_GetContextFlags(RedisModuleCtx *ctx) { flags |= REDISMODULE_CTX_FLAGS_SLAVE; if (server.repl_slave_ro) flags |= REDISMODULE_CTX_FLAGS_READONLY; + + /* Replica state flags. */ + if (server.repl_state == REPL_STATE_CONNECT || + server.repl_state == REPL_STATE_CONNECTING) + { + flags |= REDISMODULE_CTX_FLAGS_REPLICA_IS_CONNECTING; + } else if (server.repl_state == REPL_STATE_TRANSFER) { + flags |= REDISMODULE_CTX_FLAGS_REPLICA_IS_TRANSFERRING; + } else if (server.repl_state == REPL_STATE_CONNECTED) { + flags |= REDISMODULE_CTX_FLAGS_REPLICA_IS_ONLINE; + } + + if (server.repl_state != REPL_STATE_CONNECTED) + flags |= REDISMODULE_CTX_FLAGS_REPLICA_IS_STALE; } /* OOM flag. */ @@ -1425,9 +1902,36 @@ int RM_GetContextFlags(RedisModuleCtx *ctx) { if (retval == C_ERR) flags |= REDISMODULE_CTX_FLAGS_OOM; if (level > 0.75) flags |= REDISMODULE_CTX_FLAGS_OOM_WARNING; + /* Presence of children processes. */ + if (hasActiveChildProcess()) flags |= REDISMODULE_CTX_FLAGS_ACTIVE_CHILD; + return flags; } +/* Returns true if some client sent the CLIENT PAUSE command to the server or + * if Redis Cluster is doing a manual failover, and paused tue clients. + * This is needed when we have a master with replicas, and want to write, + * without adding further data to the replication channel, that the replicas + * replication offset, match the one of the master. When this happens, it is + * safe to failover the master without data loss. + * + * However modules may generate traffic by calling RedisModule_Call() with + * the "!" flag, or by calling RedisModule_Replicate(), in a context outside + * commands execution, for instance in timeout callbacks, threads safe + * contexts, and so forth. When modules will generate too much traffic, it + * will be hard for the master and replicas offset to match, because there + * is more data to send in the replication channel. + * + * So modules may want to try to avoid very heavy background work that has + * the effect of creating data to the replication channel, when this function + * returns true. This is mostly useful for modules that have background + * garbage collection tasks, or that do writes and replicate such writes + * periodically in timer callbacks or other periodic callbacks. + */ +int RM_AvoidReplicaTraffic() { + return clientsArePaused(); +} + /* Change the currently selected DB. Returns an error if the id * is out of range. * @@ -1443,6 +1947,18 @@ int RM_SelectDb(RedisModuleCtx *ctx, int newid) { return (retval == C_OK) ? REDISMODULE_OK : REDISMODULE_ERR; } +/* Initialize a RedisModuleKey struct */ +static void moduleInitKey(RedisModuleKey *kp, RedisModuleCtx *ctx, robj *keyname, robj *value, int mode){ + kp->ctx = ctx; + kp->db = ctx->client->db; + kp->key = keyname; + incrRefCount(keyname); + kp->value = value; + kp->iter = NULL; + kp->mode = mode; + zsetKeyReset(kp); +} + /* Return an handle representing a Redis key, so that it is possible * to call other APIs with the key handle as argument to perform * operations on the key. @@ -1460,11 +1976,12 @@ int RM_SelectDb(RedisModuleCtx *ctx, int newid) { void *RM_OpenKey(RedisModuleCtx *ctx, robj *keyname, int mode) { RedisModuleKey *kp; robj *value; + int flags = mode & REDISMODULE_OPEN_KEY_NOTOUCH? LOOKUP_NOTOUCH: 0; if (mode & REDISMODULE_WRITE) { - value = lookupKeyWrite(ctx->client->db,keyname); + value = lookupKeyWriteWithFlags(ctx->client->db,keyname, flags); } else { - value = lookupKeyRead(ctx->client->db,keyname); + value = lookupKeyReadWithFlags(ctx->client->db,keyname, flags); if (value == NULL) { return NULL; } @@ -1472,25 +1989,25 @@ void *RM_OpenKey(RedisModuleCtx *ctx, robj *keyname, int mode) { /* Setup the key handle. */ kp = zmalloc(sizeof(*kp)); - kp->ctx = ctx; - kp->db = ctx->client->db; - kp->key = keyname; - incrRefCount(keyname); - kp->value = value; - kp->iter = NULL; - kp->mode = mode; - zsetKeyReset(kp); + moduleInitKey(kp, ctx, keyname, value, mode); autoMemoryAdd(ctx,REDISMODULE_AM_KEY,kp); return (void*)kp; } +/* Destroy a RedisModuleKey struct (freeing is the responsibility of the caller). */ +static void moduleCloseKey(RedisModuleKey *key) { + int signal = SHOULD_SIGNAL_MODIFIED_KEYS(key->ctx); + if ((key->mode & REDISMODULE_WRITE) && signal) + signalModifiedKey(key->db,key->key); + /* TODO: if (key->iter) RM_KeyIteratorStop(kp); */ + RM_ZsetRangeStop(key); + decrRefCount(key->key); +} + /* Close a key handle. */ void RM_CloseKey(RedisModuleKey *key) { if (key == NULL) return; - if (key->mode & REDISMODULE_WRITE) signalModifiedKey(key->db,key->key); - /* TODO: if (key->iter) RM_KeyIteratorStop(kp); */ - RM_ZsetRangeStop(key); - decrRefCount(key->key); + moduleCloseKey(key); autoMemoryFreed(key->ctx,REDISMODULE_AM_KEY,key); zfree(key); } @@ -1508,6 +2025,7 @@ int RM_KeyType(RedisModuleKey *key) { case OBJ_ZSET: return REDISMODULE_KEYTYPE_ZSET; case OBJ_HASH: return REDISMODULE_KEYTYPE_HASH; case OBJ_MODULE: return REDISMODULE_KEYTYPE_MODULE; + case OBJ_STREAM: return REDISMODULE_KEYTYPE_STREAM; default: return 0; } } @@ -1525,6 +2043,7 @@ size_t RM_ValueLength(RedisModuleKey *key) { case OBJ_SET: return setTypeSize(key->value); case OBJ_ZSET: return zsetLength(key->value); case OBJ_HASH: return hashTypeLength(key->value); + case OBJ_STREAM: return streamLength(key->value); default: return 0; } } @@ -1542,7 +2061,7 @@ int RM_DeleteKey(RedisModuleKey *key) { return REDISMODULE_OK; } -/* If the key is open for writing, unlink it (that is delete it in a +/* If the key is open for writing, unlink it (that is delete it in a * non-blocking way, not reclaiming memory immediately) and setup the key to * accept new writes as an empty key (that will be created on demand). * On success REDISMODULE_OK is returned. If the key is not open for @@ -1587,6 +2106,28 @@ int RM_SetExpire(RedisModuleKey *key, mstime_t expire) { return REDISMODULE_OK; } +/* Performs similar operation to FLUSHALL, and optionally start a new AOF file (if enabled) + * If restart_aof is true, you must make sure the command that triggered this call is not + * propagated to the AOF file. + * When async is set to true, db contents will be freed by a background thread. */ +void RM_ResetDataset(int restart_aof, int async) { + if (restart_aof && server.aof_state != AOF_OFF) stopAppendOnly(); + flushAllDataAndResetRDB(async? EMPTYDB_ASYNC: EMPTYDB_NO_FLAGS); + if (server.aof_enabled && restart_aof) restartAOFAfterSYNC(); +} + +/* Returns the number of keys in the current db. */ +unsigned long long RM_DbSize(RedisModuleCtx *ctx) { + return dictSize(ctx->client->db->dict); +} + +/* Returns a name of a random key, or NULL if current db is empty. */ +RedisModuleString *RM_RandomKey(RedisModuleCtx *ctx) { + robj *key = dbRandomKey(ctx->client->db); + autoMemoryAdd(ctx,REDISMODULE_AM_STRING,key); + return key; +} + /* -------------------------------------------------------------------------- * Key API for String type * -------------------------------------------------------------------------- */ @@ -2315,7 +2856,7 @@ int RM_HashSet(RedisModuleKey *key, int flags, ...) { * * REDISMODULE_HASH_EXISTS: instead of setting the value of the field * expecting a RedisModuleString pointer to pointer, the function just - * reports if the field esists or not and expects an integer pointer + * reports if the field exists or not and expects an integer pointer * as the second element of each pair. * * Example of REDISMODULE_HASH_CFIELD: @@ -2604,12 +3145,11 @@ RedisModuleString *RM_CreateStringFromCallReply(RedisModuleCallReply *reply) { * to special modifiers in "fmt". For now only one exists: * * "!" -> REDISMODULE_ARGV_REPLICATE + * "A" -> REDISMODULE_ARGV_NO_AOF + * "R" -> REDISMODULE_ARGV_NO_REPLICAS * * On error (format specifier error) NULL is returned and nothing is * allocated. On success the argument vector is returned. */ - -#define REDISMODULE_ARGV_REPLICATE (1<<0) - robj **moduleCreateArgvFromUserFormat(const char *cmdname, const char *fmt, int *argcp, int *flags, va_list ap) { int argc = 0, argv_size, j; robj **argv = NULL; @@ -2638,7 +3178,7 @@ robj **moduleCreateArgvFromUserFormat(const char *cmdname, const char *fmt, int size_t len = va_arg(ap,size_t); argv[argc++] = createStringObject(buf,len); } else if (*p == 'l') { - long ll = va_arg(ap,long long); + long long ll = va_arg(ap,long long); argv[argc++] = createObject(OBJ_STRING,sdsfromlonglong(ll)); } else if (*p == 'v') { /* A vector of strings */ @@ -2658,6 +3198,10 @@ robj **moduleCreateArgvFromUserFormat(const char *cmdname, const char *fmt, int } } else if (*p == '!') { if (flags) (*flags) |= REDISMODULE_ARGV_REPLICATE; + } else if (*p == 'A') { + if (flags) (*flags) |= REDISMODULE_ARGV_NO_AOF; + } else if (*p == 'R') { + if (flags) (*flags) |= REDISMODULE_ARGV_NO_REPLICAS; } else { goto fmterr; } @@ -2677,8 +3221,16 @@ fmterr: * On success a RedisModuleCallReply object is returned, otherwise * NULL is returned and errno is set to the following values: * - * EINVAL: command non existing, wrong arity, wrong format specifier. - * EPERM: operation in Cluster instance with key in non local slot. */ + * EBADF: wrong format specifier. + * EINVAL: wrong command arity. + * ENOENT: command does not exist. + * EPERM: operation in Cluster instance with key in non local slot. + * EROFS: operation in Cluster instance when a write command is sent + * in a readonly state. + * ENETDOWN: operation in Cluster instance when cluster is down. + * + * This API is documented here: https://redis.io/topics/modules-intro + */ RedisModuleCallReply *RM_Call(RedisModuleCtx *ctx, const char *cmdname, const char *fmt, ...) { struct redisCommand *cmd; client *c = NULL; @@ -2688,15 +3240,9 @@ RedisModuleCallReply *RM_Call(RedisModuleCtx *ctx, const char *cmdname, const ch RedisModuleCallReply *reply = NULL; int replicate = 0; /* Replicate this command? */ - cmd = lookupCommandByCString((char*)cmdname); - if (!cmd) { - errno = EINVAL; - return NULL; - } - /* Create the client and dispatch the command. */ va_start(ap, fmt); - c = createClient(-1); + c = createClient(NULL); c->user = NULL; /* Root user. */ argv = moduleCreateArgvFromUserFormat(cmdname,fmt,&argc,&flags,ap); replicate = flags & REDISMODULE_ARGV_REPLICATE; @@ -2707,10 +3253,27 @@ RedisModuleCallReply *RM_Call(RedisModuleCtx *ctx, const char *cmdname, const ch c->db = ctx->client->db; c->argv = argv; c->argc = argc; - c->cmd = c->lastcmd = cmd; + if (ctx->module) ctx->module->in_call++; + /* We handle the above format error only when the client is setup so that * we can free it normally. */ - if (argv == NULL) goto cleanup; + if (argv == NULL) { + errno = EBADF; + goto cleanup; + } + + /* Call command filters */ + moduleCallCommandFilters(c); + + /* Lookup command now, after filters had a chance to make modifications + * if necessary. + */ + cmd = lookupCommand(c->argv[0]->ptr); + if (!cmd) { + errno = ENOENT; + goto cleanup; + } + c->cmd = c->lastcmd = cmd; /* Basic arity checks. */ if ((cmd->arity > 0 && cmd->arity != argc) || (argc < -cmd->arity)) { @@ -2722,13 +3285,20 @@ RedisModuleCallReply *RM_Call(RedisModuleCtx *ctx, const char *cmdname, const ch * trying to access non-local keys, with the exception of commands * received from our master. */ if (server.cluster_enabled && !(ctx->client->flags & CLIENT_MASTER)) { + int error_code; /* Duplicate relevant flags in the module client. */ c->flags &= ~(CLIENT_READONLY|CLIENT_ASKING); c->flags |= ctx->client->flags & (CLIENT_READONLY|CLIENT_ASKING); - if (getNodeByQuery(c,c->cmd,c->argv,c->argc,NULL,NULL) != + if (getNodeByQuery(c,c->cmd,c->argv,c->argc,NULL,&error_code) != server.cluster->myself) { - errno = EPERM; + if (error_code == CLUSTER_REDIR_DOWN_RO_STATE) { + errno = EROFS; + } else if (error_code == CLUSTER_REDIR_DOWN_STATE) { + errno = ENETDOWN; + } else { + errno = EPERM; + } goto cleanup; } } @@ -2738,11 +3308,18 @@ RedisModuleCallReply *RM_Call(RedisModuleCtx *ctx, const char *cmdname, const ch * a Lua script in the context of AOF and slaves. */ if (replicate) moduleReplicateMultiIfNeeded(ctx); + if (ctx->client->flags & CLIENT_MULTI || + ctx->flags & REDISMODULE_CTX_MULTI_EMITTED) { + c->flags |= CLIENT_MULTI; + } + /* Run the command */ int call_flags = CMD_CALL_SLOWLOG | CMD_CALL_STATS; if (replicate) { - call_flags |= CMD_CALL_PROPAGATE_AOF; - call_flags |= CMD_CALL_PROPAGATE_REPL; + if (!(flags & REDISMODULE_ARGV_NO_AOF)) + call_flags |= CMD_CALL_PROPAGATE_AOF; + if (!(flags & REDISMODULE_ARGV_NO_REPLICAS)) + call_flags |= CMD_CALL_PROPAGATE_REPL; } call(c,call_flags); @@ -2761,6 +3338,7 @@ RedisModuleCallReply *RM_Call(RedisModuleCtx *ctx, const char *cmdname, const ch autoMemoryAdd(ctx,REDISMODULE_AM_REPLY,reply); cleanup: + if (ctx->module) ctx->module->in_call--; freeClient(c); return reply; } @@ -2996,6 +3574,11 @@ moduleType *RM_CreateDataType(RedisModuleCtx *ctx, const char *name, int encver, moduleTypeMemUsageFunc mem_usage; moduleTypeDigestFunc digest; moduleTypeFreeFunc free; + struct { + moduleTypeAuxLoadFunc aux_load; + moduleTypeAuxSaveFunc aux_save; + int aux_save_triggers; + } v2; } *tms = (struct typemethods*) typemethods_ptr; moduleType *mt = zcalloc(sizeof(*mt)); @@ -3007,6 +3590,11 @@ moduleType *RM_CreateDataType(RedisModuleCtx *ctx, const char *name, int encver, mt->mem_usage = tms->mem_usage; mt->digest = tms->digest; mt->free = tms->free; + if (tms->version >= 2) { + mt->aux_load = tms->v2.aux_load; + mt->aux_save = tms->v2.aux_save; + mt->aux_save_triggers = tms->v2.aux_save_triggers; + } memcpy(mt->name,name,sizeof(mt->name)); listAddNodeTail(ctx->module->types,mt); return mt; @@ -3057,17 +3645,50 @@ void *RM_ModuleTypeGetValue(RedisModuleKey *key) { * RDB loading and saving functions * -------------------------------------------------------------------------- */ -/* Called when there is a load error in the context of a module. This cannot - * be recovered like for the built-in types. */ +/* Called when there is a load error in the context of a module. On some + * modules this cannot be recovered, but if the module declared capability + * to handle errors, we'll raise a flag rather than exiting. */ void moduleRDBLoadError(RedisModuleIO *io) { - serverLog(LL_WARNING, + if (io->type->module->options & REDISMODULE_OPTIONS_HANDLE_IO_ERRORS) { + io->error = 1; + return; + } + serverPanic( "Error loading data from RDB (short read or EOF). " "Read performed by module '%s' about type '%s' " - "after reading '%llu' bytes of a value.", + "after reading '%llu' bytes of a value " + "for key named: '%s'.", io->type->module->name, io->type->name, - (unsigned long long)io->bytes); - exit(1); + (unsigned long long)io->bytes, + io->key? (char*)io->key->ptr: "(null)"); +} + +/* Returns 0 if there's at least one registered data type that did not declare + * REDISMODULE_OPTIONS_HANDLE_IO_ERRORS, in which case diskless loading should + * be avoided since it could cause data loss. */ +int moduleAllDatatypesHandleErrors() { + dictIterator *di = dictGetIterator(modules); + dictEntry *de; + + while ((de = dictNext(di)) != NULL) { + struct RedisModule *module = dictGetVal(de); + if (listLength(module->types) && + !(module->options & REDISMODULE_OPTIONS_HANDLE_IO_ERRORS)) + { + dictReleaseIterator(di); + return 0; + } + } + dictReleaseIterator(di); + return 1; +} + +/* Returns true if any previous IO API failed. + * for Load* APIs the REDISMODULE_OPTIONS_HANDLE_IO_ERRORS flag must be set with + * RediModule_SetModuleOptions first. */ +int RM_IsIOError(RedisModuleIO *io) { + return io->error; } /* Save an unsigned 64 bit value into the RDB file. This function should only @@ -3093,6 +3714,7 @@ saveerr: * be called in the context of the rdb_load method of modules implementing * new data types. */ uint64_t RM_LoadUnsigned(RedisModuleIO *io) { + if (io->error) return 0; if (io->ver == 2) { uint64_t opcode = rdbLoadLen(io->rio,NULL); if (opcode != RDB_MODULE_OPCODE_UINT) goto loaderr; @@ -3104,7 +3726,7 @@ uint64_t RM_LoadUnsigned(RedisModuleIO *io) { loaderr: moduleRDBLoadError(io); - return 0; /* Never reached. */ + return 0; } /* Like RedisModule_SaveUnsigned() but for signed 64 bit values. */ @@ -3163,6 +3785,7 @@ saveerr: /* Implements RM_LoadString() and RM_LoadStringBuffer() */ void *moduleLoadString(RedisModuleIO *io, int plain, size_t *lenptr) { + if (io->error) return NULL; if (io->ver == 2) { uint64_t opcode = rdbLoadLen(io->rio,NULL); if (opcode != RDB_MODULE_OPCODE_STRING) goto loaderr; @@ -3174,7 +3797,7 @@ void *moduleLoadString(RedisModuleIO *io, int plain, size_t *lenptr) { loaderr: moduleRDBLoadError(io); - return NULL; /* Never reached. */ + return NULL; } /* In the context of the rdb_load method of a module data type, loads a string @@ -3195,7 +3818,7 @@ RedisModuleString *RM_LoadString(RedisModuleIO *io) { * RedisModule_Realloc() or RedisModule_Free(). * * The size of the string is stored at '*lenptr' if not NULL. - * The returned string is not automatically NULL termianted, it is loaded + * The returned string is not automatically NULL terminated, it is loaded * exactly as it was stored inisde the RDB file. */ char *RM_LoadStringBuffer(RedisModuleIO *io, size_t *lenptr) { return moduleLoadString(io,1,lenptr); @@ -3223,6 +3846,7 @@ saveerr: /* In the context of the rdb_save method of a module data type, loads back the * double value saved by RedisModule_SaveDouble(). */ double RM_LoadDouble(RedisModuleIO *io) { + if (io->error) return 0; if (io->ver == 2) { uint64_t opcode = rdbLoadLen(io->rio,NULL); if (opcode != RDB_MODULE_OPCODE_DOUBLE) goto loaderr; @@ -3234,7 +3858,7 @@ double RM_LoadDouble(RedisModuleIO *io) { loaderr: moduleRDBLoadError(io); - return 0; /* Never reached. */ + return 0; } /* In the context of the rdb_save method of a module data type, saves a float @@ -3259,6 +3883,7 @@ saveerr: /* In the context of the rdb_save method of a module data type, loads back the * float value saved by RedisModule_SaveFloat(). */ float RM_LoadFloat(RedisModuleIO *io) { + if (io->error) return 0; if (io->ver == 2) { uint64_t opcode = rdbLoadLen(io->rio,NULL); if (opcode != RDB_MODULE_OPCODE_FLOAT) goto loaderr; @@ -3270,7 +3895,62 @@ float RM_LoadFloat(RedisModuleIO *io) { loaderr: moduleRDBLoadError(io); - return 0; /* Never reached. */ + return 0; +} + +/* In the context of the rdb_save method of a module data type, saves a long double + * value to the RDB file. The double can be a valid number, a NaN or infinity. + * It is possible to load back the value with RedisModule_LoadLongDouble(). */ +void RM_SaveLongDouble(RedisModuleIO *io, long double value) { + if (io->error) return; + char buf[MAX_LONG_DOUBLE_CHARS]; + /* Long double has different number of bits in different platforms, so we + * save it as a string type. */ + size_t len = ld2string(buf,sizeof(buf),value,LD_STR_HEX); + RM_SaveStringBuffer(io,buf,len); +} + +/* In the context of the rdb_save method of a module data type, loads back the + * long double value saved by RedisModule_SaveLongDouble(). */ +long double RM_LoadLongDouble(RedisModuleIO *io) { + if (io->error) return 0; + long double value; + size_t len; + char* str = RM_LoadStringBuffer(io,&len); + if (!str) return 0; + string2ld(str,len,&value); + RM_Free(str); + return value; +} + +/* Iterate over modules, and trigger rdb aux saving for the ones modules types + * who asked for it. */ +ssize_t rdbSaveModulesAux(rio *rdb, int when) { + size_t total_written = 0; + dictIterator *di = dictGetIterator(modules); + dictEntry *de; + + while ((de = dictNext(di)) != NULL) { + struct RedisModule *module = dictGetVal(de); + listIter li; + listNode *ln; + + listRewind(module->types,&li); + while((ln = listNext(&li))) { + moduleType *mt = ln->value; + if (!mt->aux_save || !(mt->aux_save_triggers & when)) + continue; + ssize_t ret = rdbSaveSingleModuleAux(rdb, when, mt); + if (ret==-1) { + dictReleaseIterator(di); + return -1; + } + total_written += ret; + } + } + + dictReleaseIterator(di); + return total_written; } /* -------------------------------------------------------------------------- @@ -3333,6 +4013,69 @@ void RM_DigestEndSequence(RedisModuleDigest *md) { memset(md->o,0,sizeof(md->o)); } +/* Decode a serialized representation of a module data type 'mt' from string + * 'str' and return a newly allocated value, or NULL if decoding failed. + * + * This call basically reuses the 'rdb_load' callback which module data types + * implement in order to allow a module to arbitrarily serialize/de-serialize + * keys, similar to how the Redis 'DUMP' and 'RESTORE' commands are implemented. + * + * Modules should generally use the REDISMODULE_OPTIONS_HANDLE_IO_ERRORS flag and + * make sure the de-serialization code properly checks and handles IO errors + * (freeing allocated buffers and returning a NULL). + * + * If this is NOT done, Redis will handle corrupted (or just truncated) serialized + * data by producing an error message and terminating the process. + */ + +void *RM_LoadDataTypeFromString(const RedisModuleString *str, const moduleType *mt) { + rio payload; + RedisModuleIO io; + void *ret; + + rioInitWithBuffer(&payload, str->ptr); + moduleInitIOContext(io,(moduleType *)mt,&payload,NULL); + + /* All RM_Save*() calls always write a version 2 compatible format, so we + * need to make sure we read the same. + */ + io.ver = 2; + ret = mt->rdb_load(&io,0); + if (io.ctx) { + moduleFreeContext(io.ctx); + zfree(io.ctx); + } + return ret; +} + +/* Encode a module data type 'mt' value 'data' into serialized form, and return it + * as a newly allocated RedisModuleString. + * + * This call basically reuses the 'rdb_save' callback which module data types + * implement in order to allow a module to arbitrarily serialize/de-serialize + * keys, similar to how the Redis 'DUMP' and 'RESTORE' commands are implemented. + */ + +RedisModuleString *RM_SaveDataTypeToString(RedisModuleCtx *ctx, void *data, const moduleType *mt) { + rio payload; + RedisModuleIO io; + + rioInitWithBuffer(&payload,sdsempty()); + moduleInitIOContext(io,(moduleType *)mt,&payload,NULL); + mt->rdb_save(&io,data); + if (io.ctx) { + moduleFreeContext(io.ctx); + zfree(io.ctx); + } + if (io.error) { + return NULL; + } else { + robj *str = createObject(OBJ_STRING,payload.io.buffer.ptr); + if (ctx != NULL) autoMemoryAdd(ctx,REDISMODULE_AM_STRING,str); + return str; + } +} + /* -------------------------------------------------------------------------- * AOF API for modules data types * -------------------------------------------------------------------------- */ @@ -3402,6 +4145,19 @@ RedisModuleCtx *RM_GetContextFromIO(RedisModuleIO *io) { return io->ctx; } +/* Returns a RedisModuleString with the name of the key currently saving or + * loading, when an IO data type callback is called. There is no guarantee + * that the key name is always available, so this may return NULL. + */ +const RedisModuleString *RM_GetKeyNameFromIO(RedisModuleIO *io) { + return io->key; +} + +/* Returns a RedisModuleString with the name of the key from RedisModuleKey */ +const RedisModuleString *RM_GetKeyNameFromModuleKey(RedisModuleKey *key) { + return key ? key->key : NULL; +} + /* -------------------------------------------------------------------------- * Logging * -------------------------------------------------------------------------- */ @@ -3423,7 +4179,9 @@ void RM_LogRaw(RedisModule *module, const char *levelstr, const char *fmt, va_li else if (!strcasecmp(levelstr,"warning")) level = LL_WARNING; else level = LL_VERBOSE; /* Default. */ - name_len = snprintf(msg, sizeof(msg),"<%s> ", module->name); + if (level < server.verbosity) return; + + name_len = snprintf(msg, sizeof(msg),"<%s> ", module? module->name: "module"); vsnprintf(msg + name_len, sizeof(msg) - name_len, fmt, ap); serverLogRaw(level,msg); } @@ -3441,13 +4199,15 @@ void RM_LogRaw(RedisModule *module, const char *levelstr, const char *fmt, va_li * There is a fixed limit to the length of the log line this function is able * to emit, this limit is not specified but is guaranteed to be more than * a few lines of text. + * + * The ctx argument may be NULL if cannot be provided in the context of the + * caller for instance threads or callbacks, in which case a generic "module" + * will be used instead of the module name. */ void RM_Log(RedisModuleCtx *ctx, const char *levelstr, const char *fmt, ...) { - if (!ctx->module) return; /* Can only log if module is initialized */ - va_list ap; va_start(ap, fmt); - RM_LogRaw(ctx->module,levelstr,fmt,ap); + RM_LogRaw(ctx? ctx->module: NULL,levelstr,fmt,ap); va_end(ap); } @@ -3463,6 +4223,23 @@ void RM_LogIOError(RedisModuleIO *io, const char *levelstr, const char *fmt, ... va_end(ap); } +/* Redis-like assert function. + * + * A failed assertion will shut down the server and produce logging information + * that looks identical to information generated by Redis itself. + */ +void RM__Assert(const char *estr, const char *file, int line) { + _serverAssert(estr, file, line); +} + +/* Allows adding event to the latency monitor to be observed by the LATENCY + * command. The call is skipped if the latency is smaller than the configured + * latency-monitor-threshold. */ +void RM_LatencyAddSample(const char *event, mstime_t latency) { + if (latency >= server.latency_monitor_threshold) + latencyAddSample(event, latency); +} + /* -------------------------------------------------------------------------- * Blocking clients from modules * -------------------------------------------------------------------------- */ @@ -3493,7 +4270,10 @@ void moduleBlockedClientPipeReadable(aeEventLoop *el, int fd, void *privdata, in void unblockClientFromModule(client *c) { RedisModuleBlockedClient *bc = c->bpop.module_blocked_handle; - /* Call the disconnection callback if any. */ + /* Call the disconnection callback if any. Note that + * bc->disconnect_callback is set to NULL if the client gets disconnected + * by the module itself or because of a timeout, so the callback will NOT + * get called if this is not an actual disconnection event. */ if (bc->disconnect_callback) { RedisModuleCtx ctx = REDISMODULE_CTX_INIT; ctx.blocked_privdata = bc->privdata; @@ -3503,6 +4283,21 @@ void unblockClientFromModule(client *c) { moduleFreeContext(&ctx); } + /* If we made it here and client is still blocked it means that the command + * timed-out, client was killed or disconnected and disconnect_callback was + * not implemented (or it was, but RM_UnblockClient was not called from + * within it, as it should). + * We must call moduleUnblockClient in order to free privdata and + * RedisModuleBlockedClient. + * + * Note that clients implementing threads and working with private data, + * should make sure to stop the threads or protect the private data + * in some other way in the disconnection and timeout callback, because + * here we are going to free the private data associated with the + * blocked client. */ + if (!bc->unblocked) + moduleUnblockClient(c); + bc->client = NULL; /* Reset the client for a new query since, for blocking commands implemented * into modules, we do not it immediately after the command returns (and @@ -3511,6 +4306,94 @@ void unblockClientFromModule(client *c) { resetClient(c); } +/* Block a client in the context of a module: this function implements both + * RM_BlockClient() and RM_BlockClientOnKeys() depending on the fact the + * keys are passed or not. + * + * When not blocking for keys, the keys, numkeys, and privdata parameters are + * not needed. The privdata in that case must be NULL, since later is + * RM_UnblockClient() that will provide some private data that the reply + * callback will receive. + * + * Instead when blocking for keys, normally RM_UnblockClient() will not be + * called (because the client will unblock when the key is modified), so + * 'privdata' should be provided in that case, so that once the client is + * unlocked and the reply callback is called, it will receive its associated + * private data. + * + * Even when blocking on keys, RM_UnblockClient() can be called however, but + * in that case the privdata argument is disregarded, because we pass the + * reply callback the privdata that is set here while blocking. + */ +RedisModuleBlockedClient *moduleBlockClient(RedisModuleCtx *ctx, RedisModuleCmdFunc reply_callback, RedisModuleCmdFunc timeout_callback, void (*free_privdata)(RedisModuleCtx*,void*), long long timeout_ms, RedisModuleString **keys, int numkeys, void *privdata) { + client *c = ctx->client; + int islua = c->flags & CLIENT_LUA; + int ismulti = c->flags & CLIENT_MULTI; + + c->bpop.module_blocked_handle = zmalloc(sizeof(RedisModuleBlockedClient)); + RedisModuleBlockedClient *bc = c->bpop.module_blocked_handle; + ctx->module->blocked_clients++; + + /* We need to handle the invalid operation of calling modules blocking + * commands from Lua or MULTI. We actually create an already aborted + * (client set to NULL) blocked client handle, and actually reply with + * an error. */ + mstime_t timeout = timeout_ms ? (mstime()+timeout_ms) : 0; + bc->client = (islua || ismulti) ? NULL : c; + bc->module = ctx->module; + bc->reply_callback = reply_callback; + bc->timeout_callback = timeout_callback; + bc->disconnect_callback = NULL; /* Set by RM_SetDisconnectCallback() */ + bc->free_privdata = free_privdata; + bc->privdata = privdata; + bc->reply_client = createClient(NULL); + bc->reply_client->flags |= CLIENT_MODULE; + bc->dbid = c->db->id; + bc->blocked_on_keys = keys != NULL; + bc->unblocked = 0; + c->bpop.timeout = timeout; + + if (islua || ismulti) { + c->bpop.module_blocked_handle = NULL; + addReplyError(c, islua ? + "Blocking module command called from Lua script" : + "Blocking module command called from transaction"); + } else { + if (keys) { + blockForKeys(c,BLOCKED_MODULE,keys,numkeys,timeout,NULL,NULL); + } else { + blockClient(c,BLOCKED_MODULE); + } + } + return bc; +} + +/* This function is called from module.c in order to check if a module + * blocked for BLOCKED_MODULE and subtype 'on keys' (bc->blocked_on_keys true) + * can really be unblocked, since the module was able to serve the client. + * If the callback returns REDISMODULE_OK, then the client can be unblocked, + * otherwise the client remains blocked and we'll retry again when one of + * the keys it blocked for becomes "ready" again. */ +int moduleTryServeClientBlockedOnKey(client *c, robj *key) { + int served = 0; + RedisModuleBlockedClient *bc = c->bpop.module_blocked_handle; + /* Protect against re-processing: don't serve clients that are already + * in the unblocking list for any reason (including RM_UnblockClient() + * explicit call). */ + if (bc->unblocked) return REDISMODULE_ERR; + RedisModuleCtx ctx = REDISMODULE_CTX_INIT; + ctx.flags |= REDISMODULE_CTX_BLOCKED_REPLY; + ctx.blocked_ready_key = key; + ctx.blocked_privdata = bc->privdata; + ctx.module = bc->module; + ctx.client = bc->client; + ctx.blocked_client = bc; + if (bc->reply_callback(&ctx,(void**)c->argv,c->argc) == REDISMODULE_OK) + served = 1; + moduleFreeContext(&ctx); + return served; +} + /* Block a client in the context of a blocking command, returning an handle * which will be used, later, in order to unblock the client with a call to * RedisModule_UnblockClient(). The arguments specify callback functions @@ -3528,38 +4411,96 @@ void unblockClientFromModule(client *c) { * by RedisModule_UnblockClient() call. */ RedisModuleBlockedClient *RM_BlockClient(RedisModuleCtx *ctx, RedisModuleCmdFunc reply_callback, RedisModuleCmdFunc timeout_callback, void (*free_privdata)(RedisModuleCtx*,void*), long long timeout_ms) { - client *c = ctx->client; - int islua = c->flags & CLIENT_LUA; - int ismulti = c->flags & CLIENT_MULTI; + return moduleBlockClient(ctx,reply_callback,timeout_callback,free_privdata,timeout_ms, NULL,0,NULL); +} - c->bpop.module_blocked_handle = zmalloc(sizeof(RedisModuleBlockedClient)); - RedisModuleBlockedClient *bc = c->bpop.module_blocked_handle; +/* This call is similar to RedisModule_BlockClient(), however in this case we + * don't just block the client, but also ask Redis to unblock it automatically + * once certain keys become "ready", that is, contain more data. + * + * Basically this is similar to what a typical Redis command usually does, + * like BLPOP or ZPOPMAX: the client blocks if it cannot be served ASAP, + * and later when the key receives new data (a list push for instance), the + * client is unblocked and served. + * + * However in the case of this module API, when the client is unblocked? + * + * 1. If you block ok a key of a type that has blocking operations associated, + * like a list, a sorted set, a stream, and so forth, the client may be + * unblocked once the relevant key is targeted by an operation that normally + * unblocks the native blocking operations for that type. So if we block + * on a list key, an RPUSH command may unblock our client and so forth. + * 2. If you are implementing your native data type, or if you want to add new + * unblocking conditions in addition to "1", you can call the modules API + * RedisModule_SignalKeyAsReady(). + * + * Anyway we can't be sure if the client should be unblocked just because the + * key is signaled as ready: for instance a successive operation may change the + * key, or a client in queue before this one can be served, modifying the key + * as well and making it empty again. So when a client is blocked with + * RedisModule_BlockClientOnKeys() the reply callback is not called after + * RM_UnblockCLient() is called, but every time a key is signaled as ready: + * if the reply callback can serve the client, it returns REDISMODULE_OK + * and the client is unblocked, otherwise it will return REDISMODULE_ERR + * and we'll try again later. + * + * The reply callback can access the key that was signaled as ready by + * calling the API RedisModule_GetBlockedClientReadyKey(), that returns + * just the string name of the key as a RedisModuleString object. + * + * Thanks to this system we can setup complex blocking scenarios, like + * unblocking a client only if a list contains at least 5 items or other + * more fancy logics. + * + * Note that another difference with RedisModule_BlockClient(), is that here + * we pass the private data directly when blocking the client: it will + * be accessible later in the reply callback. Normally when blocking with + * RedisModule_BlockClient() the private data to reply to the client is + * passed when calling RedisModule_UnblockClient() but here the unblocking + * is performed by Redis itself, so we need to have some private data before + * hand. The private data is used to store any information about the specific + * unblocking operation that you are implementing. Such information will be + * freed using the free_privdata callback provided by the user. + * + * However the reply callback will be able to access the argument vector of + * the command, so the private data is often not needed. */ +RedisModuleBlockedClient *RM_BlockClientOnKeys(RedisModuleCtx *ctx, RedisModuleCmdFunc reply_callback, RedisModuleCmdFunc timeout_callback, void (*free_privdata)(RedisModuleCtx*,void*), long long timeout_ms, RedisModuleString **keys, int numkeys, void *privdata) { + return moduleBlockClient(ctx,reply_callback,timeout_callback,free_privdata,timeout_ms, keys,numkeys,privdata); +} - /* We need to handle the invalid operation of calling modules blocking - * commands from Lua or MULTI. We actually create an already aborted - * (client set to NULL) blocked client handle, and actually reply with - * an error. */ - bc->client = (islua || ismulti) ? NULL : c; - bc->module = ctx->module; - bc->reply_callback = reply_callback; - bc->timeout_callback = timeout_callback; - bc->disconnect_callback = NULL; /* Set by RM_SetDisconnectCallback() */ - bc->free_privdata = free_privdata; - bc->privdata = NULL; - bc->reply_client = createClient(-1); - bc->reply_client->flags |= CLIENT_MODULE; - bc->dbid = c->db->id; - c->bpop.timeout = timeout_ms ? (mstime()+timeout_ms) : 0; +/* This function is used in order to potentially unblock a client blocked + * on keys with RedisModule_BlockClientOnKeys(). When this function is called, + * all the clients blocked for this key will get their reply callback called, + * and if the callback returns REDISMODULE_OK the client will be unblocked. */ +void RM_SignalKeyAsReady(RedisModuleCtx *ctx, RedisModuleString *key) { + signalKeyAsReady(ctx->client->db, key); +} - if (islua || ismulti) { - c->bpop.module_blocked_handle = NULL; - addReplyError(c, islua ? - "Blocking module command called from Lua script" : - "Blocking module command called from transaction"); - } else { - blockClient(c,BLOCKED_MODULE); +/* Implements RM_UnblockClient() and moduleUnblockClient(). */ +int moduleUnblockClientByHandle(RedisModuleBlockedClient *bc, void *privdata) { + pthread_mutex_lock(&moduleUnblockedClientsMutex); + if (!bc->blocked_on_keys) bc->privdata = privdata; + bc->unblocked = 1; + listAddNodeTail(moduleUnblockedClients,bc); + if (write(server.module_blocked_pipe[1],"A",1) != 1) { + /* Ignore the error, this is best-effort. */ } - return bc; + pthread_mutex_unlock(&moduleUnblockedClientsMutex); + return REDISMODULE_OK; +} + +/* This API is used by the Redis core to unblock a client that was blocked + * by a module. */ +void moduleUnblockClient(client *c) { + RedisModuleBlockedClient *bc = c->bpop.module_blocked_handle; + moduleUnblockClientByHandle(bc,NULL); +} + +/* Return true if the client 'c' was blocked by a module using + * RM_BlockClientOnKeys(). */ +int moduleClientIsBlockedOnKeys(client *c) { + RedisModuleBlockedClient *bc = c->bpop.module_blocked_handle; + return bc->blocked_on_keys; } /* Unblock a client blocked by `RedisModule_BlockedClient`. This will trigger @@ -3572,15 +4513,25 @@ RedisModuleBlockedClient *RM_BlockClient(RedisModuleCtx *ctx, RedisModuleCmdFunc * needs to be passed to the client, included but not limited some slow * to compute reply or some reply obtained via networking. * - * Note: this function can be called from threads spawned by the module. */ + * Note 1: this function can be called from threads spawned by the module. + * + * Note 2: when we unblock a client that is blocked for keys using + * the API RedisModule_BlockClientOnKeys(), the privdata argument here is + * not used, and the reply callback is called with the privdata pointer that + * was passed when blocking the client. + * + * Unblocking a client that was blocked for keys using this API will still + * require the client to get some reply, so the function will use the + * "timeout" handler in order to do so. */ int RM_UnblockClient(RedisModuleBlockedClient *bc, void *privdata) { - pthread_mutex_lock(&moduleUnblockedClientsMutex); - bc->privdata = privdata; - listAddNodeTail(moduleUnblockedClients,bc); - if (write(server.module_blocked_pipe[1],"A",1) != 1) { - /* Ignore the error, this is best-effort. */ + if (bc->blocked_on_keys) { + /* In theory the user should always pass the timeout handler as an + * argument, but better to be safe than sorry. */ + if (bc->timeout_callback == NULL) return REDISMODULE_ERR; + if (bc->unblocked) return REDISMODULE_OK; + if (bc->client) moduleBlockedClientTimedOut(bc->client); } - pthread_mutex_unlock(&moduleUnblockedClientsMutex); + moduleUnblockClientByHandle(bc,privdata); return REDISMODULE_OK; } @@ -3640,16 +4591,19 @@ void moduleHandleBlockedClients(void) { * touch the shared list. */ /* Call the reply callback if the client is valid and we have - * any callback. */ - if (c && bc->reply_callback) { + * any callback. However the callback is not called if the client + * was blocked on keys (RM_BlockClientOnKeys()), because we already + * called such callback in moduleTryServeClientBlockedOnKey() when + * the key was signaled as ready. */ + if (c && !bc->blocked_on_keys && bc->reply_callback) { RedisModuleCtx ctx = REDISMODULE_CTX_INIT; ctx.flags |= REDISMODULE_CTX_BLOCKED_REPLY; ctx.blocked_privdata = bc->privdata; + ctx.blocked_ready_key = NULL; ctx.module = bc->module; ctx.client = bc->client; ctx.blocked_client = bc; bc->reply_callback(&ctx,(void**)c->argv,c->argc); - moduleHandlePropagationAfterCommandCallback(&ctx); moduleFreeContext(&ctx); } @@ -3669,14 +4623,7 @@ void moduleHandleBlockedClients(void) { * replies to send to the client in a thread safe context. * We need to glue such replies to the client output buffer and * free the temporary client we just used for the replies. */ - if (c) { - if (bc->reply_client->bufpos) - addReplyProto(c,bc->reply_client->buf, - bc->reply_client->bufpos); - if (listLength(bc->reply_client->reply)) - listJoin(c->reply,bc->reply_client->reply); - c->reply_bytes += bc->reply_client->reply_bytes; - } + if (c) AddReplyFromClient(c, bc->reply_client); freeClient(bc->reply_client); if (c != NULL) { @@ -3699,6 +4646,7 @@ void moduleHandleBlockedClients(void) { /* Free 'bc' only after unblocking the client, since it is * referenced in the client blocking context, and must be valid * when calling unblockClient(). */ + bc->module->blocked_clients--; zfree(bc); /* Lock again before to iterate the loop. */ @@ -3743,6 +4691,12 @@ void *RM_GetBlockedClientPrivateData(RedisModuleCtx *ctx) { return ctx->blocked_privdata; } +/* Get the key that is ready when the reply callback is called in the context + * of a client blocked by RedisModule_BlockClientOnKeys(). */ +RedisModuleString *RM_GetBlockedClientReadyKey(RedisModuleCtx *ctx) { + return ctx->blocked_ready_key; +} + /* Get the blocked client associated with a given context. * This is useful in the reply and timeout callbacks of blocked clients, * before sometimes the module has the blocked client handle references @@ -3794,8 +4748,11 @@ RedisModuleCtx *RM_GetThreadSafeContext(RedisModuleBlockedClient *bc) { * access it safely from another thread, so we create a fake client here * in order to keep things like the currently selected database and similar * things. */ - ctx->client = createClient(-1); - if (bc) selectDb(ctx->client,bc->dbid); + ctx->client = createClient(NULL); + if (bc) { + selectDb(ctx->client,bc->dbid); + if (bc->client) ctx->client->id = bc->client->id; + } return ctx; } @@ -3853,7 +4810,8 @@ void moduleReleaseGIL(void) { * - REDISMODULE_NOTIFY_EXPIRED: Expiration events * - REDISMODULE_NOTIFY_EVICTED: Eviction events * - REDISMODULE_NOTIFY_STREAM: Stream events - * - REDISMODULE_NOTIFY_ALL: All events + * - REDISMODULE_NOTIFY_KEYMISS: Key-miss events + * - REDISMODULE_NOTIFY_ALL: All events (Excluding REDISMODULE_NOTIFY_KEYMISS) * * We do not distinguish between key events and keyspace events, and it is up * to the module to filter the actions taken based on the key. @@ -3892,6 +4850,20 @@ int RM_SubscribeToKeyspaceEvents(RedisModuleCtx *ctx, int types, RedisModuleNoti return REDISMODULE_OK; } +/* Get the configured bitmap of notify-keyspace-events (Could be used + * for additional filtering in RedisModuleNotificationFunc) */ +int RM_GetNotifyKeyspaceEvents() { + return server.notify_keyspace_events; +} + +/* Expose notifyKeyspaceEvent to modules */ +int RM_NotifyKeyspaceEvent(RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key) { + if (!ctx || !ctx->client) + return REDISMODULE_ERR; + notifyKeyspaceEvent(type, (char *)event, key, ctx->client->db->id); + return REDISMODULE_OK; +} + /* Dispatcher for keyspace notifications to module subscriber functions. * This gets called only if at least one module requested to be notified on * keyspace notifications */ @@ -4138,10 +5110,13 @@ int RM_GetClusterNodeInfo(RedisModuleCtx *ctx, const char *id, char *ip, char *m UNUSED(ctx); clusterNode *node = clusterLookupNode(id); - if (node->flags & (CLUSTER_NODE_NOADDR|CLUSTER_NODE_HANDSHAKE)) + if (node == NULL || + node->flags & (CLUSTER_NODE_NOADDR|CLUSTER_NODE_HANDSHAKE)) + { return REDISMODULE_ERR; + } - if (ip) memcpy(ip,node->name,REDISMODULE_NODE_ID_LEN); + if (ip) strncpy(ip,node->ip,NET_IP_STR_LEN); if (master_id) { /* If the information is not available, the function will set the @@ -4275,7 +5250,7 @@ RedisModuleTimerID RM_CreateTimer(RedisModuleCtx *ctx, mstime_t period, RedisMod timer->module = ctx->module; timer->callback = callback; timer->data = data; - timer->dbid = ctx->client->db->id; + timer->dbid = ctx->client ? ctx->client->db->id : 0; uint64_t expiretime = ustime()+period*1000; uint64_t key; @@ -4347,6 +5322,200 @@ int RM_GetTimerInfo(RedisModuleCtx *ctx, RedisModuleTimerID id, uint64_t *remain return REDISMODULE_OK; } +/* -------------------------------------------------------------------------- + * Modules ACL API + * + * Implements a hook into the authentication and authorization within Redis. + * --------------------------------------------------------------------------*/ + +/* This function is called when a client's user has changed and invokes the + * client's user changed callback if it was set. This callback should + * cleanup any state the module was tracking about this client. + * + * A client's user can be changed through the AUTH command, module + * authentication, and when a client is freed. */ +void moduleNotifyUserChanged(client *c) { + if (c->auth_callback) { + c->auth_callback(c->id, c->auth_callback_privdata); + + /* The callback will fire exactly once, even if the user remains + * the same. It is expected to completely clean up the state + * so all references are cleared here. */ + c->auth_callback = NULL; + c->auth_callback_privdata = NULL; + c->auth_module = NULL; + } +} + +void revokeClientAuthentication(client *c) { + /* Freeing the client would result in moduleNotifyUserChanged() to be + * called later, however since we use revokeClientAuthentication() also + * in moduleFreeAuthenticatedClients() to implement module unloading, we + * do this action ASAP: this way if the module is unloaded, when the client + * is eventually freed we don't rely on the module to still exist. */ + moduleNotifyUserChanged(c); + + c->user = DefaultUser; + c->authenticated = 0; + freeClientAsync(c); +} + +/* Cleanup all clients that have been authenticated with this module. This + * is called from onUnload() to give the module a chance to cleanup any + * resources associated with clients it has authenticated. */ +static void moduleFreeAuthenticatedClients(RedisModule *module) { + listIter li; + listNode *ln; + listRewind(server.clients,&li); + while ((ln = listNext(&li)) != NULL) { + client *c = listNodeValue(ln); + if (!c->auth_module) continue; + + RedisModule *auth_module = (RedisModule *) c->auth_module; + if (auth_module == module) { + revokeClientAuthentication(c); + } + } +} + +/* Creates a Redis ACL user that the module can use to authenticate a client. + * After obtaining the user, the module should set what such user can do + * using the RM_SetUserACL() function. Once configured, the user + * can be used in order to authenticate a connection, with the specified + * ACL rules, using the RedisModule_AuthClientWithUser() function. + * + * Note that: + * + * * Users created here are not listed by the ACL command. + * * Users created here are not checked for duplicated name, so it's up to + * the module calling this function to take care of not creating users + * with the same name. + * * The created user can be used to authenticate multiple Redis connections. + * + * The caller can later free the user using the function + * RM_FreeModuleUser(). When this function is called, if there are + * still clients authenticated with this user, they are disconnected. + * The function to free the user should only be used when the caller really + * wants to invalidate the user to define a new one with different + * capabilities. */ +RedisModuleUser *RM_CreateModuleUser(const char *name) { + RedisModuleUser *new_user = zmalloc(sizeof(RedisModuleUser)); + new_user->user = ACLCreateUnlinkedUser(); + + /* Free the previous temporarily assigned name to assign the new one */ + sdsfree(new_user->user->name); + new_user->user->name = sdsnew(name); + return new_user; +} + +/* Frees a given user and disconnects all of the clients that have been + * authenticated with it. See RM_CreateModuleUser for detailed usage.*/ +int RM_FreeModuleUser(RedisModuleUser *user) { + ACLFreeUserAndKillClients(user->user); + zfree(user); + return REDISMODULE_OK; +} + +/* Sets the permissions of a user created through the redis module + * interface. The syntax is the same as ACL SETUSER, so refer to the + * documentation in acl.c for more information. See RM_CreateModuleUser + * for detailed usage. + * + * Returns REDISMODULE_OK on success and REDISMODULE_ERR on failure + * and will set an errno describing why the operation failed. */ +int RM_SetModuleUserACL(RedisModuleUser *user, const char* acl) { + return ACLSetUser(user->user, acl, -1); +} + +/* Authenticate the client associated with the context with + * the provided user. Returns REDISMODULE_OK on success and + * REDISMODULE_ERR on error. + * + * This authentication can be tracked with the optional callback and private + * data fields. The callback will be called whenever the user of the client + * changes. This callback should be used to cleanup any state that is being + * kept in the module related to the client authentication. It will only be + * called once, even when the user hasn't changed, in order to allow for a + * new callback to be specified. If this authentication does not need to be + * tracked, pass in NULL for the callback and privdata. + * + * If client_id is not NULL, it will be filled with the id of the client + * that was authenticated. This can be used with the + * RM_DeauthenticateAndCloseClient() API in order to deauthenticate a + * previously authenticated client if the authentication is no longer valid. + * + * For expensive authentication operations, it is recommended to block the + * client and do the authentication in the background and then attach the user + * to the client in a threadsafe context. */ +static int authenticateClientWithUser(RedisModuleCtx *ctx, user *user, RedisModuleUserChangedFunc callback, void *privdata, uint64_t *client_id) { + if (user->flags & USER_FLAG_DISABLED) { + return REDISMODULE_ERR; + } + + moduleNotifyUserChanged(ctx->client); + + ctx->client->user = user; + ctx->client->authenticated = 1; + + if (callback) { + ctx->client->auth_callback = callback; + ctx->client->auth_callback_privdata = privdata; + ctx->client->auth_module = ctx->module; + } + + if (client_id) { + *client_id = ctx->client->id; + } + + return REDISMODULE_OK; +} + + +/* Authenticate the current context's user with the provided redis acl user. + * Returns REDISMODULE_ERR if the user is disabled. + * + * See authenticateClientWithUser for information about callback, client_id, + * and general usage for authentication. */ +int RM_AuthenticateClientWithUser(RedisModuleCtx *ctx, RedisModuleUser *module_user, RedisModuleUserChangedFunc callback, void *privdata, uint64_t *client_id) { + return authenticateClientWithUser(ctx, module_user->user, callback, privdata, client_id); +} + +/* Authenticate the current context's user with the provided redis acl user. + * Returns REDISMODULE_ERR if the user is disabled or the user does not exist. + * + * See authenticateClientWithUser for information about callback, client_id, + * and general usage for authentication. */ +int RM_AuthenticateClientWithACLUser(RedisModuleCtx *ctx, const char *name, size_t len, RedisModuleUserChangedFunc callback, void *privdata, uint64_t *client_id) { + user *acl_user = ACLGetUserByName(name, len); + + if (!acl_user) { + return REDISMODULE_ERR; + } + return authenticateClientWithUser(ctx, acl_user, callback, privdata, client_id); +} + +/* Deauthenticate and close the client. The client resources will not be + * be immediately freed, but will be cleaned up in a background job. This is + * the recommended way to deauthenicate a client since most clients can't + * handle users becomming deauthenticated. Returns REDISMODULE_ERR when the + * client doesn't exist and REDISMODULE_OK when the operation was successful. + * + * The client ID is returned from the RM_AuthenticateClientWithUser and + * RM_AuthenticateClientWithACLUser APIs, but can be obtained through + * the CLIENT api or through server events. + * + * This function is not thread safe, and must be executed within the context + * of a command or thread safe context. */ +int RM_DeauthenticateAndCloseClient(RedisModuleCtx *ctx, uint64_t client_id) { + UNUSED(ctx); + client *c = lookupClientByID(client_id); + if (c == NULL) return REDISMODULE_ERR; + + /* Revoke also marks client to be closed ASAP */ + revokeClientAuthentication(c); + return REDISMODULE_OK; +} + /* -------------------------------------------------------------------------- * Modules Dictionary API * @@ -4598,6 +5767,314 @@ int RM_DictCompare(RedisModuleDictIter *di, const char *op, RedisModuleString *k return res ? REDISMODULE_OK : REDISMODULE_ERR; } + + + +/* -------------------------------------------------------------------------- + * Modules Info fields + * -------------------------------------------------------------------------- */ + +int RM_InfoEndDictField(RedisModuleInfoCtx *ctx); + +/* Used to start a new section, before adding any fields. the section name will + * be prefixed by "_" and must only include A-Z,a-z,0-9. + * NULL or empty string indicates the default section (only ) is used. + * When return value is REDISMODULE_ERR, the section should and will be skipped. */ +int RM_InfoAddSection(RedisModuleInfoCtx *ctx, char *name) { + sds full_name = sdsdup(ctx->module->name); + if (name != NULL && strlen(name) > 0) + full_name = sdscatfmt(full_name, "_%s", name); + + /* Implicitly end dicts, instead of returning an error which is likely un checked. */ + if (ctx->in_dict_field) + RM_InfoEndDictField(ctx); + + /* proceed only if: + * 1) no section was requested (emit all) + * 2) the module name was requested (emit all) + * 3) this specific section was requested. */ + if (ctx->requested_section) { + if (strcasecmp(ctx->requested_section, full_name) && + strcasecmp(ctx->requested_section, ctx->module->name)) { + sdsfree(full_name); + ctx->in_section = 0; + return REDISMODULE_ERR; + } + } + if (ctx->sections++) ctx->info = sdscat(ctx->info,"\r\n"); + ctx->info = sdscatfmt(ctx->info, "# %S\r\n", full_name); + ctx->in_section = 1; + sdsfree(full_name); + return REDISMODULE_OK; +} + +/* Starts a dict field, similar to the ones in INFO KEYSPACE. Use normal + * RedisModule_InfoAddField* functions to add the items to this field, and + * terminate with RedisModule_InfoEndDictField. */ +int RM_InfoBeginDictField(RedisModuleInfoCtx *ctx, char *name) { + if (!ctx->in_section) + return REDISMODULE_ERR; + /* Implicitly end dicts, instead of returning an error which is likely un checked. */ + if (ctx->in_dict_field) + RM_InfoEndDictField(ctx); + ctx->info = sdscatfmt(ctx->info, + "%s_%s:", + ctx->module->name, + name); + ctx->in_dict_field = 1; + return REDISMODULE_OK; +} + +/* Ends a dict field, see RedisModule_InfoBeginDictField */ +int RM_InfoEndDictField(RedisModuleInfoCtx *ctx) { + if (!ctx->in_dict_field) + return REDISMODULE_ERR; + /* trim the last ',' if found. */ + if (ctx->info[sdslen(ctx->info)-1]==',') + sdsIncrLen(ctx->info, -1); + ctx->info = sdscat(ctx->info, "\r\n"); + ctx->in_dict_field = 0; + return REDISMODULE_OK; +} + +/* Used by RedisModuleInfoFunc to add info fields. + * Each field will be automatically prefixed by "_". + * Field names or values must not include \r\n of ":" */ +int RM_InfoAddFieldString(RedisModuleInfoCtx *ctx, char *field, RedisModuleString *value) { + if (!ctx->in_section) + return REDISMODULE_ERR; + if (ctx->in_dict_field) { + ctx->info = sdscatfmt(ctx->info, + "%s=%S,", + field, + (sds)value->ptr); + return REDISMODULE_OK; + } + ctx->info = sdscatfmt(ctx->info, + "%s_%s:%S\r\n", + ctx->module->name, + field, + (sds)value->ptr); + return REDISMODULE_OK; +} + +int RM_InfoAddFieldCString(RedisModuleInfoCtx *ctx, char *field, char *value) { + if (!ctx->in_section) + return REDISMODULE_ERR; + if (ctx->in_dict_field) { + ctx->info = sdscatfmt(ctx->info, + "%s=%s,", + field, + value); + return REDISMODULE_OK; + } + ctx->info = sdscatfmt(ctx->info, + "%s_%s:%s\r\n", + ctx->module->name, + field, + value); + return REDISMODULE_OK; +} + +int RM_InfoAddFieldDouble(RedisModuleInfoCtx *ctx, char *field, double value) { + if (!ctx->in_section) + return REDISMODULE_ERR; + if (ctx->in_dict_field) { + ctx->info = sdscatprintf(ctx->info, + "%s=%.17g,", + field, + value); + return REDISMODULE_OK; + } + ctx->info = sdscatprintf(ctx->info, + "%s_%s:%.17g\r\n", + ctx->module->name, + field, + value); + return REDISMODULE_OK; +} + +int RM_InfoAddFieldLongLong(RedisModuleInfoCtx *ctx, char *field, long long value) { + if (!ctx->in_section) + return REDISMODULE_ERR; + if (ctx->in_dict_field) { + ctx->info = sdscatfmt(ctx->info, + "%s=%I,", + field, + value); + return REDISMODULE_OK; + } + ctx->info = sdscatfmt(ctx->info, + "%s_%s:%I\r\n", + ctx->module->name, + field, + value); + return REDISMODULE_OK; +} + +int RM_InfoAddFieldULongLong(RedisModuleInfoCtx *ctx, char *field, unsigned long long value) { + if (!ctx->in_section) + return REDISMODULE_ERR; + if (ctx->in_dict_field) { + ctx->info = sdscatfmt(ctx->info, + "%s=%U,", + field, + value); + return REDISMODULE_OK; + } + ctx->info = sdscatfmt(ctx->info, + "%s_%s:%U\r\n", + ctx->module->name, + field, + value); + return REDISMODULE_OK; +} + +int RM_RegisterInfoFunc(RedisModuleCtx *ctx, RedisModuleInfoFunc cb) { + ctx->module->info_cb = cb; + return REDISMODULE_OK; +} + +sds modulesCollectInfo(sds info, const char *section, int for_crash_report, int sections) { + dictIterator *di = dictGetIterator(modules); + dictEntry *de; + + while ((de = dictNext(di)) != NULL) { + struct RedisModule *module = dictGetVal(de); + if (!module->info_cb) + continue; + RedisModuleInfoCtx info_ctx = {module, section, info, sections, 0}; + module->info_cb(&info_ctx, for_crash_report); + /* Implicitly end dicts (no way to handle errors, and we must add the newline). */ + if (info_ctx.in_dict_field) + RM_InfoEndDictField(&info_ctx); + info = info_ctx.info; + sections = info_ctx.sections; + } + dictReleaseIterator(di); + return info; +} + +/* Get information about the server similar to the one that returns from the + * INFO command. This function takes an optional 'section' argument that may + * be NULL. The return value holds the output and can be used with + * RedisModule_ServerInfoGetField and alike to get the individual fields. + * When done, it needs to be freed with RedisModule_FreeServerInfo or with the + * automatic memory management mechanism if enabled. */ +RedisModuleServerInfoData *RM_GetServerInfo(RedisModuleCtx *ctx, const char *section) { + struct RedisModuleServerInfoData *d = zmalloc(sizeof(*d)); + d->rax = raxNew(); + if (ctx != NULL) autoMemoryAdd(ctx,REDISMODULE_AM_INFO,d); + sds info = genRedisInfoString(section); + int totlines, i; + sds *lines = sdssplitlen(info, sdslen(info), "\r\n", 2, &totlines); + for(i=0; irax,key,keylen,val,NULL)) + sdsfree(val); + } + sdsfree(info); + sdsfreesplitres(lines,totlines); + return d; +} + +/* Free data created with RM_GetServerInfo(). You need to pass the + * context pointer 'ctx' only if the dictionary was created using the + * context instead of passing NULL. */ +void RM_FreeServerInfo(RedisModuleCtx *ctx, RedisModuleServerInfoData *data) { + if (ctx != NULL) autoMemoryFreed(ctx,REDISMODULE_AM_INFO,data); + raxIterator ri; + raxStart(&ri,data->rax); + while(1) { + raxSeek(&ri,"^",NULL,0); + if (!raxNext(&ri)) break; + raxRemove(data->rax,(unsigned char*)ri.key,ri.key_len,NULL); + sdsfree(ri.data); + } + raxStop(&ri); + raxFree(data->rax); + zfree(data); +} + +/* Get the value of a field from data collected with RM_GetServerInfo(). You + * need to pass the context pointer 'ctx' only if you want to use auto memory + * mechanism to release the returned string. Return value will be NULL if the + * field was not found. */ +RedisModuleString *RM_ServerInfoGetField(RedisModuleCtx *ctx, RedisModuleServerInfoData *data, const char* field) { + sds val = raxFind(data->rax, (unsigned char *)field, strlen(field)); + if (val == raxNotFound) return NULL; + RedisModuleString *o = createStringObject(val,sdslen(val)); + if (ctx != NULL) autoMemoryAdd(ctx,REDISMODULE_AM_STRING,o); + return o; +} + +/* Similar to RM_ServerInfoGetField, but returns a char* which should not be freed but the caller. */ +const char *RM_ServerInfoGetFieldC(RedisModuleServerInfoData *data, const char* field) { + sds val = raxFind(data->rax, (unsigned char *)field, strlen(field)); + if (val == raxNotFound) return NULL; + return val; +} + +/* Get the value of a field from data collected with RM_GetServerInfo(). If the + * field is not found, or is not numerical or out of range, return value will be + * 0, and the optional out_err argument will be set to REDISMODULE_ERR. */ +long long RM_ServerInfoGetFieldSigned(RedisModuleServerInfoData *data, const char* field, int *out_err) { + long long ll; + sds val = raxFind(data->rax, (unsigned char *)field, strlen(field)); + if (val == raxNotFound) { + if (out_err) *out_err = REDISMODULE_ERR; + return 0; + } + if (!string2ll(val,sdslen(val),&ll)) { + if (out_err) *out_err = REDISMODULE_ERR; + return 0; + } + if (out_err) *out_err = REDISMODULE_OK; + return ll; +} + +/* Get the value of a field from data collected with RM_GetServerInfo(). If the + * field is not found, or is not numerical or out of range, return value will be + * 0, and the optional out_err argument will be set to REDISMODULE_ERR. */ +unsigned long long RM_ServerInfoGetFieldUnsigned(RedisModuleServerInfoData *data, const char* field, int *out_err) { + unsigned long long ll; + sds val = raxFind(data->rax, (unsigned char *)field, strlen(field)); + if (val == raxNotFound) { + if (out_err) *out_err = REDISMODULE_ERR; + return 0; + } + if (!string2ull(val,&ll)) { + if (out_err) *out_err = REDISMODULE_ERR; + return 0; + } + if (out_err) *out_err = REDISMODULE_OK; + return ll; +} + +/* Get the value of a field from data collected with RM_GetServerInfo(). If the + * field is not found, or is not a double, return value will be 0, and the + * optional out_err argument will be set to REDISMODULE_ERR. */ +double RM_ServerInfoGetFieldDouble(RedisModuleServerInfoData *data, const char* field, int *out_err) { + double dbl; + sds val = raxFind(data->rax, (unsigned char *)field, strlen(field)); + if (val == raxNotFound) { + if (out_err) *out_err = REDISMODULE_ERR; + return 0; + } + if (!string2d(val,sdslen(val),&dbl)) { + if (out_err) *out_err = REDISMODULE_ERR; + return 0; + } + if (out_err) *out_err = REDISMODULE_OK; + return dbl; +} + /* -------------------------------------------------------------------------- * Modules utility APIs * -------------------------------------------------------------------------- */ @@ -4617,6 +6094,1041 @@ void RM_GetRandomHexChars(char *dst, size_t len) { getRandomHexChars(dst,len); } +/* -------------------------------------------------------------------------- + * Modules API exporting / importing + * -------------------------------------------------------------------------- */ + +/* This function is called by a module in order to export some API with a + * given name. Other modules will be able to use this API by calling the + * symmetrical function RM_GetSharedAPI() and casting the return value to + * the right function pointer. + * + * The function will return REDISMODULE_OK if the name is not already taken, + * otherwise REDISMODULE_ERR will be returned and no operation will be + * performed. + * + * IMPORTANT: the apiname argument should be a string literal with static + * lifetime. The API relies on the fact that it will always be valid in + * the future. */ +int RM_ExportSharedAPI(RedisModuleCtx *ctx, const char *apiname, void *func) { + RedisModuleSharedAPI *sapi = zmalloc(sizeof(*sapi)); + sapi->module = ctx->module; + sapi->func = func; + if (dictAdd(server.sharedapi, (char*)apiname, sapi) != DICT_OK) { + zfree(sapi); + return REDISMODULE_ERR; + } + return REDISMODULE_OK; +} + +/* Request an exported API pointer. The return value is just a void pointer + * that the caller of this function will be required to cast to the right + * function pointer, so this is a private contract between modules. + * + * If the requested API is not available then NULL is returned. Because + * modules can be loaded at different times with different order, this + * function calls should be put inside some module generic API registering + * step, that is called every time a module attempts to execute a + * command that requires external APIs: if some API cannot be resolved, the + * command should return an error. + * + * Here is an exmaple: + * + * int ... myCommandImplementation() { + * if (getExternalAPIs() == 0) { + * reply with an error here if we cannot have the APIs + * } + * // Use the API: + * myFunctionPointer(foo); + * } + * + * And the function registerAPI() is: + * + * int getExternalAPIs(void) { + * static int api_loaded = 0; + * if (api_loaded != 0) return 1; // APIs already resolved. + * + * myFunctionPointer = RedisModule_GetOtherModuleAPI("..."); + * if (myFunctionPointer == NULL) return 0; + * + * return 1; + * } + */ +void *RM_GetSharedAPI(RedisModuleCtx *ctx, const char *apiname) { + dictEntry *de = dictFind(server.sharedapi, apiname); + if (de == NULL) return NULL; + RedisModuleSharedAPI *sapi = dictGetVal(de); + if (listSearchKey(sapi->module->usedby,ctx->module) == NULL) { + listAddNodeTail(sapi->module->usedby,ctx->module); + listAddNodeTail(ctx->module->using,sapi->module); + } + return sapi->func; +} + +/* Remove all the APIs registered by the specified module. Usually you + * want this when the module is going to be unloaded. This function + * assumes that's caller responsibility to make sure the APIs are not + * used by other modules. + * + * The number of unregistered APIs is returned. */ +int moduleUnregisterSharedAPI(RedisModule *module) { + int count = 0; + dictIterator *di = dictGetSafeIterator(server.sharedapi); + dictEntry *de; + while ((de = dictNext(di)) != NULL) { + const char *apiname = dictGetKey(de); + RedisModuleSharedAPI *sapi = dictGetVal(de); + if (sapi->module == module) { + dictDelete(server.sharedapi,apiname); + zfree(sapi); + count++; + } + } + dictReleaseIterator(di); + return count; +} + +/* Remove the specified module as an user of APIs of ever other module. + * This is usually called when a module is unloaded. + * + * Returns the number of modules this module was using APIs from. */ +int moduleUnregisterUsedAPI(RedisModule *module) { + listIter li; + listNode *ln; + int count = 0; + + listRewind(module->using,&li); + while((ln = listNext(&li))) { + RedisModule *used = ln->value; + listNode *ln = listSearchKey(used->usedby,module); + if (ln) { + listDelNode(module->using,ln); + count++; + } + } + return count; +} + +/* Unregister all filters registered by a module. + * This is called when a module is being unloaded. + * + * Returns the number of filters unregistered. */ +int moduleUnregisterFilters(RedisModule *module) { + listIter li; + listNode *ln; + int count = 0; + + listRewind(module->filters,&li); + while((ln = listNext(&li))) { + RedisModuleCommandFilter *filter = ln->value; + listNode *ln = listSearchKey(moduleCommandFilters,filter); + if (ln) { + listDelNode(moduleCommandFilters,ln); + count++; + } + zfree(filter); + } + return count; +} + +/* -------------------------------------------------------------------------- + * Module Command Filter API + * -------------------------------------------------------------------------- */ + +/* Register a new command filter function. + * + * Command filtering makes it possible for modules to extend Redis by plugging + * into the execution flow of all commands. + * + * A registered filter gets called before Redis executes *any* command. This + * includes both core Redis commands and commands registered by any module. The + * filter applies in all execution paths including: + * + * 1. Invocation by a client. + * 2. Invocation through `RedisModule_Call()` by any module. + * 3. Invocation through Lua 'redis.call()`. + * 4. Replication of a command from a master. + * + * The filter executes in a special filter context, which is different and more + * limited than a RedisModuleCtx. Because the filter affects any command, it + * must be implemented in a very efficient way to reduce the performance impact + * on Redis. All Redis Module API calls that require a valid context (such as + * `RedisModule_Call()`, `RedisModule_OpenKey()`, etc.) are not supported in a + * filter context. + * + * The `RedisModuleCommandFilterCtx` can be used to inspect or modify the + * executed command and its arguments. As the filter executes before Redis + * begins processing the command, any change will affect the way the command is + * processed. For example, a module can override Redis commands this way: + * + * 1. Register a `MODULE.SET` command which implements an extended version of + * the Redis `SET` command. + * 2. Register a command filter which detects invocation of `SET` on a specific + * pattern of keys. Once detected, the filter will replace the first + * argument from `SET` to `MODULE.SET`. + * 3. When filter execution is complete, Redis considers the new command name + * and therefore executes the module's own command. + * + * Note that in the above use case, if `MODULE.SET` itself uses + * `RedisModule_Call()` the filter will be applied on that call as well. If + * that is not desired, the `REDISMODULE_CMDFILTER_NOSELF` flag can be set when + * registering the filter. + * + * The `REDISMODULE_CMDFILTER_NOSELF` flag prevents execution flows that + * originate from the module's own `RM_Call()` from reaching the filter. This + * flag is effective for all execution flows, including nested ones, as long as + * the execution begins from the module's command context or a thread-safe + * context that is associated with a blocking command. + * + * Detached thread-safe contexts are *not* associated with the module and cannot + * be protected by this flag. + * + * If multiple filters are registered (by the same or different modules), they + * are executed in the order of registration. + */ + +RedisModuleCommandFilter *RM_RegisterCommandFilter(RedisModuleCtx *ctx, RedisModuleCommandFilterFunc callback, int flags) { + RedisModuleCommandFilter *filter = zmalloc(sizeof(*filter)); + filter->module = ctx->module; + filter->callback = callback; + filter->flags = flags; + + listAddNodeTail(moduleCommandFilters, filter); + listAddNodeTail(ctx->module->filters, filter); + return filter; +} + +/* Unregister a command filter. + */ +int RM_UnregisterCommandFilter(RedisModuleCtx *ctx, RedisModuleCommandFilter *filter) { + listNode *ln; + + /* A module can only remove its own filters */ + if (filter->module != ctx->module) return REDISMODULE_ERR; + + ln = listSearchKey(moduleCommandFilters,filter); + if (!ln) return REDISMODULE_ERR; + listDelNode(moduleCommandFilters,ln); + + ln = listSearchKey(ctx->module->filters,filter); + if (!ln) return REDISMODULE_ERR; /* Shouldn't happen */ + listDelNode(ctx->module->filters,ln); + + zfree(filter); + + return REDISMODULE_OK; +} + +void moduleCallCommandFilters(client *c) { + if (listLength(moduleCommandFilters) == 0) return; + + listIter li; + listNode *ln; + listRewind(moduleCommandFilters,&li); + + RedisModuleCommandFilterCtx filter = { + .argv = c->argv, + .argc = c->argc + }; + + while((ln = listNext(&li))) { + RedisModuleCommandFilter *f = ln->value; + + /* Skip filter if REDISMODULE_CMDFILTER_NOSELF is set and module is + * currently processing a command. + */ + if ((f->flags & REDISMODULE_CMDFILTER_NOSELF) && f->module->in_call) continue; + + /* Call filter */ + f->callback(&filter); + } + + c->argv = filter.argv; + c->argc = filter.argc; +} + +/* Return the number of arguments a filtered command has. The number of + * arguments include the command itself. + */ +int RM_CommandFilterArgsCount(RedisModuleCommandFilterCtx *fctx) +{ + return fctx->argc; +} + +/* Return the specified command argument. The first argument (position 0) is + * the command itself, and the rest are user-provided args. + */ +const RedisModuleString *RM_CommandFilterArgGet(RedisModuleCommandFilterCtx *fctx, int pos) +{ + if (pos < 0 || pos >= fctx->argc) return NULL; + return fctx->argv[pos]; +} + +/* Modify the filtered command by inserting a new argument at the specified + * position. The specified RedisModuleString argument may be used by Redis + * after the filter context is destroyed, so it must not be auto-memory + * allocated, freed or used elsewhere. + */ + +int RM_CommandFilterArgInsert(RedisModuleCommandFilterCtx *fctx, int pos, RedisModuleString *arg) +{ + int i; + + if (pos < 0 || pos > fctx->argc) return REDISMODULE_ERR; + + fctx->argv = zrealloc(fctx->argv, (fctx->argc+1)*sizeof(RedisModuleString *)); + for (i = fctx->argc; i > pos; i--) { + fctx->argv[i] = fctx->argv[i-1]; + } + fctx->argv[pos] = arg; + fctx->argc++; + + return REDISMODULE_OK; +} + +/* Modify the filtered command by replacing an existing argument with a new one. + * The specified RedisModuleString argument may be used by Redis after the + * filter context is destroyed, so it must not be auto-memory allocated, freed + * or used elsewhere. + */ + +int RM_CommandFilterArgReplace(RedisModuleCommandFilterCtx *fctx, int pos, RedisModuleString *arg) +{ + if (pos < 0 || pos >= fctx->argc) return REDISMODULE_ERR; + + decrRefCount(fctx->argv[pos]); + fctx->argv[pos] = arg; + + return REDISMODULE_OK; +} + +/* Modify the filtered command by deleting an argument at the specified + * position. + */ +int RM_CommandFilterArgDelete(RedisModuleCommandFilterCtx *fctx, int pos) +{ + int i; + if (pos < 0 || pos >= fctx->argc) return REDISMODULE_ERR; + + decrRefCount(fctx->argv[pos]); + for (i = pos; i < fctx->argc-1; i++) { + fctx->argv[i] = fctx->argv[i+1]; + } + fctx->argc--; + + return REDISMODULE_OK; +} + +/* For a given pointer allocated via RedisModule_Alloc() or + * RedisModule_Realloc(), return the amount of memory allocated for it. + * Note that this may be different (larger) than the memory we allocated + * with the allocation calls, since sometimes the underlying allocator + * will allocate more memory. + */ +size_t RM_MallocSize(void* ptr){ + return zmalloc_size(ptr); +} + +/* Return the a number between 0 to 1 indicating the amount of memory + * currently used, relative to the Redis "maxmemory" configuration. + * + * 0 - No memory limit configured. + * Between 0 and 1 - The percentage of the memory used normalized in 0-1 range. + * Exactly 1 - Memory limit reached. + * Greater 1 - More memory used than the configured limit. + */ +float RM_GetUsedMemoryRatio(){ + float level; + getMaxmemoryState(NULL, NULL, NULL, &level); + return level; +} + +/* -------------------------------------------------------------------------- + * Scanning keyspace and hashes + * -------------------------------------------------------------------------- */ + +typedef void (*RedisModuleScanCB)(RedisModuleCtx *ctx, RedisModuleString *keyname, RedisModuleKey *key, void *privdata); +typedef struct { + RedisModuleCtx *ctx; + void* user_data; + RedisModuleScanCB fn; +} ScanCBData; + +typedef struct RedisModuleScanCursor{ + int cursor; + int done; +}RedisModuleScanCursor; + +static void moduleScanCallback(void *privdata, const dictEntry *de) { + ScanCBData *data = privdata; + sds key = dictGetKey(de); + robj* val = dictGetVal(de); + RedisModuleString *keyname = createObject(OBJ_STRING,sdsdup(key)); + + /* Setup the key handle. */ + RedisModuleKey kp = {0}; + moduleInitKey(&kp, data->ctx, keyname, val, REDISMODULE_READ); + + data->fn(data->ctx, keyname, &kp, data->user_data); + + moduleCloseKey(&kp); + decrRefCount(keyname); +} + +/* Create a new cursor to be used with RedisModule_Scan */ +RedisModuleScanCursor *RM_ScanCursorCreate() { + RedisModuleScanCursor* cursor = zmalloc(sizeof(*cursor)); + cursor->cursor = 0; + cursor->done = 0; + return cursor; +} + +/* Restart an existing cursor. The keys will be rescanned. */ +void RM_ScanCursorRestart(RedisModuleScanCursor *cursor) { + cursor->cursor = 0; + cursor->done = 0; +} + +/* Destroy the cursor struct. */ +void RM_ScanCursorDestroy(RedisModuleScanCursor *cursor) { + zfree(cursor); +} + +/* Scan api that allows a module to scan all the keys and value in the selected db. + * + * Callback for scan implementation. + * void scan_callback(RedisModuleCtx *ctx, RedisModuleString *keyname, RedisModuleKey *key, void *privdata); + * - ctx - the redis module context provided to for the scan. + * - keyname - owned by the caller and need to be retained if used after this function. + * - key - holds info on the key and value, it is provided as best effort, in some cases it might + * be NULL, in which case the user should (can) use RedisModule_OpenKey (and CloseKey too). + * when it is provided, it is owned by the caller and will be free when the callback returns. + * - privdata - the user data provided to RedisModule_Scan. + * + * The way it should be used: + * RedisModuleCursor *c = RedisModule_ScanCursorCreate(); + * while(RedisModule_Scan(ctx, c, callback, privateData)); + * RedisModule_ScanCursorDestroy(c); + * + * It is also possible to use this API from another thread while the lock is acquired durring + * the actuall call to RM_Scan: + * RedisModuleCursor *c = RedisModule_ScanCursorCreate(); + * RedisModule_ThreadSafeContextLock(ctx); + * while(RedisModule_Scan(ctx, c, callback, privateData)){ + * RedisModule_ThreadSafeContextUnlock(ctx); + * // do some background job + * RedisModule_ThreadSafeContextLock(ctx); + * } + * RedisModule_ScanCursorDestroy(c); + * + * The function will return 1 if there are more elements to scan and 0 otherwise, + * possibly setting errno if the call failed. + * It is also possible to restart and existing cursor using RM_CursorRestart. */ +int RM_Scan(RedisModuleCtx *ctx, RedisModuleScanCursor *cursor, RedisModuleScanCB fn, void *privdata) { + if (cursor->done) { + errno = ENOENT; + return 0; + } + int ret = 1; + ScanCBData data = { ctx, privdata, fn }; + cursor->cursor = dictScan(ctx->client->db->dict, cursor->cursor, moduleScanCallback, NULL, &data); + if (cursor->cursor == 0) { + cursor->done = 1; + ret = 0; + } + errno = 0; + return ret; +} + +typedef void (*RedisModuleScanKeyCB)(RedisModuleKey *key, RedisModuleString *field, RedisModuleString *value, void *privdata); +typedef struct { + RedisModuleKey *key; + void* user_data; + RedisModuleScanKeyCB fn; +} ScanKeyCBData; + +static void moduleScanKeyCallback(void *privdata, const dictEntry *de) { + ScanKeyCBData *data = privdata; + sds key = dictGetKey(de); + robj *o = data->key->value; + robj *field = createStringObject(key, sdslen(key)); + robj *value = NULL; + if (o->type == OBJ_SET) { + value = NULL; + } else if (o->type == OBJ_HASH) { + sds val = dictGetVal(de); + value = createStringObject(val, sdslen(val)); + } else if (o->type == OBJ_ZSET) { + double *val = (double*)dictGetVal(de); + value = createStringObjectFromLongDouble(*val, 0); + } + + data->fn(data->key, field, value, data->user_data); + decrRefCount(field); + if (value) decrRefCount(value); +} + +/* Scan api that allows a module to scan the elements in a hash, set or sorted set key + * + * Callback for scan implementation. + * void scan_callback(RedisModuleKey *key, RedisModuleString* field, RedisModuleString* value, void *privdata); + * - key - the redis key context provided to for the scan. + * - field - field name, owned by the caller and need to be retained if used + * after this function. + * - value - value string or NULL for set type, owned by the caller and need to + * be retained if used after this function. + * - privdata - the user data provided to RedisModule_ScanKey. + * + * The way it should be used: + * RedisModuleCursor *c = RedisModule_ScanCursorCreate(); + * RedisModuleKey *key = RedisModule_OpenKey(...) + * while(RedisModule_ScanKey(key, c, callback, privateData)); + * RedisModule_CloseKey(key); + * RedisModule_ScanCursorDestroy(c); + * + * It is also possible to use this API from another thread while the lock is acquired durring + * the actuall call to RM_Scan, and re-opening the key each time: + * RedisModuleCursor *c = RedisModule_ScanCursorCreate(); + * RedisModule_ThreadSafeContextLock(ctx); + * RedisModuleKey *key = RedisModule_OpenKey(...) + * while(RedisModule_ScanKey(ctx, c, callback, privateData)){ + * RedisModule_CloseKey(key); + * RedisModule_ThreadSafeContextUnlock(ctx); + * // do some background job + * RedisModule_ThreadSafeContextLock(ctx); + * RedisModuleKey *key = RedisModule_OpenKey(...) + * } + * RedisModule_CloseKey(key); + * RedisModule_ScanCursorDestroy(c); + * + * The function will return 1 if there are more elements to scan and 0 otherwise, + * possibly setting errno if the call failed. + * It is also possible to restart and existing cursor using RM_CursorRestart. */ +int RM_ScanKey(RedisModuleKey *key, RedisModuleScanCursor *cursor, RedisModuleScanKeyCB fn, void *privdata) { + if (key == NULL || key->value == NULL) { + errno = EINVAL; + return 0; + } + dict *ht = NULL; + robj *o = key->value; + if (o->type == OBJ_SET) { + if (o->encoding == OBJ_ENCODING_HT) + ht = o->ptr; + } else if (o->type == OBJ_HASH) { + if (o->encoding == OBJ_ENCODING_HT) + ht = o->ptr; + } else if (o->type == OBJ_ZSET) { + if (o->encoding == OBJ_ENCODING_SKIPLIST) + ht = ((zset *)o->ptr)->dict; + } else { + errno = EINVAL; + return 0; + } + if (cursor->done) { + errno = ENOENT; + return 0; + } + int ret = 1; + if (ht) { + ScanKeyCBData data = { key, privdata, fn }; + cursor->cursor = dictScan(ht, cursor->cursor, moduleScanKeyCallback, NULL, &data); + if (cursor->cursor == 0) { + cursor->done = 1; + ret = 0; + } + } else if (o->type == OBJ_SET && o->encoding == OBJ_ENCODING_INTSET) { + int pos = 0; + int64_t ll; + while(intsetGet(o->ptr,pos++,&ll)) { + robj *field = createStringObjectFromLongLong(ll); + fn(key, field, NULL, privdata); + decrRefCount(field); + } + cursor->cursor = 1; + cursor->done = 1; + ret = 0; + } else if (o->type == OBJ_HASH || o->type == OBJ_ZSET) { + unsigned char *p = ziplistIndex(o->ptr,0); + unsigned char *vstr; + unsigned int vlen; + long long vll; + while(p) { + ziplistGet(p,&vstr,&vlen,&vll); + robj *field = (vstr != NULL) ? + createStringObject((char*)vstr,vlen) : + createStringObjectFromLongLong(vll); + p = ziplistNext(o->ptr,p); + ziplistGet(p,&vstr,&vlen,&vll); + robj *value = (vstr != NULL) ? + createStringObject((char*)vstr,vlen) : + createStringObjectFromLongLong(vll); + fn(key, field, value, privdata); + p = ziplistNext(o->ptr,p); + decrRefCount(field); + decrRefCount(value); + } + cursor->cursor = 1; + cursor->done = 1; + ret = 0; + } + errno = 0; + return ret; +} + + +/* -------------------------------------------------------------------------- + * Module fork API + * -------------------------------------------------------------------------- */ + +/* Create a background child process with the current frozen snaphost of the + * main process where you can do some processing in the background without + * affecting / freezing the traffic and no need for threads and GIL locking. + * Note that Redis allows for only one concurrent fork. + * When the child wants to exit, it should call RedisModule_ExitFromChild. + * If the parent wants to kill the child it should call RedisModule_KillForkChild + * The done handler callback will be executed on the parent process when the + * child existed (but not when killed) + * Return: -1 on failure, on success the parent process will get a positive PID + * of the child, and the child process will get 0. + */ +int RM_Fork(RedisModuleForkDoneHandler cb, void *user_data) { + pid_t childpid; + if (hasActiveChildProcess()) { + return -1; + } + + openChildInfoPipe(); + if ((childpid = redisFork()) == 0) { + /* Child */ + redisSetProcTitle("redis-module-fork"); + } else if (childpid == -1) { + closeChildInfoPipe(); + serverLog(LL_WARNING,"Can't fork for module: %s", strerror(errno)); + } else { + /* Parent */ + server.module_child_pid = childpid; + moduleForkInfo.done_handler = cb; + moduleForkInfo.done_handler_user_data = user_data; + serverLog(LL_NOTICE, "Module fork started pid: %d ", childpid); + } + return childpid; +} + +/* Call from the child process when you want to terminate it. + * retcode will be provided to the done handler executed on the parent process. + */ +int RM_ExitFromChild(int retcode) { + sendChildCOWInfo(CHILD_INFO_TYPE_MODULE, "Module fork"); + exitFromChild(retcode); + return REDISMODULE_OK; +} + +/* Kill the active module forked child, if there is one active and the + * pid matches, and returns C_OK. Otherwise if there is no active module + * child or the pid does not match, return C_ERR without doing anything. */ +int TerminateModuleForkChild(int child_pid, int wait) { + /* Module child should be active and pid should match. */ + if (server.module_child_pid == -1 || + server.module_child_pid != child_pid) return C_ERR; + + int statloc; + serverLog(LL_NOTICE,"Killing running module fork child: %ld", + (long) server.module_child_pid); + if (kill(server.module_child_pid,SIGUSR1) != -1 && wait) { + while(wait4(server.module_child_pid,&statloc,0,NULL) != + server.module_child_pid); + } + /* Reset the buffer accumulating changes while the child saves. */ + server.module_child_pid = -1; + moduleForkInfo.done_handler = NULL; + moduleForkInfo.done_handler_user_data = NULL; + closeChildInfoPipe(); + updateDictResizePolicy(); + return C_OK; +} + +/* Can be used to kill the forked child process from the parent process. + * child_pid whould be the return value of RedisModule_Fork. */ +int RM_KillForkChild(int child_pid) { + /* Kill module child, wait for child exit. */ + if (TerminateModuleForkChild(child_pid,1) == C_OK) + return REDISMODULE_OK; + else + return REDISMODULE_ERR; +} + +void ModuleForkDoneHandler(int exitcode, int bysignal) { + serverLog(LL_NOTICE, + "Module fork exited pid: %d, retcode: %d, bysignal: %d", + server.module_child_pid, exitcode, bysignal); + if (moduleForkInfo.done_handler) { + moduleForkInfo.done_handler(exitcode, bysignal, + moduleForkInfo.done_handler_user_data); + } + server.module_child_pid = -1; + moduleForkInfo.done_handler = NULL; + moduleForkInfo.done_handler_user_data = NULL; +} + +/* -------------------------------------------------------------------------- + * Server hooks implementation + * -------------------------------------------------------------------------- */ + +/* Register to be notified, via a callback, when the specified server event + * happens. The callback is called with the event as argument, and an additional + * argument which is a void pointer and should be cased to a specific type + * that is event-specific (but many events will just use NULL since they do not + * have additional information to pass to the callback). + * + * If the callback is NULL and there was a previous subscription, the module + * will be unsubscribed. If there was a previous subscription and the callback + * is not null, the old callback will be replaced with the new one. + * + * The callback must be of this type: + * + * int (*RedisModuleEventCallback)(RedisModuleCtx *ctx, + * RedisModuleEvent eid, + * uint64_t subevent, + * void *data); + * + * The 'ctx' is a normal Redis module context that the callback can use in + * order to call other modules APIs. The 'eid' is the event itself, this + * is only useful in the case the module subscribed to multiple events: using + * the 'id' field of this structure it is possible to check if the event + * is one of the events we registered with this callback. The 'subevent' field + * depends on the event that fired. + * + * Finally the 'data' pointer may be populated, only for certain events, with + * more relevant data. + * + * Here is a list of events you can use as 'eid' and related sub events: + * + * RedisModuleEvent_ReplicationRoleChanged + * + * This event is called when the instance switches from master + * to replica or the other way around, however the event is + * also called when the replica remains a replica but starts to + * replicate with a different master. + * + * The following sub events are available: + * + * REDISMODULE_SUBEVENT_REPLROLECHANGED_NOW_MASTER + * REDISMODULE_SUBEVENT_REPLROLECHANGED_NOW_REPLICA + * + * The 'data' field can be casted by the callback to a + * RedisModuleReplicationInfo structure with the following fields: + * + * int master; // true if master, false if replica + * char *masterhost; // master instance hostname for NOW_REPLICA + * int masterport; // master instance port for NOW_REPLICA + * char *replid1; // Main replication ID + * char *replid2; // Secondary replication ID + * uint64_t repl1_offset; // Main replication offset + * uint64_t repl2_offset; // Offset of replid2 validity + * + * RedisModuleEvent_Persistence + * + * This event is called when RDB saving or AOF rewriting starts + * and ends. The following sub events are available: + * + * REDISMODULE_SUBEVENT_PERSISTENCE_RDB_START + * REDISMODULE_SUBEVENT_PERSISTENCE_AOF_START + * REDISMODULE_SUBEVENT_PERSISTENCE_SYNC_RDB_START + * REDISMODULE_SUBEVENT_PERSISTENCE_ENDED + * REDISMODULE_SUBEVENT_PERSISTENCE_FAILED + * + * The above events are triggered not just when the user calls the + * relevant commands like BGSAVE, but also when a saving operation + * or AOF rewriting occurs because of internal server triggers. + * The SYNC_RDB_START sub events are happening in the forground due to + * SAVE command, FLUSHALL, or server shutdown, and the other RDB and + * AOF sub events are executed in a background fork child, so any + * action the module takes can only affect the generated AOF or RDB, + * but will not be reflected in the parent process and affect connected + * clients and commands. Also note that the AOF_START sub event may end + * up saving RDB content in case of an AOF with rdb-preamble. + * + * RedisModuleEvent_FlushDB + * + * The FLUSHALL, FLUSHDB or an internal flush (for instance + * because of replication, after the replica synchronization) + * happened. The following sub events are available: + * + * REDISMODULE_SUBEVENT_FLUSHDB_START + * REDISMODULE_SUBEVENT_FLUSHDB_END + * + * The data pointer can be casted to a RedisModuleFlushInfo + * structure with the following fields: + * + * int32_t async; // True if the flush is done in a thread. + * See for instance FLUSHALL ASYNC. + * In this case the END callback is invoked + * immediately after the database is put + * in the free list of the thread. + * int32_t dbnum; // Flushed database number, -1 for all the DBs + * in the case of the FLUSHALL operation. + * + * The start event is called *before* the operation is initated, thus + * allowing the callback to call DBSIZE or other operation on the + * yet-to-free keyspace. + * + * RedisModuleEvent_Loading + * + * Called on loading operations: at startup when the server is + * started, but also after a first synchronization when the + * replica is loading the RDB file from the master. + * The following sub events are available: + * + * REDISMODULE_SUBEVENT_LOADING_RDB_START + * REDISMODULE_SUBEVENT_LOADING_AOF_START + * REDISMODULE_SUBEVENT_LOADING_REPL_START + * REDISMODULE_SUBEVENT_LOADING_ENDED + * REDISMODULE_SUBEVENT_LOADING_FAILED + * + * Note that AOF loading may start with an RDB data in case of + * rdb-preamble, in which case you'll only recieve an AOF_START event. + * + * + * RedisModuleEvent_ClientChange + * + * Called when a client connects or disconnects. + * The data pointer can be casted to a RedisModuleClientInfo + * structure, documented in RedisModule_GetClientInfoById(). + * The following sub events are available: + * + * REDISMODULE_SUBEVENT_CLIENT_CHANGE_CONNECTED + * REDISMODULE_SUBEVENT_CLIENT_CHANGE_DISCONNECTED + * + * RedisModuleEvent_Shutdown + * + * The server is shutting down. No subevents are available. + * + * RedisModuleEvent_ReplicaChange + * + * This event is called when the instance (that can be both a + * master or a replica) get a new online replica, or lose a + * replica since it gets disconnected. + * The following sub events are availble: + * + * REDISMODULE_SUBEVENT_REPLICA_CHANGE_ONLINE + * REDISMODULE_SUBEVENT_REPLICA_CHANGE_OFFLINE + * + * No additional information is available so far: future versions + * of Redis will have an API in order to enumerate the replicas + * connected and their state. + * + * RedisModuleEvent_CronLoop + * + * This event is called every time Redis calls the serverCron() + * function in order to do certain bookkeeping. Modules that are + * required to do operations from time to time may use this callback. + * Normally Redis calls this function 10 times per second, but + * this changes depending on the "hz" configuration. + * No sub events are available. + * + * The data pointer can be casted to a RedisModuleCronLoop + * structure with the following fields: + * + * int32_t hz; // Approximate number of events per second. + * + * RedisModuleEvent_MasterLinkChange + * + * This is called for replicas in order to notify when the + * replication link becomes functional (up) with our master, + * or when it goes down. Note that the link is not considered + * up when we just connected to the master, but only if the + * replication is happening correctly. + * The following sub events are available: + * + * REDISMODULE_SUBEVENT_MASTER_LINK_UP + * REDISMODULE_SUBEVENT_MASTER_LINK_DOWN + * + * RedisModuleEvent_ModuleChange + * + * This event is called when a new module is loaded or one is unloaded. + * The following sub events are availble: + * + * REDISMODULE_SUBEVENT_MODULE_LOADED + * REDISMODULE_SUBEVENT_MODULE_UNLOADED + * + * The data pointer can be casted to a RedisModuleModuleChange + * structure with the following fields: + * + * const char* module_name; // Name of module loaded or unloaded. + * int32_t module_version; // Module version. + * + * RedisModuleEvent_LoadingProgress + * + * This event is called repeatedly called while an RDB or AOF file + * is being loaded. + * The following sub events are availble: + * + * REDISMODULE_SUBEVENT_LOADING_PROGRESS_RDB + * REDISMODULE_SUBEVENT_LOADING_PROGRESS_AOF + * + * The data pointer can be casted to a RedisModuleLoadingProgress + * structure with the following fields: + * + * int32_t hz; // Approximate number of events per second. + * int32_t progress; // Approximate progress between 0 and 1024, + * or -1 if unknown. + * + * The function returns REDISMODULE_OK if the module was successfully subscrived + * for the specified event. If the API is called from a wrong context then + * REDISMODULE_ERR is returned. */ +int RM_SubscribeToServerEvent(RedisModuleCtx *ctx, RedisModuleEvent event, RedisModuleEventCallback callback) { + RedisModuleEventListener *el; + + /* Protect in case of calls from contexts without a module reference. */ + if (ctx->module == NULL) return REDISMODULE_ERR; + + /* Search an event matching this module and event ID. */ + listIter li; + listNode *ln; + listRewind(RedisModule_EventListeners,&li); + while((ln = listNext(&li))) { + el = ln->value; + if (el->module == ctx->module && el->event.id == event.id) + break; /* Matching event found. */ + } + + /* Modify or remove the event listener if we already had one. */ + if (ln) { + if (callback == NULL) { + listDelNode(RedisModule_EventListeners,ln); + zfree(el); + } else { + el->callback = callback; /* Update the callback with the new one. */ + } + return REDISMODULE_OK; + } + + /* No event found, we need to add a new one. */ + el = zmalloc(sizeof(*el)); + el->module = ctx->module; + el->event = event; + el->callback = callback; + listAddNodeTail(RedisModule_EventListeners,el); + return REDISMODULE_OK; +} + +/* This is called by the Redis internals every time we want to fire an + * event that can be interceppted by some module. The pointer 'data' is useful + * in order to populate the event-specific structure when needed, in order + * to return the structure with more information to the callback. + * + * 'eid' and 'subid' are just the main event ID and the sub event associated + * with the event, depending on what exactly happened. */ +void moduleFireServerEvent(uint64_t eid, int subid, void *data) { + /* Fast path to return ASAP if there is nothing to do, avoiding to + * setup the iterator and so forth: we want this call to be extremely + * cheap if there are no registered modules. */ + if (listLength(RedisModule_EventListeners) == 0) return; + + listIter li; + listNode *ln; + listRewind(RedisModule_EventListeners,&li); + while((ln = listNext(&li))) { + RedisModuleEventListener *el = ln->value; + if (el->event.id == eid) { + RedisModuleCtx ctx = REDISMODULE_CTX_INIT; + ctx.module = el->module; + + if (ModulesInHooks == 0) { + ctx.client = moduleFreeContextReusedClient; + } else { + ctx.client = createClient(NULL); + ctx.client->flags |= CLIENT_MODULE; + ctx.client->user = NULL; /* Root user. */ + } + + void *moduledata = NULL; + RedisModuleClientInfoV1 civ1; + RedisModuleReplicationInfoV1 riv1; + RedisModuleModuleChangeV1 mcv1; + /* Start at DB zero by default when calling the handler. It's + * up to the specific event setup to change it when it makes + * sense. For instance for FLUSHDB events we select the correct + * DB automatically. */ + selectDb(ctx.client, 0); + + /* Event specific context and data pointer setup. */ + if (eid == REDISMODULE_EVENT_CLIENT_CHANGE) { + modulePopulateClientInfoStructure(&civ1,data, + el->event.dataver); + moduledata = &civ1; + } else if (eid == REDISMODULE_EVENT_REPLICATION_ROLE_CHANGED) { + modulePopulateReplicationInfoStructure(&riv1,el->event.dataver); + moduledata = &riv1; + } else if (eid == REDISMODULE_EVENT_FLUSHDB) { + moduledata = data; + RedisModuleFlushInfoV1 *fi = data; + if (fi->dbnum != -1) + selectDb(ctx.client, fi->dbnum); + } else if (eid == REDISMODULE_EVENT_MODULE_CHANGE) { + RedisModule *m = data; + if (m == el->module) + continue; + mcv1.version = REDISMODULE_MODULE_CHANGE_VERSION; + mcv1.module_name = m->name; + mcv1.module_version = m->ver; + moduledata = &mcv1; + } else if (eid == REDISMODULE_EVENT_LOADING_PROGRESS) { + moduledata = data; + } else if (eid == REDISMODULE_EVENT_CRON_LOOP) { + moduledata = data; + } + + ModulesInHooks++; + el->module->in_hook++; + el->callback(&ctx,el->event,subid,moduledata); + el->module->in_hook--; + ModulesInHooks--; + + if (ModulesInHooks != 0) freeClient(ctx.client); + moduleFreeContext(&ctx); + } + } +} + +/* Remove all the listeners for this module: this is used before unloading + * a module. */ +void moduleUnsubscribeAllServerEvents(RedisModule *module) { + RedisModuleEventListener *el; + listIter li; + listNode *ln; + listRewind(RedisModule_EventListeners,&li); + + while((ln = listNext(&li))) { + el = ln->value; + if (el->module == module) { + listDelNode(RedisModule_EventListeners,ln); + zfree(el); + } + } +} + +void processModuleLoadingProgressEvent(int is_aof) { + long long now = ustime(); + static long long next_event = 0; + if (now >= next_event) { + /* Fire the loading progress modules end event. */ + int progress = -1; + if (server.loading_total_bytes) + progress = (server.loading_total_bytes<<10) / server.loading_total_bytes; + RedisModuleFlushInfoV1 fi = {REDISMODULE_LOADING_PROGRESS_VERSION, + server.hz, + progress}; + moduleFireServerEvent(REDISMODULE_EVENT_LOADING_PROGRESS, + is_aof? + REDISMODULE_SUBEVENT_LOADING_PROGRESS_AOF: + REDISMODULE_SUBEVENT_LOADING_PROGRESS_RDB, + &fi); + /* decide when the next event should fire. */ + next_event = now + 1000000 / server.hz; + } +} + /* -------------------------------------------------------------------------- * Modules API internals * -------------------------------------------------------------------------- */ @@ -4659,10 +7171,13 @@ void moduleInitModulesSystem(void) { /* Set up the keyspace notification susbscriber list and static client */ moduleKeyspaceSubscribers = listCreate(); - moduleFreeContextReusedClient = createClient(-1); + moduleFreeContextReusedClient = createClient(NULL); moduleFreeContextReusedClient->flags |= CLIENT_MODULE; moduleFreeContextReusedClient->user = NULL; /* root user. */ + /* Set up filter list */ + moduleCommandFilters = listCreate(); + moduleRegisterCoreAPI(); if (pipe(server.module_blocked_pipe) == -1) { serverLog(LL_WARNING, @@ -4678,6 +7193,9 @@ void moduleInitModulesSystem(void) { /* Create the timers radix tree. */ Timers = raxNew(); + /* Setup the event listeners data structures. */ + RedisModule_EventListeners = listCreate(); + /* Our thread-safe contexts GIL must start with already locked: * it is just unlocked when it's safe. */ pthread_mutex_lock(&moduleGIL); @@ -4712,6 +7230,9 @@ void moduleLoadFromQueue(void) { void moduleFreeModuleStructure(struct RedisModule *module) { listRelease(module->types); + listRelease(module->filters); + listRelease(module->usedby); + listRelease(module->using); sdsfree(module->name); zfree(module); } @@ -4744,6 +7265,17 @@ int moduleLoad(const char *path, void **module_argv, int module_argc) { int (*onload)(void *, void **, int); void *handle; RedisModuleCtx ctx = REDISMODULE_CTX_INIT; + ctx.client = moduleFreeContextReusedClient; + selectDb(ctx.client, 0); + + struct stat st; + if (stat(path, &st) == 0) + { // this check is best effort + if (!(st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))) { + serverLog(LL_WARNING, "Module %s failed to load: It does not have execute permissions.", path); + return C_ERR; + } + } handle = dlopen(path,RTLD_NOW|RTLD_LOCAL); if (handle == NULL) { @@ -4761,6 +7293,8 @@ int moduleLoad(const char *path, void **module_argv, int module_argc) { if (onload((void*)&ctx,module_argv,module_argc) == REDISMODULE_ERR) { if (ctx.module) { moduleUnregisterCommands(ctx.module); + moduleUnregisterSharedAPI(ctx.module); + moduleUnregisterUsedAPI(ctx.module); moduleFreeModuleStructure(ctx.module); } dlclose(handle); @@ -4771,8 +7305,14 @@ int moduleLoad(const char *path, void **module_argv, int module_argc) { /* Redis module loaded! Register it. */ dictAdd(modules,ctx.module->name,ctx.module); + ctx.module->blocked_clients = 0; ctx.module->handle = handle; serverLog(LL_NOTICE,"Module '%s' loaded from %s",ctx.module->name,path); + /* Fire the loaded modules event. */ + moduleFireServerEvent(REDISMODULE_EVENT_MODULE_CHANGE, + REDISMODULE_SUBEVENT_MODULE_LOADED, + ctx.module); + moduleFreeContext(&ctx); return C_OK; } @@ -4790,19 +7330,43 @@ int moduleUnload(sds name) { if (module == NULL) { errno = ENOENT; return REDISMODULE_ERR; - } - - if (listLength(module->types)) { + } else if (listLength(module->types)) { errno = EBUSY; return REDISMODULE_ERR; + } else if (listLength(module->usedby)) { + errno = EPERM; + return REDISMODULE_ERR; + } else if (module->blocked_clients) { + errno = EAGAIN; + return REDISMODULE_ERR; } + /* Give module a chance to clean up. */ + int (*onunload)(void *); + onunload = (int (*)(void *))(unsigned long) dlsym(module->handle, "RedisModule_OnUnload"); + if (onunload) { + RedisModuleCtx ctx = REDISMODULE_CTX_INIT; + ctx.module = module; + ctx.client = moduleFreeContextReusedClient; + int unload_status = onunload((void*)&ctx); + moduleFreeContext(&ctx); + + if (unload_status == REDISMODULE_ERR) { + serverLog(LL_WARNING, "Module %s OnUnload failed. Unload canceled.", name); + errno = ECANCELED; + return REDISMODULE_ERR; + } + } + + moduleFreeAuthenticatedClients(module); moduleUnregisterCommands(module); + moduleUnregisterSharedAPI(module); + moduleUnregisterUsedAPI(module); + moduleUnregisterFilters(module); /* Remove any notification subscribers this module might have */ moduleUnsubscribeNotifications(module); - - /* Unregister all the hooks. TODO: Yet no hooks support here. */ + moduleUnsubscribeAllServerEvents(module); /* Unload the dynamic library. */ if (dlclose(module->handle) == -1) { @@ -4812,6 +7376,11 @@ int moduleUnload(sds name) { module->name, error); } + /* Fire the unloaded modules event. */ + moduleFireServerEvent(REDISMODULE_EVENT_MODULE_CHANGE, + REDISMODULE_SUBEVENT_MODULE_UNLOADED, + module); + /* Remove from list of modules. */ serverLog(LL_NOTICE,"Module %s unloaded",module->name); dictDelete(modules,module->name); @@ -4840,6 +7409,62 @@ void addReplyLoadedModules(client *c) { dictReleaseIterator(di); } +/* Helper for genModulesInfoString(): given a list of modules, return + * am SDS string in the form "[modulename|modulename2|...]" */ +sds genModulesInfoStringRenderModulesList(list *l) { + listIter li; + listNode *ln; + listRewind(l,&li); + sds output = sdsnew("["); + while((ln = listNext(&li))) { + RedisModule *module = ln->value; + output = sdscat(output,module->name); + } + output = sdstrim(output,"|"); + output = sdscat(output,"]"); + return output; +} + +/* Helper for genModulesInfoString(): render module options as an SDS string. */ +sds genModulesInfoStringRenderModuleOptions(struct RedisModule *module) { + sds output = sdsnew("["); + if (module->options & REDISMODULE_OPTIONS_HANDLE_IO_ERRORS) + output = sdscat(output,"handle-io-errors|"); + output = sdstrim(output,"|"); + output = sdscat(output,"]"); + return output; +} + + +/* Helper function for the INFO command: adds loaded modules as to info's + * output. + * + * After the call, the passed sds info string is no longer valid and all the + * references must be substituted with the new pointer returned by the call. */ +sds genModulesInfoString(sds info) { + dictIterator *di = dictGetIterator(modules); + dictEntry *de; + + while ((de = dictNext(di)) != NULL) { + sds name = dictGetKey(de); + struct RedisModule *module = dictGetVal(de); + + sds usedby = genModulesInfoStringRenderModulesList(module->usedby); + sds using = genModulesInfoStringRenderModulesList(module->using); + sds options = genModulesInfoStringRenderModuleOptions(module); + info = sdscatfmt(info, + "module:name=%S,ver=%i,api=%i,filters=%i," + "usedby=%S,using=%S,options=%S\r\n", + name, module->ver, module->apiver, + (int)listLength(module->filters), usedby, using, options); + sdsfree(usedby); + sdsfree(using); + sdsfree(options); + } + dictReleaseIterator(di); + return info; +} + /* Redis MODULE command. * * MODULE LOAD [args...] */ @@ -4878,7 +7503,16 @@ NULL errmsg = "no such module with that name"; break; case EBUSY: - errmsg = "the module exports one or more module-side data types, can't unload"; + errmsg = "the module exports one or more module-side data " + "types, can't unload"; + break; + case EPERM: + errmsg = "the module exports APIs used by other modules. " + "Please unload them first and try again"; + break; + case EAGAIN: + errmsg = "the module has blocked clients. " + "Please wait them unblocked and try again"; break; default: errmsg = "operation not possible."; @@ -4899,10 +7533,95 @@ size_t moduleCount(void) { return dictSize(modules); } +/* Set the key last access time for LRU based eviction. not relevent if the + * servers's maxmemory policy is LFU based. Value is idle time in milliseconds. + * returns REDISMODULE_OK if the LRU was updated, REDISMODULE_ERR otherwise. */ +int RM_SetLRU(RedisModuleKey *key, mstime_t lru_idle) { + if (!key->value) + return REDISMODULE_ERR; + if (objectSetLRUOrLFU(key->value, -1, lru_idle, lru_idle>=0 ? LRU_CLOCK() : 0, 1)) + return REDISMODULE_OK; + return REDISMODULE_ERR; +} + +/* Gets the key last access time. + * Value is idletime in milliseconds or -1 if the server's eviction policy is + * LFU based. + * returns REDISMODULE_OK if when key is valid. */ +int RM_GetLRU(RedisModuleKey *key, mstime_t *lru_idle) { + *lru_idle = -1; + if (!key->value) + return REDISMODULE_ERR; + if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) + return REDISMODULE_OK; + *lru_idle = estimateObjectIdleTime(key->value); + return REDISMODULE_OK; +} + +/* Set the key access frequency. only relevant if the server's maxmemory policy + * is LFU based. + * The frequency is a logarithmic counter that provides an indication of + * the access frequencyonly (must be <= 255). + * returns REDISMODULE_OK if the LFU was updated, REDISMODULE_ERR otherwise. */ +int RM_SetLFU(RedisModuleKey *key, long long lfu_freq) { + if (!key->value) + return REDISMODULE_ERR; + if (objectSetLRUOrLFU(key->value, lfu_freq, -1, 0, 1)) + return REDISMODULE_OK; + return REDISMODULE_ERR; +} + +/* Gets the key access frequency or -1 if the server's eviction policy is not + * LFU based. + * returns REDISMODULE_OK if when key is valid. */ +int RM_GetLFU(RedisModuleKey *key, long long *lfu_freq) { + *lfu_freq = -1; + if (!key->value) + return REDISMODULE_ERR; + if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) + *lfu_freq = LFUDecrAndReturn(key->value); + return REDISMODULE_OK; +} + +/* Replace the value assigned to a module type. + * + * The key must be open for writing, have an existing value, and have a moduleType + * that matches the one specified by the caller. + * + * Unlike RM_ModuleTypeSetValue() which will free the old value, this function + * simply swaps the old value with the new value. + * + * The function returns REDISMODULE_OK on success, REDISMODULE_ERR on errors + * such as: + * + * 1. Key is not opened for writing. + * 2. Key is not a module data type key. + * 3. Key is a module datatype other than 'mt'. + * + * If old_value is non-NULL, the old value is returned by reference. + */ +int RM_ModuleTypeReplaceValue(RedisModuleKey *key, moduleType *mt, void *new_value, void **old_value) { + if (!(key->mode & REDISMODULE_WRITE) || key->iter) + return REDISMODULE_ERR; + if (!key->value || key->value->type != OBJ_MODULE) + return REDISMODULE_ERR; + + moduleValue *mv = key->value->ptr; + if (mv->type != mt) + return REDISMODULE_ERR; + + if (old_value) + *old_value = mv->value; + mv->value = new_value; + + return REDISMODULE_OK; +} + /* Register all the APIs we export. Keep this function at the end of the * file so that's easy to seek it to add new entries. */ void moduleRegisterCoreAPI(void) { server.moduleapi = dictCreate(&moduleAPIDictType,NULL); + server.sharedapi = dictCreate(&moduleAPIDictType,NULL); REGISTER_API(Alloc); REGISTER_API(Calloc); REGISTER_API(Realloc); @@ -4916,12 +7635,18 @@ void moduleRegisterCoreAPI(void) { REGISTER_API(ReplyWithError); REGISTER_API(ReplyWithSimpleString); REGISTER_API(ReplyWithArray); + REGISTER_API(ReplyWithNullArray); + REGISTER_API(ReplyWithEmptyArray); REGISTER_API(ReplySetArrayLength); REGISTER_API(ReplyWithString); + REGISTER_API(ReplyWithEmptyString); + REGISTER_API(ReplyWithVerbatimString); REGISTER_API(ReplyWithStringBuffer); + REGISTER_API(ReplyWithCString); REGISTER_API(ReplyWithNull); REGISTER_API(ReplyWithCallReply); REGISTER_API(ReplyWithDouble); + REGISTER_API(ReplyWithLongDouble); REGISTER_API(GetSelectedDb); REGISTER_API(SelectDb); REGISTER_API(OpenKey); @@ -4932,6 +7657,7 @@ void moduleRegisterCoreAPI(void) { REGISTER_API(ListPop); REGISTER_API(StringToLongLong); REGISTER_API(StringToDouble); + REGISTER_API(StringToLongDouble); REGISTER_API(Call); REGISTER_API(CallReplyProto); REGISTER_API(FreeCallReply); @@ -4943,6 +7669,7 @@ void moduleRegisterCoreAPI(void) { REGISTER_API(CreateStringFromCallReply); REGISTER_API(CreateString); REGISTER_API(CreateStringFromLongLong); + REGISTER_API(CreateStringFromLongDouble); REGISTER_API(CreateStringFromString); REGISTER_API(CreateStringPrintf); REGISTER_API(FreeString); @@ -4957,6 +7684,9 @@ void moduleRegisterCoreAPI(void) { REGISTER_API(StringTruncate); REGISTER_API(SetExpire); REGISTER_API(GetExpire); + REGISTER_API(ResetDataset); + REGISTER_API(DbSize); + REGISTER_API(RandomKey); REGISTER_API(ZsetAdd); REGISTER_API(ZsetIncrby); REGISTER_API(ZsetScore); @@ -4976,11 +7706,16 @@ void moduleRegisterCoreAPI(void) { REGISTER_API(KeyAtPos); REGISTER_API(GetClientId); REGISTER_API(GetContextFlags); + REGISTER_API(AvoidReplicaTraffic); REGISTER_API(PoolAlloc); REGISTER_API(CreateDataType); REGISTER_API(ModuleTypeSetValue); + REGISTER_API(ModuleTypeReplaceValue); REGISTER_API(ModuleTypeGetType); REGISTER_API(ModuleTypeGetValue); + REGISTER_API(IsIOError); + REGISTER_API(SetModuleOptions); + REGISTER_API(SignalModifiedKey); REGISTER_API(SaveUnsigned); REGISTER_API(LoadUnsigned); REGISTER_API(SaveSigned); @@ -4993,13 +7728,21 @@ void moduleRegisterCoreAPI(void) { REGISTER_API(LoadDouble); REGISTER_API(SaveFloat); REGISTER_API(LoadFloat); + REGISTER_API(SaveLongDouble); + REGISTER_API(LoadLongDouble); + REGISTER_API(SaveDataTypeToString); + REGISTER_API(LoadDataTypeFromString); REGISTER_API(EmitAOF); REGISTER_API(Log); REGISTER_API(LogIOError); + REGISTER_API(_Assert); + REGISTER_API(LatencyAddSample); REGISTER_API(StringAppendBuffer); REGISTER_API(RetainString); REGISTER_API(StringCompare); REGISTER_API(GetContextFromIO); + REGISTER_API(GetKeyNameFromIO); + REGISTER_API(GetKeyNameFromModuleKey); REGISTER_API(BlockClient); REGISTER_API(UnblockClient); REGISTER_API(IsBlockedReplyRequest); @@ -5014,6 +7757,8 @@ void moduleRegisterCoreAPI(void) { REGISTER_API(DigestAddStringBuffer); REGISTER_API(DigestAddLongLong); REGISTER_API(DigestEndSequence); + REGISTER_API(NotifyKeyspaceEvent); + REGISTER_API(GetNotifyKeyspaceEvents); REGISTER_API(SubscribeToKeyspaceEvents); REGISTER_API(RegisterClusterMessageReceiver); REGISTER_API(SendClusterMessage); @@ -5053,4 +7798,55 @@ void moduleRegisterCoreAPI(void) { REGISTER_API(DictPrev); REGISTER_API(DictCompareC); REGISTER_API(DictCompare); + REGISTER_API(ExportSharedAPI); + REGISTER_API(GetSharedAPI); + REGISTER_API(RegisterCommandFilter); + REGISTER_API(UnregisterCommandFilter); + REGISTER_API(CommandFilterArgsCount); + REGISTER_API(CommandFilterArgGet); + REGISTER_API(CommandFilterArgInsert); + REGISTER_API(CommandFilterArgReplace); + REGISTER_API(CommandFilterArgDelete); + REGISTER_API(Fork); + REGISTER_API(ExitFromChild); + REGISTER_API(KillForkChild); + REGISTER_API(RegisterInfoFunc); + REGISTER_API(InfoAddSection); + REGISTER_API(InfoBeginDictField); + REGISTER_API(InfoEndDictField); + REGISTER_API(InfoAddFieldString); + REGISTER_API(InfoAddFieldCString); + REGISTER_API(InfoAddFieldDouble); + REGISTER_API(InfoAddFieldLongLong); + REGISTER_API(InfoAddFieldULongLong); + REGISTER_API(GetServerInfo); + REGISTER_API(FreeServerInfo); + REGISTER_API(ServerInfoGetField); + REGISTER_API(ServerInfoGetFieldC); + REGISTER_API(ServerInfoGetFieldSigned); + REGISTER_API(ServerInfoGetFieldUnsigned); + REGISTER_API(ServerInfoGetFieldDouble); + REGISTER_API(GetClientInfoById); + REGISTER_API(PublishMessage); + REGISTER_API(SubscribeToServerEvent); + REGISTER_API(SetLRU); + REGISTER_API(GetLRU); + REGISTER_API(SetLFU); + REGISTER_API(GetLFU); + REGISTER_API(BlockClientOnKeys); + REGISTER_API(SignalKeyAsReady); + REGISTER_API(GetBlockedClientReadyKey); + REGISTER_API(GetUsedMemoryRatio); + REGISTER_API(MallocSize); + REGISTER_API(ScanCursorCreate); + REGISTER_API(ScanCursorDestroy); + REGISTER_API(ScanCursorRestart); + REGISTER_API(Scan); + REGISTER_API(ScanKey); + REGISTER_API(CreateModuleUser); + REGISTER_API(SetModuleUserACL); + REGISTER_API(FreeModuleUser); + REGISTER_API(DeauthenticateAndCloseClient); + REGISTER_API(AuthenticateClientWithACLUser); + REGISTER_API(AuthenticateClientWithUser); } diff --git a/src/modules/Makefile b/src/modules/Makefile index 51ffac17d..5e012d6f1 100644 --- a/src/modules/Makefile +++ b/src/modules/Makefile @@ -13,7 +13,7 @@ endif .SUFFIXES: .c .so .xo .o -all: helloworld.so hellotype.so helloblock.so testmodule.so hellocluster.so hellotimer.so hellodict.so +all: helloworld.so hellotype.so helloblock.so testmodule.so hellocluster.so hellotimer.so hellodict.so hellohook.so helloacl.so .c.xo: $(CC) -I. $(CFLAGS) $(SHOBJ_CFLAGS) -fPIC -c $< -o $@ @@ -48,6 +48,16 @@ hellodict.xo: ../redismodule.h hellodict.so: hellodict.xo $(LD) -o $@ $< $(SHOBJ_LDFLAGS) $(LIBS) -lc +hellohook.xo: ../redismodule.h + +hellohook.so: hellohook.xo + $(LD) -o $@ $< $(SHOBJ_LDFLAGS) $(LIBS) -lc + +helloacl.xo: ../redismodule.h + +helloacl.so: helloacl.xo + $(LD) -o $@ $< $(SHOBJ_LDFLAGS) $(LIBS) -lc + testmodule.xo: ../redismodule.h testmodule.so: testmodule.xo diff --git a/src/modules/helloacl.c b/src/modules/helloacl.c new file mode 100644 index 000000000..6766c0a58 --- /dev/null +++ b/src/modules/helloacl.c @@ -0,0 +1,191 @@ +/* ACL API example - An example for performing custom synchronous and + * asynchronous password authentication. + * + * ----------------------------------------------------------------------------- + * + * Copyright 2019 Amazon.com, Inc. or its affiliates. + * 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 +#include + +// A simple global user +static RedisModuleUser *global; +static uint64_t global_auth_client_id = 0; + +/* HELLOACL.REVOKE + * Synchronously revoke access from a user. */ +int RevokeCommand_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + REDISMODULE_NOT_USED(argv); + REDISMODULE_NOT_USED(argc); + + if (global_auth_client_id) { + RedisModule_DeauthenticateAndCloseClient(ctx, global_auth_client_id); + return RedisModule_ReplyWithSimpleString(ctx, "OK"); + } else { + return RedisModule_ReplyWithError(ctx, "Global user currently not used"); + } +} + +/* HELLOACL.RESET + * Synchronously delete and re-create a module user. */ +int ResetCommand_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + REDISMODULE_NOT_USED(argv); + REDISMODULE_NOT_USED(argc); + + RedisModule_FreeModuleUser(global); + global = RedisModule_CreateModuleUser("global"); + RedisModule_SetModuleUserACL(global, "allcommands"); + RedisModule_SetModuleUserACL(global, "allkeys"); + RedisModule_SetModuleUserACL(global, "on"); + + return RedisModule_ReplyWithSimpleString(ctx, "OK"); +} + +/* Callback handler for user changes, use this to notify a module of + * changes to users authenticated by the module */ +void HelloACL_UserChanged(uint64_t client_id, void *privdata) { + REDISMODULE_NOT_USED(privdata); + REDISMODULE_NOT_USED(client_id); + global_auth_client_id = 0; +} + +/* HELLOACL.AUTHGLOBAL + * Synchronously assigns a module user to the current context. */ +int AuthGlobalCommand_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + REDISMODULE_NOT_USED(argv); + REDISMODULE_NOT_USED(argc); + + if (global_auth_client_id) { + return RedisModule_ReplyWithError(ctx, "Global user currently used"); + } + + RedisModule_AuthenticateClientWithUser(ctx, global, HelloACL_UserChanged, NULL, &global_auth_client_id); + + return RedisModule_ReplyWithSimpleString(ctx, "OK"); +} + +#define TIMEOUT_TIME 1000 + +/* Reply callback for auth command HELLOACL.AUTHASYNC */ +int HelloACL_Reply(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + REDISMODULE_NOT_USED(argv); + REDISMODULE_NOT_USED(argc); + size_t length; + + RedisModuleString *user_string = RedisModule_GetBlockedClientPrivateData(ctx); + const char *name = RedisModule_StringPtrLen(user_string, &length); + + if (RedisModule_AuthenticateClientWithACLUser(ctx, name, length, NULL, NULL, NULL) == + REDISMODULE_ERR) { + return RedisModule_ReplyWithError(ctx, "Invalid Username or password"); + } + return RedisModule_ReplyWithSimpleString(ctx, "OK"); +} + +/* Timeout callback for auth command HELLOACL.AUTHASYNC */ +int HelloACL_Timeout(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + REDISMODULE_NOT_USED(argv); + REDISMODULE_NOT_USED(argc); + return RedisModule_ReplyWithSimpleString(ctx, "Request timedout"); +} + +/* Private data frees data for HELLOACL.AUTHASYNC command. */ +void HelloACL_FreeData(RedisModuleCtx *ctx, void *privdata) { + REDISMODULE_NOT_USED(ctx); + RedisModule_FreeString(NULL, privdata); +} + +/* Background authentication can happen here. */ +void *HelloACL_ThreadMain(void *args) { + void **targs = args; + RedisModuleBlockedClient *bc = targs[0]; + RedisModuleString *user = targs[1]; + RedisModule_Free(targs); + + RedisModule_UnblockClient(bc,user); + return NULL; +} + +/* HELLOACL.AUTHASYNC + * Asynchronously assigns an ACL user to the current context. */ +int AuthAsyncCommand_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + if (argc != 2) return RedisModule_WrongArity(ctx); + + pthread_t tid; + RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx, HelloACL_Reply, HelloACL_Timeout, HelloACL_FreeData, TIMEOUT_TIME); + + + void **targs = RedisModule_Alloc(sizeof(void*)*2); + targs[0] = bc; + targs[1] = RedisModule_CreateStringFromString(NULL, argv[1]); + + if (pthread_create(&tid, NULL, HelloACL_ThreadMain, targs) != 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,"helloacl",1,REDISMODULE_APIVER_1) + == REDISMODULE_ERR) return REDISMODULE_ERR; + + if (RedisModule_CreateCommand(ctx,"helloacl.reset", + ResetCommand_RedisCommand,"",0,0,0) == REDISMODULE_ERR) + return REDISMODULE_ERR; + + if (RedisModule_CreateCommand(ctx,"helloacl.revoke", + RevokeCommand_RedisCommand,"",0,0,0) == REDISMODULE_ERR) + return REDISMODULE_ERR; + + if (RedisModule_CreateCommand(ctx,"helloacl.authglobal", + AuthGlobalCommand_RedisCommand,"no-auth",0,0,0) == REDISMODULE_ERR) + return REDISMODULE_ERR; + + if (RedisModule_CreateCommand(ctx,"helloacl.authasync", + AuthAsyncCommand_RedisCommand,"no-auth",0,0,0) == REDISMODULE_ERR) + return REDISMODULE_ERR; + + global = RedisModule_CreateModuleUser("global"); + RedisModule_SetModuleUserACL(global, "allcommands"); + RedisModule_SetModuleUserACL(global, "allkeys"); + RedisModule_SetModuleUserACL(global, "on"); + + global_auth_client_id = 0; + + return REDISMODULE_OK; +} diff --git a/src/modules/hellohook.c b/src/modules/hellohook.c new file mode 100644 index 000000000..7ab78ed07 --- /dev/null +++ b/src/modules/hellohook.c @@ -0,0 +1,93 @@ +/* Server hooks API example + * + * ----------------------------------------------------------------------------- + * + * Copyright (c) 2019, Salvatore Sanfilippo + * 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 +#include +#include +#include + +/* Client state change callback. */ +void clientChangeCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data) +{ + REDISMODULE_NOT_USED(ctx); + REDISMODULE_NOT_USED(e); + + RedisModuleClientInfo *ci = data; + printf("Client %s event for client #%llu %s:%d\n", + (sub == REDISMODULE_SUBEVENT_CLIENT_CHANGE_CONNECTED) ? + "connection" : "disconnection", + ci->id,ci->addr,ci->port); +} + +void flushdbCallback(RedisModuleCtx *ctx, RedisModuleEvent e, uint64_t sub, void *data) +{ + REDISMODULE_NOT_USED(ctx); + REDISMODULE_NOT_USED(e); + + RedisModuleFlushInfo *fi = data; + if (sub == REDISMODULE_SUBEVENT_FLUSHDB_START) { + if (fi->dbnum != -1) { + RedisModuleCallReply *reply; + reply = RedisModule_Call(ctx,"DBSIZE",""); + long long numkeys = RedisModule_CallReplyInteger(reply); + printf("FLUSHDB event of database %d started (%lld keys in DB)\n", + fi->dbnum, numkeys); + RedisModule_FreeCallReply(reply); + } else { + printf("FLUSHALL event started\n"); + } + } else { + if (fi->dbnum != -1) { + printf("FLUSHDB event of database %d ended\n",fi->dbnum); + } else { + printf("FLUSHALL event ended\n"); + } + } +} + +/* 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,"hellohook",1,REDISMODULE_APIVER_1) + == REDISMODULE_ERR) return REDISMODULE_ERR; + + RedisModule_SubscribeToServerEvent(ctx, + RedisModuleEvent_ClientChange, clientChangeCallback); + RedisModule_SubscribeToServerEvent(ctx, + RedisModuleEvent_FlushDB, flushdbCallback); + return REDISMODULE_OK; +} diff --git a/src/modules/hellotype.c b/src/modules/hellotype.c index ba634c4a1..4f2d1d730 100644 --- a/src/modules/hellotype.c +++ b/src/modules/hellotype.c @@ -129,6 +129,7 @@ int HelloTypeInsert_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, /* Insert the new element. */ HelloTypeInsert(hto,value); + RedisModule_SignalKeyAsReady(ctx,argv[1]); RedisModule_ReplyWithLongLong(ctx,hto->len); RedisModule_ReplicateVerbatim(ctx); @@ -190,6 +191,77 @@ int HelloTypeLen_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int return REDISMODULE_OK; } +/* ====================== Example of a blocking command ==================== */ + +/* Reply callback for blocking command HELLOTYPE.BRANGE, this will get + * called when the key we blocked for is ready: we need to check if we + * can really serve the client, and reply OK or ERR accordingly. */ +int HelloBlock_Reply(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + REDISMODULE_NOT_USED(argv); + REDISMODULE_NOT_USED(argc); + + RedisModuleString *keyname = RedisModule_GetBlockedClientReadyKey(ctx); + RedisModuleKey *key = RedisModule_OpenKey(ctx,keyname,REDISMODULE_READ); + int type = RedisModule_KeyType(key); + if (type != REDISMODULE_KEYTYPE_MODULE || + RedisModule_ModuleTypeGetType(key) != HelloType) + { + RedisModule_CloseKey(key); + return REDISMODULE_ERR; + } + + /* In case the key is able to serve our blocked client, let's directly + * use our original command implementation to make this example simpler. */ + RedisModule_CloseKey(key); + return HelloTypeRange_RedisCommand(ctx,argv,argc-1); +} + +/* Timeout callback for blocking command HELLOTYPE.BRANGE */ +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 HELLOTYPE.BRANGE command. */ +void HelloBlock_FreeData(RedisModuleCtx *ctx, void *privdata) { + REDISMODULE_NOT_USED(ctx); + RedisModule_Free(privdata); +} + +/* HELLOTYPE.BRANGE key first count timeout -- This is a blocking verison of + * the RANGE operation, in order to show how to use the API + * RedisModule_BlockClientOnKeys(). */ +int HelloTypeBRange_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + if (argc != 5) return RedisModule_WrongArity(ctx); + RedisModule_AutoMemory(ctx); /* Use automatic memory management. */ + RedisModuleKey *key = RedisModule_OpenKey(ctx,argv[1], + REDISMODULE_READ|REDISMODULE_WRITE); + int type = RedisModule_KeyType(key); + if (type != REDISMODULE_KEYTYPE_EMPTY && + RedisModule_ModuleTypeGetType(key) != HelloType) + { + return RedisModule_ReplyWithError(ctx,REDISMODULE_ERRORMSG_WRONGTYPE); + } + + /* Parse the timeout before even trying to serve the client synchronously, + * so that we always fail ASAP on syntax errors. */ + long long timeout; + if (RedisModule_StringToLongLong(argv[4],&timeout) != REDISMODULE_OK) { + return RedisModule_ReplyWithError(ctx, + "ERR invalid timeout parameter"); + } + + /* Can we serve the reply synchronously? */ + if (type != REDISMODULE_KEYTYPE_EMPTY) { + return HelloTypeRange_RedisCommand(ctx,argv,argc-1); + } + + /* Otherwise let's block on the key. */ + void *privdata = RedisModule_Alloc(100); + RedisModule_BlockClientOnKeys(ctx,HelloBlock_Reply,HelloBlock_Timeout,HelloBlock_FreeData,timeout,argv+1,1,privdata); + return REDISMODULE_OK; +} /* ========================== "hellotype" type methods ======================= */ @@ -282,5 +354,9 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) HelloTypeLen_RedisCommand,"readonly",1,1,1) == REDISMODULE_ERR) return REDISMODULE_ERR; + if (RedisModule_CreateCommand(ctx,"hellotype.brange", + HelloTypeBRange_RedisCommand,"readonly",1,1,1) == REDISMODULE_ERR) + return REDISMODULE_ERR; + return REDISMODULE_OK; } diff --git a/src/modules/testmodule.c b/src/modules/testmodule.c index 67a861704..5381380e5 100644 --- a/src/modules/testmodule.c +++ b/src/modules/testmodule.c @@ -109,9 +109,9 @@ int TestStringPrintf(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { if (argc < 3) { return RedisModule_WrongArity(ctx); } - RedisModuleString *s = RedisModule_CreateStringPrintf(ctx, - "Got %d args. argv[1]: %s, argv[2]: %s", - argc, + RedisModuleString *s = RedisModule_CreateStringPrintf(ctx, + "Got %d args. argv[1]: %s, argv[2]: %s", + argc, RedisModule_StringPtrLen(argv[1], NULL), RedisModule_StringPtrLen(argv[2], NULL) ); @@ -133,7 +133,7 @@ int TestUnlink(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { RedisModuleKey *k = RedisModule_OpenKey(ctx, RedisModule_CreateStringPrintf(ctx, "unlinked"), REDISMODULE_WRITE | REDISMODULE_READ); if (!k) return failTest(ctx, "Could not create key"); - + if (REDISMODULE_ERR == RedisModule_StringSet(k, RedisModule_CreateStringPrintf(ctx, "Foobar"))) { return failTest(ctx, "Could not set string value"); } @@ -152,7 +152,7 @@ int TestUnlink(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { return failTest(ctx, "Could not verify key to be unlinked"); } return RedisModule_ReplyWithSimpleString(ctx, "OK"); - + } int NotifyCallback(RedisModuleCtx *ctx, int type, const char *event, @@ -188,6 +188,10 @@ int TestNotifications(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { RedisModule_Call(ctx, "LPUSH", "cc", "l", "y"); RedisModule_Call(ctx, "LPUSH", "cc", "l", "y"); + /* Miss some keys intentionally so we will get a "keymiss" notification. */ + RedisModule_Call(ctx, "GET", "c", "nosuchkey"); + RedisModule_Call(ctx, "SMEMBERS", "c", "nosuchkey"); + size_t sz; const char *rep; RedisModuleCallReply *r = RedisModule_Call(ctx, "HGET", "cc", "notifications", "foo"); @@ -225,6 +229,16 @@ int TestNotifications(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { FAIL("Wrong reply for l"); } + r = RedisModule_Call(ctx, "HGET", "cc", "notifications", "nosuchkey"); + if (r == NULL || RedisModule_CallReplyType(r) != REDISMODULE_REPLY_STRING) { + FAIL("Wrong or no reply for nosuchkey"); + } else { + rep = RedisModule_CallReplyStringPtr(r, &sz); + if (sz != 1 || *rep != '2') { + FAIL("Got reply '%.*s'. expected '2'", sz, rep); + } + } + RedisModule_Call(ctx, "FLUSHDB", ""); return RedisModule_ReplyWithSimpleString(ctx, "OK"); @@ -423,7 +437,7 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) if (RedisModule_CreateCommand(ctx,"test.ctxflags", TestCtxFlags,"readonly",1,1,1) == REDISMODULE_ERR) return REDISMODULE_ERR; - + if (RedisModule_CreateCommand(ctx,"test.unlink", TestUnlink,"write deny-oom",1,1,1) == REDISMODULE_ERR) return REDISMODULE_ERR; @@ -435,7 +449,8 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) RedisModule_SubscribeToKeyspaceEvents(ctx, REDISMODULE_NOTIFY_HASH | REDISMODULE_NOTIFY_SET | - REDISMODULE_NOTIFY_STRING, + REDISMODULE_NOTIFY_STRING | + REDISMODULE_NOTIFY_KEY_MISS, NotifyCallback); if (RedisModule_CreateCommand(ctx,"test.notify", TestNotifications,"write deny-oom",1,1,1) == REDISMODULE_ERR) diff --git a/src/multi.c b/src/multi.c index 71090d8ed..cbbd2c513 100644 --- a/src/multi.c +++ b/src/multi.c @@ -106,11 +106,13 @@ void discardCommand(client *c) { /* Send a MULTI command to all the slaves and AOF file. Check the execCommand * implementation for more information. */ void execCommandPropagateMulti(client *c) { - robj *multistring = createStringObject("MULTI",5); - - propagate(server.multiCommand,c->db->id,&multistring,1, + propagate(server.multiCommand,c->db->id,&shared.multi,1, + PROPAGATE_AOF|PROPAGATE_REPL); +} + +void execCommandPropagateExec(client *c) { + propagate(server.execCommand,c->db->id,&shared.exec,1, PROPAGATE_AOF|PROPAGATE_REPL); - decrRefCount(multistring); } void execCommand(client *c) { @@ -175,7 +177,21 @@ void execCommand(client *c) { must_propagate = 1; } - call(c,server.loading ? CMD_CALL_NONE : CMD_CALL_FULL); + int acl_keypos; + int acl_retval = ACLCheckCommandPerm(c,&acl_keypos); + if (acl_retval != ACL_OK) { + addACLLogEntry(c,acl_retval,acl_keypos,NULL); + addReplyErrorFormat(c, + "-NOPERM ACLs rules changed between the moment the " + "transaction was accumulated and the EXEC call. " + "This command is no longer allowed for the " + "following reason: %s", + (acl_retval == ACL_DENIED_CMD) ? + "no permission to execute the command or subcommand" : + "no permission to touch the specified keys"); + } else { + call(c,server.loading ? CMD_CALL_NONE : CMD_CALL_FULL); + } /* Commands may alter argc/argv, restore mstate. */ c->mstate.commands[j].argc = c->argc; diff --git a/src/networking.c b/src/networking.c index 7479b72a8..5b1229fde 100644 --- a/src/networking.c +++ b/src/networking.c @@ -29,11 +29,13 @@ #include "server.h" #include "atomicvar.h" +#include #include #include #include static void setProtocolError(const char *errstr, client *c); +int postponeClientRead(client *c); /* Return the size consumed from the allocator, for the specified SDS string, * including internal fragmentation. This function is used in order to compute @@ -82,33 +84,27 @@ void linkClient(client *c) { raxInsert(server.clients_index,(unsigned char*)&id,sizeof(id),c,NULL); } -client *createClient(int fd) { +client *createClient(connection *conn) { client *c = zmalloc(sizeof(client)); - /* passing -1 as fd it is possible to create a non connected client. + /* passing NULL as conn it is possible to create a non connected client. * This is useful since all the commands needs to be executed * in the context of a client. When commands are executed in other * contexts (for instance a Lua script) we need a non connected client. */ - if (fd != -1) { - anetNonBlock(NULL,fd); - anetEnableTcpNoDelay(NULL,fd); + if (conn) { + connNonBlock(conn); + connEnableTcpNoDelay(conn); if (server.tcpkeepalive) - anetKeepAlive(NULL,fd,server.tcpkeepalive); - if (aeCreateFileEvent(server.el,fd,AE_READABLE, - readQueryFromClient, c) == AE_ERR) - { - close(fd); - zfree(c); - return NULL; - } + connKeepAlive(conn,server.tcpkeepalive); + connSetReadHandler(conn, readQueryFromClient); + connSetPrivateData(conn, c); } selectDb(c,0); - uint64_t client_id; - atomicGetIncr(server.next_client_id,client_id,1); + uint64_t client_id = ++server.next_client_id; c->id = client_id; c->resp = 2; - c->fd = fd; + c->conn = conn; c->name = NULL; c->bufpos = 0; c->qb_pos = 0; @@ -157,9 +153,14 @@ client *createClient(int fd) { c->pubsub_patterns = listCreate(); c->peerid = NULL; c->client_list_node = NULL; + c->client_tracking_redirection = 0; + c->client_tracking_prefixes = NULL; + c->auth_callback = NULL; + c->auth_callback_privdata = NULL; + c->auth_module = NULL; listSetFreeMethod(c->pubsub_patterns,decrRefCountVoid); listSetMatchMethod(c->pubsub_patterns,listMatchObjects); - if (fd != -1) linkClient(c); + if (conn) linkClient(c); initClientMultiState(c); return c; } @@ -225,7 +226,7 @@ int prepareClientToWrite(client *c) { if ((c->flags & CLIENT_MASTER) && !(c->flags & CLIENT_MASTER_FORCE_REPLY)) return C_ERR; - if (c->fd <= 0) return C_ERR; /* Fake client for AOF loading. */ + if (!c->conn) return C_ERR; /* Fake client for AOF loading. */ /* Schedule the client to write the output buffers to the socket, unless * it should already be setup to do so (it has already pending data). */ @@ -369,9 +370,10 @@ void addReplyErrorLength(client *c, const char *s, size_t len) { * Where the master must propagate the first change even if the second * will produce an error. However it is useful to log such events since * they are rare and may hint at errors in a script or a bug in Redis. */ - if (c->flags & (CLIENT_MASTER|CLIENT_SLAVE) && !(c->flags & CLIENT_MONITOR)) { - char* to = c->flags & CLIENT_MASTER? "master": "replica"; - char* from = c->flags & CLIENT_MASTER? "replica": "master"; + int ctype = getClientType(c); + if (ctype == CLIENT_TYPE_MASTER || ctype == CLIENT_TYPE_SLAVE) { + char* to = ctype == CLIENT_TYPE_MASTER? "master": "replica"; + char* from = ctype == CLIENT_TYPE_MASTER? "replica": "master"; char *cmdname = c->lastcmd ? c->lastcmd->name : ""; serverLog(LL_WARNING,"== CRITICAL == This %s is sending an error " "to its %s: '%s' after processing the command " @@ -505,7 +507,7 @@ void addReplyDouble(client *c, double d) { if (c->resp == 2) { addReplyBulkCString(c, d > 0 ? "inf" : "-inf"); } else { - addReplyProto(c, d > 0 ? ",inf\r\n" : "-inf\r\n", + addReplyProto(c, d > 0 ? ",inf\r\n" : ",-inf\r\n", d > 0 ? 6 : 7); } } else { @@ -533,7 +535,7 @@ void addReplyHumanLongDouble(client *c, long double d) { decrRefCount(o); } else { char buf[MAX_LONG_DOUBLE_CHARS]; - int len = ld2string(buf,sizeof(buf),d,1); + int len = ld2string(buf,sizeof(buf),d,LD_STR_HUMAN); addReplyProto(c,",",1); addReplyProto(c,buf,len); addReplyProto(c,"\r\n",2); @@ -744,6 +746,19 @@ void addReplySubcommandSyntaxError(client *c) { sdsfree(cmd); } +/* Append 'src' client output buffers into 'dst' client output buffers. + * This function clears the output buffers of 'src' */ +void AddReplyFromClient(client *dst, client *src) { + if (prepareClientToWrite(dst) != C_OK) + return; + addReplyProto(dst,src->buf, src->bufpos); + if (listLength(src->reply)) + listJoin(dst->reply,src->reply); + dst->reply_bytes += src->reply_bytes; + src->reply_bytes = 0; + src->bufpos = 0; +} + /* Copy 'src' client output buffers into 'dst' client output buffers. * The function takes care of freeing the old output buffers of the * destination client. */ @@ -762,28 +777,13 @@ int clientHasPendingReplies(client *c) { return c->bufpos || listLength(c->reply); } -#define MAX_ACCEPTS_PER_CALL 1000 -static void acceptCommonHandler(int fd, int flags, char *ip) { - client *c; - if ((c = createClient(fd)) == NULL) { - serverLog(LL_WARNING, - "Error registering fd event for the new client: %s (fd=%d)", - strerror(errno),fd); - close(fd); /* May be already closed, just ignore errors */ - return; - } - /* If maxclient directive is set and this is one client more... close the - * connection. Note that we create the client instead to check before - * for this condition, since now the socket is already set in non-blocking - * mode and we can send an error for free using the Kernel I/O */ - if (listLength(server.clients) > server.maxclients) { - char *err = "-ERR max number of clients reached\r\n"; +void clientAcceptHandler(connection *conn) { + client *c = connGetPrivateData(conn); - /* That's a best effort error message, don't check write errors */ - if (write(c->fd,err,strlen(err)) == -1) { - /* Nothing to do, Just to avoid the warning... */ - } - server.stat_rejected_conn++; + if (connGetState(conn) != CONN_STATE_CONNECTED) { + serverLog(LL_WARNING, + "Error accepting a client connection: %s", + connGetLastError(conn)); freeClient(c); return; } @@ -795,10 +795,12 @@ static void acceptCommonHandler(int fd, int flags, char *ip) { if (server.protected_mode && server.bindaddr_count == 0 && DefaultUser->flags & USER_FLAG_NOPASS && - !(flags & CLIENT_UNIX_SOCKET) && - ip != NULL) + !(c->flags & CLIENT_UNIX_SOCKET)) { - if (strcmp(ip,"127.0.0.1") && strcmp(ip,"::1")) { + char cip[NET_IP_STR_LEN+1] = { 0 }; + connPeerToString(conn, cip, sizeof(cip)-1, NULL); + + if (strcmp(cip,"127.0.0.1") && strcmp(cip,"::1")) { char *err = "-DENIED Redis is running in protected mode because protected " "mode is enabled, no bind address was specified, no " @@ -820,7 +822,7 @@ static void acceptCommonHandler(int fd, int flags, char *ip) { "4) Setup a bind address or an authentication password. " "NOTE: You only need to do one of the above things in order for " "the server to start accepting connections from the outside.\r\n"; - if (write(c->fd,err,strlen(err)) == -1) { + if (connWrite(c->conn,err,strlen(err)) == -1) { /* Nothing to do, Just to avoid the warning... */ } server.stat_rejected_conn++; @@ -830,7 +832,65 @@ static void acceptCommonHandler(int fd, int flags, char *ip) { } server.stat_numconnections++; + moduleFireServerEvent(REDISMODULE_EVENT_CLIENT_CHANGE, + REDISMODULE_SUBEVENT_CLIENT_CHANGE_CONNECTED, + c); +} + +#define MAX_ACCEPTS_PER_CALL 1000 +static void acceptCommonHandler(connection *conn, int flags, char *ip) { + client *c; + UNUSED(ip); + + /* Admission control will happen before a client is created and connAccept() + * called, because we don't want to even start transport-level negotiation + * if rejected. + */ + if (listLength(server.clients) >= server.maxclients) { + char *err = "-ERR max number of clients reached\r\n"; + + /* That's a best effort error message, don't check write errors. + * Note that for TLS connections, no handshake was done yet so nothing is written + * and the connection will just drop. + */ + if (connWrite(conn,err,strlen(err)) == -1) { + /* Nothing to do, Just to avoid the warning... */ + } + server.stat_rejected_conn++; + connClose(conn); + return; + } + + /* Create connection and client */ + if ((c = createClient(conn)) == NULL) { + char conninfo[100]; + serverLog(LL_WARNING, + "Error registering fd event for the new client: %s (conn: %s)", + connGetLastError(conn), + connGetInfo(conn, conninfo, sizeof(conninfo))); + connClose(conn); /* May be already closed, just ignore errors */ + return; + } + + /* Last chance to keep flags */ c->flags |= flags; + + /* Initiate accept. + * + * Note that connAccept() is free to do two things here: + * 1. Call clientAcceptHandler() immediately; + * 2. Schedule a future call to clientAcceptHandler(). + * + * Because of that, we must do nothing else afterwards. + */ + if (connAccept(conn, clientAcceptHandler) == C_ERR) { + char conninfo[100]; + serverLog(LL_WARNING, + "Error accepting a client connection: %s (conn: %s)", + connGetLastError(conn), connGetInfo(conn, conninfo, sizeof(conninfo))); + freeClient(connGetPrivateData(conn)); + return; + } } void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) { @@ -849,7 +909,27 @@ void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) { return; } serverLog(LL_VERBOSE,"Accepted %s:%d", cip, cport); - acceptCommonHandler(cfd,0,cip); + acceptCommonHandler(connCreateAcceptedSocket(cfd),0,cip); + } +} + +void acceptTLSHandler(aeEventLoop *el, int fd, void *privdata, int mask) { + int cport, cfd, max = MAX_ACCEPTS_PER_CALL; + char cip[NET_IP_STR_LEN]; + UNUSED(el); + UNUSED(mask); + UNUSED(privdata); + + while(max--) { + cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport); + if (cfd == ANET_ERR) { + if (errno != EWOULDBLOCK) + serverLog(LL_WARNING, + "Accepting client connection: %s", server.neterr); + return; + } + serverLog(LL_VERBOSE,"Accepted %s:%d", cip, cport); + acceptCommonHandler(connCreateAcceptedTLS(cfd, server.tls_auth_clients),0,cip); } } @@ -868,7 +948,7 @@ void acceptUnixHandler(aeEventLoop *el, int fd, void *privdata, int mask) { return; } serverLog(LL_VERBOSE,"Accepted connection to %s", server.unixsocket); - acceptCommonHandler(cfd,CLIENT_UNIX_SOCKET,NULL); + acceptCommonHandler(connCreateAcceptedSocket(cfd),CLIENT_UNIX_SOCKET,NULL); } } @@ -899,10 +979,10 @@ void unlinkClient(client *c) { /* If this is marked as current client unset it. */ if (server.current_client == c) server.current_client = NULL; - /* Certain operations must be done only if the client has an active socket. + /* Certain operations must be done only if the client has an active connection. * If the client was already unlinked or if it's a "fake client" the - * fd is already set to -1. */ - if (c->fd != -1) { + * conn is already set to NULL. */ + if (c->conn) { /* Remove from the list of active clients. */ if (c->client_list_node) { uint64_t id = htonu64(c->id); @@ -911,11 +991,23 @@ void unlinkClient(client *c) { c->client_list_node = NULL; } - /* Unregister async I/O handlers and close the socket. */ - aeDeleteFileEvent(server.el,c->fd,AE_READABLE); - aeDeleteFileEvent(server.el,c->fd,AE_WRITABLE); - close(c->fd); - c->fd = -1; + /* Check if this is a replica waiting for diskless replication (rdb pipe), + * in which case it needs to be cleaned from that list */ + if (c->flags & CLIENT_SLAVE && + c->replstate == SLAVE_STATE_WAIT_BGSAVE_END && + server.rdb_pipe_conns) + { + int i; + for (i=0; i < server.rdb_pipe_numconns; i++) { + if (server.rdb_pipe_conns[i] == c->conn) { + rdbPipeWriteHandlerConnRemoved(c->conn); + server.rdb_pipe_conns[i] = NULL; + break; + } + } + } + connClose(c->conn); + c->conn = NULL; } /* Remove from the list of pending writes if needed. */ @@ -926,6 +1018,14 @@ void unlinkClient(client *c) { c->flags &= ~CLIENT_PENDING_WRITE; } + /* Remove from the list of pending reads if needed. */ + if (c->flags & CLIENT_PENDING_READ) { + ln = listSearchKey(server.clients_pending_read,c); + serverAssert(ln != NULL); + listDelNode(server.clients_pending_read,ln); + c->flags &= ~CLIENT_PENDING_READ; + } + /* When client was just unblocked because of a blocking operation, * remove it from the list of unblocked clients. */ if (c->flags & CLIENT_UNBLOCKED) { @@ -934,6 +1034,9 @@ void unlinkClient(client *c) { listDelNode(server.unblocked_clients,ln); c->flags &= ~CLIENT_UNBLOCKED; } + + /* Clear the tracking status. */ + if (c->flags & CLIENT_TRACKING) disableTracking(c); } void freeClient(client *c) { @@ -946,6 +1049,16 @@ void freeClient(client *c) { return; } + /* For connected clients, call the disconnection event of modules hooks. */ + if (c->conn) { + moduleFireServerEvent(REDISMODULE_EVENT_CLIENT_CHANGE, + REDISMODULE_SUBEVENT_CLIENT_CHANGE_DISCONNECTED, + c); + } + + /* Notify module system that this client auth status changed. */ + moduleNotifyUserChanged(c); + /* If it is our master that's beging disconnected we should make sure * to cache the state to try a partial resynchronization later. * @@ -963,7 +1076,7 @@ void freeClient(client *c) { } /* Log link disconnection with slave */ - if ((c->flags & CLIENT_SLAVE) && !(c->flags & CLIENT_MONITOR)) { + if (getClientType(c) == CLIENT_TYPE_SLAVE) { serverLog(LL_WARNING,"Connection with replica %s lost.", replicationGetSlaveName(c)); } @@ -1010,9 +1123,14 @@ void freeClient(client *c) { /* We need to remember the time when we started to have zero * attached slaves, as after some time we'll free the replication * backlog. */ - if (c->flags & CLIENT_SLAVE && listLength(server.slaves) == 0) + if (getClientType(c) == CLIENT_TYPE_SLAVE && listLength(server.slaves) == 0) server.repl_no_slaves_since = server.unixtime; refreshGoodSlavesCount(); + /* Fire the replica change modules event. */ + if (c->replstate == SLAVE_STATE_ONLINE) + moduleFireServerEvent(REDISMODULE_EVENT_REPLICA_CHANGE, + REDISMODULE_SUBEVENT_REPLICA_CHANGE_OFFLINE, + NULL); } /* Master/slave cleanup Case 2: @@ -1041,9 +1159,22 @@ void freeClient(client *c) { * a context where calling freeClient() is not possible, because the client * should be valid for the continuation of the flow of the program. */ void freeClientAsync(client *c) { + /* We need to handle concurrent access to the server.clients_to_close list + * only in the freeClientAsync() function, since it's the only function that + * may access the list while Redis uses I/O threads. All the other accesses + * are in the context of the main thread while the other threads are + * idle. */ if (c->flags & CLIENT_CLOSE_ASAP || c->flags & CLIENT_LUA) return; c->flags |= CLIENT_CLOSE_ASAP; + if (server.io_threads_num == 1) { + /* no need to bother with locking if there's just one thread (the main thread) */ + listAddNodeTail(server.clients_to_close,c); + return; + } + static pthread_mutex_t async_free_queue_mutex = PTHREAD_MUTEX_INITIALIZER; + pthread_mutex_lock(&async_free_queue_mutex); listAddNodeTail(server.clients_to_close,c); + pthread_mutex_unlock(&async_free_queue_mutex); } void freeClientsInAsyncFreeQueue(void) { @@ -1067,15 +1198,21 @@ client *lookupClientByID(uint64_t id) { } /* Write data in output buffers to client. Return C_OK if the client - * is still valid after the call, C_ERR if it was freed. */ -int writeToClient(int fd, client *c, int handler_installed) { + * is still valid after the call, C_ERR if it was freed because of some + * error. If handler_installed is set, it will attempt to clear the + * write event. + * + * This function is called by threads, but always with handler_installed + * set to 0. So when handler_installed is set to 0 the function must be + * thread safe. */ +int writeToClient(client *c, int handler_installed) { ssize_t nwritten = 0, totwritten = 0; size_t objlen; clientReplyBlock *o; while(clientHasPendingReplies(c)) { if (c->bufpos > 0) { - nwritten = write(fd,c->buf+c->sentlen,c->bufpos-c->sentlen); + nwritten = connWrite(c->conn,c->buf+c->sentlen,c->bufpos-c->sentlen); if (nwritten <= 0) break; c->sentlen += nwritten; totwritten += nwritten; @@ -1096,7 +1233,7 @@ int writeToClient(int fd, client *c, int handler_installed) { continue; } - nwritten = write(fd, o->buf + c->sentlen, objlen - c->sentlen); + nwritten = connWrite(c->conn, o->buf + c->sentlen, objlen - c->sentlen); if (nwritten <= 0) break; c->sentlen += nwritten; totwritten += nwritten; @@ -1122,8 +1259,8 @@ int writeToClient(int fd, client *c, int handler_installed) { * just deliver as much data as it is possible to deliver. * * Moreover, we also send as much as possible if the client is - * a slave (otherwise, on high-speed traffic, the replication - * buffer will grow indefinitely) */ + * a slave or a monitor (otherwise, on high-speed traffic, the + * replication/output buffer will grow indefinitely) */ if (totwritten > NET_MAX_WRITES_PER_EVENT && (server.maxmemory == 0 || zmalloc_used_memory() < server.maxmemory) && @@ -1131,12 +1268,12 @@ int writeToClient(int fd, client *c, int handler_installed) { } server.stat_net_output_bytes += totwritten; if (nwritten == -1) { - if (errno == EAGAIN) { + if (connGetState(c->conn) == CONN_STATE_CONNECTED) { nwritten = 0; } else { serverLog(LL_VERBOSE, - "Error writing to client: %s", strerror(errno)); - freeClient(c); + "Error writing to client: %s", connGetLastError(c->conn)); + freeClientAsync(c); return C_ERR; } } @@ -1149,11 +1286,15 @@ int writeToClient(int fd, client *c, int handler_installed) { } if (!clientHasPendingReplies(c)) { c->sentlen = 0; - if (handler_installed) aeDeleteFileEvent(server.el,c->fd,AE_WRITABLE); + /* Note that writeToClient() is called in a threaded way, but + * adDeleteFileEvent() is not thread safe: however writeToClient() + * is always called with handler_installed set to 0 from threads + * so we are fine. */ + if (handler_installed) connSetWriteHandler(c->conn, NULL); /* Close connection after entire reply has been sent. */ if (c->flags & CLIENT_CLOSE_AFTER_REPLY) { - freeClient(c); + freeClientAsync(c); return C_ERR; } } @@ -1161,10 +1302,9 @@ int writeToClient(int fd, client *c, int handler_installed) { } /* Write event handler. Just send data to the client. */ -void sendReplyToClient(aeEventLoop *el, int fd, void *privdata, int mask) { - UNUSED(el); - UNUSED(mask); - writeToClient(fd,privdata,1); +void sendReplyToClient(connection *conn) { + client *c = connGetPrivateData(conn); + writeToClient(c,1); } /* This function is called just before entering the event loop, in the hope @@ -1187,26 +1327,24 @@ int handleClientsWithPendingWrites(void) { if (c->flags & CLIENT_PROTECTED) continue; /* Try to write buffers to the client socket. */ - if (writeToClient(c->fd,c,0) == C_ERR) continue; + if (writeToClient(c,0) == C_ERR) continue; /* If after the synchronous writes above we still have data to * output to the client, we need to install the writable handler. */ if (clientHasPendingReplies(c)) { - int ae_flags = AE_WRITABLE; + int ae_barrier = 0; /* For the fsync=always policy, we want that a given FD is never * served for reading and writing in the same event loop iteration, * so that in the middle of receiving the query, and serving it * to the client, we'll call beforeSleep() that will do the - * actual fsync of AOF to disk. AE_BARRIER ensures that. */ + * actual fsync of AOF to disk. the write barrier ensures that. */ if (server.aof_state == AOF_ON && server.aof_fsync == AOF_FSYNC_ALWAYS) { - ae_flags |= AE_BARRIER; + ae_barrier = 1; } - if (aeCreateFileEvent(server.el, c->fd, ae_flags, - sendReplyToClient, c) == AE_ERR) - { - freeClientAsync(c); + if (connSetWriteHandlerWithBarrier(c->conn, sendReplyToClient, ae_barrier) == C_ERR) { + freeClientAsync(c); } } } @@ -1252,15 +1390,15 @@ void resetClient(client *c) { * path, it is not really released, but only marked for later release. */ void protectClient(client *c) { c->flags |= CLIENT_PROTECTED; - aeDeleteFileEvent(server.el,c->fd,AE_READABLE); - aeDeleteFileEvent(server.el,c->fd,AE_WRITABLE); + connSetReadHandler(c->conn,NULL); + connSetWriteHandler(c->conn,NULL); } /* This will undo the client protection done by protectClient() */ void unprotectClient(client *c) { if (c->flags & CLIENT_PROTECTED) { c->flags &= ~CLIENT_PROTECTED; - aeCreateFileEvent(server.el,c->fd,AE_READABLE,readQueryFromClient,c); + connSetReadHandler(c->conn,readQueryFromClient); if (clientHasPendingReplies(c)) clientInstallWriteHandler(c); } } @@ -1308,7 +1446,7 @@ int processInlineBuffer(client *c) { /* Newline from slaves can be used to refresh the last ACK time. * This is useful for a slave to ping back while loading a big * RDB file. */ - if (querylen == 0 && c->flags & CLIENT_SLAVE) + if (querylen == 0 && getClientType(c) == CLIENT_TYPE_SLAVE) c->repl_ack_time = server.unixtime; /* Move querybuffer position to the next query in the buffer. */ @@ -1322,12 +1460,8 @@ int processInlineBuffer(client *c) { /* Create redis objects for all arguments. */ for (c->argc = 0, j = 0; j < argc; j++) { - if (sdslen(argv[j])) { - c->argv[c->argc] = createObject(OBJ_STRING,argv[j]); - c->argc++; - } else { - sdsfree(argv[j]); - } + c->argv[c->argc] = createObject(OBJ_STRING,argv[j]); + c->argc++; } zfree(argv); return C_OK; @@ -1509,13 +1643,47 @@ int processMultibulkBuffer(client *c) { return C_ERR; } +/* This function calls processCommand(), but also performs a few sub tasks + * that are useful in that context: + * + * 1. It sets the current client to the client 'c'. + * 2. In the case of master clients, the replication offset is updated. + * 3. The client is reset unless there are reasons to avoid doing it. + * + * The function returns C_ERR in case the client was freed as a side effect + * of processing the command, otherwise C_OK is returned. */ +int processCommandAndResetClient(client *c) { + int deadclient = 0; + server.current_client = c; + 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) + c->qb_pos; + } + + /* 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); + } + } + if (server.current_client == NULL) deadclient = 1; + server.current_client = NULL; + /* freeMemoryIfNeeded may flush slave output buffers. This may + * result into a slave, that may be the active client, to be + * freed. */ + return deadclient ? C_ERR : C_OK; +} + /* 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 */ while(c->qb_pos < sdslen(c->querybuf)) { /* Return if clients are paused. */ @@ -1524,6 +1692,10 @@ void processInputBuffer(client *c) { /* Immediately abort if the client is in the middle of something. */ if (c->flags & CLIENT_BLOCKED) break; + /* Don't process more buffers from clients that have already pending + * commands to execute in c->argv. */ + if (c->flags & CLIENT_PENDING_COMMAND) break; + /* Don't process input from the master while there is a busy script * condition on the slave. We want just to accumulate the replication * stream (instead of replying -BUSY like we do with other clients) and @@ -1550,7 +1722,10 @@ void processInputBuffer(client *c) { if (processInlineBuffer(c) != C_OK) break; /* If the Gopher mode and we got zero or one argument, process * the request in Gopher mode. */ - if (server.gopher_enabled && (c->argc == 1 || c->argc == 0)) { + if (server.gopher_enabled && + ((c->argc == 1 && ((char*)(c->argv[0]->ptr))[0] == '/') || + c->argc == 0)) + { processGopherRequest(c); resetClient(c); c->flags |= CLIENT_CLOSE_AFTER_REPLY; @@ -1566,44 +1741,45 @@ void processInputBuffer(client *c) { if (c->argc == 0) { resetClient(c); } else { - /* Only reset the client when the command was executed. */ - 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) + c->qb_pos; - } - - /* 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); + /* If we are in the context of an I/O thread, we can't really + * execute the command here. All we can do is to flag the client + * as one that needs to process the command. */ + if (c->flags & CLIENT_PENDING_READ) { + c->flags |= CLIENT_PENDING_COMMAND; + break; + } + + /* We are finally ready to execute the command. */ + if (processCommandAndResetClient(c) == C_ERR) { + /* If the client is no longer valid, we avoid exiting this + * loop and trimming the client buffer later. So we return + * ASAP in that case. */ + return; } - /* 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; } } /* Trim to pos */ - if (server.current_client != NULL && c->qb_pos) { + if (c->qb_pos) { sdsrange(c->querybuf,c->qb_pos,-1); c->qb_pos = 0; } - - server.current_client = NULL; } /* This is a wrapper for processInputBuffer that also cares about handling - * the replication forwarding to the sub-slaves, in case the client 'c' + * the replication forwarding to the sub-replicas, in case the client 'c' * is flagged as master. Usually you want to call this instead of the * raw processInputBuffer(). */ void processInputBufferAndReplicate(client *c) { if (!(c->flags & CLIENT_MASTER)) { processInputBuffer(c); } else { + /* 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-replicas and to the replication backlog. */ size_t prev_offset = c->reploff; processInputBuffer(c); size_t applied = c->reploff - prev_offset; @@ -1615,12 +1791,14 @@ void processInputBufferAndReplicate(client *c) { } } -void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) { - client *c = (client*) privdata; +void readQueryFromClient(connection *conn) { + client *c = connGetPrivateData(conn); int nread, readlen; size_t qblen; - UNUSED(el); - UNUSED(mask); + + /* Check if we want to read from the client later when exiting from + * the event loop. This is the case if threaded I/O is enabled. */ + if (postponeClientRead(c)) return; readlen = PROTO_IOBUF_LEN; /* If this is a multi bulk request, and we are processing a bulk reply @@ -1642,18 +1820,18 @@ void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) { qblen = sdslen(c->querybuf); if (c->querybuf_peak < qblen) c->querybuf_peak = qblen; c->querybuf = sdsMakeRoomFor(c->querybuf, readlen); - nread = read(fd, c->querybuf+qblen, readlen); + nread = connRead(c->conn, c->querybuf+qblen, readlen); if (nread == -1) { - if (errno == EAGAIN) { + if (connGetState(conn) == CONN_STATE_CONNECTED) { return; } else { - serverLog(LL_VERBOSE, "Reading from client: %s",strerror(errno)); - freeClient(c); + serverLog(LL_VERBOSE, "Reading from client: %s",connGetLastError(c->conn)); + freeClientAsync(c); return; } } else if (nread == 0) { serverLog(LL_VERBOSE, "Client closed connection"); - freeClient(c); + freeClientAsync(c); return; } else if (c->flags & CLIENT_MASTER) { /* Append the query buffer to the pending (not applied) buffer @@ -1674,17 +1852,13 @@ void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) { serverLog(LL_WARNING,"Closing client that reached max query buffer length: %s (qbuf initial bytes: %s)", ci, bytes); sdsfree(ci); sdsfree(bytes); - freeClient(c); + freeClientAsync(c); return; } - /* 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. */ - processInputBufferAndReplicate(c); + /* There is more data in the client input buffer, continue parsing it + * in case to check if there is a full command to execute. */ + processInputBufferAndReplicate(c); } void getClientsMaxBuffers(unsigned long *longest_output_list, @@ -1723,7 +1897,7 @@ void genClientPeerId(client *client, char *peerid, snprintf(peerid,peerid_len,"%s:0",server.unixsocket); } else { /* TCP client. */ - anetFormatPeer(client->fd,peerid,peerid_len); + connFormatPeer(client->conn,peerid,peerid_len); } } @@ -1744,8 +1918,7 @@ char *getClientPeerId(client *c) { /* Concatenate a string representing the state of a client in an human * readable format, into the sds string 's'. */ sds catClientInfoString(sds s, client *client) { - char flags[16], events[3], *p; - int emask; + char flags[16], events[3], conninfo[CONN_INFO_LEN], *p; p = flags; if (client->flags & CLIENT_SLAVE) { @@ -1758,6 +1931,8 @@ sds catClientInfoString(sds s, client *client) { if (client->flags & CLIENT_PUBSUB) *p++ = 'P'; if (client->flags & CLIENT_MULTI) *p++ = 'x'; if (client->flags & CLIENT_BLOCKED) *p++ = 'b'; + if (client->flags & CLIENT_TRACKING) *p++ = 't'; + if (client->flags & CLIENT_TRACKING_BROKEN_REDIR) *p++ = 'R'; if (client->flags & CLIENT_DIRTY_CAS) *p++ = 'd'; if (client->flags & CLIENT_CLOSE_AFTER_REPLY) *p++ = 'c'; if (client->flags & CLIENT_UNBLOCKED) *p++ = 'u'; @@ -1767,16 +1942,17 @@ sds catClientInfoString(sds s, client *client) { if (p == flags) *p++ = 'N'; *p++ = '\0'; - emask = client->fd == -1 ? 0 : aeGetFileEvents(server.el,client->fd); p = events; - if (emask & AE_READABLE) *p++ = 'r'; - if (emask & AE_WRITABLE) *p++ = 'w'; + if (client->conn) { + if (connHasReadHandler(client->conn)) *p++ = 'r'; + if (connHasWriteHandler(client->conn)) *p++ = 'w'; + } *p = '\0'; return sdscatfmt(s, - "id=%U addr=%s fd=%i name=%s age=%I idle=%I flags=%s db=%i sub=%i psub=%i multi=%i qbuf=%U qbuf-free=%U obl=%U oll=%U omem=%U events=%s cmd=%s user=%s", + "id=%U addr=%s %s name=%s age=%I idle=%I flags=%s db=%i sub=%i psub=%i multi=%i qbuf=%U qbuf-free=%U obl=%U oll=%U omem=%U events=%s cmd=%s user=%s", (unsigned long long) client->id, getClientPeerId(client), - client->fd, + connGetInfo(client->conn, conninfo, sizeof(conninfo)), client->name ? (char*)client->name->ptr : "", (long long)(server.unixtime - client->ctime), (long long)(server.unixtime - client->lastinteraction), @@ -1829,7 +2005,6 @@ int clientSetNameOrReply(client *c, robj *name) { if (len == 0) { if (c->name) decrRefCount(c->name); c->name = NULL; - addReply(c,shared.ok); return C_OK; } @@ -1853,23 +2028,24 @@ int clientSetNameOrReply(client *c, robj *name) { void clientCommand(client *c) { listNode *ln; listIter li; - client *client; if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"help")) { const char *help[] = { -"id -- Return the ID of the current connection.", -"getname -- Return the name of the current connection.", -"kill -- Kill connection made from .", -"kill