diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..1d563276 --- /dev/null +++ b/.clang-format @@ -0,0 +1,4 @@ +BasedOnStyle: LLVM +IndentWidth: 4 +BreakBeforeTernaryOperators: false +ReflowComments: false \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..05d92650 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# Automated dependency updates. +# +# For configuration options see: +# https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..429da711 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,150 @@ +name: CI + +on: [push, pull_request] + +permissions: + contents: read + +jobs: + checkers: + name: Run static checkers + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - name: Run clang-format style check (.c and .h) + uses: jidicula/clang-format-action@f62da5e3d3a2d88ff364771d9d938773a618ab5e # v4.11.0 + + ubuntu: + name: ${{ matrix.cmake-build-type }}-build [${{ matrix.compiler }}, cmake-${{ matrix.cmake-version }} sanitizer="${{ matrix.sanitizer }}"] + runs-on: ubuntu-20.04 + strategy: + fail-fast: false + matrix: + compiler: [gcc-11, clang-12] + cmake-version: [3.19] + cmake-build-type: [Release, RelWithDebInfo] + sanitizer: ["", thread, undefined, leak, address] + include: + - compiler: gcc-7 + cmake-version: 3.14 + cmake-build-type: Release + sanitizer: "" + - compiler: gcc-8 + cmake-version: 3.15 + cmake-build-type: Release + sanitizer: "" + - compiler: clang-7 + cmake-version: 3.17 + cmake-build-type: Release + sanitizer: "" + - compiler: clang-9 + cmake-version: 3.18 + cmake-build-type: Release + sanitizer: "" + + steps: + - name: Prepare + uses: awalsh128/cache-apt-pkgs-action@a6c3917cc929dd0345bfb2d3feaf9101823370ad # v1.4.2 + with: + packages: libevent-dev libuv1-dev libev-dev libglib2.0-dev ${{ matrix.compiler }} + version: 1.0 + - name: Install hiredis + env: + VERSION: 1.2.0 + run: | + curl -L https://github.com/redis/hiredis/archive/v${VERSION}.tar.gz | tar -xz + cmake -S hiredis-${VERSION} -B hiredis-build -DENABLE_SSL=ON + sudo make -C hiredis-build install + - name: Setup cmake + uses: jwlawson/actions-setup-cmake@802fa1a2c4e212495c05bf94dba2704a92a472be # v2.0.2 + with: + cmake-version: ${{ matrix.cmake-version }} + - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - name: Create build folder + run: cmake -E make_directory build + - name: Generate makefiles + shell: bash + env: + CC: ${{ matrix.compiler }} + working-directory: build + run: cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DENABLE_SSL=ON -DENABLE_IPV6_TESTS=ON -DDOWNLOAD_HIREDIS=OFF -DUSE_SANITIZER=${{ matrix.sanitizer }} .. + - name: Build + shell: bash + working-directory: build + run: VERBOSE=1 make + - name: Setup clusters + shell: bash + working-directory: build + run: make start + - name: Wait for clusters to start.. + uses: kibertoad/wait-action@99f6f101c5be7b88bb9b41c0d3b810722491b8e5 # 1.0.1 + with: + time: '20s' + - name: Run tests + shell: bash + working-directory: build + run: make CTEST_OUTPUT_ON_FAILURE=1 test + - name: Teardown clusters + working-directory: build + shell: bash + run: make stop + - name: Build examples + shell: bash + env: + CC: ${{ matrix.compiler }} + run: | + examples/using_cmake_externalproject/build.sh + examples/using_cmake_separate/build.sh + examples/using_cmake_and_make_mixed/build.sh + examples/using_make/build.sh + + macos: + name: macOS + runs-on: macos-latest + steps: + - name: Prepare + run: | + brew install cmake ninja openssl + - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - name: Build + run: | + mkdir build && cd build + cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DENABLE_SSL=ON + ninja -v + + windows: + name: Windows + runs-on: windows-latest + steps: + - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1.13.0 + - name: Prepare + run: | + choco install -y ninja + vcpkg install --triplet x64-windows pkgconf libevent + - name: Build + run: | + mkdir build && cd build + cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=C:\vcpkg\scripts\buildsystems\vcpkg.cmake + ninja -v + + windows-mingw64: + name: Windows (MinGW64) + runs-on: windows-latest + steps: + - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - name: Set up MinGW + uses: msys2/setup-msys2@d0e80f58dffbc64f6a3a1f43527d469b4fc7b6c8 # v2.23.0 + with: + msystem: mingw64 + install: | + mingw-w64-x86_64-gcc + mingw-w64-x86_64-cmake + mingw-w64-x86_64-ninja + mingw-w64-x86_64-libevent + - name: Build + shell: msys2 {0} + run: | + mkdir build && cd build + cmake .. -G Ninja + cmake --build . diff --git a/.github/workflows/coverity.yml b/.github/workflows/coverity.yml new file mode 100644 index 00000000..335164a9 --- /dev/null +++ b/.github/workflows/coverity.yml @@ -0,0 +1,54 @@ +name: "Coverity" +on: + schedule: + - cron: '0 1 * * 0' + +permissions: + contents: read + +jobs: + analyze: + name: Analyze + if: github.repository == 'Nordix/hiredis-cluster' + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + with: + fetch-depth: 1 + + - name: Download Coverity + run: | + cd .. + wget -q https://scan.coverity.com/download/linux64 --post-data "token=${COVERITY_TOKEN}&project=hiredis-cluster" -O coverity_tool.tgz + mkdir coverity + tar xzf coverity_tool.tgz --strip 1 -C coverity + echo "$(pwd)/coverity/bin" >> $GITHUB_PATH + env: + COVERITY_TOKEN: ${{ secrets.COVERITY_TOKEN }} + + - name: Prepare + uses: awalsh128/cache-apt-pkgs-action@a6c3917cc929dd0345bfb2d3feaf9101823370ad # v1.4.2 + with: + packages: libevent-dev cmake + version: 1.0 + + - name: Build with Coverity + run: | + mkdir build; cd build + cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -DENABLE_SSL=ON .. + cov-build --dir cov-int make + + - name: Submit the result to Coverity + run: | + tar czvf hiredis_cluster.tgz cov-int + curl \ + --form token=${COVERITY_TOKEN} \ + --form email=bjorn.a.svensson@est.tech \ + --form file=@hiredis_cluster.tgz \ + --form version=${GITHUB_SHA} \ + https://scan.coverity.com/builds?project=hiredis-cluster + working-directory: build + env: + COVERITY_TOKEN: ${{ secrets.COVERITY_TOKEN }} diff --git a/.github/workflows/db-compatibility.yml b/.github/workflows/db-compatibility.yml new file mode 100644 index 00000000..b2c8d57b --- /dev/null +++ b/.github/workflows/db-compatibility.yml @@ -0,0 +1,103 @@ +name: DB compatibility testing + +on: [push, pull_request] + +permissions: + contents: read + +jobs: + valkey: + name: Valkey ${{ matrix.valkey-version }} + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + include: + - valkey-version: 7.2.5 + steps: + - name: Prepare + uses: awalsh128/cache-apt-pkgs-action@a6c3917cc929dd0345bfb2d3feaf9101823370ad # v1.4.2 + with: + packages: libevent-dev + version: 1.0 + - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - name: Create build folder + run: cmake -E make_directory build + - name: Generate makefiles + shell: bash + working-directory: build + run: cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=Release -DENABLE_IPV6_TESTS=ON -DTEST_WITH_VALKEY_VERSION=${{ matrix.valkey-version }} .. + - name: Build + shell: bash + working-directory: build + run: VERBOSE=1 make + - name: Setup clusters + shell: bash + working-directory: build + run: make start + - name: Wait for clusters to start.. + uses: kibertoad/wait-action@99f6f101c5be7b88bb9b41c0d3b810722491b8e5 # 1.0.1 + with: + time: '40s' + - name: Run tests + shell: bash + working-directory: build + run: make CTEST_OUTPUT_ON_FAILURE=1 test + - name: Teardown clusters + working-directory: build + shell: bash + run: make stop + + redis-comp: + name: Redis ${{ matrix.redis-version }} + runs-on: ubuntu-20.04 + strategy: + fail-fast: false + matrix: + include: + - redis-version: 7.2.4 + - redis-version: 7.0.15 + - redis-version: 6.2.14 + - redis-version: 6.0.20 + - redis-version: 5.0.14 + steps: + - name: Prepare + uses: awalsh128/cache-apt-pkgs-action@a6c3917cc929dd0345bfb2d3feaf9101823370ad # v1.4.2 + with: + packages: libevent-dev + version: 1.0 + + - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + + - name: Create build folder + run: cmake -E make_directory build + + - name: Generate makefiles + shell: bash + working-directory: build + run: cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=Release -DENABLE_IPV6_TESTS=ON -DTEST_WITH_REDIS_VERSION=${{ matrix.redis-version }} .. + + - name: Build + shell: bash + working-directory: build + run: VERBOSE=1 make + + - name: Setup clusters + shell: bash + working-directory: build + run: make start + + - name: Wait for clusters to start.. + uses: kibertoad/wait-action@99f6f101c5be7b88bb9b41c0d3b810722491b8e5 # 1.0.1 + with: + time: '40s' + + - name: Run tests + shell: bash + working-directory: build + run: make CTEST_OUTPUT_ON_FAILURE=1 test + + - name: Teardown clusters + working-directory: build + shell: bash + run: make stop diff --git a/crc16.c b/crc16.c new file mode 100644 index 00000000..a5fb0747 --- /dev/null +++ b/crc16.c @@ -0,0 +1,83 @@ +/* + * Copyright 2001-2010 Georges Menie (www.menie.org) + * Copyright 2010-2012 Salvatore Sanfilippo (adapted to Redis coding style) + * 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 the University of California, Berkeley 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 REGENTS 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 REGENTS AND 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. + */ + +/* CRC16 implementation according to CCITT standards. + * + * Note by @antirez: this is actually the XMODEM CRC 16 algorithm, using the + * following parameters: + * + * Name : "XMODEM", also known as "ZMODEM", "CRC-16/ACORN" + * Width : 16 bit + * Poly : 1021 (That is actually x^16 + x^12 + x^5 + 1) + * Initialization : 0000 + * Reflect Input byte : False + * Reflect Output CRC : False + * Xor constant to output CRC : 0000 + * Output for "123456789" : 31C3 + */ +#include "hiutil.h" + +static const uint16_t crc16tab[256] = { + 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x8108, + 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 0x1231, 0x0210, + 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, 0x9339, 0x8318, 0xb37b, + 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, 0x2462, 0x3443, 0x0420, 0x1401, + 0x64e6, 0x74c7, 0x44a4, 0x5485, 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, + 0xf5cf, 0xc5ac, 0xd58d, 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, + 0x5695, 0x46b4, 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, + 0xc7bc, 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, + 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, 0x5af5, + 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, 0xdbfd, 0xcbdc, + 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, 0x6ca6, 0x7c87, 0x4ce4, + 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, 0xedae, 0xfd8f, 0xcdec, 0xddcd, + 0xad2a, 0xbd0b, 0x8d68, 0x9d49, 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, + 0x2e32, 0x1e51, 0x0e70, 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, + 0x9f59, 0x8f78, 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, + 0xe16f, 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, + 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, 0x02b1, + 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, 0xb5ea, 0xa5cb, + 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, 0x34e2, 0x24c3, 0x14a0, + 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xa7db, 0xb7fa, 0x8799, 0x97b8, + 0xe75f, 0xf77e, 0xc71d, 0xd73c, 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, + 0x7676, 0x4615, 0x5634, 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, + 0xb98a, 0xa9ab, 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, + 0x28a3, 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, + 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, 0xfd2e, + 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, 0x7c26, 0x6c07, + 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, 0xef1f, 0xff3e, 0xcf5d, + 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, 0x6e17, 0x7e36, 0x4e55, 0x5e74, + 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0}; + +uint16_t crc16(const char *buf, int len) { + int counter; + uint16_t crc = 0; + for (counter = 0; counter < len; counter++) + crc = (crc << 8) ^ crc16tab[((crc >> 8) ^ *buf++) & 0x00FF]; + return crc; +} diff --git a/libvalkeycluster/.gitignore b/libvalkeycluster/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/libvalkeycluster/.gitignore @@ -0,0 +1 @@ +/build diff --git a/libvalkeycluster/CHANGELOG.md b/libvalkeycluster/CHANGELOG.md new file mode 100644 index 00000000..aa100cc1 --- /dev/null +++ b/libvalkeycluster/CHANGELOG.md @@ -0,0 +1,123 @@ +### 0.13.0 - Mar 18, 2024 + +* Add non-const connect callback (#205) +* Invoke all callbacks during redisClusterAsyncDisconnect() (#204) +* Better support for JSON files describing commands metadata (#202) +* Fix a compilation warning in command.c relating to kpos shadowing (#201) + +### 0.12.0 - Feb 9, 2024 + +* Update slotmap when slot is not served by any node (#192) +* Prevent slotmap updates during redisClusterAsyncFree() (#195) +* Fix builds when using CMake < v3.15 (#196) +* Support adding custom commands at compile time (#200) +* Fix the check for ":0@0" in CLUSTER NODES result (#199) + +### 0.11.0 - Sep 15, 2023 + +* Add event callback for events like 'slotmap updated'. +* Add connect callback for the sync API. +* Add connect function in the async API for fully asynchronous startup. +* Update the slotmap asynchronously in the async API. +* Follow MOVED redirect and update slot mapping concurrently. +* Update slotmap on error. + When connect failed, update slotmap instead of sending command to random node. + When command fails (timeout, etc.) schedule slotmap update for next command. +* Update slotmap when redisClusterCommandToNode() fails. +* Correct parsing of an IPv6 address in an ASK redirect. +* Correct handling of XREAD and XREADGROUP. +* Rename of some types and functions. + (Old names are still defined by default for backward compability.) +* Update hiredis to v1.2.0 when the CMake build handles the download. +* Build improvements. + +### 0.10.0 - Feb 02, 2023 + +* More commands are supported. +* New logic for finding the key of each command. +* Build improvements. + +### 0.9.0 - Dec 22, 2022 + +* Fixed a crash in the asynchronous API triggered by timed out commands. +* Fixed a crash when using "CLUSTER NODES" on a non-ready cluster. +* Fixed a crash when sending commands from a failure-reply callback. +* Corrected and enabled connection timeout in the asynchronous API. +* Corrected the handling of multiple ASK-redirect reply callbacks. +* Updated hiredis to v1.1.0 when the CMake build handles the download. +* Removed the unused cluster_slots array in the cluster context. +* Updates to resolve build issues on Windows platforms. + +### 0.8.1 - Aug 31, 2022 + +* Fixed crash and use-after-free in the asynchronous API. +* Use identical warning flags in CMake and Makefile. +* Corrected CROSSSLOT errors to not to be retried. + +### 0.8.0 - Jun 15, 2022 + +* Basic Redis 7.0 support. +* SSL/TLS handling in separate library. +* Command timeout corrections. +* Builds on Windows and macOS. + +### 0.7.0 - Sep 22, 2021 + +* Added support for stream commands in regular API. +* Added support for authentication using AUTH with username. +* Added adapters for event libraries libuv, libev and GLib. +* Improved memory efficiency. +* Renamed API function `redisClusterSetOptionMaxRedirect()` + to `redisClusterSetOptionMaxRetry()`. + +### 0.6.0 - Feb 09, 2021 + +* Minimum required version of CMake changed to 3.11 (from 3.14) +* Re-added the Makefile for symmetry with hiredis, which also enables + support for statically-linked libraries. +* Improved examples +* Corrected crashes and leaks in OOM scenarios +* New API for sending commands to specific node +* New API for node iteration, can be used for sending commands + to some or all nodes. + +### 0.5.0 - Dec 07, 2020 + +* Renamed to [hiredis-cluster](https://github.com/Nordix/hiredis-cluster) +* The C library `hiredis` is an external dependency rather than a builtin part + of the cluster client, meaning that `hiredis` v1.0.0 or later can be used. +* Support for SSL/TLS introduced in Redis 6 +* Support for IPv6 +* Support authentication using AUTH +* Handle variable number of keys in command EXISTS +* Improved CMake build +* Code style guide (using clang-format) +* Improved testing +* Memory leak corrections and allocation failure handling + +### 0.4.0 - Jan 24, 2019 + +* Updated underlying hiredis version to 0.14.0 +* Added CMake files to enable Windows and Mac builds +* Fixed bug due to CLUSTER NODES reply format change + +https://github.com/heronr/hiredis-vip + +### 0.3.0 - Dec 07, 2016 + +* Support redisClustervCommand, redisClustervAppendCommand and redisClustervAsyncCommand api. (deep011) +* Add flags HIRCLUSTER_FLAG_ADD_OPENSLOT and HIRCLUSTER_FLAG_ROUTE_USE_SLOTS. (deep011) +* Support redisClusterCommandArgv related api. (deep011) +* Fix some serious bugs. (deep011) + +https://github.com/vipshop/hiredis-vip + +### 0.2.1 - Nov 24, 2015 + +This release support redis cluster api. + +* Add hiredis 0.3.1. (deep011) +* Support cluster synchronous API. (deep011) +* Support multi-key command(mget/mset/del) for redis cluster. (deep011) +* Support cluster pipelining. (deep011) +* Support cluster asynchronous API. (deep011) diff --git a/libvalkeycluster/CMakeLists.txt b/libvalkeycluster/CMakeLists.txt new file mode 100644 index 00000000..6fc039e3 --- /dev/null +++ b/libvalkeycluster/CMakeLists.txt @@ -0,0 +1,267 @@ +cmake_minimum_required(VERSION 3.11) +project(hiredis-cluster) +include(GNUInstallDirs) + +# Options +option(DOWNLOAD_HIREDIS "Download the dependency hiredis from GitHub" ON) +option(ENABLE_SSL "Enable SSL/TLS support" OFF) +option(DISABLE_TESTS "Disable compilation of test" OFF) +option(ENABLE_IPV6_TESTS "Enable IPv6 tests requiring special prerequisites" OFF) +option(ENABLE_COVERAGE "Enable test coverage reporting" OFF) + +macro(getVersionBit name) + set(VERSION_REGEX "^#define ${name} (.+)$") + file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/hircluster.h" + VERSION_BIT REGEX ${VERSION_REGEX}) + string(REGEX REPLACE ${VERSION_REGEX} "\\1" ${name} "${VERSION_BIT}") +endmacro(getVersionBit) + +# Get version information from src +getVersionBit(HIREDIS_CLUSTER_MAJOR) +getVersionBit(HIREDIS_CLUSTER_MINOR) +getVersionBit(HIREDIS_CLUSTER_PATCH) +getVersionBit(HIREDIS_CLUSTER_SONAME) +set(VERSION "${HIREDIS_CLUSTER_MAJOR}.${HIREDIS_CLUSTER_MINOR}.${HIREDIS_CLUSTER_PATCH}") +message("Detected version: ${VERSION}") + +project(hiredis-cluster + VERSION "${VERSION}" + LANGUAGES C) + +# Use plain C99 (-std=c99) without extensions like GNU C (-std=gnu99) +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_EXTENSIONS OFF) + +# Build using a sanitizer +if(USE_SANITIZER) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fno-omit-frame-pointer -fsanitize=${USE_SANITIZER}") +endif() + +if(ENABLE_COVERAGE) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --coverage -O0" ) + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage" ) +endif() + +SET(hiredis_cluster_sources + adlist.c + command.c + crc16.c + dict.c + hiarray.c + hircluster.c + hiutil.c) + +if(WIN32 OR MINGW) + add_compile_definitions(_CRT_SECURE_NO_WARNINGS WIN32_LEAN_AND_MEAN) + set(hiredis_cluster_sources + ${hiredis_cluster_sources} + hiredis_cluster.def) +endif() + +add_library(hiredis_cluster + SHARED + ${hiredis_cluster_sources}) + +if(NOT MSVC) + target_compile_options(hiredis_cluster PRIVATE -Wall -Wextra -pedantic -Werror + -Wstrict-prototypes -Wwrite-strings -Wno-missing-field-initializers) + + # Add extra defines when CMAKE_BUILD_TYPE is set to Debug + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -DHI_ASSERT_PANIC -DHI_HAVE_BACKTRACE") + # Alternative: -DHI_ASSERT_LOG) +endif() + +set_target_properties(hiredis_cluster + PROPERTIES + VERSION "${HIREDIS_CLUSTER_SONAME}") + +if(DOWNLOAD_HIREDIS) + if(${CMAKE_VERSION} VERSION_LESS "3.20") + message(FATAL_ERROR + "Downloading of the dependency 'hiredis' requires CMake >= v3.20.\n" + "Upgrade CMake or manually install 'hiredis' and use -DDOWNLOAD_HIREDIS=OFF") + endif() + + set(HIREDIS_VERSION "1.2.0") + message("Downloading dependency: hiredis v${HIREDIS_VERSION}") + + include(FetchContent) + FetchContent_Declare(hiredis + GIT_REPOSITORY https://github.com/redis/hiredis + GIT_TAG "v${HIREDIS_VERSION}" + SOURCE_DIR "${CMAKE_CURRENT_BINARY_DIR}/_deps/hiredis" + ) + + # Disable tests in hiredis + set(DISABLE_TESTS_OLD ${DISABLE_TESTS}) + set(DISABLE_TESTS ON CACHE INTERNAL "") + FetchContent_GetProperties(hiredis) + if(NOT hiredis_POPULATED) + FetchContent_Populate(hiredis) + add_subdirectory(${hiredis_SOURCE_DIR} ${hiredis_BINARY_DIR}) + endif() + set(DISABLE_TESTS ${DISABLE_TESTS_OLD} CACHE INTERNAL "") + + # Create an empty *-config.cmake for find_package + # See: https://github.com/abandonware-pjz37/cmake-find-package-include/blob/master/hooks/fetch.cmake + set(stub_dir "${CMAKE_CURRENT_BINARY_DIR}/generated/pkg") + + file(WRITE "${stub_dir}/hiredis-config.cmake" "") + file(WRITE "${stub_dir}/hiredis-config-version.cmake" "set(PACKAGE_VERSION ${HIREDIS_VERSION})") + set(hiredis_DIR ${stub_dir}) + # Set variables normally got from hiredis-config.cmake + set(hiredis_LIBRARIES hiredis::hiredis) + set(hiredis_INCLUDE_DIRS "${CMAKE_CURRENT_BINARY_DIR}/_deps") + + if(ENABLE_SSL) + file(WRITE "${stub_dir}/hiredis_ssl-config.cmake" "") + set(hiredis_ssl_DIR ${stub_dir}) + endif() + +else() + message("Expecting to find dependencies in path..") +endif() + +find_package(hiredis QUIET) +if(NOT hiredis_FOUND) + message("CMake package for 'hiredis' not found, searching for the library..") + find_library(HIREDIS_LIB hiredis REQUIRED) + find_path(HIREDIS_INCLUDES hiredis/hiredis.h) + add_library(hiredis::hiredis UNKNOWN IMPORTED GLOBAL) + set_target_properties(hiredis::hiredis PROPERTIES IMPORTED_LOCATION ${HIREDIS_LIB}) + set_target_properties(hiredis::hiredis PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${HIREDIS_INCLUDES}) +endif() + +if(NOT TARGET hiredis::hiredis) + # Add target to support older hiredis releases + add_library(hiredis::hiredis ALIAS hiredis) +endif() + +target_include_directories(hiredis_cluster PUBLIC + $ + $ + $) + +if(WIN32 OR MINGW) + target_link_libraries(hiredis_cluster PUBLIC ws2_32 hiredis::hiredis) +else() + target_link_libraries(hiredis_cluster PUBLIC hiredis::hiredis) +endif() + +if(ENABLE_SSL) + find_package(hiredis_ssl QUIET) + if(NOT hiredis_ssl_FOUND) + message("CMake package for 'hiredis_ssl' not found, searching for the library..") + find_library(HIREDIS_SSL_LIB hiredis_ssl REQUIRED) + find_path(HIREDIS_SSL_INCLUDES hiredis/hiredis_ssl.h) + add_library(hiredis::hiredis_ssl UNKNOWN IMPORTED GLOBAL) + set_target_properties(hiredis::hiredis_ssl PROPERTIES IMPORTED_LOCATION ${HIREDIS_SSL_LIB}) + set_target_properties(hiredis::hiredis_ssl PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${HIREDIS_SSL_INCLUDES}) + endif() + + if(NOT TARGET hiredis::hiredis_ssl) + # Add target to support older hiredis releases + add_library(hiredis::hiredis_ssl ALIAS hiredis_ssl) + endif() + + add_library(hiredis_cluster_ssl + SHARED hircluster_ssl.c) + set_target_properties(hiredis_cluster_ssl + PROPERTIES VERSION "${HIREDIS_CLUSTER_SONAME}") + target_link_libraries(hiredis_cluster_ssl + PRIVATE hiredis_cluster + PUBLIC hiredis::hiredis_ssl) +endif() + +if(NOT DISABLE_TESTS) + include(CTest) + add_subdirectory(tests) +endif() + +# Code formatting target +find_program(CLANG_FORMAT "clang-format") +file(GLOB_RECURSE FILES_TO_FORMAT + ${PROJECT_SOURCE_DIR}/*.[ch] +) +add_custom_target(format + COMMAND ${CLANG_FORMAT} -i ${FILES_TO_FORMAT} +) + +# Code coverage target +if(ENABLE_COVERAGE) + find_program(GCOVR "gcovr") + + add_custom_command(OUTPUT _run_gcovr + POST_BUILD + COMMAND ${GCOVR} -r ${CMAKE_SOURCE_DIR} --object-dir=${CMAKE_BINARY_DIR} --html-details coverage.html + COMMAND echo "Coverage report generated: ${CMAKE_BINARY_DIR}/coverage.html" + WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) + add_custom_target (coverage DEPENDS _run_gcovr) +endif() + +configure_file(hiredis_cluster.pc.in hiredis_cluster.pc @ONLY) + +install(TARGETS hiredis_cluster + EXPORT hiredis_cluster-targets + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) + +install(FILES hircluster.h adlist.h hiarray.h dict.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/hiredis_cluster) + +install(DIRECTORY adapters + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/hiredis_cluster) + +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/hiredis_cluster.pc + DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) + +export(EXPORT hiredis_cluster-targets + FILE ${CMAKE_CURRENT_BINARY_DIR}/hiredis_cluster-targets.cmake + NAMESPACE hiredis_cluster::) + +set(CMAKE_CONF_INSTALL_DIR share/hiredis_cluster) +set(INCLUDE_INSTALL_DIR include) +include(CMakePackageConfigHelpers) +configure_package_config_file(hiredis_cluster-config.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/hiredis_cluster-config.cmake + INSTALL_DESTINATION ${CMAKE_CONF_INSTALL_DIR} + PATH_VARS INCLUDE_INSTALL_DIR) +write_basic_package_version_file(${CMAKE_CURRENT_BINARY_DIR}/hiredis_cluster-config-version.cmake + COMPATIBILITY SameMinorVersion) + +install(EXPORT hiredis_cluster-targets + FILE hiredis_cluster-targets.cmake + NAMESPACE hiredis_cluster:: + DESTINATION ${CMAKE_CONF_INSTALL_DIR}) + +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/hiredis_cluster-config.cmake + ${CMAKE_CURRENT_BINARY_DIR}/hiredis_cluster-config-version.cmake + DESTINATION ${CMAKE_CONF_INSTALL_DIR}) + +# Install target for hiredis_cluster_ssl +if(ENABLE_SSL) + configure_file(hiredis_cluster_ssl.pc.in hiredis_cluster_ssl.pc @ONLY) + + install(TARGETS hiredis_cluster_ssl + EXPORT hiredis_cluster_ssl-targets + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) + install(FILES hircluster_ssl.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/hiredis_cluster) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/hiredis_cluster_ssl.pc + DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) + export(EXPORT hiredis_cluster_ssl-targets + FILE ${CMAKE_CURRENT_BINARY_DIR}/hiredis_cluster_ssl-targets.cmake + NAMESPACE hiredis_cluster::) + set(CMAKE_CONF_INSTALL_DIR share/hiredis_cluster_ssl) + configure_package_config_file(hiredis_cluster_ssl-config.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/hiredis_cluster_ssl-config.cmake + INSTALL_DESTINATION ${CMAKE_CONF_INSTALL_DIR} + PATH_VARS INCLUDE_INSTALL_DIR) + install(EXPORT hiredis_cluster_ssl-targets + FILE hiredis_cluster_ssl-targets.cmake + NAMESPACE hiredis_cluster:: + DESTINATION ${CMAKE_CONF_INSTALL_DIR}) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/hiredis_cluster_ssl-config.cmake + DESTINATION ${CMAKE_CONF_INSTALL_DIR}) +endif() diff --git a/libvalkeycluster/CONTRIBUTING.md b/libvalkeycluster/CONTRIBUTING.md new file mode 100644 index 00000000..47349085 --- /dev/null +++ b/libvalkeycluster/CONTRIBUTING.md @@ -0,0 +1,64 @@ +# Contributing + +:tada:Thanks for taking the time to contribute!:tada: + +The following is a set of guidelines for contributing to hiredis-cluster. + +The basics about setting up the project, building and testing is covered in +the [README](README.md). + +## Coding conventions + +### Code style + +Adhere to the existing coding style and make sure to mimic best possible. + +### Code formatting + +To have a common look-and-feel [clang-format](https://clang.llvm.org/docs/ClangFormat.html) +is used for code formatting. The formatting rules can be applied to the +source code by running `make format` in your CMake build directory. + +```sh +$ mkdir -p build; cd build +$ cmake .. +$ make format +``` + +## Test coverage + +Make sure changes are covered by tests. + +Code coverage instrumentation can be enabled using a build option and +a detailed html report can be viewed using following example: + +```sh +$ mkdir -p build; cd build +$ cmake -DENABLE_COVERAGE=ON .. +$ make all test coverage +$ xdg-open ./coverage.html +``` + +The report generation requires that [gcovr](https://gcovr.com/en/stable/index.html) +is installed in your path. Any reporting tool of choice can be used, as long as +it reads .gcda and .gcno files created during the test run. + +## Debugging + +Unfortunately, the output of tests are hidden by default. To develop or debug +tests using printouts, try `make CTEST_OUTPUT_ON_FAILURE=1 test` or call `ctest` +directly with your prefered args, such as `-V` (check the manpage for ctest), in +your CMake build directory. + +If you have problems with the linker not finding certain functions in the +Windows builds, try adding those functions to the file `hiredis_cluster.def`. +All functions called from the tests need to be in this file. + +## Submitting changes + +* Run the formatter before committing when contributing to this project (`make format`). +* Cover new behaviour with tests when possible. + +## Links + +* [clang-format](https://apt.llvm.org/) for code formatting diff --git a/libvalkeycluster/COPYING b/libvalkeycluster/COPYING new file mode 100644 index 00000000..375f0fb9 --- /dev/null +++ b/libvalkeycluster/COPYING @@ -0,0 +1,34 @@ +Copyright (c) 2009-2011, Salvatore Sanfilippo +Copyright (c) 2010-2011, Pieter Noordhuis +Copyright (c) 2015-2017, Ieshen Zheng +Copyright (c) 2020, Nick +Copyright (c) 2020-2022, Bjorn Svensson +Copyright (c) 2020-2022, Viktor Söderqvist +Copyright (c) 2021, Red Hat + +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. diff --git a/libvalkeycluster/Makefile b/libvalkeycluster/Makefile new file mode 100644 index 00000000..606eb256 --- /dev/null +++ b/libvalkeycluster/Makefile @@ -0,0 +1,188 @@ +# Hiredis-cluster Makefile, based on the Makefile in hiredis. +# +# Copyright (C) 2021 Bjorn Svensson +# Copyright (C) 2010-2011 Salvatore Sanfilippo +# Copyright (C) 2010-2011 Pieter Noordhuis +# This file is released under the BSD license, see the COPYING file + +OBJ=adlist.o command.o crc16.o dict.o hiarray.o hircluster.o hiutil.o +EXAMPLES=hiredis-cluster-example +LIBNAME=libhiredis_cluster +PKGCONFNAME=hiredis_cluster.pc + +HIREDIS_CLUSTER_MAJOR=$(shell grep HIREDIS_CLUSTER_MAJOR hircluster.h | awk '{print $$3}') +HIREDIS_CLUSTER_MINOR=$(shell grep HIREDIS_CLUSTER_MINOR hircluster.h | awk '{print $$3}') +HIREDIS_CLUSTER_PATCH=$(shell grep HIREDIS_CLUSTER_PATCH hircluster.h | awk '{print $$3}') +HIREDIS_CLUSTER_SONAME=$(shell grep HIREDIS_CLUSTER_SONAME hircluster.h | awk '{print $$3}') + +# Installation related variables and target +PREFIX?=/usr/local +INCLUDE_PATH?=include/hiredis_cluster +LIBRARY_PATH?=lib +PKGCONF_PATH?=pkgconfig +INSTALL_INCLUDE_PATH= $(DESTDIR)$(PREFIX)/$(INCLUDE_PATH) +INSTALL_LIBRARY_PATH= $(DESTDIR)$(PREFIX)/$(LIBRARY_PATH) +INSTALL_PKGCONF_PATH= $(INSTALL_LIBRARY_PATH)/$(PKGCONF_PATH) + +# Fallback to gcc when $CC is not in $PATH. +CC:=$(shell sh -c 'type $${CC%% *} >/dev/null 2>/dev/null && echo $(CC) || echo gcc') +OPTIMIZATION?=-O3 +WARNINGS=-Wall -Wextra -pedantic -Werror -Wstrict-prototypes -Wwrite-strings -Wno-missing-field-initializers +DEBUG_FLAGS?= -g -ggdb +REAL_CFLAGS=$(OPTIMIZATION) -std=c99 -fPIC $(CFLAGS) $(WARNINGS) $(DEBUG_FLAGS) +REAL_LDFLAGS=$(LDFLAGS) + +DYLIBSUFFIX=so +STLIBSUFFIX=a +DYLIB_MINOR_NAME=$(LIBNAME).$(DYLIBSUFFIX).$(HIREDIS_CLUSTER_SONAME) +DYLIB_MAJOR_NAME=$(LIBNAME).$(DYLIBSUFFIX).$(HIREDIS_CLUSTER_MAJOR) +DYLIBNAME=$(LIBNAME).$(DYLIBSUFFIX) + +DYLIB_MAKE_CMD=$(CC) -shared -Wl,-soname,$(DYLIB_MINOR_NAME) +STLIBNAME=$(LIBNAME).$(STLIBSUFFIX) +STLIB_MAKE_CMD=$(AR) rcs + +SSL_OBJ=hircluster_ssl.o +SSL_LIBNAME=libhiredis_cluster_ssl +SSL_PKGCONFNAME=hiredis_cluster_ssl.pc +SSL_INSTALLNAME=install-ssl +SSL_DYLIB_MINOR_NAME=$(SSL_LIBNAME).$(DYLIBSUFFIX).$(HIREDIS_SONAME) +SSL_DYLIB_MAJOR_NAME=$(SSL_LIBNAME).$(DYLIBSUFFIX).$(HIREDIS_MAJOR) +SSL_DYLIBNAME=$(SSL_LIBNAME).$(DYLIBSUFFIX) +SSL_STLIBNAME=$(SSL_LIBNAME).$(STLIBSUFFIX) +SSL_DYLIB_MAKE_CMD=$(CC) -shared -Wl,-soname,$(SSL_DYLIB_MINOR_NAME) + +USE_SSL?=0 +ifeq ($(USE_SSL),1) + SSL_STLIB=$(SSL_STLIBNAME) + SSL_DYLIB=$(SSL_DYLIBNAME) + SSL_PKGCONF=$(SSL_PKGCONFNAME) + SSL_INSTALL=$(SSL_INSTALLNAME) + EXAMPLES+=hiredis-cluster-example-tls +endif + +# Platform-specific overrides +uname_S := $(shell sh -c 'uname -s 2>/dev/null || echo not') + +ifeq ($(USE_SSL),1) +ifeq ($(uname_S),Linux) + REAL_LDFLAGS+=-lssl -lcrypto +endif +endif + +all: $(DYLIBNAME) $(SSL_DYLIBNAME) $(STLIBNAME) $(SSL_STLIBNAME) $(PKGCONFNAME) $(SSL_PKGCONFNAME) + +# Deps (use `USE_SSL=1 make dep` to generate this) +adlist.o: adlist.c adlist.h hiutil.h +command.o: command.c command.h adlist.h cmddef.h hiarray.h hiutil.h \ + win32.h +crc16.o: crc16.c hiutil.h +dict.o: dict.c dict.h +hiarray.o: hiarray.c hiarray.h hiutil.h +hircluster.o: hircluster.c adlist.h command.h cmddef.h dict.h hiarray.h \ + hircluster.h hiutil.h win32.h +hiutil.o: hiutil.c hiutil.h win32.h +hircluster_ssl.o: hircluster_ssl.c hircluster_ssl.h hircluster.h dict.h + +$(DYLIBNAME): $(OBJ) + $(DYLIB_MAKE_CMD) -o $(DYLIBNAME) $(OBJ) $(REAL_LDFLAGS) + +$(STLIBNAME): $(OBJ) + $(STLIB_MAKE_CMD) $(STLIBNAME) $(OBJ) + +$(SSL_DYLIBNAME): $(SSL_OBJ) + $(SSL_DYLIB_MAKE_CMD) $(DYLIB_PLUGIN) -o $(SSL_DYLIBNAME) $(SSL_OBJ) $(REAL_LDFLAGS) $(SSL_LDFLAGS) + +$(SSL_STLIBNAME): $(SSL_OBJ) + $(STLIB_MAKE_CMD) $(SSL_STLIBNAME) $(SSL_OBJ) + +$(SSL_OBJ): hircluster_ssl.c + +dynamic: $(DYLIBNAME) $(SSL_DYLIB) +static: $(STLIBNAME) $(SSL_STLIB) + +# Binaries: +hiredis-cluster-example: examples/src/example.c + $(CC) -o examples/$@ $(REAL_CFLAGS) $< $(REAL_LDFLAGS) +hiredis-cluster-example-tls: examples/src/example_tls.c + $(CC) -o examples/$@ $(REAL_CFLAGS) $< $(REAL_LDFLAGS) + +examples: $(EXAMPLES) + +.c.o: + $(CC) -c $(REAL_CFLAGS) $< + +clean: + rm -rf $(DYLIBNAME) $(STLIBNAME) $(SSL_DYLIBNAME) $(SSL_STLIBNAME) $(PKGCONFNAME) $(SSL_PKGCONFNAME) examples/hiredis-cluster-example* *.o *.gcda *.gcno *.gcov + +dep: + $(CC) $(CPPFLAGS) $(CFLAGS) -MM *.c + +INSTALL?= cp -pPR + +$(PKGCONFNAME): hircluster.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-cluster >> $@ + @echo Description: Minimalistic C client library for Redis Cluster. >> $@ + @echo Version: $(HIREDIS_CLUSTER_MAJOR).$(HIREDIS_CLUSTER_MINOR).$(HIREDIS_CLUSTER_PATCH) >> $@ + @echo Libs: -L\$${libdir} -lhiredis_cluster >> $@ + @echo Cflags: -I\$${includedir} -D_FILE_OFFSET_BITS=64 >> $@ + +$(SSL_PKGCONFNAME): hircluster_ssl.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-cluster-ssl >> $@ + @echo Description: SSL support for hiredis-cluster. >> $@ + @echo Version: $(HIREDIS_CLUSTER_MAJOR).$(HIREDIS_CLUSTER_MINOR).$(HIREDIS_CLUSTER_PATCH) >> $@ + @echo Requires: hiredis >> $@ + @echo Libs: -L\$${libdir} -lhiredis_cluster_ssl >> $@ + @echo Libs.private: -lhiredis_ssl -lssl -lcrypto >> $@ + +install: $(DYLIBNAME) $(STLIBNAME) $(PKGCONFNAME) $(SSL_INSTALL) + mkdir -p $(INSTALL_INCLUDE_PATH) $(INSTALL_INCLUDE_PATH)/adapters $(INSTALL_LIBRARY_PATH) + $(INSTALL) adlist.h dict.h hiarray.h hircluster.h hiutil.h win32.h $(INSTALL_INCLUDE_PATH) + $(INSTALL) adapters/*.h $(INSTALL_INCLUDE_PATH)/adapters + $(INSTALL) $(DYLIBNAME) $(INSTALL_LIBRARY_PATH)/$(DYLIB_MINOR_NAME) + cd $(INSTALL_LIBRARY_PATH) && ln -sf $(DYLIB_MINOR_NAME) $(DYLIBNAME) + $(INSTALL) $(STLIBNAME) $(INSTALL_LIBRARY_PATH) + mkdir -p $(INSTALL_PKGCONF_PATH) + $(INSTALL) $(PKGCONFNAME) $(INSTALL_PKGCONF_PATH) + +install-ssl: $(SSL_DYLIBNAME) $(SSL_STLIBNAME) $(SSL_PKGCONFNAME) + mkdir -p $(INSTALL_INCLUDE_PATH) $(INSTALL_LIBRARY_PATH) + $(INSTALL) hircluster_ssl.h $(INSTALL_INCLUDE_PATH) + $(INSTALL) $(SSL_DYLIBNAME) $(INSTALL_LIBRARY_PATH)/$(SSL_DYLIB_MINOR_NAME) + cd $(INSTALL_LIBRARY_PATH) && ln -sf $(SSL_DYLIB_MINOR_NAME) $(SSL_DYLIBNAME) + $(INSTALL) $(SSL_STLIBNAME) $(INSTALL_LIBRARY_PATH) + mkdir -p $(INSTALL_PKGCONF_PATH) + $(INSTALL) $(SSL_PKGCONFNAME) $(INSTALL_PKGCONF_PATH) + +32bit: + @echo "" + @echo "WARNING: if this fails under Linux you probably need to install libc6-dev-i386" + @echo "" + $(MAKE) CFLAGS="-m32" LDFLAGS="-m32" + +32bit-vars: + $(eval CFLAGS=-m32) + $(eval LDFLAGS=-m32) + +gprof: + $(MAKE) CFLAGS="-pg" LDFLAGS="-pg" + +gcov: + $(MAKE) CFLAGS="-fprofile-arcs -ftest-coverage" LDFLAGS="-fprofile-arcs" + +noopt: + $(MAKE) OPTIMIZATION="" + +.PHONY: all clean dep install 32bit 32bit-vars gprof gcov noopt diff --git a/libvalkeycluster/README.md b/libvalkeycluster/README.md new file mode 100644 index 00000000..396509b9 --- /dev/null +++ b/libvalkeycluster/README.md @@ -0,0 +1,637 @@ +# Hiredis-cluster + +Hiredis-cluster is a C client library for cluster deployments of the +[Redis](http://redis.io/) database. + +Hiredis-cluster is using [Hiredis](https://github.com/redis/hiredis) for the +connections to each Redis node. + +Hiredis-cluster is a fork of Hiredis-vip, with the following improvements: + +* The C library `hiredis` is an external dependency rather than a builtin part + of the cluster client, meaning that the latest `hiredis` can be used. +* Support for SSL/TLS introduced in Redis 6 +* Support for IPv6 +* Support authentication using AUTH +* Uses CMake (3.11+) as the primary build system, but optionally Make can be used directly +* Code style guide (using clang-format) +* Improved testing +* Memory leak corrections and allocation failure handling +* Low-level API for sending commands to specific node + +## Features + +* Redis Cluster + * Connect to a Redis cluster and run commands. + +* Multi-key commands + * Support `MSET`, `MGET` and `DEL`. + * Multi-key commands will be processed and sent to slot owning nodes. + (This breaks the atomicity of the commands if the keys reside on different + nodes so if atomicity is important, use these only with keys in the same + cluster slot.) + +* Pipelining + * Send multiple commands at once to speed up queries. + * Supports multi-key commands described in above bullet. + +* Asynchronous API + * Send commands asynchronously and let a callback handle the response. + * Needs an external event loop system that can be attached using an adapter. + +* SSL/TLS + * Connect to Redis nodes using SSL/TLS (supported from Redis 6) + +* IPv6 + * Handles clusters on IPv6 networks + +## Build instructions + +Prerequisites: + +* A C compiler (GCC or Clang) +* CMake and GNU Make (but see [Alternative build using Makefile + directly](#alternative-build-using-makefile-directly) below for how to build + without CMake) +* [hiredis >= v1.0.0](https://github.com/redis/hiredis); downloaded automatically by + default, see [build options](#build-options) to disable. +* [libevent](https://libevent.org/) (`libevent-dev` in Debian); can be avoided + if building without tests (DISABLE_TESTS=ON) +* OpenSSL (`libssl-dev` in Debian) if building with TLS support + +Hiredis-cluster will be built as a shared library `libhiredis_cluster.so` and +it depends on the hiredis shared library `libhiredis.so`. + +When SSL/TLS support is enabled an extra library `libhiredis_cluster_ssl.so` +is built, which depends on the hiredis SSL support library `libhiredis_ssl.a`. + +A user project that needs SSL/TLS support should link to both `libhiredis_cluster.so` +and `libhiredis_cluster_ssl.so` to enable the SSL/TLS configuration API. + +```sh +$ mkdir build; cd build +$ cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -DENABLE_SSL=ON .. +$ make +``` + +### Build options + +The following CMake options are available: + +* `DOWNLOAD_HIREDIS` + * `OFF` CMake will search for an already installed hiredis (for example the + the Debian package `libhiredis-dev`) for header files and linkage. + * `ON` (default) hiredis will be downloaded from + [Github](https://github.com/redis/hiredis), built and installed locally in + the build folder. +* `ENABLE_SSL` + * `OFF` (default) + * `ON` Enable SSL/TLS support and build its tests (also affect hiredis when + `DOWNLOAD_HIREDIS=ON`). +* `DISABLE_TESTS` + * `OFF` (default) + * `ON` Disable compilation of tests (also affect hiredis when + `DOWNLOAD_HIREDIS=ON`). +* `ENABLE_IPV6_TESTS` + * `OFF` (default) + * `ON` Enable IPv6 tests. Requires that IPv6 is + [setup](https://docs.docker.com/config/daemon/ipv6/) in Docker. +* `ENABLE_COVERAGE` + * `OFF` (default) + * `ON` Compile using build flags that enables the GNU coverage tool `gcov` + to provide test coverage information. This CMake option also enables a new + build target `coverage` to generate a test coverage report using + [gcovr](https://gcovr.com/en/stable/index.html). +* `USE_SANITIZER` + Compile using a specific sanitizer that detect issues. The value of this + option specifies which sanitizer to activate, but it depends on support in the + [compiler](https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html#index-fsanitize_003daddress). + Common option values are: `address`, `thread`, `undefined`, `leak` + +Options needs to be set with the `-D` flag when generating makefiles, e.g. + +`cmake -DENABLE_SSL=ON -DUSE_SANITIZER=address ..` + +### Build details + +The build uses CMake's [find_package](https://cmake.org/cmake/help/latest/command/find_package.html#search-procedure) +to search for a `hiredis` installation. CMake will search for a `hiredis` +installation in the default paths, searching for a file called `hiredis-config.cmake`. +The default search path can be altered via `CMAKE_PREFIX_PATH` or +as described in the CMake docs; a specific path can be set using a flag like: +`-Dhiredis_DIR:PATH=${MY_DIR}/hiredis/share/hiredis` + +See `examples/using_cmake_separate/build.sh` or +`examples/using_cmake_externalproject/build.sh` for alternative CMake builds. + +### Extend the list of supported commands + +The list of commands and the position of the first key in the command line is +defined in `cmddef.h` which is included in this repo. It has been generated +using the JSON files describing the syntax of each command in the Redis +repository, which makes sure hiredis-cluster supports all commands in Redis, at +least in terms of cluster routing. To add support for custom commands defined in +Redis modules, you can regenerate `cmddef.h` using the script `gencommands.py`. +Use the JSON files from Redis and any additional files on the same format as +arguments to the script. For details, see the comments inside `gencommands.py`. + +### Alternative build using Makefile directly + +When a simpler build setup is preferred a provided Makefile can be used directly +when building. A benefit of this, instead of using CMake, is that it also provides +a static library, a similar limitation exists in the CMake files in hiredis v1.0.0. + +The only option that exists in the Makefile is to enable SSL/TLS support via `USE_SSL=1` + +By default the hiredis library (and headers) installed on the system is used, +but alternative installations can be used by defining the compiler flags +`CFLAGS` and `LDFLAGS`. + +See [`examples/using_make/build.sh`](examples/using_make/build.sh) for an +example build using an alternative hiredis installation. + +Build failures like +`hircluster_ssl.h:33:10: fatal error: hiredis/hiredis_ssl.h: No such file or directory` +indicates that hiredis is not installed on the system, or that a given `CFLAGS` is wrong. +Use the previous mentioned build example as reference. + +### Running the tests + +Prerequisites: + +* Perl with [JSON module](https://metacpan.org/pod/JSON). + Can be installed using `sudo cpan JSON`. +* [Docker](https://docs.docker.com/engine/install/) + +Some tests needs a Redis cluster and that can be setup by the make targets +`start`/`stop`. The clusters will be setup using Docker and it may take a while +for them to be ready and accepting requests. Run `make start` to start the +clusters and then wait a few seconds before running `make test`. +To stop the running cluster containers run `make stop`. + +```sh +$ make start +$ make test +$ make stop +``` + +If you want to set up the Redis clusters manually they should run on localhost +using following access ports: + +| Cluster type | Access port | +| ---------------------------------- | -------: | +| IPv4 | 7000 | +| IPv4, authentication needed, password: `secretword` | 7100 | +| IPv6 | 7200 | +| IPv4, using TLS/SSL | 7300 | + +## Quick usage + +## Cluster synchronous API + +### Connecting + +The function `redisClusterContextInit` is used to create a `redisClusterContext`. +The context is where the state for connections is kept. + +The function `redisClusterSetOptionAddNodes` is used to add one or many Redis Cluster addresses. + +The functions `redisClusterSetOptionUsername` and +`redisClusterSetOptionPassword` are used to configure authentication, causing +the AUTH command to be sent on every new connection to Redis. + +For more options, see the file [`hircluster.h`](hircluster.h). + +The function `redisClusterConnect2` is used to connect to the Redis Cluster. + +The `redisClusterContext` struct has an integer `err` field that is non-zero when the connection is +in an error state. The field `errstr` will contain a string with a description of the error. +After trying to connect to Redis using `redisClusterContext` you should check the `err` field to see +if establishing the connection was successful: +```c +redisClusterContext *cc = redisClusterContextInit(); +redisClusterSetOptionAddNodes(cc, "127.0.0.1:6379,127.0.0.1:6380"); +redisClusterConnect2(cc); +if (cc != NULL && cc->err) { + printf("Error: %s\n", cc->errstr); + // handle error +} +``` + +#### Events per cluster context + +There is a hook to get notified when certain events occur. + +```c +int redisClusterSetEventCallback(redisClusterContext *cc, + void(fn)(const redisClusterContext *cc, int event, + void *privdata), + void *privdata); +``` + +The callback is called with `event` set to one of the following values: + +* `HIRCLUSTER_EVENT_SLOTMAP_UPDATED` when the slot mapping has been updated; +* `HIRCLUSTER_EVENT_READY` when the slot mapping has been fetched for the first + time and the client is ready to accept commands, useful when initiating the + client with `redisClusterAsyncConnect2()` where a client is not immediately + ready after a successful call; +* `HIRCLUSTER_EVENT_FREE_CONTEXT` when the cluster context is being freed, so + that the user can free the event privdata. + +#### Events per connection + +There is a hook to get notified about connect and reconnect attempts. +This is useful for applying socket options or access endpoint information for a connection to a particular node. +The callback is registered using the following function: + +```c +int redisClusterSetConnectCallback(redisClusterContext *cc, + void(fn)(const redisContext *c, int status)); +``` + +The callback is called just after connect, before TLS handshake and Redis authentication. + +On successful connection, `status` is set to `REDIS_OK` and the redisContext +(defined in hiredis.h) can be used, for example, to see which IP and port it's +connected to or to set socket options directly on the file descriptor which can +be accessed as `c->fd`. + +On failed connection attempt, this callback is called with `status` set to +`REDIS_ERR`. The `err` field in the `redisContext` can be used to find out +the cause of the error. + +### Sending commands + +The function `redisClusterCommand` takes a format similar to printf. +In the simplest form it is used like: +```c +reply = redisClusterCommand(clustercontext, "SET foo bar"); +``` + +The specifier `%s` interpolates a string in the command, and uses `strlen` to +determine the length of the string: +```c +reply = redisClusterCommand(clustercontext, "SET foo %s", value); +``` +Internally, hiredis-cluster splits the command in different arguments and will +convert it to the protocol used to communicate with Redis. +One or more spaces separates arguments, so you can use the specifiers +anywhere in an argument: +```c +reply = redisClusterCommand(clustercontext, "SET key:%s %s", myid, value); +``` + +Commands will be sent to the cluster node that the client perceives handling the given key. +If the cluster topology has changed the Redis node might respond with a redirection error +which the client will handle, update its slotmap and resend the command to correct node. +The reply will in this case arrive from the correct node. + +If a node is unreachable, for example if the command times out or if the connect +times out, it can indicated that there has been a failover and the node is no +longer part of the cluster. In this case, `redisClusterCommand` returns NULL and +sets `err` and `errstr` on the cluster context, but additionally, hiredis +cluster schedules a slotmap update to be performed when the next command is +sent. That means that if you try the same command again, there is a good chance +the command will be sent to another node and the command may succeed. + +### Sending multi-key commands + +Hiredis-cluster supports mget/mset/del multi-key commands. +The command will be splitted per slot and sent to correct Redis nodes. + +Example: +```c +reply = redisClusterCommand(clustercontext, "mget %s %s %s %s", key1, key2, key3, key4); +``` + +### Sending commands to a specific node + +When there is a need to send commands to a specific node, the following low-level API can be used. + +```c +reply = redisClusterCommandToNode(clustercontext, node, "DBSIZE"); +``` + +This function handles printf like arguments similar to `redisClusterCommand()`, but will +only attempt to send the command to the given node and will not perform redirects or retries. + +If the command times out or the connection to the node fails, a slotmap update +is scheduled to be performed when the next command is sent. +`redisClusterCommandToNode` also performs a slotmap update if it has previously +been scheduled. + +### Teardown + +To disconnect and free the context the following function can be used: +```c +void redisClusterFree(redisClusterContext *cc); +``` +This function closes the sockets and deallocates the context. + +### Cluster pipelining + +The function `redisClusterGetReply` is exported as part of the Hiredis API and can be used +when a reply is expected on the socket. To pipeline commands, the only things that needs +to be done is filling up the output buffer. For this cause, the following commands can be used that +are identical to the `redisClusterCommand` family, apart from not returning a reply: +```c +int redisClusterAppendCommand(redisClusterContext *cc, const char *format, ...); +int redisClusterAppendCommandArgv(redisClusterContext *cc, int argc, const char **argv); + +/* Send a command to a specific cluster node */ +int redisClusterAppendCommandToNode(redisClusterContext *cc, redisClusterNode *node, + const char *format, ...); +``` +After calling either function one or more times, `redisClusterGetReply` can be used to receive the +subsequent replies. The return value for this function is either `REDIS_OK` or `REDIS_ERR`, where +the latter means an error occurred while reading a reply. Just as with the other commands, +the `err` field in the context can be used to find out what the cause of this error is. +```c +void redisClusterReset(redisClusterContext *cc); +``` +Warning: You must call `redisClusterReset` function after one pipelining anyway. + +Warning: Calling `redisClusterReset` without pipelining first will reset all Redis connections. + +The following examples shows a simple cluster pipeline: +```c +redisReply *reply; +redisClusterAppendCommand(clusterContext,"SET foo bar"); +redisClusterAppendCommand(clusterContext,"GET foo"); +redisClusterGetReply(clusterContext,&reply); // reply for SET +freeReplyObject(reply); +redisClusterGetReply(clusterContext,&reply); // reply for GET +freeReplyObject(reply); +redisClusterReset(clusterContext); +``` + +## Cluster asynchronous API + +Hiredis-cluster comes with an asynchronous cluster API that works with many event systems. +Currently there are adapters that enables support for `libevent`, `libev`, `libuv`, `glib` +and Redis Event Library (`ae`). For usage examples, see the test programs with the different +event libraries `tests/ct_async_{libev,libuv,glib}.c`. + +The hiredis library has adapters for additional event systems that easily can be adapted +for hiredis-cluster as well. + +### Connecting + +There are two alternative ways to initiate a cluster client which also determines +how the client behaves during the initial connect. + +The first alternative is to use the function `redisClusterAsyncConnect`, which initially +connects to the cluster in a blocking fashion and waits for the slotmap before returning. +Any command sent by the user thereafter will create a new non-blocking connection, +unless a non-blocking connection already exists to the destination. +The function returns a pointer to a newly created `redisClusterAsyncContext` struct and +its `err` field should be checked to make sure the initial slotmap update was successful. + +```c +// Insufficient error handling for brevity. +redisClusterAsyncContext *acc = redisClusterAsyncConnect("127.0.0.1:6379", HIRCLUSTER_FLAG_NULL); +if (acc->err) { + printf("error: %s\n", acc->errstr); + exit(1); +} + +// Attach an event engine. In this example we use libevent. +struct event_base *base = event_base_new(); +redisClusterLibeventAttach(acc, base); +``` + +The second alternative is to use `redisClusterAsyncContextInit` and `redisClusterAsyncConnect2` +which avoids the initial blocking connect. This connection alternative requires an attached +event engine when `redisClusterAsyncConnect2` is called, but the connect and the initial +slotmap update is done in a non-blocking fashion. + +This means that commands sent directly after `redisClusterAsyncConnect2` may fail +because the initial slotmap has not yet been retrieved and the client doesn't know which +cluster node to send the command to. You may use the [eventCallback](#events-per-cluster-context) +to be notified when the slotmap is updated and the client is ready to accept commands. +An crude example of using the eventCallback can be found in [this testcase](tests/ct_async.c). + +```c +// Insufficient error handling for brevity. +redisClusterAsyncContext *acc = redisClusterAsyncContextInit(); + +// Add a cluster node address for the initial connect. +redisClusterSetOptionAddNodes(acc->cc, "127.0.0.1:6379"); + +// Attach an event engine. In this example we use libevent. +struct event_base *base = event_base_new(); +redisClusterLibeventAttach(acc, base); + +if (redisClusterAsyncConnect2(acc) != REDIS_OK) { + printf("error: %s\n", acc->errstr); + exit(1); +} +``` + +#### Events per cluster context + +Use [`redisClusterSetEventCallback`](#events-per-cluster-context) with `acc->cc` +as the context to get notified when certain events occur. + +#### Events per connection + +Because the connections that will be created are non-blocking, +the kernel is not able to instantly return if the specified +host and port is able to accept a connection. +Instead, use a connect callback to be notified when a connection +is established or failed. +Similarily, a disconnect callback can be used to be notified about +a disconnected connection (either because of an error or per user request). +The callbacks are installed using the following functions: + +```c +int redisClusterAsyncSetConnectCallback(redisClusterAsyncContext *acc, + redisConnectCallback *fn); +int redisClusterAsyncSetDisonnectCallback(redisClusterAsyncContext *acc, + redisConnectCallback *fn); +``` + +The callback functions should have the following prototype, +aliased to `redisConnectCallback`: + +```c +void(const redisAsyncContext *ac, int status); +``` + +Alternatively, if `hiredis` >= v1.1.0 is used, you set a connect callback +that will be passed a non-const `redisAsyncContext*` on invocation (e.g. +to be able to set a push callback on it). + +```c +int redisClusterAsyncSetConnectCallbackNC(redisClusterAsyncContext *acc, + redisConnectCallbackNC *fn); +``` + +The callback function should have the following prototype, +aliased to `redisConnectCallbackNC`: +```c +void(redisAsyncContext *ac, int status); +``` + +On a connection attempt, the `status` argument is set to `REDIS_OK` +when the connection was successful. +The file description of the connection socket can be retrieved +from a redisAsyncContext as `ac->c->fd`. +On a disconnect, the `status` argument is set to `REDIS_OK` +when disconnection was initiated by the user, +or `REDIS_ERR` when the disconnection was caused by an error. +When it is `REDIS_ERR`, the `err` field in the context can be accessed +to find out the cause of the error. + +You don't need to reconnect in the disconnect callback. +Hiredis-cluster will reconnect by itself when the next command for this Redis node is handled. + +Setting the connect and disconnect callbacks can only be done once per context. +For subsequent calls it will return `REDIS_ERR`. + +### Sending commands and their callbacks + +In an asynchronous cluster context, commands are automatically pipelined due to the nature of an event loop. +Therefore, unlike the synchronous API, there is only a single way to send commands. +Because commands are sent to Redis Cluster asynchronously, issuing a command requires a callback function +that is called when the reply is received. Reply callbacks should have the following prototype: +```c +void(redisClusterAsyncContext *acc, void *reply, void *privdata); +``` +The `privdata` argument can be used to carry arbitrary data to the callback from the point where +the command is initially queued for execution. + +The most commonly used functions to issue commands in an asynchronous context are: +```c +int redisClusterAsyncCommand(redisClusterAsyncContext *acc, + redisClusterCallbackFn *fn, + void *privdata, const char *format, ...); +int redisClusterAsyncCommandArgv(redisClusterAsyncContext *acc, + redisClusterCallbackFn *fn, void *privdata, + int argc, const char **argv, + const size_t *argvlen); +int redisClusterAsyncFormattedCommand(redisClusterAsyncContext *acc, + redisClusterCallbackFn *fn, + void *privdata, char *cmd, int len); +``` +These functions works like their blocking counterparts. The return value is `REDIS_OK` when the command +was successfully added to the output buffer and `REDIS_ERR` otherwise. When the connection is being +disconnected per user-request, no new commands may be added to the output buffer and `REDIS_ERR` is +returned. + +If the reply for a command with a `NULL` callback is read, it is immediately freed. When the callback +for a command is non-`NULL`, the memory is freed immediately following the callback: the reply is only +valid for the duration of the callback. + +All pending callbacks are called with a `NULL` reply when the context encountered an error. + +### Sending commands to a specific node + +When there is a need to send commands to a specific node, the following low-level API can be used. + +```c +int redisClusterAsyncCommandToNode(redisClusterAsyncContext *acc, + redisClusterNode *node, + redisClusterCallbackFn *fn, void *privdata, + const char *format, ...); +int redisClusterAsyncCommandArgvToNode(redisClusterAsyncContext *acc, + redisClusterNode *node, + redisClusterCallbackFn *fn, + void *privdata, int argc, + const char **argv, + const size_t *argvlen); +int redisClusterAsyncFormattedCommandToNode(redisClusterAsyncContext *acc, + redisClusterNode *node, + redisClusterCallbackFn *fn, + void *privdata, char *cmd, int len); +``` + +These functions will only attempt to send the command to a specific node and will not perform redirects or retries, +but communication errors will trigger a slotmap update just like the commonly used API. + +### Disconnecting + +Asynchronous cluster connections can be terminated using: +```c +void redisClusterAsyncDisconnect(redisClusterAsyncContext *acc); +``` +When this function is called, connections are **not** immediately terminated. Instead, new +commands are no longer accepted and connections are only terminated when all pending commands +have been written to a socket, their respective replies have been read and their respective +callbacks have been executed. After this, the disconnection callback is executed with the +`REDIS_OK` status and the context object is freed. + +### Using event library *X* + +There are a few hooks that need to be set on the cluster context object after it is created. +See the `adapters/` directory for bindings to *libevent* and a range of other event libraries. + +## Other details + +### Cluster node iterator + +A `redisClusterNodeIterator` can be used to iterate on all known master nodes in a cluster context. +First it needs to be initiated using `redisClusterInitNodeIterator()` and then you can repeatedly +call `redisClusterNodeNext()` to get the next node from the iterator. + +```c +void redisClusterInitNodeIterator(redisClusterNodeIterator *iter, + redisClusterContext *cc); +redisClusterNode *redisClusterNodeNext(redisClusterNodeIterator *iter); +``` + +The iterator will handle changes due to slotmap updates by restarting the iteration, but on the new +set of master nodes. There is no bookkeeping for already iterated nodes when a restart is triggered, +which means that a node can be iterated over more than once depending on when the slotmap update happened +and the change of cluster nodes. + +Note that when `redisClusterCommandToNode` is called, a slotmap update can +happen if it has been scheduled by the previous command, for example if the +previous call to `redisClusterCommandToNode` timed out or the node wasn't +reachable. + +To detect when the slotmap has been updated, you can check if the iterator's +slotmap version (`iter.route_version`) is equal to the current cluster context's +slotmap version (`cc->route_version`). If it isn't, it means that the slotmap +has been updated and the iterator will restart itself at the next call to +`redisClusterNodeNext`. + +Another way to detect that the slotmap has been updated is to [register an event +callback](#events-per-cluster-context) and look for the event +`HIRCLUSTER_EVENT_SLOTMAP_UPDATED`. + +### Random number generator + +This library uses [random()](https://linux.die.net/man/3/random) while selecting +a node used for requesting the cluster topology (slotmap). A user should seed +the random number generator using [srandom()](https://linux.die.net/man/3/srandom) +to get less predictability in the node selection. + +### Allocator injection + +Hiredis-cluster uses hiredis allocation structure with configurable allocation and deallocation functions. By default they just point to libc (`malloc`, `calloc`, `realloc`, etc). + +#### Overriding + +If you have your own allocator or if you expect an abort in out-of-memory cases, +you can configure the used functions in the following way: + +```c +hiredisAllocFuncs myfuncs = { + .mallocFn = my_malloc, + .callocFn = my_calloc, + .reallocFn = my_realloc, + .strdupFn = my_strdup, + .freeFn = my_free, +}; + +// Override allocators (function returns current allocators if needed) +hiredisAllocFuncs orig = hiredisSetAllocators(&myfuncs); +``` + +To reset the allocators to their default libc functions simply call: + +```c +hiredisResetAllocators(); +``` diff --git a/libvalkeycluster/adapters/ae.h b/libvalkeycluster/adapters/ae.h new file mode 100644 index 00000000..9314c2b7 --- /dev/null +++ b/libvalkeycluster/adapters/ae.h @@ -0,0 +1,54 @@ +/* + * 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_CLUSTER_AE_H__ +#define __HIREDIS_CLUSTER_AE_H__ + +#include "../hircluster.h" +#include + +static int redisAeAttach_link(redisAsyncContext *ac, void *base) { + return redisAeAttach((aeEventLoop *)base, ac); +} + +static int redisClusterAeAttach(aeEventLoop *loop, + redisClusterAsyncContext *acc) { + + if (acc == NULL || loop == NULL) { + return REDIS_ERR; + } + + acc->adapter = loop; + acc->attach_fn = redisAeAttach_link; + + return REDIS_OK; +} + +#endif diff --git a/libvalkeycluster/adapters/glib.h b/libvalkeycluster/adapters/glib.h new file mode 100644 index 00000000..c3718a21 --- /dev/null +++ b/libvalkeycluster/adapters/glib.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2021, Björn Svensson + * + * 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_CLUSTER_GLIB_H__ +#define __HIREDIS_CLUSTER_GLIB_H__ + +#include "../hircluster.h" +#include + +typedef struct redisClusterGlibAdapter { + GMainContext *context; +} redisClusterGlibAdapter; + +static int redisGlibAttach_link(redisAsyncContext *ac, void *adapter) { + GMainContext *context = ((redisClusterGlibAdapter *)adapter)->context; + if (g_source_attach(redis_source_new(ac), context) > 0) { + return REDIS_OK; + } + return REDIS_ERR; +} + +static int redisClusterGlibAttach(redisClusterAsyncContext *acc, + redisClusterGlibAdapter *adapter) { + if (acc == NULL || adapter == NULL) { + return REDIS_ERR; + } + + acc->adapter = adapter; + acc->attach_fn = redisGlibAttach_link; + + return REDIS_OK; +} + +#endif diff --git a/libvalkeycluster/adapters/libev.h b/libvalkeycluster/adapters/libev.h new file mode 100644 index 00000000..084fd34f --- /dev/null +++ b/libvalkeycluster/adapters/libev.h @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2021, Björn Svensson + * + * 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_CLUSTER_LIBEV_H__ +#define __HIREDIS_CLUSTER_LIBEV_H__ + +#include "../hircluster.h" +#include + +static int redisLibevAttach_link(redisAsyncContext *ac, void *loop) { + return redisLibevAttach((struct ev_loop *)loop, ac); +} + +static int redisClusterLibevAttach(redisClusterAsyncContext *acc, + struct ev_loop *loop) { + if (loop == NULL || acc == NULL) { + return REDIS_ERR; + } + + acc->adapter = loop; + acc->attach_fn = redisLibevAttach_link; + + return REDIS_OK; +} + +#endif diff --git a/libvalkeycluster/adapters/libevent.h b/libvalkeycluster/adapters/libevent.h new file mode 100644 index 00000000..87d312f0 --- /dev/null +++ b/libvalkeycluster/adapters/libevent.h @@ -0,0 +1,54 @@ +/* + * 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_CLUSTER_LIBEVENT_H__ +#define __HIREDIS_CLUSTER_LIBEVENT_H__ + +#include "../hircluster.h" +#include + +static int redisLibeventAttach_link(redisAsyncContext *ac, void *base) { + return redisLibeventAttach(ac, (struct event_base *)base); +} + +static int redisClusterLibeventAttach(redisClusterAsyncContext *acc, + struct event_base *base) { + + if (acc == NULL || base == NULL) { + return REDIS_ERR; + } + + acc->adapter = base; + acc->attach_fn = redisLibeventAttach_link; + + return REDIS_OK; +} + +#endif diff --git a/libvalkeycluster/adapters/libuv.h b/libvalkeycluster/adapters/libuv.h new file mode 100644 index 00000000..527419e2 --- /dev/null +++ b/libvalkeycluster/adapters/libuv.h @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2021, Red Hat + * + * 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_CLUSTER_LIBUV_H__ +#define __HIREDIS_CLUSTER_LIBUV_H__ + +#include "../hircluster.h" +#include + +static int redisLibuvAttach_link(redisAsyncContext *ac, void *loop) { + return redisLibuvAttach(ac, (uv_loop_t *)loop); +} + +static int redisClusterLibuvAttach(redisClusterAsyncContext *acc, + uv_loop_t *loop) { + + if (acc == NULL || loop == NULL) { + return REDIS_ERR; + } + + acc->adapter = loop; + acc->attach_fn = redisLibuvAttach_link; + + return REDIS_OK; +} + +#endif diff --git a/libvalkeycluster/adlist.c b/libvalkeycluster/adlist.c new file mode 100644 index 00000000..18b04d44 --- /dev/null +++ b/libvalkeycluster/adlist.c @@ -0,0 +1,331 @@ +/* adlist.c - A generic doubly linked list implementation + * + * Copyright (c) 2006-2010, 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. + */ +#include +#include + +#include "adlist.h" +#include "hiutil.h" + +/* Create a new list. The created list can be freed with + * AlFreeList(), but private value of every node need to be freed + * by the user before to call AlFreeList(). + * + * On error, NULL is returned. Otherwise the pointer to the new list. */ +hilist *listCreate(void) { + struct hilist *list; + + if ((list = hi_malloc(sizeof(*list))) == NULL) + return NULL; + list->head = list->tail = NULL; + list->len = 0; + list->dup = NULL; + list->free = NULL; + list->match = NULL; + return list; +} + +/* Free the whole list. + * + * This function can't fail. */ +void listRelease(hilist *list) { + unsigned long len; + listNode *current, *next; + + current = list->head; + len = list->len; + while (len--) { + next = current->next; + if (list->free) + list->free(current->value); + hi_free(current); + current = next; + } + hi_free(list); +} + +/* Add a new node to the list, to head, containing the specified 'value' + * pointer as value. + * + * On error, NULL is returned and no operation is performed (i.e. the + * list remains unaltered). + * On success the 'list' pointer you pass to the function is returned. */ +hilist *listAddNodeHead(hilist *list, void *value) { + listNode *node; + + if ((node = hi_malloc(sizeof(*node))) == NULL) + return NULL; + node->value = value; + if (list->len == 0) { + list->head = list->tail = node; + node->prev = node->next = NULL; + } else { + node->prev = NULL; + node->next = list->head; + list->head->prev = node; + list->head = node; + } + list->len++; + return list; +} + +/* Add a new node to the list, to tail, containing the specified 'value' + * pointer as value. + * + * On error, NULL is returned and no operation is performed (i.e. the + * list remains unaltered). + * On success the 'list' pointer you pass to the function is returned. */ +hilist *listAddNodeTail(hilist *list, void *value) { + listNode *node; + + if ((node = hi_malloc(sizeof(*node))) == NULL) + return NULL; + node->value = value; + if (list->len == 0) { + list->head = list->tail = node; + node->prev = node->next = NULL; + } else { + node->prev = list->tail; + node->next = NULL; + list->tail->next = node; + list->tail = node; + } + list->len++; + return list; +} + +hilist *listInsertNode(hilist *list, listNode *old_node, void *value, + int after) { + listNode *node; + + if ((node = hi_malloc(sizeof(*node))) == NULL) + return NULL; + node->value = value; + if (after) { + node->prev = old_node; + node->next = old_node->next; + if (list->tail == old_node) { + list->tail = node; + } + } else { + node->next = old_node; + node->prev = old_node->prev; + if (list->head == old_node) { + list->head = node; + } + } + if (node->prev != NULL) { + node->prev->next = node; + } + if (node->next != NULL) { + node->next->prev = node; + } + list->len++; + return list; +} + +/* Remove the specified node from the specified list. + * It's up to the caller to free the private value of the node. + * + * This function can't fail. */ +void listDelNode(hilist *list, listNode *node) { + if (node->prev) + node->prev->next = node->next; + else + list->head = node->next; + if (node->next) + node->next->prev = node->prev; + else + list->tail = node->prev; + if (list->free) + list->free(node->value); + hi_free(node); + list->len--; +} + +/* Returns a list iterator 'iter'. After the initialization every + * call to listNext() will return the next element of the list. + * + * This function can't fail. */ +listIter *listGetIterator(hilist *list, int direction) { + listIter *iter; + + if ((iter = hi_malloc(sizeof(*iter))) == NULL) + return NULL; + if (direction == AL_START_HEAD) + iter->next = list->head; + else + iter->next = list->tail; + iter->direction = direction; + return iter; +} + +/* Release the iterator memory */ +void listReleaseIterator(listIter *iter) { hi_free(iter); } + +/* Create an iterator in the list private iterator structure */ +void listRewind(hilist *list, listIter *li) { + li->next = list->head; + li->direction = AL_START_HEAD; +} + +void listRewindTail(hilist *list, listIter *li) { + li->next = list->tail; + li->direction = AL_START_TAIL; +} + +/* Return the next element of an iterator. + * It's valid to remove the currently returned element using + * listDelNode(), but not to remove other elements. + * + * The function returns a pointer to the next element of the list, + * or NULL if there are no more elements, so the classical usage patter + * is: + * + * iter = listGetIterator(list,); + * while ((node = listNext(iter)) != NULL) { + * doSomethingWith(listNodeValue(node)); + * } + * + * */ +listNode *listNext(listIter *iter) { + listNode *current = iter->next; + + if (current != NULL) { + if (iter->direction == AL_START_HEAD) + iter->next = current->next; + else + iter->next = current->prev; + } + return current; +} + +/* Duplicate the whole list. On out of memory NULL is returned. + * On success a copy of the original list is returned. + * + * The 'Dup' method set with listSetDupMethod() function is used + * to copy the node value. Otherwise the same pointer value of + * the original node is used as value of the copied node. + * + * The original list both on success or error is never modified. */ +hilist *listDup(hilist *orig) { + hilist *copy; + listIter iter; + listNode *node; + + if ((copy = listCreate()) == NULL) + return NULL; + copy->dup = orig->dup; + copy->free = orig->free; + copy->match = orig->match; + listRewind(orig, &iter); + while ((node = listNext(&iter)) != NULL) { + void *value; + + if (copy->dup) { + value = copy->dup(node->value); + if (value == NULL) { + listRelease(copy); + return NULL; + } + } else + value = node->value; + if (listAddNodeTail(copy, value) == NULL) { + listRelease(copy); + return NULL; + } + } + return copy; +} + +/* Search the list for a node matching a given key. + * The match is performed using the 'match' method + * set with listSetMatchMethod(). If no 'match' method + * is set, the 'value' pointer of every node is directly + * compared with the 'key' pointer. + * + * On success the first matching node pointer is returned + * (search starts from head). If no matching node exists + * NULL is returned. */ +listNode *listSearchKey(hilist *list, void *key) { + listIter iter; + listNode *node; + + listRewind(list, &iter); + while ((node = listNext(&iter)) != NULL) { + if (list->match) { + if (list->match(node->value, key)) { + return node; + } + } else { + if (key == node->value) { + return node; + } + } + } + return NULL; +} + +/* Return the element at the specified zero-based index + * where 0 is the head, 1 is the element next to head + * and so on. Negative integers are used in order to count + * from the tail, -1 is the last element, -2 the penultimate + * and so on. If the index is out of range NULL is returned. */ +listNode *listIndex(hilist *list, long index) { + listNode *n; + + if (index < 0) { + index = (-index) - 1; + n = list->tail; + while (index-- && n) + n = n->prev; + } else { + n = list->head; + while (index-- && n) + n = n->next; + } + return n; +} + +/* Rotate the list removing the tail node and inserting it to the head. */ +void listRotate(hilist *list) { + listNode *tail = list->tail; + + if (listLength(list) <= 1) + return; + + /* Detach current tail */ + list->tail = tail->prev; + list->tail->next = NULL; + /* Move it as head */ + list->head->prev = tail; + tail->prev = NULL; + tail->next = list->head; + list->head = tail; +} diff --git a/libvalkeycluster/adlist.h b/libvalkeycluster/adlist.h new file mode 100644 index 00000000..982894e4 --- /dev/null +++ b/libvalkeycluster/adlist.h @@ -0,0 +1,94 @@ +/* adlist.h - A generic doubly linked list implementation + * + * Copyright (c) 2006-2012, 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. + */ + +#ifndef __ADLIST_H__ +#define __ADLIST_H__ + +/* Node, List, and Iterator are the only data structures used currently. */ + +typedef struct listNode { + struct listNode *prev; + struct listNode *next; + void *value; +} listNode; + +typedef struct listIter { + listNode *next; + int direction; +} listIter; + +typedef struct hilist { + listNode *head; + listNode *tail; + void *(*dup)(void *ptr); + void (*free)(void *ptr); + int (*match)(void *ptr, void *key); + unsigned long len; +} hilist; + +/* Functions implemented as macros */ +#define listLength(l) ((l)->len) +#define listFirst(l) ((l)->head) +#define listLast(l) ((l)->tail) +#define listPrevNode(n) ((n)->prev) +#define listNextNode(n) ((n)->next) +#define listNodeValue(n) ((n)->value) + +#define listSetDupMethod(l, m) ((l)->dup = (m)) +#define listSetFreeMethod(l, m) ((l)->free = (m)) +#define listSetMatchMethod(l, m) ((l)->match = (m)) + +#define listGetDupMethod(l) ((l)->dup) +#define listGetFree(l) ((l)->free) +#define listGetMatchMethod(l) ((l)->match) + +/* Prototypes */ +hilist *listCreate(void); +void listRelease(hilist *list); +hilist *listAddNodeHead(hilist *list, void *value); +hilist *listAddNodeTail(hilist *list, void *value); +hilist *listInsertNode(hilist *list, listNode *old_node, void *value, + int after); +void listDelNode(hilist *list, listNode *node); +listIter *listGetIterator(hilist *list, int direction); +listNode *listNext(listIter *iter); +void listReleaseIterator(listIter *iter); +hilist *listDup(hilist *orig); +listNode *listSearchKey(hilist *list, void *key); +listNode *listIndex(hilist *list, long index); +void listRewind(hilist *list, listIter *li); +void listRewindTail(hilist *list, listIter *li); +void listRotate(hilist *list); + +/* Directions for iterators */ +#define AL_START_HEAD 0 +#define AL_START_TAIL 1 + +#endif /* __ADLIST_H__ */ diff --git a/libvalkeycluster/cmddef.h b/libvalkeycluster/cmddef.h new file mode 100644 index 00000000..3735204d --- /dev/null +++ b/libvalkeycluster/cmddef.h @@ -0,0 +1,379 @@ +/* This file was generated using gencommands.py */ + +/* clang-format off */ +COMMAND(ACL_CAT, "ACL", "CAT", -2, NONE, 0) +COMMAND(ACL_DELUSER, "ACL", "DELUSER", -3, NONE, 0) +COMMAND(ACL_DRYRUN, "ACL", "DRYRUN", -4, NONE, 0) +COMMAND(ACL_GENPASS, "ACL", "GENPASS", -2, NONE, 0) +COMMAND(ACL_GETUSER, "ACL", "GETUSER", 3, NONE, 0) +COMMAND(ACL_HELP, "ACL", "HELP", 2, NONE, 0) +COMMAND(ACL_LIST, "ACL", "LIST", 2, NONE, 0) +COMMAND(ACL_LOAD, "ACL", "LOAD", 2, NONE, 0) +COMMAND(ACL_LOG, "ACL", "LOG", -2, NONE, 0) +COMMAND(ACL_SAVE, "ACL", "SAVE", 2, NONE, 0) +COMMAND(ACL_SETUSER, "ACL", "SETUSER", -3, NONE, 0) +COMMAND(ACL_USERS, "ACL", "USERS", 2, NONE, 0) +COMMAND(ACL_WHOAMI, "ACL", "WHOAMI", 2, NONE, 0) +COMMAND(APPEND, "APPEND", NULL, 3, INDEX, 1) +COMMAND(ASKING, "ASKING", NULL, 1, NONE, 0) +COMMAND(AUTH, "AUTH", NULL, -2, NONE, 0) +COMMAND(BGREWRITEAOF, "BGREWRITEAOF", NULL, 1, NONE, 0) +COMMAND(BGSAVE, "BGSAVE", NULL, -1, NONE, 0) +COMMAND(BITCOUNT, "BITCOUNT", NULL, -2, INDEX, 1) +COMMAND(BITFIELD, "BITFIELD", NULL, -2, INDEX, 1) +COMMAND(BITFIELD_RO, "BITFIELD_RO", NULL, -2, INDEX, 1) +COMMAND(BITOP, "BITOP", NULL, -4, INDEX, 2) +COMMAND(BITPOS, "BITPOS", NULL, -3, INDEX, 1) +COMMAND(BLMOVE, "BLMOVE", NULL, 6, INDEX, 1) +COMMAND(BLMPOP, "BLMPOP", NULL, -5, KEYNUM, 2) +COMMAND(BLPOP, "BLPOP", NULL, -3, INDEX, 1) +COMMAND(BRPOP, "BRPOP", NULL, -3, INDEX, 1) +COMMAND(BRPOPLPUSH, "BRPOPLPUSH", NULL, 4, INDEX, 1) +COMMAND(BZMPOP, "BZMPOP", NULL, -5, KEYNUM, 2) +COMMAND(BZPOPMAX, "BZPOPMAX", NULL, -3, INDEX, 1) +COMMAND(BZPOPMIN, "BZPOPMIN", NULL, -3, INDEX, 1) +COMMAND(CLIENT_CACHING, "CLIENT", "CACHING", 3, NONE, 0) +COMMAND(CLIENT_GETNAME, "CLIENT", "GETNAME", 2, NONE, 0) +COMMAND(CLIENT_GETREDIR, "CLIENT", "GETREDIR", 2, NONE, 0) +COMMAND(CLIENT_HELP, "CLIENT", "HELP", 2, NONE, 0) +COMMAND(CLIENT_ID, "CLIENT", "ID", 2, NONE, 0) +COMMAND(CLIENT_INFO, "CLIENT", "INFO", 2, NONE, 0) +COMMAND(CLIENT_KILL, "CLIENT", "KILL", -3, NONE, 0) +COMMAND(CLIENT_LIST, "CLIENT", "LIST", -2, NONE, 0) +COMMAND(CLIENT_NO_EVICT, "CLIENT", "NO-EVICT", 3, NONE, 0) +COMMAND(CLIENT_NO_TOUCH, "CLIENT", "NO-TOUCH", 3, NONE, 0) +COMMAND(CLIENT_PAUSE, "CLIENT", "PAUSE", -3, NONE, 0) +COMMAND(CLIENT_REPLY, "CLIENT", "REPLY", 3, NONE, 0) +COMMAND(CLIENT_SETINFO, "CLIENT", "SETINFO", 4, NONE, 0) +COMMAND(CLIENT_SETNAME, "CLIENT", "SETNAME", 3, NONE, 0) +COMMAND(CLIENT_TRACKING, "CLIENT", "TRACKING", -3, NONE, 0) +COMMAND(CLIENT_TRACKINGINFO, "CLIENT", "TRACKINGINFO", 2, NONE, 0) +COMMAND(CLIENT_UNBLOCK, "CLIENT", "UNBLOCK", -3, NONE, 0) +COMMAND(CLIENT_UNPAUSE, "CLIENT", "UNPAUSE", 2, NONE, 0) +COMMAND(CLUSTER_ADDSLOTS, "CLUSTER", "ADDSLOTS", -3, NONE, 0) +COMMAND(CLUSTER_ADDSLOTSRANGE, "CLUSTER", "ADDSLOTSRANGE", -4, NONE, 0) +COMMAND(CLUSTER_BUMPEPOCH, "CLUSTER", "BUMPEPOCH", 2, NONE, 0) +COMMAND(CLUSTER_COUNT_FAILURE_REPORTS, "CLUSTER", "COUNT-FAILURE-REPORTS", 3, NONE, 0) +COMMAND(CLUSTER_COUNTKEYSINSLOT, "CLUSTER", "COUNTKEYSINSLOT", 3, NONE, 0) +COMMAND(CLUSTER_DELSLOTS, "CLUSTER", "DELSLOTS", -3, NONE, 0) +COMMAND(CLUSTER_DELSLOTSRANGE, "CLUSTER", "DELSLOTSRANGE", -4, NONE, 0) +COMMAND(CLUSTER_FAILOVER, "CLUSTER", "FAILOVER", -2, NONE, 0) +COMMAND(CLUSTER_FLUSHSLOTS, "CLUSTER", "FLUSHSLOTS", 2, NONE, 0) +COMMAND(CLUSTER_FORGET, "CLUSTER", "FORGET", 3, NONE, 0) +COMMAND(CLUSTER_GETKEYSINSLOT, "CLUSTER", "GETKEYSINSLOT", 4, NONE, 0) +COMMAND(CLUSTER_HELP, "CLUSTER", "HELP", 2, NONE, 0) +COMMAND(CLUSTER_INFO, "CLUSTER", "INFO", 2, NONE, 0) +COMMAND(CLUSTER_KEYSLOT, "CLUSTER", "KEYSLOT", 3, NONE, 0) +COMMAND(CLUSTER_LINKS, "CLUSTER", "LINKS", 2, NONE, 0) +COMMAND(CLUSTER_MEET, "CLUSTER", "MEET", -4, NONE, 0) +COMMAND(CLUSTER_MYID, "CLUSTER", "MYID", 2, NONE, 0) +COMMAND(CLUSTER_MYSHARDID, "CLUSTER", "MYSHARDID", 2, NONE, 0) +COMMAND(CLUSTER_NODES, "CLUSTER", "NODES", 2, NONE, 0) +COMMAND(CLUSTER_REPLICAS, "CLUSTER", "REPLICAS", 3, NONE, 0) +COMMAND(CLUSTER_REPLICATE, "CLUSTER", "REPLICATE", 3, NONE, 0) +COMMAND(CLUSTER_RESET, "CLUSTER", "RESET", -2, NONE, 0) +COMMAND(CLUSTER_SAVECONFIG, "CLUSTER", "SAVECONFIG", 2, NONE, 0) +COMMAND(CLUSTER_SET_CONFIG_EPOCH, "CLUSTER", "SET-CONFIG-EPOCH", 3, NONE, 0) +COMMAND(CLUSTER_SETSLOT, "CLUSTER", "SETSLOT", -4, NONE, 0) +COMMAND(CLUSTER_SHARDS, "CLUSTER", "SHARDS", 2, NONE, 0) +COMMAND(CLUSTER_SLAVES, "CLUSTER", "SLAVES", 3, NONE, 0) +COMMAND(CLUSTER_SLOTS, "CLUSTER", "SLOTS", 2, NONE, 0) +COMMAND(COMMAND_COUNT, "COMMAND", "COUNT", 2, NONE, 0) +COMMAND(COMMAND_DOCS, "COMMAND", "DOCS", -2, NONE, 0) +COMMAND(COMMAND_GETKEYS, "COMMAND", "GETKEYS", -3, NONE, 0) +COMMAND(COMMAND_GETKEYSANDFLAGS, "COMMAND", "GETKEYSANDFLAGS", -3, NONE, 0) +COMMAND(COMMAND_HELP, "COMMAND", "HELP", 2, NONE, 0) +COMMAND(COMMAND_INFO, "COMMAND", "INFO", -2, NONE, 0) +COMMAND(COMMAND_LIST, "COMMAND", "LIST", -2, NONE, 0) +COMMAND(CONFIG_GET, "CONFIG", "GET", -3, NONE, 0) +COMMAND(CONFIG_HELP, "CONFIG", "HELP", 2, NONE, 0) +COMMAND(CONFIG_RESETSTAT, "CONFIG", "RESETSTAT", 2, NONE, 0) +COMMAND(CONFIG_REWRITE, "CONFIG", "REWRITE", 2, NONE, 0) +COMMAND(CONFIG_SET, "CONFIG", "SET", -4, NONE, 0) +COMMAND(COPY, "COPY", NULL, -3, INDEX, 1) +COMMAND(DBSIZE, "DBSIZE", NULL, 1, NONE, 0) +COMMAND(DEBUG, "DEBUG", NULL, -2, NONE, 0) +COMMAND(DECR, "DECR", NULL, 2, INDEX, 1) +COMMAND(DECRBY, "DECRBY", NULL, 3, INDEX, 1) +COMMAND(DEL, "DEL", NULL, -2, INDEX, 1) +COMMAND(DISCARD, "DISCARD", NULL, 1, NONE, 0) +COMMAND(DUMP, "DUMP", NULL, 2, INDEX, 1) +COMMAND(ECHO, "ECHO", NULL, 2, NONE, 0) +COMMAND(EVAL, "EVAL", NULL, -3, KEYNUM, 2) +COMMAND(EVALSHA, "EVALSHA", NULL, -3, KEYNUM, 2) +COMMAND(EVALSHA_RO, "EVALSHA_RO", NULL, -3, KEYNUM, 2) +COMMAND(EVAL_RO, "EVAL_RO", NULL, -3, KEYNUM, 2) +COMMAND(EXEC, "EXEC", NULL, 1, NONE, 0) +COMMAND(EXISTS, "EXISTS", NULL, -2, INDEX, 1) +COMMAND(EXPIRE, "EXPIRE", NULL, -3, INDEX, 1) +COMMAND(EXPIREAT, "EXPIREAT", NULL, -3, INDEX, 1) +COMMAND(EXPIRETIME, "EXPIRETIME", NULL, 2, INDEX, 1) +COMMAND(FAILOVER, "FAILOVER", NULL, -1, NONE, 0) +COMMAND(FCALL, "FCALL", NULL, -3, KEYNUM, 2) +COMMAND(FCALL_RO, "FCALL_RO", NULL, -3, KEYNUM, 2) +COMMAND(FLUSHALL, "FLUSHALL", NULL, -1, NONE, 0) +COMMAND(FLUSHDB, "FLUSHDB", NULL, -1, NONE, 0) +COMMAND(FUNCTION_DELETE, "FUNCTION", "DELETE", 3, NONE, 0) +COMMAND(FUNCTION_DUMP, "FUNCTION", "DUMP", 2, NONE, 0) +COMMAND(FUNCTION_FLUSH, "FUNCTION", "FLUSH", -2, NONE, 0) +COMMAND(FUNCTION_HELP, "FUNCTION", "HELP", 2, NONE, 0) +COMMAND(FUNCTION_KILL, "FUNCTION", "KILL", 2, NONE, 0) +COMMAND(FUNCTION_LIST, "FUNCTION", "LIST", -2, NONE, 0) +COMMAND(FUNCTION_LOAD, "FUNCTION", "LOAD", -3, NONE, 0) +COMMAND(FUNCTION_RESTORE, "FUNCTION", "RESTORE", -3, NONE, 0) +COMMAND(FUNCTION_STATS, "FUNCTION", "STATS", 2, NONE, 0) +COMMAND(GEOADD, "GEOADD", NULL, -5, INDEX, 1) +COMMAND(GEODIST, "GEODIST", NULL, -4, INDEX, 1) +COMMAND(GEOHASH, "GEOHASH", NULL, -2, INDEX, 1) +COMMAND(GEOPOS, "GEOPOS", NULL, -2, INDEX, 1) +COMMAND(GEORADIUS, "GEORADIUS", NULL, -6, INDEX, 1) +COMMAND(GEORADIUSBYMEMBER, "GEORADIUSBYMEMBER", NULL, -5, INDEX, 1) +COMMAND(GEORADIUSBYMEMBER_RO, "GEORADIUSBYMEMBER_RO", NULL, -5, INDEX, 1) +COMMAND(GEORADIUS_RO, "GEORADIUS_RO", NULL, -6, INDEX, 1) +COMMAND(GEOSEARCH, "GEOSEARCH", NULL, -7, INDEX, 1) +COMMAND(GEOSEARCHSTORE, "GEOSEARCHSTORE", NULL, -8, INDEX, 1) +COMMAND(GET, "GET", NULL, 2, INDEX, 1) +COMMAND(GETBIT, "GETBIT", NULL, 3, INDEX, 1) +COMMAND(GETDEL, "GETDEL", NULL, 2, INDEX, 1) +COMMAND(GETEX, "GETEX", NULL, -2, INDEX, 1) +COMMAND(GETRANGE, "GETRANGE", NULL, 4, INDEX, 1) +COMMAND(GETSET, "GETSET", NULL, 3, INDEX, 1) +COMMAND(HDEL, "HDEL", NULL, -3, INDEX, 1) +COMMAND(HELLO, "HELLO", NULL, -1, NONE, 0) +COMMAND(HEXISTS, "HEXISTS", NULL, 3, INDEX, 1) +COMMAND(HGET, "HGET", NULL, 3, INDEX, 1) +COMMAND(HGETALL, "HGETALL", NULL, 2, INDEX, 1) +COMMAND(HINCRBY, "HINCRBY", NULL, 4, INDEX, 1) +COMMAND(HINCRBYFLOAT, "HINCRBYFLOAT", NULL, 4, INDEX, 1) +COMMAND(HKEYS, "HKEYS", NULL, 2, INDEX, 1) +COMMAND(HLEN, "HLEN", NULL, 2, INDEX, 1) +COMMAND(HMGET, "HMGET", NULL, -3, INDEX, 1) +COMMAND(HMSET, "HMSET", NULL, -4, INDEX, 1) +COMMAND(HRANDFIELD, "HRANDFIELD", NULL, -2, INDEX, 1) +COMMAND(HSCAN, "HSCAN", NULL, -3, INDEX, 1) +COMMAND(HSET, "HSET", NULL, -4, INDEX, 1) +COMMAND(HSETNX, "HSETNX", NULL, 4, INDEX, 1) +COMMAND(HSTRLEN, "HSTRLEN", NULL, 3, INDEX, 1) +COMMAND(HVALS, "HVALS", NULL, 2, INDEX, 1) +COMMAND(INCR, "INCR", NULL, 2, INDEX, 1) +COMMAND(INCRBY, "INCRBY", NULL, 3, INDEX, 1) +COMMAND(INCRBYFLOAT, "INCRBYFLOAT", NULL, 3, INDEX, 1) +COMMAND(INFO, "INFO", NULL, -1, NONE, 0) +COMMAND(KEYS, "KEYS", NULL, 2, NONE, 0) +COMMAND(LASTSAVE, "LASTSAVE", NULL, 1, NONE, 0) +COMMAND(LATENCY_DOCTOR, "LATENCY", "DOCTOR", 2, NONE, 0) +COMMAND(LATENCY_GRAPH, "LATENCY", "GRAPH", 3, NONE, 0) +COMMAND(LATENCY_HELP, "LATENCY", "HELP", 2, NONE, 0) +COMMAND(LATENCY_HISTOGRAM, "LATENCY", "HISTOGRAM", -2, NONE, 0) +COMMAND(LATENCY_HISTORY, "LATENCY", "HISTORY", 3, NONE, 0) +COMMAND(LATENCY_LATEST, "LATENCY", "LATEST", 2, NONE, 0) +COMMAND(LATENCY_RESET, "LATENCY", "RESET", -2, NONE, 0) +COMMAND(LCS, "LCS", NULL, -3, INDEX, 1) +COMMAND(LINDEX, "LINDEX", NULL, 3, INDEX, 1) +COMMAND(LINSERT, "LINSERT", NULL, 5, INDEX, 1) +COMMAND(LLEN, "LLEN", NULL, 2, INDEX, 1) +COMMAND(LMOVE, "LMOVE", NULL, 5, INDEX, 1) +COMMAND(LMPOP, "LMPOP", NULL, -4, KEYNUM, 1) +COMMAND(LOLWUT, "LOLWUT", NULL, -1, NONE, 0) +COMMAND(LPOP, "LPOP", NULL, -2, INDEX, 1) +COMMAND(LPOS, "LPOS", NULL, -3, INDEX, 1) +COMMAND(LPUSH, "LPUSH", NULL, -3, INDEX, 1) +COMMAND(LPUSHX, "LPUSHX", NULL, -3, INDEX, 1) +COMMAND(LRANGE, "LRANGE", NULL, 4, INDEX, 1) +COMMAND(LREM, "LREM", NULL, 4, INDEX, 1) +COMMAND(LSET, "LSET", NULL, 4, INDEX, 1) +COMMAND(LTRIM, "LTRIM", NULL, 4, INDEX, 1) +COMMAND(MEMORY_DOCTOR, "MEMORY", "DOCTOR", 2, NONE, 0) +COMMAND(MEMORY_HELP, "MEMORY", "HELP", 2, NONE, 0) +COMMAND(MEMORY_MALLOC_STATS, "MEMORY", "MALLOC-STATS", 2, NONE, 0) +COMMAND(MEMORY_PURGE, "MEMORY", "PURGE", 2, NONE, 0) +COMMAND(MEMORY_STATS, "MEMORY", "STATS", 2, NONE, 0) +COMMAND(MEMORY_USAGE, "MEMORY", "USAGE", -3, INDEX, 2) +COMMAND(MGET, "MGET", NULL, -2, INDEX, 1) +COMMAND(MIGRATE, "MIGRATE", NULL, -6, INDEX, 3) +COMMAND(MODULE_HELP, "MODULE", "HELP", 2, NONE, 0) +COMMAND(MODULE_LIST, "MODULE", "LIST", 2, NONE, 0) +COMMAND(MODULE_LOAD, "MODULE", "LOAD", -3, NONE, 0) +COMMAND(MODULE_LOADEX, "MODULE", "LOADEX", -3, NONE, 0) +COMMAND(MODULE_UNLOAD, "MODULE", "UNLOAD", 3, NONE, 0) +COMMAND(MONITOR, "MONITOR", NULL, 1, NONE, 0) +COMMAND(MOVE, "MOVE", NULL, 3, INDEX, 1) +COMMAND(MSET, "MSET", NULL, -3, INDEX, 1) +COMMAND(MSETNX, "MSETNX", NULL, -3, INDEX, 1) +COMMAND(MULTI, "MULTI", NULL, 1, NONE, 0) +COMMAND(OBJECT_ENCODING, "OBJECT", "ENCODING", 3, INDEX, 2) +COMMAND(OBJECT_FREQ, "OBJECT", "FREQ", 3, INDEX, 2) +COMMAND(OBJECT_HELP, "OBJECT", "HELP", 2, NONE, 0) +COMMAND(OBJECT_IDLETIME, "OBJECT", "IDLETIME", 3, INDEX, 2) +COMMAND(OBJECT_REFCOUNT, "OBJECT", "REFCOUNT", 3, INDEX, 2) +COMMAND(PERSIST, "PERSIST", NULL, 2, INDEX, 1) +COMMAND(PEXPIRE, "PEXPIRE", NULL, -3, INDEX, 1) +COMMAND(PEXPIREAT, "PEXPIREAT", NULL, -3, INDEX, 1) +COMMAND(PEXPIRETIME, "PEXPIRETIME", NULL, 2, INDEX, 1) +COMMAND(PFADD, "PFADD", NULL, -2, INDEX, 1) +COMMAND(PFCOUNT, "PFCOUNT", NULL, -2, INDEX, 1) +COMMAND(PFDEBUG, "PFDEBUG", NULL, 3, INDEX, 2) +COMMAND(PFMERGE, "PFMERGE", NULL, -2, INDEX, 1) +COMMAND(PFSELFTEST, "PFSELFTEST", NULL, 1, NONE, 0) +COMMAND(PING, "PING", NULL, -1, NONE, 0) +COMMAND(PSETEX, "PSETEX", NULL, 4, INDEX, 1) +COMMAND(PSUBSCRIBE, "PSUBSCRIBE", NULL, -2, NONE, 0) +COMMAND(PSYNC, "PSYNC", NULL, -3, NONE, 0) +COMMAND(PTTL, "PTTL", NULL, 2, INDEX, 1) +COMMAND(PUBLISH, "PUBLISH", NULL, 3, NONE, 0) +COMMAND(PUBSUB_CHANNELS, "PUBSUB", "CHANNELS", -2, NONE, 0) +COMMAND(PUBSUB_HELP, "PUBSUB", "HELP", 2, NONE, 0) +COMMAND(PUBSUB_NUMPAT, "PUBSUB", "NUMPAT", 2, NONE, 0) +COMMAND(PUBSUB_NUMSUB, "PUBSUB", "NUMSUB", -2, NONE, 0) +COMMAND(PUBSUB_SHARDCHANNELS, "PUBSUB", "SHARDCHANNELS", -2, NONE, 0) +COMMAND(PUBSUB_SHARDNUMSUB, "PUBSUB", "SHARDNUMSUB", -2, NONE, 0) +COMMAND(PUNSUBSCRIBE, "PUNSUBSCRIBE", NULL, -1, NONE, 0) +COMMAND(QUIT, "QUIT", NULL, -1, NONE, 0) +COMMAND(RANDOMKEY, "RANDOMKEY", NULL, 1, NONE, 0) +COMMAND(READONLY, "READONLY", NULL, 1, NONE, 0) +COMMAND(READWRITE, "READWRITE", NULL, 1, NONE, 0) +COMMAND(RENAME, "RENAME", NULL, 3, INDEX, 1) +COMMAND(RENAMENX, "RENAMENX", NULL, 3, INDEX, 1) +COMMAND(REPLCONF, "REPLCONF", NULL, -1, NONE, 0) +COMMAND(REPLICAOF, "REPLICAOF", NULL, 3, NONE, 0) +COMMAND(RESET, "RESET", NULL, 1, NONE, 0) +COMMAND(RESTORE, "RESTORE", NULL, -4, INDEX, 1) +COMMAND(RESTORE_ASKING, "RESTORE-ASKING", NULL, -4, INDEX, 1) +COMMAND(ROLE, "ROLE", NULL, 1, NONE, 0) +COMMAND(RPOP, "RPOP", NULL, -2, INDEX, 1) +COMMAND(RPOPLPUSH, "RPOPLPUSH", NULL, 3, INDEX, 1) +COMMAND(RPUSH, "RPUSH", NULL, -3, INDEX, 1) +COMMAND(RPUSHX, "RPUSHX", NULL, -3, INDEX, 1) +COMMAND(SADD, "SADD", NULL, -3, INDEX, 1) +COMMAND(SAVE, "SAVE", NULL, 1, NONE, 0) +COMMAND(SCAN, "SCAN", NULL, -2, NONE, 0) +COMMAND(SCARD, "SCARD", NULL, 2, INDEX, 1) +COMMAND(SCRIPT_DEBUG, "SCRIPT", "DEBUG", 3, NONE, 0) +COMMAND(SCRIPT_EXISTS, "SCRIPT", "EXISTS", -3, NONE, 0) +COMMAND(SCRIPT_FLUSH, "SCRIPT", "FLUSH", -2, NONE, 0) +COMMAND(SCRIPT_HELP, "SCRIPT", "HELP", 2, NONE, 0) +COMMAND(SCRIPT_KILL, "SCRIPT", "KILL", 2, NONE, 0) +COMMAND(SCRIPT_LOAD, "SCRIPT", "LOAD", 3, NONE, 0) +COMMAND(SDIFF, "SDIFF", NULL, -2, INDEX, 1) +COMMAND(SDIFFSTORE, "SDIFFSTORE", NULL, -3, INDEX, 1) +COMMAND(SELECT, "SELECT", NULL, 2, NONE, 0) +COMMAND(SENTINEL_CKQUORUM, "SENTINEL", "CKQUORUM", 3, NONE, 0) +COMMAND(SENTINEL_CONFIG, "SENTINEL", "CONFIG", -4, NONE, 0) +COMMAND(SENTINEL_DEBUG, "SENTINEL", "DEBUG", -2, NONE, 0) +COMMAND(SENTINEL_FAILOVER, "SENTINEL", "FAILOVER", 3, NONE, 0) +COMMAND(SENTINEL_FLUSHCONFIG, "SENTINEL", "FLUSHCONFIG", 2, NONE, 0) +COMMAND(SENTINEL_GET_MASTER_ADDR_BY_NAME, "SENTINEL", "GET-MASTER-ADDR-BY-NAME", 3, NONE, 0) +COMMAND(SENTINEL_HELP, "SENTINEL", "HELP", 2, NONE, 0) +COMMAND(SENTINEL_INFO_CACHE, "SENTINEL", "INFO-CACHE", -3, NONE, 0) +COMMAND(SENTINEL_IS_MASTER_DOWN_BY_ADDR, "SENTINEL", "IS-MASTER-DOWN-BY-ADDR", 6, NONE, 0) +COMMAND(SENTINEL_MASTER, "SENTINEL", "MASTER", 3, NONE, 0) +COMMAND(SENTINEL_MASTERS, "SENTINEL", "MASTERS", 2, NONE, 0) +COMMAND(SENTINEL_MONITOR, "SENTINEL", "MONITOR", 6, NONE, 0) +COMMAND(SENTINEL_MYID, "SENTINEL", "MYID", 2, NONE, 0) +COMMAND(SENTINEL_PENDING_SCRIPTS, "SENTINEL", "PENDING-SCRIPTS", 2, NONE, 0) +COMMAND(SENTINEL_REMOVE, "SENTINEL", "REMOVE", 3, NONE, 0) +COMMAND(SENTINEL_REPLICAS, "SENTINEL", "REPLICAS", 3, NONE, 0) +COMMAND(SENTINEL_RESET, "SENTINEL", "RESET", 3, NONE, 0) +COMMAND(SENTINEL_SENTINELS, "SENTINEL", "SENTINELS", 3, NONE, 0) +COMMAND(SENTINEL_SET, "SENTINEL", "SET", -5, NONE, 0) +COMMAND(SENTINEL_SIMULATE_FAILURE, "SENTINEL", "SIMULATE-FAILURE", -3, NONE, 0) +COMMAND(SENTINEL_SLAVES, "SENTINEL", "SLAVES", 3, NONE, 0) +COMMAND(SET, "SET", NULL, -3, INDEX, 1) +COMMAND(SETBIT, "SETBIT", NULL, 4, INDEX, 1) +COMMAND(SETEX, "SETEX", NULL, 4, INDEX, 1) +COMMAND(SETNX, "SETNX", NULL, 3, INDEX, 1) +COMMAND(SETRANGE, "SETRANGE", NULL, 4, INDEX, 1) +COMMAND(SHUTDOWN, "SHUTDOWN", NULL, -1, NONE, 0) +COMMAND(SINTER, "SINTER", NULL, -2, INDEX, 1) +COMMAND(SINTERCARD, "SINTERCARD", NULL, -3, KEYNUM, 1) +COMMAND(SINTERSTORE, "SINTERSTORE", NULL, -3, INDEX, 1) +COMMAND(SISMEMBER, "SISMEMBER", NULL, 3, INDEX, 1) +COMMAND(SLAVEOF, "SLAVEOF", NULL, 3, NONE, 0) +COMMAND(SLOWLOG_GET, "SLOWLOG", "GET", -2, NONE, 0) +COMMAND(SLOWLOG_HELP, "SLOWLOG", "HELP", 2, NONE, 0) +COMMAND(SLOWLOG_LEN, "SLOWLOG", "LEN", 2, NONE, 0) +COMMAND(SLOWLOG_RESET, "SLOWLOG", "RESET", 2, NONE, 0) +COMMAND(SMEMBERS, "SMEMBERS", NULL, 2, INDEX, 1) +COMMAND(SMISMEMBER, "SMISMEMBER", NULL, -3, INDEX, 1) +COMMAND(SMOVE, "SMOVE", NULL, 4, INDEX, 1) +COMMAND(SORT, "SORT", NULL, -2, INDEX, 1) +COMMAND(SORT_RO, "SORT_RO", NULL, -2, INDEX, 1) +COMMAND(SPOP, "SPOP", NULL, -2, INDEX, 1) +COMMAND(SPUBLISH, "SPUBLISH", NULL, 3, INDEX, 1) +COMMAND(SRANDMEMBER, "SRANDMEMBER", NULL, -2, INDEX, 1) +COMMAND(SREM, "SREM", NULL, -3, INDEX, 1) +COMMAND(SSCAN, "SSCAN", NULL, -3, INDEX, 1) +COMMAND(SSUBSCRIBE, "SSUBSCRIBE", NULL, -2, INDEX, 1) +COMMAND(STRLEN, "STRLEN", NULL, 2, INDEX, 1) +COMMAND(SUBSCRIBE, "SUBSCRIBE", NULL, -2, NONE, 0) +COMMAND(SUBSTR, "SUBSTR", NULL, 4, INDEX, 1) +COMMAND(SUNION, "SUNION", NULL, -2, INDEX, 1) +COMMAND(SUNIONSTORE, "SUNIONSTORE", NULL, -3, INDEX, 1) +COMMAND(SUNSUBSCRIBE, "SUNSUBSCRIBE", NULL, -1, INDEX, 1) +COMMAND(SWAPDB, "SWAPDB", NULL, 3, NONE, 0) +COMMAND(SYNC, "SYNC", NULL, 1, NONE, 0) +COMMAND(TIME, "TIME", NULL, 1, NONE, 0) +COMMAND(TOUCH, "TOUCH", NULL, -2, INDEX, 1) +COMMAND(TTL, "TTL", NULL, 2, INDEX, 1) +COMMAND(TYPE, "TYPE", NULL, 2, INDEX, 1) +COMMAND(UNLINK, "UNLINK", NULL, -2, INDEX, 1) +COMMAND(UNSUBSCRIBE, "UNSUBSCRIBE", NULL, -1, NONE, 0) +COMMAND(UNWATCH, "UNWATCH", NULL, 1, NONE, 0) +COMMAND(WAIT, "WAIT", NULL, 3, NONE, 0) +COMMAND(WAITAOF, "WAITAOF", NULL, 4, NONE, 0) +COMMAND(WATCH, "WATCH", NULL, -2, INDEX, 1) +COMMAND(XACK, "XACK", NULL, -4, INDEX, 1) +COMMAND(XADD, "XADD", NULL, -5, INDEX, 1) +COMMAND(XAUTOCLAIM, "XAUTOCLAIM", NULL, -6, INDEX, 1) +COMMAND(XCLAIM, "XCLAIM", NULL, -6, INDEX, 1) +COMMAND(XDEL, "XDEL", NULL, -3, INDEX, 1) +COMMAND(XGROUP_CREATE, "XGROUP", "CREATE", -5, INDEX, 2) +COMMAND(XGROUP_CREATECONSUMER, "XGROUP", "CREATECONSUMER", 5, INDEX, 2) +COMMAND(XGROUP_DELCONSUMER, "XGROUP", "DELCONSUMER", 5, INDEX, 2) +COMMAND(XGROUP_DESTROY, "XGROUP", "DESTROY", 4, INDEX, 2) +COMMAND(XGROUP_HELP, "XGROUP", "HELP", 2, NONE, 0) +COMMAND(XGROUP_SETID, "XGROUP", "SETID", -5, INDEX, 2) +COMMAND(XINFO_CONSUMERS, "XINFO", "CONSUMERS", 4, INDEX, 2) +COMMAND(XINFO_GROUPS, "XINFO", "GROUPS", 3, INDEX, 2) +COMMAND(XINFO_HELP, "XINFO", "HELP", 2, NONE, 0) +COMMAND(XINFO_STREAM, "XINFO", "STREAM", -3, INDEX, 2) +COMMAND(XLEN, "XLEN", NULL, 2, INDEX, 1) +COMMAND(XPENDING, "XPENDING", NULL, -3, INDEX, 1) +COMMAND(XRANGE, "XRANGE", NULL, -4, INDEX, 1) +COMMAND(XREAD, "XREAD", NULL, -4, UNKNOWN, 0) +COMMAND(XREADGROUP, "XREADGROUP", NULL, -7, UNKNOWN, 0) +COMMAND(XREVRANGE, "XREVRANGE", NULL, -4, INDEX, 1) +COMMAND(XSETID, "XSETID", NULL, -3, INDEX, 1) +COMMAND(XTRIM, "XTRIM", NULL, -4, INDEX, 1) +COMMAND(ZADD, "ZADD", NULL, -4, INDEX, 1) +COMMAND(ZCARD, "ZCARD", NULL, 2, INDEX, 1) +COMMAND(ZCOUNT, "ZCOUNT", NULL, 4, INDEX, 1) +COMMAND(ZDIFF, "ZDIFF", NULL, -3, KEYNUM, 1) +COMMAND(ZDIFFSTORE, "ZDIFFSTORE", NULL, -4, INDEX, 1) +COMMAND(ZINCRBY, "ZINCRBY", NULL, 4, INDEX, 1) +COMMAND(ZINTER, "ZINTER", NULL, -3, KEYNUM, 1) +COMMAND(ZINTERCARD, "ZINTERCARD", NULL, -3, KEYNUM, 1) +COMMAND(ZINTERSTORE, "ZINTERSTORE", NULL, -4, INDEX, 1) +COMMAND(ZLEXCOUNT, "ZLEXCOUNT", NULL, 4, INDEX, 1) +COMMAND(ZMPOP, "ZMPOP", NULL, -4, KEYNUM, 1) +COMMAND(ZMSCORE, "ZMSCORE", NULL, -3, INDEX, 1) +COMMAND(ZPOPMAX, "ZPOPMAX", NULL, -2, INDEX, 1) +COMMAND(ZPOPMIN, "ZPOPMIN", NULL, -2, INDEX, 1) +COMMAND(ZRANDMEMBER, "ZRANDMEMBER", NULL, -2, INDEX, 1) +COMMAND(ZRANGE, "ZRANGE", NULL, -4, INDEX, 1) +COMMAND(ZRANGEBYLEX, "ZRANGEBYLEX", NULL, -4, INDEX, 1) +COMMAND(ZRANGEBYSCORE, "ZRANGEBYSCORE", NULL, -4, INDEX, 1) +COMMAND(ZRANGESTORE, "ZRANGESTORE", NULL, -5, INDEX, 1) +COMMAND(ZRANK, "ZRANK", NULL, -3, INDEX, 1) +COMMAND(ZREM, "ZREM", NULL, -3, INDEX, 1) +COMMAND(ZREMRANGEBYLEX, "ZREMRANGEBYLEX", NULL, 4, INDEX, 1) +COMMAND(ZREMRANGEBYRANK, "ZREMRANGEBYRANK", NULL, 4, INDEX, 1) +COMMAND(ZREMRANGEBYSCORE, "ZREMRANGEBYSCORE", NULL, 4, INDEX, 1) +COMMAND(ZREVRANGE, "ZREVRANGE", NULL, -4, INDEX, 1) +COMMAND(ZREVRANGEBYLEX, "ZREVRANGEBYLEX", NULL, -4, INDEX, 1) +COMMAND(ZREVRANGEBYSCORE, "ZREVRANGEBYSCORE", NULL, -4, INDEX, 1) +COMMAND(ZREVRANK, "ZREVRANK", NULL, -3, INDEX, 1) +COMMAND(ZSCAN, "ZSCAN", NULL, -3, INDEX, 1) +COMMAND(ZSCORE, "ZSCORE", NULL, 3, INDEX, 1) +COMMAND(ZUNION, "ZUNION", NULL, -3, KEYNUM, 1) +COMMAND(ZUNIONSTORE, "ZUNIONSTORE", NULL, -4, INDEX, 1) diff --git a/libvalkeycluster/command.c b/libvalkeycluster/command.c new file mode 100644 index 00000000..30fc7ca5 --- /dev/null +++ b/libvalkeycluster/command.c @@ -0,0 +1,486 @@ +/* + * Copyright (c) 2015-2017, Ieshen Zheng + * Copyright (c) 2020, Nick + * Copyright (c) 2020-2021, Bjorn Svensson + * 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 +#include +#include +#ifndef _WIN32 +#include +#include +#else +#include +#endif +#include + +#include "command.h" +#include "hiarray.h" +#include "hiutil.h" +#include "win32.h" + +#define LF (uint8_t)10 +#define CR (uint8_t)13 + +static uint64_t cmd_id = 0; /* command id counter */ + +typedef enum { + KEYPOS_NONE, + KEYPOS_UNKNOWN, + KEYPOS_INDEX, + KEYPOS_KEYNUM +} cmd_keypos; + +typedef struct { + cmd_type_t type; /* A constant identifying the command. */ + const char *name; /* Command name */ + const char *subname; /* Subcommand name or NULL */ + cmd_keypos firstkeymethod; /* First key none, unknown, pos or keynum */ + int8_t firstkeypos; /* Position of first key or the arg */ + int8_t arity; /* Arity, neg number means min num args */ +} cmddef; + +/* Populate the table with code in cmddef.h generated from Redis JSON files. */ +static cmddef redis_commands[] = { +#define COMMAND(_type, _name, _subname, _arity, _keymethod, _keypos) \ + {.type = CMD_REQ_REDIS_##_type, \ + .name = _name, \ + .subname = _subname, \ + .firstkeymethod = KEYPOS_##_keymethod, \ + .firstkeypos = _keypos, \ + .arity = _arity}, +#include "cmddef.h" +#undef COMMAND +}; + +static inline void to_upper(char *dst, const char *src, uint32_t len) { + uint32_t i; + for (i = 0; i < len; i++) { + if (src[i] >= 'a' && src[i] <= 'z') + dst[i] = src[i] - ('a' - 'A'); + else + dst[i] = src[i]; + } +} + +/* Looks up a command or subcommand in the command table. Arg0 and arg1 are used + * to lookup the command. The function returns CMD_UNKNOWN on failure. On + * success, the command type is returned and *firstkey and *arity are + * populated. */ +cmddef *redis_lookup_cmd(const char *arg0, uint32_t arg0_len, const char *arg1, + uint32_t arg1_len) { + int num_commands = sizeof(redis_commands) / sizeof(cmddef); + /* Compare command name in uppercase. */ + char *cmd = alloca(arg0_len); + to_upper(cmd, arg0, arg0_len); + char *subcmd = NULL; /* Alloca later on demand. */ + /* Find the command using binary search. */ + int left = 0, right = num_commands - 1; + while (left <= right) { + int i = (left + right) / 2; + cmddef *c = &redis_commands[i]; + + int cmp = strncmp(c->name, cmd, arg0_len); + if (cmp == 0 && strlen(c->name) > arg0_len) + cmp = 1; /* "HGETALL" vs "HGET" */ + + /* If command name matches, compare subcommand if any */ + if (cmp == 0 && c->subname != NULL) { + if (arg1 == NULL) { + /* Command has subcommands, but none given. */ + return NULL; + } + if (subcmd == NULL) { + subcmd = alloca(arg1_len); + to_upper(subcmd, arg1, arg1_len); + } + cmp = strncmp(c->subname, subcmd, arg1_len); + if (cmp == 0 && strlen(c->subname) > arg1_len) + cmp = 1; + } + + if (cmp < 0) { + left = i + 1; + } else if (cmp > 0) { + right = i - 1; + } else { + /* Found it. */ + return c; + } + } + return NULL; +} + +/* + * Return true, if the redis command is a vector command accepting one or + * more keys, otherwise return false + * Format: command key [ key ... ] + */ +static int redis_argx(struct cmd *r) { + switch (r->type) { + case CMD_REQ_REDIS_EXISTS: + case CMD_REQ_REDIS_MGET: + case CMD_REQ_REDIS_DEL: + return 1; + + default: + break; + } + + return 0; +} + +/* + * Return true, if the redis command is a vector command accepting one or + * more key-value pairs, otherwise return false + * Format: command key value [ key value ... ] + */ +static int redis_argkvx(struct cmd *r) { + switch (r->type) { + case CMD_REQ_REDIS_MSET: + return 1; + + default: + break; + } + + return 0; +} + +/* Parses a bulk string starting at 'p' and ending somewhere before 'end'. + * Returns the remaining of the input after consuming the bulk string. The + * pointers *str and *len are pointed to the parsed string and its length. On + * parse error, NULL is returned. */ +char *redis_parse_bulk(char *p, char *end, char **str, uint32_t *len) { + uint32_t length = 0; + if (p >= end || *p++ != '$') + return NULL; + while (p < end && *p >= '0' && *p <= '9') { + length = length * 10 + (uint32_t)(*p++ - '0'); + } + if (p >= end || *p++ != CR) + return NULL; + if (p >= end || *p++ != LF) + return NULL; + if (str) + *str = p; + if (len) + *len = length; + p += length; + if (p >= end || *p++ != CR) + return NULL; + if (p >= end || *p++ != LF) + return NULL; + return p; +} + +static inline int push_keypos(struct cmd *r, char *arg, uint32_t arglen) { + struct keypos *kpos = hiarray_push(r->keys); + if (kpos == NULL) + return 0; + kpos->start = arg; + kpos->end = arg + arglen; + return 1; +} + +/* + * Reference: http://redis.io/topics/protocol + * + * Redis >= 1.2 uses the unified protocol to send requests to the Redis + * server. In the unified protocol all the arguments sent to the server + * are binary safe and every request has the following general form: + * + * * CR LF + * $ CR LF + * CR LF + * ... + * $ CR LF + * CR LF + * + * Before the unified request protocol, redis protocol for requests supported + * the following commands + * 1). Inline commands: simple commands where arguments are just space + * separated strings. No binary safeness is possible. + * 2). Bulk commands: bulk commands are exactly like inline commands, but + * the last argument is handled in a special way in order to allow for + * a binary-safe last argument. + * + * only supports the Redis unified protocol for requests. + */ +void redis_parse_cmd(struct cmd *r) { + ASSERT(r->cmd != NULL && r->clen > 0); + char *p = r->cmd; + char *end = r->cmd + r->clen; + uint32_t rnarg = 0; /* Number of args including cmd name */ + int argidx = -1; /* Index of last parsed arg */ + char *arg; /* Last parsed arg */ + uint32_t arglen; /* Length of arg */ + char *arg0 = NULL, *arg1 = NULL; /* The first two args */ + uint32_t arg0_len = 0, arg1_len = 0; /* Lengths of arg0 and arg1 */ + cmddef *info = NULL; /* Command info, when found */ + + /* Check that the command line is multi-bulk. */ + if (*p++ != '*') + goto error; + + /* Parse multi-bulk size (rnarg). */ + while (p < end && *p >= '0' && *p <= '9') { + rnarg = rnarg * 10 + (uint32_t)(*p++ - '0'); + } + if (p == end || *p++ != CR) + goto error; + if (p == end || *p++ != LF) + goto error; + if (rnarg == 0) + goto error; + r->narg = rnarg; + + /* Parse the first two args. */ + if ((p = redis_parse_bulk(p, end, &arg0, &arg0_len)) == NULL) + goto error; + argidx++; + if (rnarg > 1) { + if ((p = redis_parse_bulk(p, end, &arg1, &arg1_len)) == NULL) + goto error; + argidx++; + } + + /* Lookup command. */ + if ((info = redis_lookup_cmd(arg0, arg0_len, arg1, arg1_len)) == NULL) + goto error; /* Command not found. */ + r->type = info->type; + + /* Arity check (negative arity means minimum num args) */ + if ((info->arity >= 0 && (int)rnarg != info->arity) || + (info->arity < 0 && (int)rnarg < -info->arity)) { + goto error; + } + if (info->firstkeymethod == KEYPOS_NONE) + goto done; /* Command takes no keys. */ + if (arg1 == NULL) + goto error; /* Command takes keys, but no args given. Quick abort. */ + + /* Below we assume arg1 != NULL, */ + + /* Handle commands where firstkey depends on special logic. */ + if (info->firstkeymethod == KEYPOS_UNKNOWN) { + /* Keyword-based first key position */ + const char *keyword; + int startfrom; + if (r->type == CMD_REQ_REDIS_XREAD) { + keyword = "STREAMS"; + startfrom = 1; + } else if (r->type == CMD_REQ_REDIS_XREADGROUP) { + keyword = "STREAMS"; + startfrom = 4; + } else { + /* Not reached, but can be reached if Redis adds more commands. */ + goto error; + } + + /* Skip forward to the 'startfrom' arg index, then search for the keyword. */ + arg = arg1; + arglen = arg1_len; + while (argidx < (int)rnarg - 1) { + if ((p = redis_parse_bulk(p, end, &arg, &arglen)) == NULL) + goto error; /* Keyword not provided, thus no keys. */ + if (argidx++ < startfrom) + continue; /* Keyword can't appear in a position before 'startfrom' */ + if (!strncasecmp(keyword, arg, arglen)) { + /* Keyword found. Now the first key is the next arg. */ + if ((p = redis_parse_bulk(p, end, &arg, &arglen)) == NULL) + goto error; + if (!push_keypos(r, arg, arglen)) + goto oom; + goto done; + } + } + + /* Keyword not provided. */ + goto error; + } + + /* Find first key arg. */ + arg = arg1; + arglen = arg1_len; + for (; argidx < info->firstkeypos; argidx++) { + if ((p = redis_parse_bulk(p, end, &arg, &arglen)) == NULL) + goto error; + } + + if (info->firstkeymethod == KEYPOS_KEYNUM) { + /* The arg specifies the number of keys and the first key is the next + * arg. Example: + * + * EVAL script numkeys [key [key ...]] [arg [arg ...]] */ + if (!strncmp("0", arg, arglen)) + goto done; /* No args. */ + /* One or more args. The first key is the arg after the 'numkeys' arg. */ + if ((p = redis_parse_bulk(p, end, &arg, &arglen)) == NULL) + goto error; + argidx++; + } + + /* Now arg is the first key and arglen is its length. */ + + if (info->type == CMD_REQ_REDIS_MIGRATE && arglen == 0 && + info->firstkeymethod == KEYPOS_INDEX && info->firstkeypos == 3) { + /* MIGRATE host port destination-db timeout [COPY] [REPLACE] + * [[AUTH password] | [AUTH2 username password]] [KEYS key [key ...]] + * + * The key spec points out arg3 as the first key, but if it's an empty + * string, we would need to search for the KEYS keyword arg backwards + * from the end of the command line. This is not implemented. */ + goto error; + } + + if (!push_keypos(r, arg, arglen)) + goto oom; + + /* Special commands where we want all keys (not only the first key). */ + if (redis_argx(r) || redis_argkvx(r)) { + /* argx: MGET key [ key ... ] */ + /* argkvx: MSET key value [ key value ... ] */ + if (redis_argkvx(r) && rnarg % 2 == 0) + goto error; + for (uint32_t i = 2; i < rnarg; i++) { + if ((p = redis_parse_bulk(p, end, &arg, &arglen)) == NULL) + goto error; + if (redis_argkvx(r) && i % 2 == 0) + continue; /* not a key */ + if (!push_keypos(r, arg, arglen)) + goto oom; + } + } + +done: + ASSERT(r->type > CMD_UNKNOWN && r->type < CMD_SENTINEL); + r->result = CMD_PARSE_OK; + return; + +error: + r->result = CMD_PARSE_ERROR; + errno = EINVAL; + size_t errmaxlen = 100; /* Enough for the error messages below. */ + if (r->errstr == NULL) { + r->errstr = hi_malloc(errmaxlen); + if (r->errstr == NULL) { + goto oom; + } + } + + if (info != NULL && info->subname != NULL) + snprintf(r->errstr, errmaxlen, "Failed to find keys of command %s %s", + info->name, info->subname); + else if (info != NULL) + snprintf(r->errstr, errmaxlen, "Failed to find keys of command %s", + info->name); + else if (r->type == CMD_UNKNOWN && arg0 != NULL && arg1 != NULL) + snprintf(r->errstr, errmaxlen, "Unknown command %.*s %.*s", arg0_len, + arg0, arg1_len, arg1); + else if (r->type == CMD_UNKNOWN && arg0 != NULL) + snprintf(r->errstr, errmaxlen, "Unknown command %.*s", arg0_len, arg0); + else + snprintf(r->errstr, errmaxlen, "Command parse error"); + return; + +oom: + r->result = CMD_PARSE_ENOMEM; +} + +struct cmd *command_get(void) { + struct cmd *command; + command = hi_malloc(sizeof(struct cmd)); + if (command == NULL) { + return NULL; + } + + command->id = ++cmd_id; + command->result = CMD_PARSE_OK; + command->errstr = NULL; + command->type = CMD_UNKNOWN; + command->cmd = NULL; + command->clen = 0; + command->keys = NULL; + command->narg = 0; + command->quit = 0; + command->noforward = 0; + command->slot_num = -1; + command->frag_seq = NULL; + command->reply = NULL; + command->sub_commands = NULL; + command->node_addr = NULL; + + command->keys = hiarray_create(1, sizeof(struct keypos)); + if (command->keys == NULL) { + hi_free(command); + return NULL; + } + + return command; +} + +void command_destroy(struct cmd *command) { + if (command == NULL) { + return; + } + + if (command->cmd != NULL) { + hi_free(command->cmd); + command->cmd = NULL; + } + + if (command->errstr != NULL) { + hi_free(command->errstr); + command->errstr = NULL; + } + + if (command->keys != NULL) { + command->keys->nelem = 0; + hiarray_destroy(command->keys); + command->keys = NULL; + } + + if (command->frag_seq != NULL) { + hi_free(command->frag_seq); + command->frag_seq = NULL; + } + + freeReplyObject(command->reply); + + if (command->sub_commands != NULL) { + listRelease(command->sub_commands); + } + + if (command->node_addr != NULL) { + sdsfree(command->node_addr); + command->node_addr = NULL; + } + + hi_free(command); +} diff --git a/libvalkeycluster/command.h b/libvalkeycluster/command.h new file mode 100644 index 00000000..f2d7aab6 --- /dev/null +++ b/libvalkeycluster/command.h @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2015-2017, Ieshen Zheng + * Copyright (c) 2020, Nick + * Copyright (c) 2020-2021, Bjorn Svensson + * 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 __COMMAND_H_ +#define __COMMAND_H_ + +#include + +#include "adlist.h" +#include + +typedef enum cmd_parse_result { + CMD_PARSE_OK, /* parsing ok */ + CMD_PARSE_ENOMEM, /* out of memory */ + CMD_PARSE_ERROR, /* parsing error */ + CMD_PARSE_REPAIR, /* more to parse -> repair parsed & unparsed data */ + CMD_PARSE_AGAIN, /* incomplete -> parse again */ +} cmd_parse_result_t; + +typedef enum cmd_type { + CMD_UNKNOWN, +/* Request commands */ +#define COMMAND(_type, _name, _subname, _arity, _keymethod, _keypos) \ + CMD_REQ_REDIS_##_type, +#include "cmddef.h" +#undef COMMAND + /* Response types */ + CMD_RSP_REDIS_STATUS, /* simple string */ + CMD_RSP_REDIS_ERROR, + CMD_RSP_REDIS_INTEGER, + CMD_RSP_REDIS_BULK, + CMD_RSP_REDIS_MULTIBULK, + CMD_SENTINEL +} cmd_type_t; + +struct keypos { + char *start; /* key start pos */ + char *end; /* key end pos */ + uint32_t remain_len; /* remain length after keypos->end for more key-value + pairs in command, like mset */ +}; + +struct cmd { + + uint64_t id; /* command id */ + + cmd_parse_result_t result; /* command parsing result */ + char *errstr; /* error info when the command parse failed */ + + cmd_type_t type; /* command type */ + + char *cmd; + uint32_t clen; /* command length */ + + struct hiarray *keys; /* array of keypos, for req */ + + uint32_t narg; /* # arguments (redis) */ + + unsigned quit : 1; /* quit request? */ + unsigned noforward : 1; /* not need forward (example: ping) */ + + /* Command destination */ + int slot_num; /* Command should be sent to slot. + * Set to -1 if command is sent to a given node, + * or if a slot can not be found or calculated, + * or if its a multi-key command cross different + * nodes (cross slot) */ + char *node_addr; /* Command sent to this node address */ + + struct cmd * + *frag_seq; /* sequence of fragment command, map from keys to fragments*/ + + redisReply *reply; + + hilist *sub_commands; /* just for pipeline and multi-key commands */ +}; + +void redis_parse_cmd(struct cmd *r); + +struct cmd *command_get(void); +void command_destroy(struct cmd *command); + +#endif diff --git a/libvalkeycluster/dict.c b/libvalkeycluster/dict.c new file mode 100644 index 00000000..0a154499 --- /dev/null +++ b/libvalkeycluster/dict.c @@ -0,0 +1,287 @@ +/* Hash table implementation. + * + * This file implements in memory hash tables with insert/del/replace/find/ + * get-random-element operations. Hash tables will auto resize if needed + * tables of power of two in size are used, collisions are handled by + * chaining. See the source code for more information... :) + * + * Copyright (c) 2006-2010, 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. + */ + +#include +#include +#include +#include + +#include "dict.h" + +/* -------------------------- private prototypes ---------------------------- */ + +static int _dictExpandIfNeeded(dict *ht); +static unsigned long _dictNextPower(unsigned long size); +static int _dictKeyIndex(dict *ht, const void *key); +static int _dictInit(dict *ht, dictType *type, void *privDataPtr); + +/* -------------------------- hash functions -------------------------------- */ + +/* Generic hash function (a popular one from Bernstein). + * I tested a few and this was the best. */ +unsigned int dictGenHashFunction(const unsigned char *buf, int len) { + unsigned int hash = 5381; + + while (len--) + hash = ((hash << 5) + hash) + (*buf++); /* hash * 33 + c */ + return hash; +} + +/* ----------------------------- API implementation ------------------------- */ + +/* Reset an hashtable already initialized with ht_init(). + * NOTE: This function should only called by ht_destroy(). */ +static void _dictReset(dict *ht) { + ht->table = NULL; + ht->size = 0; + ht->sizemask = 0; + ht->used = 0; +} + +/* Create a new hash table */ +dict *dictCreate(dictType *type, void *privDataPtr) { + dict *ht = hi_malloc(sizeof(*ht)); + if (ht == NULL) + return NULL; + + _dictInit(ht, type, privDataPtr); + return ht; +} + +/* Initialize the hash table */ +static int _dictInit(dict *ht, dictType *type, void *privDataPtr) { + _dictReset(ht); + ht->type = type; + ht->privdata = privDataPtr; + return DICT_OK; +} + +/* Expand or create the hashtable */ +int dictExpand(dict *ht, unsigned long size) { + dict n; /* the new hashtable */ + unsigned long realsize = _dictNextPower(size), i; + + /* the size is invalid if it is smaller than the number of + * elements already inside the hashtable */ + if (ht->used > size) + return DICT_ERR; + + _dictInit(&n, ht->type, ht->privdata); + n.size = realsize; + n.sizemask = realsize - 1; + n.table = hi_calloc(realsize, sizeof(dictEntry *)); + if (n.table == NULL) + return DICT_ERR; + + /* Copy all the elements from the old to the new table: + * note that if the old hash table is empty ht->size is zero, + * so dictExpand just creates an hash table. */ + n.used = ht->used; + for (i = 0; i < ht->size && ht->used > 0; i++) { + dictEntry *he, *nextHe; + + if (ht->table[i] == NULL) + continue; + + /* For each hash entry on this slot... */ + he = ht->table[i]; + while (he) { + unsigned int h; + + nextHe = he->next; + /* Get the new element index */ + h = dictHashKey(ht, he->key) & n.sizemask; + he->next = n.table[h]; + n.table[h] = he; + ht->used--; + /* Pass to the next element */ + he = nextHe; + } + } + assert(ht->used == 0); + hi_free(ht->table); + + /* Remap the new hashtable in the old */ + *ht = n; + return DICT_OK; +} + +/* Add an element to the target hash table */ +int dictAdd(dict *ht, void *key, void *val) { + int index; + dictEntry *entry; + + /* Get the index of the new element, or -1 if + * the element already exists. */ + if ((index = _dictKeyIndex(ht, key)) == -1) + return DICT_ERR; + + /* Allocates the memory and stores key */ + entry = hi_malloc(sizeof(*entry)); + if (entry == NULL) + return DICT_ERR; + + entry->next = ht->table[index]; + ht->table[index] = entry; + + /* Set the hash entry fields. */ + dictSetHashKey(ht, entry, key); + dictSetHashVal(ht, entry, val); + ht->used++; + return DICT_OK; +} + +/* Destroy an entire hash table */ +static int _dictClear(dict *ht) { + unsigned long i; + + /* Free all the elements */ + for (i = 0; i < ht->size && ht->used > 0; i++) { + dictEntry *he, *nextHe; + + if ((he = ht->table[i]) == NULL) + continue; + while (he) { + nextHe = he->next; + dictFreeEntryKey(ht, he); + dictFreeEntryVal(ht, he); + hi_free(he); + ht->used--; + he = nextHe; + } + } + /* Free the table and the allocated cache structure */ + hi_free(ht->table); + /* Re-initialize the table */ + _dictReset(ht); + return DICT_OK; /* never fails */ +} + +/* Clear & Release the hash table */ +void dictRelease(dict *ht) { + _dictClear(ht); + hi_free(ht); +} + +dictEntry *dictFind(dict *ht, const void *key) { + dictEntry *he; + unsigned int h; + + if (ht->size == 0) + return NULL; + h = dictHashKey(ht, key) & ht->sizemask; + he = ht->table[h]; + while (he) { + if (dictCompareHashKeys(ht, key, he->key)) + return he; + he = he->next; + } + return NULL; +} + +void dictInitIterator(dictIterator *iter, dict *ht) { + iter->ht = ht; + iter->index = -1; + iter->entry = NULL; + iter->nextEntry = NULL; +} + +dictEntry *dictNext(dictIterator *iter) { + while (1) { + if (iter->entry == NULL) { + iter->index++; + if (iter->index >= (signed)iter->ht->size) + break; + iter->entry = iter->ht->table[iter->index]; + } else { + iter->entry = iter->nextEntry; + } + if (iter->entry) { + /* We need to save the 'next' here, the iterator user + * may delete the entry we are returning. */ + iter->nextEntry = iter->entry->next; + return iter->entry; + } + } + return NULL; +} + +/* ------------------------- private functions ------------------------------ */ + +/* Expand the hash table if needed */ +static int _dictExpandIfNeeded(dict *ht) { + /* If the hash table is empty expand it to the initial size, + * if the table is "full" double its size. */ + if (ht->size == 0) + return dictExpand(ht, DICT_HT_INITIAL_SIZE); + if (ht->used == ht->size) + return dictExpand(ht, ht->size * 2); + return DICT_OK; +} + +/* Our hash table capability is a power of two */ +static unsigned long _dictNextPower(unsigned long size) { + unsigned long i = DICT_HT_INITIAL_SIZE; + + if (size >= LONG_MAX) + return LONG_MAX; + while (1) { + if (i >= size) + return i; + i *= 2; + } +} + +/* Returns the index of a free slot that can be populated with + * an hash entry for the given 'key'. + * If the key already exists, -1 is returned. */ +static int _dictKeyIndex(dict *ht, const void *key) { + unsigned int h; + dictEntry *he; + + /* Expand the hashtable if needed */ + if (_dictExpandIfNeeded(ht) == DICT_ERR) + return -1; + /* Compute the key hash value */ + h = dictHashKey(ht, key) & ht->sizemask; + /* Search if this slot does not already contain the given key */ + he = ht->table[h]; + while (he) { + if (dictCompareHashKeys(ht, key, he->key)) + return -1; + he = he->next; + } + return h; +} diff --git a/libvalkeycluster/dict.h b/libvalkeycluster/dict.h new file mode 100644 index 00000000..7e1d70d0 --- /dev/null +++ b/libvalkeycluster/dict.h @@ -0,0 +1,125 @@ +/* Hash table implementation. + * + * This file implements in memory hash tables with insert/del/replace/find/ + * get-random-element operations. Hash tables will auto resize if needed + * tables of power of two in size are used, collisions are handled by + * chaining. See the source code for more information... :) + * + * Copyright (c) 2006-2010, 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. + */ + +#ifndef __DICT_H +#define __DICT_H + +#define DICT_OK 0 +#define DICT_ERR 1 + +/* Unused arguments generate annoying warnings... */ +#define DICT_NOTUSED(V) ((void)V) + +typedef struct dictEntry { + void *key; + void *val; + struct dictEntry *next; +} dictEntry; + +typedef struct dictType { + unsigned int (*hashFunction)(const void *key); + void *(*keyDup)(void *privdata, const void *key); + void *(*valDup)(void *privdata, const void *obj); + int (*keyCompare)(void *privdata, const void *key1, const void *key2); + void (*keyDestructor)(void *privdata, void *key); + void (*valDestructor)(void *privdata, void *obj); +} dictType; + +typedef struct dict { + dictEntry **table; + dictType *type; + unsigned long size; + unsigned long sizemask; + unsigned long used; + void *privdata; +} dict; + +typedef struct dictIterator { + dict *ht; + int index; + dictEntry *entry, *nextEntry; +} dictIterator; + +/* This is the initial size of every hash table */ +#define DICT_HT_INITIAL_SIZE 4 + +/* ------------------------------- Macros ------------------------------------*/ +#define dictFreeEntryVal(ht, entry) \ + if ((ht)->type->valDestructor) \ + (ht)->type->valDestructor((ht)->privdata, (entry)->val) + +#define dictSetHashVal(ht, entry, _val_) \ + do { \ + if ((ht)->type->valDup) \ + entry->val = (ht)->type->valDup((ht)->privdata, _val_); \ + else \ + entry->val = (_val_); \ + } while (0) + +#define dictFreeEntryKey(ht, entry) \ + if ((ht)->type->keyDestructor) \ + (ht)->type->keyDestructor((ht)->privdata, (entry)->key) + +#define dictSetHashKey(ht, entry, _key_) \ + do { \ + if ((ht)->type->keyDup) \ + entry->key = (ht)->type->keyDup((ht)->privdata, _key_); \ + else \ + entry->key = (_key_); \ + } while (0) + +#define dictCompareHashKeys(ht, key1, key2) \ + (((ht)->type->keyCompare) ? \ + (ht)->type->keyCompare((ht)->privdata, key1, key2) : \ + (key1) == (key2)) + +#define dictHashKey(ht, key) (ht)->type->hashFunction(key) + +#define dictGetEntryKey(he) ((he)->key) +#define dictGetEntryVal(he) ((he)->val) +#define dictSlots(ht) ((ht)->size) +#define dictSize(ht) ((ht)->used) + +/* API */ +unsigned int dictGenHashFunction(const unsigned char *buf, int len); +dict *dictCreate(dictType *type, void *privDataPtr); +int dictExpand(dict *ht, unsigned long size); +int dictAdd(dict *ht, void *key, void *val); +void dictRelease(dict *ht); +dictEntry *dictFind(dict *ht, const void *key); +void dictInitIterator(dictIterator *iter, dict *ht); +dictEntry *dictNext(dictIterator *iter); + +#endif /* __DICT_H */ diff --git a/libvalkeycluster/examples/src/CMakeLists.txt b/libvalkeycluster/examples/src/CMakeLists.txt new file mode 100644 index 00000000..b27bd339 --- /dev/null +++ b/libvalkeycluster/examples/src/CMakeLists.txt @@ -0,0 +1,35 @@ +cmake_minimum_required(VERSION 3.11) +project(examples LANGUAGES C) + +# Handle libevent and hiredis +find_library(EVENT_LIBRARY event HINTS /usr/lib/x86_64-linux-gnu) +find_package(hiredis REQUIRED) +find_package(hiredis_cluster REQUIRED) + +# Executable: IPv4 +add_executable(example_ipv4 example.c) +target_link_libraries(example_ipv4 + hiredis_cluster::hiredis_cluster) + +# Executable: async +add_executable(example_async example_async.c) +target_link_libraries(example_async + hiredis_cluster::hiredis_cluster + ${EVENT_LIBRARY}) + +add_executable(clientside_caching_async clientside_caching_async.c) +target_link_libraries(clientside_caching_async + hiredis_cluster::hiredis_cluster + ${EVENT_LIBRARY}) + +# Executable: tls +if(ENABLE_SSL) + find_package(hiredis_ssl REQUIRED) + find_package(hiredis_cluster_ssl REQUIRED) + + add_executable(example_tls example_tls.c) + target_link_libraries(example_tls + hiredis_cluster::hiredis_cluster + hiredis_cluster::hiredis_cluster_ssl + ${EVENT_LIBRARY}) +endif() diff --git a/libvalkeycluster/examples/src/clientside_caching_async.c b/libvalkeycluster/examples/src/clientside_caching_async.c new file mode 100644 index 00000000..345b399c --- /dev/null +++ b/libvalkeycluster/examples/src/clientside_caching_async.c @@ -0,0 +1,167 @@ +/* + * Simple example how to enable client tracking to implement client side caching. + * Tracking can be enabled via a registered connect callback and invalidation + * messages are received via the registered push callback. + * The disconnect callback should also be used as an indication of invalidation. + */ +#include +#include + +#include +#include +#include +#include + +#define CLUSTER_NODE "127.0.0.1:7000" +#define KEY "key:1" + +void pushCallback(redisAsyncContext *ac, void *r); +void setCallback(redisClusterAsyncContext *acc, void *r, void *privdata); +void getCallback1(redisClusterAsyncContext *acc, void *r, void *privdata); +void getCallback2(redisClusterAsyncContext *acc, void *r, void *privdata); +void modifyKey(const char *key, const char *value); + +/* The connect callback enables RESP3 and client tracking. + The non-const connect callback is used since we want to + set the push callback in the hiredis context. */ +void connectCallbackNC(redisAsyncContext *ac, int status) { + assert(status == REDIS_OK); + redisAsyncSetPushCallback(ac, pushCallback); + redisAsyncCommand(ac, NULL, NULL, "HELLO 3"); + redisAsyncCommand(ac, NULL, NULL, "CLIENT TRACKING ON"); + printf("Connected to %s:%d\n", ac->c.tcp.host, ac->c.tcp.port); +} + +/* The event callback issues a 'SET' command when the client is ready to accept + commands. A reply is expected via a call to 'setCallback()' */ +void eventCallback(const redisClusterContext *cc, int event, void *privdata) { + (void)cc; + redisClusterAsyncContext *acc = (redisClusterAsyncContext *)privdata; + + /* We send our commands when the client is ready to accept commands. */ + if (event == HIRCLUSTER_EVENT_READY) { + printf("Client is ready to accept commands\n"); + + int status = + redisClusterAsyncCommand(acc, setCallback, NULL, "SET %s 1", KEY); + assert(status == REDIS_OK); + } +} + +/* Message callback for 'SET' commands. Issues a 'GET' command and a reply is + expected as a call to 'getCallback1()' */ +void setCallback(redisClusterAsyncContext *acc, void *r, void *privdata) { + (void)privdata; + redisReply *reply = (redisReply *)r; + assert(reply != NULL); + printf("Callback for 'SET', reply: %s\n", reply->str); + + int status = + redisClusterAsyncCommand(acc, getCallback1, NULL, "GET %s", KEY); + assert(status == REDIS_OK); +} + +/* Message callback for the first 'GET' command. Modifies the key to + trigger Redis to send a key invalidation message and then sends another + 'GET' command. The invalidation message is received via the registered + push callback. */ +void getCallback1(redisClusterAsyncContext *acc, void *r, void *privdata) { + (void)privdata; + redisReply *reply = (redisReply *)r; + assert(reply != NULL); + + printf("Callback for first 'GET', reply: %s\n", reply->str); + + /* Modify the key from another client which will invalidate a cached value. + Redis will send an invalidation message via a push message. */ + modifyKey(KEY, "99"); + + int status = + redisClusterAsyncCommand(acc, getCallback2, NULL, "GET %s", KEY); + assert(status == REDIS_OK); +} + +/* Push message callback handling invalidation messages. */ +void pushCallback(redisAsyncContext *ac, void *r) { + redisReply *reply = r; + if (!(reply->type == REDIS_REPLY_PUSH && reply->elements == 2 && + reply->element[0]->type == REDIS_REPLY_STRING && + !strncmp(reply->element[0]->str, "invalidate", 10) && + reply->element[1]->type == REDIS_REPLY_ARRAY)) { + /* Not an 'invalidate' message. Ignore. */ + return; + } + redisReply *payload = reply->element[1]; + size_t i; + for (i = 0; i < payload->elements; i++) { + redisReply *key = payload->element[i]; + if (key->type == REDIS_REPLY_STRING) + printf("Invalidate key '%.*s'\n", (int)key->len, key->str); + else if (key->type == REDIS_REPLY_NIL) + printf("Invalidate all\n"); + } +} + +/* Message callback for 'GET' commands. Exits program. */ +void getCallback2(redisClusterAsyncContext *acc, void *r, void *privdata) { + (void)privdata; + redisReply *reply = (redisReply *)r; + assert(reply != NULL); + + printf("Callback for second 'GET', reply: %s\n", reply->str); + + /* Exit the eventloop after a couple of sent commands. */ + redisClusterAsyncDisconnect(acc); +} + +/* A disconnect callback should invalidate all cached keys. */ +void disconnectCallback(const redisAsyncContext *ac, int status) { + assert(status == REDIS_OK); + printf("Disconnected from %s:%d\n", ac->c.tcp.host, ac->c.tcp.port); + + printf("Invalidate all\n"); +} + +/* Helper to modify keys using a separate client. */ +void modifyKey(const char *key, const char *value) { + printf("Modify key: '%s'\n", key); + redisClusterContext *cc = redisClusterContextInit(); + int status = redisClusterSetOptionAddNodes(cc, CLUSTER_NODE); + assert(status == REDIS_OK); + status = redisClusterConnect2(cc); + assert(status == REDIS_OK); + + redisReply *reply = redisClusterCommand(cc, "SET %s %s", key, value); + assert(reply != NULL); + freeReplyObject(reply); + + redisClusterFree(cc); +} + +int main(int argc, char **argv) { + redisClusterAsyncContext *acc = redisClusterAsyncContextInit(); + assert(acc); + + int status; + status = redisClusterAsyncSetConnectCallbackNC(acc, connectCallbackNC); + assert(status == REDIS_OK); + status = redisClusterAsyncSetDisconnectCallback(acc, disconnectCallback); + assert(status == REDIS_OK); + status = redisClusterSetEventCallback(acc->cc, eventCallback, acc); + assert(status == REDIS_OK); + status = redisClusterSetOptionAddNodes(acc->cc, CLUSTER_NODE); + assert(status == REDIS_OK); + + struct event_base *base = event_base_new(); + status = redisClusterLibeventAttach(acc, base); + assert(status == REDIS_OK); + + status = redisClusterAsyncConnect2(acc); + assert(status == REDIS_OK); + + event_base_dispatch(base); + + redisClusterAsyncFree(acc); + event_base_free(base); + return 0; +} diff --git a/libvalkeycluster/examples/src/example.c b/libvalkeycluster/examples/src/example.c new file mode 100644 index 00000000..a6e8041c --- /dev/null +++ b/libvalkeycluster/examples/src/example.c @@ -0,0 +1,32 @@ +#include +#include +#include + +int main(int argc, char **argv) { + UNUSED(argc); + UNUSED(argv); + struct timeval timeout = {1, 500000}; // 1.5s + + redisClusterContext *cc = redisClusterContextInit(); + redisClusterSetOptionAddNodes(cc, "127.0.0.1:7000"); + redisClusterSetOptionConnectTimeout(cc, timeout); + redisClusterSetOptionRouteUseSlots(cc); + redisClusterConnect2(cc); + if (cc && cc->err) { + printf("Error: %s\n", cc->errstr); + // handle error + exit(-1); + } + + redisReply *reply = + (redisReply *)redisClusterCommand(cc, "SET %s %s", "key", "value"); + printf("SET: %s\n", reply->str); + freeReplyObject(reply); + + redisReply *reply2 = (redisReply *)redisClusterCommand(cc, "GET %s", "key"); + printf("GET: %s\n", reply2->str); + freeReplyObject(reply2); + + redisClusterFree(cc); + return 0; +} diff --git a/libvalkeycluster/examples/src/example_async.c b/libvalkeycluster/examples/src/example_async.c new file mode 100644 index 00000000..0e91830b --- /dev/null +++ b/libvalkeycluster/examples/src/example_async.c @@ -0,0 +1,93 @@ +#include +#include +#include +#include + +void getCallback(redisClusterAsyncContext *cc, void *r, void *privdata) { + redisReply *reply = (redisReply *)r; + if (reply == NULL) { + if (cc->errstr) { + printf("errstr: %s\n", cc->errstr); + } + return; + } + printf("privdata: %s reply: %s\n", (char *)privdata, reply->str); + + /* Disconnect after receiving the reply to GET */ + redisClusterAsyncDisconnect(cc); +} + +void setCallback(redisClusterAsyncContext *cc, void *r, void *privdata) { + redisReply *reply = (redisReply *)r; + if (reply == NULL) { + if (cc->errstr) { + printf("errstr: %s\n", cc->errstr); + } + return; + } + printf("privdata: %s reply: %s\n", (char *)privdata, reply->str); +} + +void connectCallback(const redisAsyncContext *ac, int status) { + if (status != REDIS_OK) { + printf("Error: %s\n", ac->errstr); + return; + } + printf("Connected to %s:%d\n", ac->c.tcp.host, ac->c.tcp.port); +} + +void disconnectCallback(const redisAsyncContext *ac, int status) { + if (status != REDIS_OK) { + printf("Error: %s\n", ac->errstr); + return; + } + printf("Disconnected from %s:%d\n", ac->c.tcp.host, ac->c.tcp.port); +} + +int main(int argc, char **argv) { + printf("Connecting...\n"); + redisClusterAsyncContext *cc = + redisClusterAsyncConnect("127.0.0.1:7000", HIRCLUSTER_FLAG_NULL); + if (cc && cc->err) { + printf("Error: %s\n", cc->errstr); + return 1; + } + + struct event_base *base = event_base_new(); + redisClusterLibeventAttach(cc, base); + redisClusterAsyncSetConnectCallback(cc, connectCallback); + redisClusterAsyncSetDisconnectCallback(cc, disconnectCallback); + + int status; + status = redisClusterAsyncCommand(cc, setCallback, (char *)"THE_ID", + "SET %s %s", "key", "value"); + if (status != REDIS_OK) { + printf("error: err=%d errstr=%s\n", cc->err, cc->errstr); + } + + status = redisClusterAsyncCommand(cc, getCallback, (char *)"THE_ID", + "GET %s", "key"); + if (status != REDIS_OK) { + printf("error: err=%d errstr=%s\n", cc->err, cc->errstr); + } + + status = redisClusterAsyncCommand(cc, setCallback, (char *)"THE_ID", + "SET %s %s", "key2", "value2"); + if (status != REDIS_OK) { + printf("error: err=%d errstr=%s\n", cc->err, cc->errstr); + } + + status = redisClusterAsyncCommand(cc, getCallback, (char *)"THE_ID", + "GET %s", "key2"); + if (status != REDIS_OK) { + printf("error: err=%d errstr=%s\n", cc->err, cc->errstr); + } + + printf("Dispatch..\n"); + event_base_dispatch(base); + + printf("Done..\n"); + redisClusterAsyncFree(cc); + event_base_free(base); + return 0; +} diff --git a/libvalkeycluster/examples/src/example_tls.c b/libvalkeycluster/examples/src/example_tls.c new file mode 100644 index 00000000..b989a9e8 --- /dev/null +++ b/libvalkeycluster/examples/src/example_tls.c @@ -0,0 +1,60 @@ +#include +#include +#include +#include +#include +#include + +#define CLUSTER_NODE_TLS "127.0.0.1:7301" + +int main(int argc, char **argv) { + UNUSED(argc); + UNUSED(argv); + + redisSSLContext *ssl; + redisSSLContextError ssl_error; + + redisInitOpenSSL(); + ssl = redisCreateSSLContext("ca.crt", NULL, "client.crt", "client.key", + NULL, &ssl_error); + if (!ssl) { + printf("SSL Context error: %s\n", redisSSLContextGetError(ssl_error)); + exit(1); + } + + struct timeval timeout = {1, 500000}; // 1.5s + + redisClusterContext *cc = redisClusterContextInit(); + redisClusterSetOptionAddNodes(cc, CLUSTER_NODE_TLS); + redisClusterSetOptionConnectTimeout(cc, timeout); + redisClusterSetOptionRouteUseSlots(cc); + redisClusterSetOptionParseSlaves(cc); + redisClusterSetOptionEnableSSL(cc, ssl); + redisClusterConnect2(cc); + if (cc && cc->err) { + printf("Error: %s\n", cc->errstr); + // handle error + exit(-1); + } + + redisReply *reply = + (redisReply *)redisClusterCommand(cc, "SET %s %s", "key", "value"); + if (!reply) { + printf("Reply missing: %s\n", cc->errstr); + exit(-1); + } + printf("SET: %s\n", reply->str); + freeReplyObject(reply); + + redisReply *reply2 = (redisReply *)redisClusterCommand(cc, "GET %s", "key"); + if (!reply2) { + printf("Reply missing: %s\n", cc->errstr); + exit(-1); + } + printf("GET: %s\n", reply2->str); + freeReplyObject(reply2); + + redisClusterFree(cc); + redisFreeSSLContext(ssl); + return 0; +} diff --git a/libvalkeycluster/examples/using_cmake_and_make_mixed/build.sh b/libvalkeycluster/examples/using_cmake_and_make_mixed/build.sh new file mode 100755 index 00000000..8b747d11 --- /dev/null +++ b/libvalkeycluster/examples/using_cmake_and_make_mixed/build.sh @@ -0,0 +1,36 @@ +#!/bin/sh +set -e + +# This script builds and installs hiredis using GNU Make and hiredis-cluster using CMake. +# The shared library variants are used when building the examples. + +script_dir=$(realpath "${0%/*}") +repo_dir=$(git rev-parse --show-toplevel) + +# Download hiredis +hiredis_version=1.1.0 +curl -L https://github.com/redis/hiredis/archive/v${hiredis_version}.tar.gz | tar -xz -C ${script_dir} + +# Build and install downloaded hiredis using GNU Make +make -C ${script_dir}/hiredis-${hiredis_version} \ + USE_SSL=1 \ + DESTDIR=${script_dir}/install \ + all install + + +# Build and install hiredis-cluster from the repo using CMake. +mkdir -p ${script_dir}/hiredis_cluster_build +cd ${script_dir}/hiredis_cluster_build +cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -DDISABLE_TESTS=ON -DENABLE_SSL=ON -DDOWNLOAD_HIREDIS=OFF \ + -DCMAKE_PREFIX_PATH=${script_dir}/install/usr/local \ + ${repo_dir} +make DESTDIR=${script_dir}/install clean install + + +# Build example binaries by providing shared libraries +make -C ${repo_dir} CFLAGS="-I${script_dir}/install/usr/local/include" \ + LDFLAGS="-lhiredis_cluster -lhiredis_cluster_ssl -lhiredis -lhiredis_ssl \ + -L${script_dir}/install/usr/local/lib/ \ + -Wl,-rpath=${script_dir}/install/usr/local/lib/" \ + USE_SSL=1 \ + clean examples diff --git a/libvalkeycluster/examples/using_cmake_externalproject/CMakeLists.txt b/libvalkeycluster/examples/using_cmake_externalproject/CMakeLists.txt new file mode 100644 index 00000000..ea8b2e49 --- /dev/null +++ b/libvalkeycluster/examples/using_cmake_externalproject/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.11) +project(hiredis_cluster_externalproject) +include(ExternalProject) + +ExternalProject_Add(hiredis + PREFIX hiredis + GIT_REPOSITORY https://github.com/redis/hiredis + GIT_TAG v1.0.2 + CMAKE_ARGS + "-DCMAKE_C_FLAGS:STRING=-std=c99" + "-DENABLE_SSL:BOOL=ON" + "-DDISABLE_TESTS:BOOL=ON" + "-DCMAKE_BUILD_TYPE:STRING=Debug" + "-DCMAKE_INSTALL_PREFIX:PATH=${CMAKE_CURRENT_BINARY_DIR}/install" +) + +ExternalProject_Add(hiredis_cluster + PREFIX hiredis_cluster + SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../.." + CMAKE_ARGS + "-DENABLE_SSL:BOOL=ON" + "-DDISABLE_TESTS:BOOL=ON" + "-DDOWNLOAD_HIREDIS:BOOL=OFF" + "-DCMAKE_BUILD_TYPE:STRING=Debug" + "-DCMAKE_INSTALL_PREFIX:PATH=${CMAKE_CURRENT_BINARY_DIR}/install" + "-DCMAKE_PREFIX_PATH:PATH=${CMAKE_CURRENT_BINARY_DIR}/install/usr/local" + DEPENDS hiredis +) + +ExternalProject_Add(examples + PREFIX examples + SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../src" + INSTALL_COMMAND "" + CMAKE_ARGS + "-DENABLE_SSL:BOOL=ON" + "-DCMAKE_BUILD_TYPE:STRING=Debug" + "-DCMAKE_INSTALL_PREFIX:PATH=${CMAKE_CURRENT_BINARY_DIR}/install" + "-DCMAKE_PREFIX_PATH:PATH=${CMAKE_CURRENT_BINARY_DIR}/install/usr/local" + DEPENDS hiredis_cluster +) diff --git a/libvalkeycluster/examples/using_cmake_externalproject/build.sh b/libvalkeycluster/examples/using_cmake_externalproject/build.sh new file mode 100755 index 00000000..0c2e2d20 --- /dev/null +++ b/libvalkeycluster/examples/using_cmake_externalproject/build.sh @@ -0,0 +1,18 @@ +#!/bin/sh +set -e + +# This script builds and installs hiredis and hiredis-cluster using +# CMakes ExternalProject module. +# The shared library variants are used when building the examples. + +script_dir=$(realpath "${0%/*}") + +# Prepare a build directory +mkdir -p ${script_dir}/build +cd ${script_dir}/build + +# Generate makefiles +cmake .. + +# Build +VERBOSE=1 make diff --git a/libvalkeycluster/examples/using_cmake_separate/build.sh b/libvalkeycluster/examples/using_cmake_separate/build.sh new file mode 100755 index 00000000..dbf254ac --- /dev/null +++ b/libvalkeycluster/examples/using_cmake_separate/build.sh @@ -0,0 +1,40 @@ +#!/bin/sh +set -e + +# This script builds and installs hiredis and hiredis-cluster as separate +# steps using CMake. +# The shared library variants are used when building the examples. + +script_dir=$(realpath "${0%/*}") +repo_dir=$(git rev-parse --show-toplevel) + +# Download hiredis +hiredis_version=1.1.0 +curl -L https://github.com/redis/hiredis/archive/v${hiredis_version}.tar.gz | tar -xz -C ${script_dir} + +# Build and install downloaded hiredis using CMake +mkdir -p ${script_dir}/hiredis_build +cd ${script_dir}/hiredis_build +cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -DDISABLE_TESTS=ON -DENABLE_SSL=ON \ + -DCMAKE_C_FLAGS="-std=c99" \ + ${script_dir}/hiredis-${hiredis_version} +make DESTDIR=${script_dir}/install install + + +# Build and install hiredis-cluster from the repo using CMake. +mkdir -p ${script_dir}/hiredis_cluster_build +cd ${script_dir}/hiredis_cluster_build +cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -DDISABLE_TESTS=ON -DENABLE_SSL=ON -DDOWNLOAD_HIREDIS=OFF \ + -DCMAKE_PREFIX_PATH=${script_dir}/install/usr/local \ + ${repo_dir} +make DESTDIR=${script_dir}/install clean install + + +# Build examples using headers and libraries installed in previous steps. +mkdir -p ${script_dir}/example_build +cd ${script_dir}/example_build +cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DENABLE_SSL=ON \ + -DCMAKE_PREFIX_PATH=${script_dir}/install/usr/local \ + ${script_dir}/../src +make diff --git a/libvalkeycluster/examples/using_make/build.sh b/libvalkeycluster/examples/using_make/build.sh new file mode 100755 index 00000000..54e7db13 --- /dev/null +++ b/libvalkeycluster/examples/using_make/build.sh @@ -0,0 +1,67 @@ +#!/bin/sh +set -e + +# This script builds and installs hiredis and hiredis-cluster using GNU Make directly. +# The static library variants are used when building the examples. + +script_dir=$(realpath "${0%/*}") +repo_dir=$(git rev-parse --show-toplevel) + +# Download hiredis +hiredis_version=1.0.2 +curl -L https://github.com/redis/hiredis/archive/v${hiredis_version}.tar.gz | tar -xz -C ${script_dir} + +# Build and install downloaded hiredis using GNU Make +make -C ${script_dir}/hiredis-${hiredis_version} \ + USE_SSL=1 \ + DESTDIR=${script_dir}/install \ + all install + + +# Build and install hiredis-cluster from the repo using GNU Make +make -C ${repo_dir} \ + CFLAGS="-I${script_dir}/install/usr/local/include" \ + LDFLAGS="-L${script_dir}/install/usr/local/lib" \ + USE_SSL=1 \ + DESTDIR=${script_dir}/install \ + clean install + + +# Build example binaries by providing static libraries +make -C ${repo_dir} CFLAGS="-I${script_dir}/install/usr/local/include" \ + LDFLAGS="${script_dir}/install/usr/local/lib/libhiredis_cluster.a \ + ${script_dir}/install/usr/local/lib/libhiredis_cluster_ssl.a \ + ${script_dir}/install/usr/local/lib/libhiredis.a \ + ${script_dir}/install/usr/local/lib/libhiredis_ssl.a" \ + USE_SSL=1 \ + clean examples + + +# Run simple example: +# ./examples/hiredis-cluster-example +# +# To get a simple Redis Cluster to run towards: +# docker run --name docker-cluster -d -p 7000-7006:7000-7006 "bjosv/redis-cluster:latest" +# + +# Run TLS/SSL example: +# ./examples/hiredis-cluster-example-tls +# +# Prepare a Redis Cluster to run towards: +# openssl genrsa -out ca.key 4096 +# openssl req -x509 -new -nodes -sha256 -key ca.key -days 3650 -subj '/CN=Redis Test CA' -out ca.crt +# openssl genrsa -out redis.key 2048 +# openssl req -new -sha256 -key redis.key -subj '/CN=Redis Server Test Cert' | openssl x509 -req -sha256 -CA ca.crt -CAkey ca.key -CAserial ca.txt -CAcreateserial -days 365 -out redis.crt +# openssl genrsa -out client.key 2048 +# openssl req -new -sha256 -key client.key -subj '/CN=Redis Client Test Cert' | openssl x509 -req -sha256 -CA ca.crt -CAkey ca.key -CAserial ca.txt -CAcreateserial -days 365 -out client.crt +# +# chmod 777 redis.key +# +# docker run --name redis-tls-1 -d --net=host -v $PWD:/tls:ro redis:6.0.9 redis-server --cluster-enabled yes --tls-cluster yes --port 0 --tls-ca-cert-file /tls/ca.crt --tls-cert-file /tls/redis.crt --tls-key-file /tls/redis.key --tls-port 7301 +# docker run --name redis-tls-2 -d --net=host -v $PWD:/tls:ro redis:6.0.9 redis-server --cluster-enabled yes --tls-cluster yes --port 0 --tls-ca-cert-file /tls/ca.crt --tls-cert-file /tls/redis.crt --tls-key-file /tls/redis.key --tls-port 7302 +# docker run --name redis-tls-3 -d --net=host -v $PWD:/tls:ro redis:6.0.9 redis-server --cluster-enabled yes --tls-cluster yes --port 0 --tls-ca-cert-file /tls/ca.crt --tls-cert-file /tls/redis.crt --tls-key-file /tls/redis.key --tls-port 7303 +# docker run --name redis-tls-4 -d --net=host -v $PWD:/tls:ro redis:6.0.9 redis-server --cluster-enabled yes --tls-cluster yes --port 0 --tls-ca-cert-file /tls/ca.crt --tls-cert-file /tls/redis.crt --tls-key-file /tls/redis.key --tls-port 7304 +# docker run --name redis-tls-5 -d --net=host -v $PWD:/tls:ro redis:6.0.9 redis-server --cluster-enabled yes --tls-cluster yes --port 0 --tls-ca-cert-file /tls/ca.crt --tls-cert-file /tls/redis.crt --tls-key-file /tls/redis.key --tls-port 7305 +# docker run --name redis-tls-6 -d --net=host -v $PWD:/tls:ro redis:6.0.9 redis-server --cluster-enabled yes --tls-cluster yes --port 0 --tls-ca-cert-file /tls/ca.crt --tls-cert-file /tls/redis.crt --tls-key-file /tls/redis.key --tls-port 7306 +# +# echo 'yes' | docker run --name redis-cli-tls -i --rm --net=host -v $PWD:/tls:ro redis:6.0.9 redis-cli --cluster create --tls --cacert /tls/ca.crt --cert /tls/redis.crt --key /tls/redis.key 127.0.0.1:7301 127.0.0.1:7302 127.0.0.1:7303 127.0.0.1:7304 127.0.0.1:7305 127.0.0.1:7306 --cluster-replicas 1 diff --git a/libvalkeycluster/gencommands.py b/libvalkeycluster/gencommands.py new file mode 100755 index 00000000..cc5235c7 --- /dev/null +++ b/libvalkeycluster/gencommands.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2023 Viktor Soderqvist +# This file is released under the BSD license, see the COPYING file + +# This script generates cmddef.h from the JSON files in the Redis repo +# describing the commands. This is done manually when commands have been added +# to Redis or when you want add more commands implemented in modules, etc. +# +# Usage: ./gencommands.py path/to/redis/src/commands/*.json > cmddef.h +# +# Alternatively, the output of the script utils/generate-commands-json.py (which +# fetches the command metadata from a running Redis node) or the file +# commands.json from the redis-doc repo can be used as input to this script: +# https://github.com/redis/redis-doc/blob/master/commands.json +# +# Additional JSON files can be added to extend support for custom commands. The +# JSON file format is not fully documented but hopefully the format can be +# understood from reading the existing JSON files. Alternatively, you can read +# the source code of this script to see what it does. +# +# The key specifications part is documented here: +# https://redis.io/docs/reference/key-specs/ +# +# The discussion where this JSON format was added in Redis is here: +# https://github.com/redis/redis/issues/9359 +# +# For convenience, files on the output format like cmddef.h can also be used as +# input files to this script. It can be used for adding more commands to the +# existing set of commands, but please do not abuse it. Do not to write commands +# information directly in this format. + +import glob +import json +import os +import sys +import re + +# Returns True if any of the nested arguments is a key; False otherwise. +def any_argument_is_key(arguments): + for arg in arguments: + if arg.get("type") == "key": + return True + if "arguments" in arg and any_argument_is_key(arg["arguments"]): + return True + return False + +# Returns a tuple (method, index) where method is one of the following: +# +# NONE = No keys +# UNKNOWN = The position of the first key is unknown or too +# complex to describe (example XREAD) +# INDEX = The first key is the argument at index i +# KEYNUM = The argument at index i specifies the number of keys +# and the first key is the next argument, unless the +# number of keys is zero in which case there are no +# keys (example EVAL) +def firstkey(props): + if not "key_specs" in props: + # Key specs missing. Best-effort fallback to "arguments". + if "arguments" in props: + args = props["arguments"] + for i in range(1, len(args)): + arg = args[i - 1] + if arg.get("type") == "key": + return ("INDEX", i) + elif arg.get("type") == "string" and arg.get("name") == "key": + # add-hoc case for RediSearch + return ("INDEX", i) + elif arg.get("optional") or arg.get("multiple") or "arguments" in arg: + # Too complex for this fallback. + if any_argument_is_key(args): + return ("UNKNOWN", 0) + else: + return ("NONE", 0) + return ("NONE", 0) + + if len(props["key_specs"]) == 0: + return ("NONE", 0) + + # We detect the first key spec and only if the begin_search is by index. + # Otherwise we return -1 for unknown (for example if the first key is + # indicated by a keyword like KEYS or STREAMS). + begin_search = props["key_specs"][0]["begin_search"] + if "index" in begin_search: + # Redis source JSON files have this syntax + pos = begin_search["index"]["pos"] + elif begin_search.get("type") == "index" and "spec" in begin_search: + # generate-commands-json.py returns this syntax + pos = begin_search["spec"]["index"] + else: + return ("UNKNOWN", 0) + + find_keys = props["key_specs"][0]["find_keys"] + if "range" in find_keys or find_keys.get("type") == "range": + # The first key is the arg at index pos. + # Redis source JSON files have this syntax: + # "find_keys": { + # "range": {...} + # } + # generate-commands-json.py returns this syntax: + # "find_keys": { + # "type": "range", + # "spec": {...} + # }, + return ("INDEX", pos) + elif "keynum" in find_keys: + # The arg at pos is the number of keys and the next arg is the first key + # Redis source JSON files have this syntax + assert find_keys["keynum"]["keynumidx"] == 0 + assert find_keys["keynum"]["firstkey"] == 1 + return ("KEYNUM", pos) + elif find_keys.get("type") == "keynum": + # generate-commands-json.py returns this syntax + assert find_keys["spec"]["keynumidx"] == 0 + assert find_keys["spec"]["firstkey"] == 1 + return ("KEYNUM", pos) + else: + return ("UNKNOWN", 0) + +def extract_command_info(name, props): + (firstkeymethod, firstkeypos) = firstkey(props) + container = props.get("container", "") + name = name.upper() + subcommand = None + if container != "": + subcommand = name + name = container.upper() + else: + # Ad-hoc handling of command and subcommand in the same string, + # sepatated by a space. This form is used in e.g. RediSearch's JSON file + # in commands like "FT.CONFIG GET". + tokens = name.split(maxsplit=1) + if len(tokens) > 1: + name, subcommand = tokens + if firstkeypos > 0 and not "key_specs" in props: + # Position was inferred from "arguments" + firstkeypos += 1 + + arity = props["arity"] if "arity" in props else -1 + return (name, subcommand, arity, firstkeymethod, firstkeypos); + +# Parses a file with lines like +# COMMAND(identifier, cmd, subcmd, arity, firstkeymethod, firstkeypos) +def collect_command_from_cmddef_h(f, commands): + for line in f: + m = re.match(r'^COMMAND\(\S+, *"(\S+)", NULL, *(-?\d+), *(\w+), *(\d+)\)', line) + if m: + commands[m.group(1)] = (m.group(1), None, int(m.group(2)), m.group(3), int(m.group(4))) + continue + m = re.match(r'^COMMAND\(\S+, *"(\S+)", *"(\S+)", *(-?\d+), *(\w+), *(\d)\)', line) + if m: + key = m.group(1) + "_" + m.group(2) + commands[key] = (m.group(1), m.group(2), int(m.group(3)), m.group(4), int(m.group(5))) + continue + if re.match(r'^(?:/\*.*\*/)?\s*$', line): + # Comment or blank line + continue + else: + print("Error processing line: %s" % (line)) + exit(1) + +def collect_commands_from_files(filenames): + # The keys in the dicts are "command" or "command_subcommand". + commands = dict() + commands_that_have_subcommands = set() + for filename in filenames: + with open(filename, "r") as f: + if filename.endswith(".h"): + collect_command_from_cmddef_h(f, commands) + continue + try: + d = json.load(f) + for name, props in d.items(): + cmd = extract_command_info(name, props) + (name, subcmd, _, _, _) = cmd + + # For commands with subcommands, we want only the + # command-subcommand pairs, not the container command alone + if subcmd is not None: + commands_that_have_subcommands.add(name) + if name in commands: + del commands[name] + name += "_" + subcmd + elif name in commands_that_have_subcommands: + continue + + commands[name] = cmd + + except json.decoder.JSONDecodeError as err: + print("Error processing %s: %s" % (filename, err)) + exit(1) + return commands + +def generate_c_code(commands): + print("/* This file was generated using gencommands.py */") + print("") + print("/* clang-format off */") + for key in sorted(commands): + (name, subcmd, arity, firstkeymethod, firstkeypos) = commands[key] + # Make valid C identifier (macro name) + key = re.sub(r'\W', '_', key) + if subcmd is None: + print("COMMAND(%s, \"%s\", NULL, %d, %s, %d)" % + (key, name, arity, firstkeymethod, firstkeypos)) + else: + print("COMMAND(%s, \"%s\", \"%s\", %d, %s, %d)" % + (key, name, subcmd, arity, firstkeymethod, firstkeypos)) + +# MAIN + +if len(sys.argv) < 2 or sys.argv[1] == "--help": + print("Usage: %s path/to/redis/src/commands/*.json > cmddef.h" % sys.argv[0]) + exit(1) + +# Find all JSON files +filenames = [] +for filename in sys.argv[1:]: + if os.path.isdir(filename): + # A redis repo root dir (accepted for backward compatibility) + jsondir = os.path.join(filename, "src", "commands") + if not os.path.isdir(jsondir): + print("The directory %s is not a Redis source directory." % filename) + exit(1) + + filenames += glob.glob(os.path.join(jsondir, "*.json")) + else: + filenames.append(filename) + +# Collect all command info +commands = collect_commands_from_files(filenames) + +# Print C code +generate_c_code(commands) diff --git a/libvalkeycluster/hiarray.c b/libvalkeycluster/hiarray.c new file mode 100644 index 00000000..4e70049d --- /dev/null +++ b/libvalkeycluster/hiarray.c @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2015-2017, Ieshen Zheng + * Copyright (c) 2020, Nick + * Copyright (c) 2020-2021, Bjorn Svensson + * Copyright (c) 2020-2021, Viktor Söderqvist + * 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 +#include + +#include "hiarray.h" +#include "hiutil.h" + +struct hiarray *hiarray_create(uint32_t n, size_t size) { + struct hiarray *a; + + ASSERT(n != 0 && size != 0); + + a = hi_malloc(sizeof(*a)); + if (a == NULL) { + return NULL; + } + + a->elem = hi_malloc(n * size); + if (a->elem == NULL) { + hi_free(a); + return NULL; + } + + a->nelem = 0; + a->size = size; + a->nalloc = n; + + return a; +} + +void hiarray_destroy(struct hiarray *a) { + hiarray_deinit(a); + hi_free(a); +} + +void hiarray_deinit(struct hiarray *a) { + ASSERT(a->nelem == 0); + + hi_free(a->elem); + a->elem = NULL; +} + +uint32_t hiarray_idx(struct hiarray *a, void *elem) { + uint8_t *p, *q; + uint32_t off, idx; + + ASSERT(elem >= a->elem); + + p = a->elem; + q = elem; + off = (uint32_t)(q - p); + + ASSERT(off % (uint32_t)a->size == 0); + + idx = off / (uint32_t)a->size; + + return idx; +} + +void *hiarray_push(struct hiarray *a) { + void *elem, *new; + size_t size; + + if (a->nelem == a->nalloc) { + + /* the array is full; allocate new array */ + size = a->size * a->nalloc; + new = hi_realloc(a->elem, 2 * size); + if (new == NULL) { + return NULL; + } + + a->elem = new; + a->nalloc *= 2; + } + + elem = (uint8_t *)a->elem + a->size * a->nelem; + a->nelem++; + + return elem; +} + +void *hiarray_pop(struct hiarray *a) { + void *elem; + + ASSERT(a->nelem != 0); + + a->nelem--; + elem = (uint8_t *)a->elem + a->size * a->nelem; + + return elem; +} + +void *hiarray_get(struct hiarray *a, uint32_t idx) { + void *elem; + + ASSERT(a->nelem != 0); + ASSERT(idx < a->nelem); + + elem = (uint8_t *)a->elem + (a->size * idx); + + return elem; +} + +void *hiarray_top(struct hiarray *a) { + ASSERT(a->nelem != 0); + + return hiarray_get(a, a->nelem - 1); +} + +void hiarray_swap(struct hiarray *a, struct hiarray *b) { + struct hiarray tmp; + + tmp = *a; + *a = *b; + *b = tmp; +} + +/* + * Sort nelem elements of the array in ascending order based on the + * compare comparator. + */ +void hiarray_sort(struct hiarray *a, hiarray_compare_t compare) { + ASSERT(a->nelem != 0); + + qsort(a->elem, a->nelem, a->size, compare); +} + +/* + * Calls the func once for each element in the array as long as func returns + * success. On failure short-circuits and returns the error status. + */ +int hiarray_each(struct hiarray *a, hiarray_each_t func, void *data) { + uint32_t i, nelem; + + ASSERT(hiarray_n(a) != 0); + ASSERT(func != NULL); + + for (i = 0, nelem = hiarray_n(a); i < nelem; i++) { + void *elem = hiarray_get(a, i); + rstatus_t status; + + status = func(elem, data); + if (status != HI_OK) { + return status; + } + } + + return HI_OK; +} diff --git a/libvalkeycluster/hiarray.h b/libvalkeycluster/hiarray.h new file mode 100644 index 00000000..86355cf0 --- /dev/null +++ b/libvalkeycluster/hiarray.h @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2015-2017, Ieshen Zheng + * Copyright (c) 2020-2021, Bjorn Svensson + * 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 __HIARRAY_H_ +#define __HIARRAY_H_ + +#include + +typedef int (*hiarray_compare_t)(const void *, const void *); +typedef int (*hiarray_each_t)(void *, void *); + +struct hiarray { + uint32_t nelem; /* # element */ + void *elem; /* element */ + size_t size; /* element size */ + uint32_t nalloc; /* # allocated element */ +}; + +#define null_hiarray \ + { 0, NULL, 0, 0 } + +static inline void hiarray_null(struct hiarray *a) { + a->nelem = 0; + a->elem = NULL; + a->size = 0; + a->nalloc = 0; +} + +static inline void hiarray_set(struct hiarray *a, void *elem, size_t size, + uint32_t nalloc) { + a->nelem = 0; + a->elem = elem; + a->size = size; + a->nalloc = nalloc; +} + +static inline uint32_t hiarray_n(const struct hiarray *a) { return a->nelem; } + +struct hiarray *hiarray_create(uint32_t n, size_t size); +void hiarray_destroy(struct hiarray *a); +void hiarray_deinit(struct hiarray *a); + +uint32_t hiarray_idx(struct hiarray *a, void *elem); +void *hiarray_push(struct hiarray *a); +void *hiarray_pop(struct hiarray *a); +void *hiarray_get(struct hiarray *a, uint32_t idx); +void *hiarray_top(struct hiarray *a); +void hiarray_swap(struct hiarray *a, struct hiarray *b); +void hiarray_sort(struct hiarray *a, hiarray_compare_t compare); +int hiarray_each(struct hiarray *a, hiarray_each_t func, void *data); + +#endif diff --git a/libvalkeycluster/hircluster.c b/libvalkeycluster/hircluster.c new file mode 100644 index 00000000..2326b0a7 --- /dev/null +++ b/libvalkeycluster/hircluster.c @@ -0,0 +1,4488 @@ +/* + * Copyright (c) 2015-2017, Ieshen Zheng + * Copyright (c) 2020, Nick + * Copyright (c) 2020-2021, Bjorn Svensson + * Copyright (c) 2020-2021, Viktor Söderqvist + * Copyright (c) 2021, Red Hat + * 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 _XOPEN_SOURCE 600 +#include +#include +#include +#include +#include +#include +#include + +#include "adlist.h" +#include "command.h" +#include "dict.h" +#include "hiarray.h" +#include "hircluster.h" +#include "hiutil.h" +#include "win32.h" + +// Cluster errors are offset by 100 to be sufficiently out of range of +// standard Redis errors +#define REDIS_ERR_CLUSTER_TOO_MANY_RETRIES 100 + +#define REDIS_ERROR_MOVED "MOVED" +#define REDIS_ERROR_ASK "ASK" +#define REDIS_ERROR_TRYAGAIN "TRYAGAIN" +#define REDIS_ERROR_CLUSTERDOWN "CLUSTERDOWN" + +#define REDIS_STATUS_OK "OK" + +#define REDIS_COMMAND_CLUSTER_NODES "CLUSTER NODES" +#define REDIS_COMMAND_CLUSTER_SLOTS "CLUSTER SLOTS" +#define REDIS_COMMAND_ASKING "ASKING" + +#define IP_PORT_SEPARATOR ':' + +#define PORT_CPORT_SEPARATOR '@' + +#define CLUSTER_ADDRESS_SEPARATOR "," + +#define CLUSTER_DEFAULT_MAX_RETRY_COUNT 5 +#define NO_RETRY -1 + +#define CRLF "\x0d\x0a" +#define CRLF_LEN (sizeof("\x0d\x0a") - 1) + +#define SLOTMAP_UPDATE_THROTTLE_USEC 1000000 +#define SLOTMAP_UPDATE_ONGOING INT64_MAX + +typedef struct cluster_async_data { + redisClusterAsyncContext *acc; + struct cmd *command; + redisClusterCallbackFn *callback; + int retry_count; + void *privdata; +} cluster_async_data; + +typedef enum CLUSTER_ERR_TYPE { + CLUSTER_NOT_ERR = 0, + CLUSTER_ERR_MOVED, + CLUSTER_ERR_ASK, + CLUSTER_ERR_TRYAGAIN, + CLUSTER_ERR_CLUSTERDOWN, + CLUSTER_ERR_SENTINEL +} CLUSTER_ERR_TYPE; + +static void freeRedisClusterNode(redisClusterNode *node); +static void cluster_slot_destroy(cluster_slot *slot); +static void cluster_open_slot_destroy(copen_slot *oslot); +static int updateNodesAndSlotmap(redisClusterContext *cc, dict *nodes); +static int updateSlotMapAsync(redisClusterAsyncContext *acc, + redisAsyncContext *ac); + +void listClusterNodeDestructor(void *val) { freeRedisClusterNode(val); } + +void listClusterSlotDestructor(void *val) { cluster_slot_destroy(val); } + +unsigned int dictSdsHash(const void *key) { + return dictGenHashFunction((unsigned char *)key, sdslen((char *)key)); +} + +int dictSdsKeyCompare(void *privdata, const void *key1, const void *key2) { + int l1, l2; + DICT_NOTUSED(privdata); + + l1 = sdslen((sds)key1); + l2 = sdslen((sds)key2); + if (l1 != l2) + return 0; + return memcmp(key1, key2, l1) == 0; +} + +void dictSdsDestructor(void *privdata, void *val) { + DICT_NOTUSED(privdata); + + sdsfree(val); +} + +void dictClusterNodeDestructor(void *privdata, void *val) { + DICT_NOTUSED(privdata); + freeRedisClusterNode(val); +} + +/* Cluster node hash table + * maps node address (1.2.3.4:6379) to a redisClusterNode + * Has ownership of redisClusterNode memory + */ +dictType clusterNodesDictType = { + dictSdsHash, /* hash function */ + NULL, /* key dup */ + NULL, /* val dup */ + dictSdsKeyCompare, /* key compare */ + dictSdsDestructor, /* key destructor */ + dictClusterNodeDestructor /* val destructor */ +}; + +/* Referenced cluster node hash table + * maps node id (437c719f5.....) to a redisClusterNode + * No ownership of redisClusterNode memory + */ +dictType clusterNodesRefDictType = { + dictSdsHash, /* hash function */ + NULL, /* key dup */ + NULL, /* val dup */ + dictSdsKeyCompare, /* key compare */ + dictSdsDestructor, /* key destructor */ + NULL /* val destructor */ +}; + +void listCommandFree(void *command) { + struct cmd *cmd = command; + command_destroy(cmd); +} + +/* ----------------------------------------------------------------------------- + * Key space handling + * -------------------------------------------------------------------------- */ + +/* We have 16384 hash slots. The hash slot of a given key is obtained + * as the least significant 14 bits of the crc16 of the key. + * + * However if the key contains the {...} pattern, only the part between + * { and } is hashed. This may be useful in the future to force certain + * keys to be in the same node (assuming no resharding is in progress). */ +static unsigned int keyHashSlot(char *key, int keylen) { + int s, e; /* start-end indexes of { and } */ + + for (s = 0; s < keylen; s++) + if (key[s] == '{') + break; + + /* No '{' ? Hash the whole key. This is the base case. */ + if (s == keylen) + return crc16(key, keylen) & 0x3FFF; + + /* '{' found? Check if we have the corresponding '}'. */ + for (e = s + 1; e < keylen; e++) + if (key[e] == '}') + break; + + /* No '}' or nothing betweeen {} ? Hash the whole key. */ + if (e == keylen || e == s + 1) + return crc16(key, keylen) & 0x3FFF; + + /* If we are here there is both a { and a } on its right. Hash + * what is in the middle between { and }. */ + return crc16(key + s + 1, e - s - 1) & 0x3FFF; +} + +static void __redisClusterSetError(redisClusterContext *cc, int type, + const char *str) { + size_t len; + + if (cc == NULL) { + return; + } + + cc->err = type; + if (str != NULL) { + len = strlen(str); + len = len < (sizeof(cc->errstr) - 1) ? len : (sizeof(cc->errstr) - 1); + memcpy(cc->errstr, str, len); + cc->errstr[len] = '\0'; + } else { + /* Only REDIS_ERR_IO may lack a description! */ + assert(type == REDIS_ERR_IO); + strerror_r(errno, cc->errstr, sizeof(cc->errstr)); + } +} + +static int cluster_reply_error_type(redisReply *reply) { + + if (reply == NULL) { + return REDIS_ERR; + } + + if (reply->type == REDIS_REPLY_ERROR) { + if ((int)strlen(REDIS_ERROR_MOVED) < reply->len && + memcmp(reply->str, REDIS_ERROR_MOVED, strlen(REDIS_ERROR_MOVED)) == + 0) { + return CLUSTER_ERR_MOVED; + } else if ((int)strlen(REDIS_ERROR_ASK) < reply->len && + memcmp(reply->str, REDIS_ERROR_ASK, + strlen(REDIS_ERROR_ASK)) == 0) { + return CLUSTER_ERR_ASK; + } else if ((int)strlen(REDIS_ERROR_TRYAGAIN) < reply->len && + memcmp(reply->str, REDIS_ERROR_TRYAGAIN, + strlen(REDIS_ERROR_TRYAGAIN)) == 0) { + return CLUSTER_ERR_TRYAGAIN; + } else if ((int)strlen(REDIS_ERROR_CLUSTERDOWN) < reply->len && + memcmp(reply->str, REDIS_ERROR_CLUSTERDOWN, + strlen(REDIS_ERROR_CLUSTERDOWN)) == 0) { + return CLUSTER_ERR_CLUSTERDOWN; + } else { + return CLUSTER_ERR_SENTINEL; + } + } + + return CLUSTER_NOT_ERR; +} + +/* Create and initiate the cluster node structure */ +static redisClusterNode *createRedisClusterNode(void) { + /* use calloc to guarantee all fields are zeroed */ + return hi_calloc(1, sizeof(redisClusterNode)); +} + +/* Cleanup the cluster node structure */ +static void freeRedisClusterNode(redisClusterNode *node) { + if (node == NULL) { + return; + } + + sdsfree(node->name); + sdsfree(node->addr); + sdsfree(node->host); + redisFree(node->con); + + if (node->acon != NULL) { + /* Detach this cluster node from the async context. This makes sure + * that redisAsyncFree() wont attempt to update the pointer via its + * dataCleanup and unlinkAsyncContextAndNode() */ + node->acon->data = NULL; + redisAsyncFree(node->acon); + } + if (node->slots != NULL) { + listRelease(node->slots); + } + if (node->slaves != NULL) { + listRelease(node->slaves); + } + + copen_slot **oslot; + if (node->migrating) { + while (hiarray_n(node->migrating)) { + oslot = hiarray_pop(node->migrating); + cluster_open_slot_destroy(*oslot); + } + hiarray_destroy(node->migrating); + } + if (node->importing) { + while (hiarray_n(node->importing)) { + oslot = hiarray_pop(node->importing); + cluster_open_slot_destroy(*oslot); + } + hiarray_destroy(node->importing); + } + hi_free(node); +} + +static cluster_slot *cluster_slot_create(redisClusterNode *node) { + cluster_slot *slot; + + slot = hi_calloc(1, sizeof(*slot)); + if (slot == NULL) { + return NULL; + } + slot->node = node; + + if (node != NULL) { + ASSERT(node->role == REDIS_ROLE_MASTER); + if (node->slots == NULL) { + node->slots = listCreate(); + if (node->slots == NULL) { + cluster_slot_destroy(slot); + return NULL; + } + + node->slots->free = listClusterSlotDestructor; + } + + if (listAddNodeTail(node->slots, slot) == NULL) { + cluster_slot_destroy(slot); + return NULL; + } + } + + return slot; +} + +static int cluster_slot_ref_node(cluster_slot *slot, redisClusterNode *node) { + if (slot == NULL || node == NULL) { + return REDIS_ERR; + } + + if (node->role != REDIS_ROLE_MASTER) { + return REDIS_ERR; + } + + if (node->slots == NULL) { + node->slots = listCreate(); + if (node->slots == NULL) { + return REDIS_ERR; + } + + node->slots->free = listClusterSlotDestructor; + } + + if (listAddNodeTail(node->slots, slot) == NULL) { + return REDIS_ERR; + } + slot->node = node; + + return REDIS_OK; +} + +static void cluster_slot_destroy(cluster_slot *slot) { + slot->start = 0; + slot->end = 0; + slot->node = NULL; + + hi_free(slot); +} + +static copen_slot *cluster_open_slot_create(uint32_t slot_num, int migrate, + sds remote_name, + redisClusterNode *node) { + copen_slot *oslot; + + oslot = hi_calloc(1, sizeof(*oslot)); + if (oslot == NULL) { + return NULL; + } + + oslot->slot_num = slot_num; + oslot->migrate = migrate; + oslot->node = node; + oslot->remote_name = sdsdup(remote_name); + if (oslot->remote_name == NULL) { + hi_free(oslot); + return NULL; + } + + return oslot; +} + +static void cluster_open_slot_destroy(copen_slot *oslot) { + oslot->slot_num = 0; + oslot->migrate = 0; + oslot->node = NULL; + sdsfree(oslot->remote_name); + oslot->remote_name = NULL; + hi_free(oslot); +} + +/** + * Handle password authentication in the synchronous API + */ +static int authenticate(redisClusterContext *cc, redisContext *c) { + if (cc == NULL || c == NULL) { + return REDIS_ERR; + } + + // Skip if no password configured + if (cc->password == NULL) { + return REDIS_OK; + } + + redisReply *reply; + if (cc->username != NULL) { + reply = redisCommand(c, "AUTH %s %s", cc->username, cc->password); + } else { + reply = redisCommand(c, "AUTH %s", cc->password); + } + + if (reply == NULL) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, + "Command AUTH reply error (NULL)"); + goto error; + } + + if (reply->type == REDIS_REPLY_ERROR) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, reply->str); + goto error; + } + + freeReplyObject(reply); + return REDIS_OK; + +error: + freeReplyObject(reply); + + return REDIS_ERR; +} + +/** + * Return a new node with the "cluster slots" command reply. + */ +static redisClusterNode *node_get_with_slots(redisClusterContext *cc, + redisReply *host_elem, + redisReply *port_elem, + uint8_t role) { + redisClusterNode *node = NULL; + + if (host_elem == NULL || port_elem == NULL) { + return NULL; + } + + if (host_elem->type != REDIS_REPLY_STRING || host_elem->len <= 0) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, + "Command(cluster slots) reply error: " + "node ip is not string."); + goto error; + } + + if (port_elem->type != REDIS_REPLY_INTEGER || port_elem->integer <= 0) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, + "Command(cluster slots) reply error: " + "node port is not integer."); + goto error; + } + + if (!hi_valid_port((int)port_elem->integer)) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, + "Command(cluster slots) reply error: " + "node port is not valid."); + goto error; + } + + node = createRedisClusterNode(); + if (node == NULL) { + goto oom; + } + + if (role == REDIS_ROLE_MASTER) { + node->slots = listCreate(); + if (node->slots == NULL) { + goto oom; + } + + node->slots->free = listClusterSlotDestructor; + } + + node->addr = sdsnewlen(host_elem->str, host_elem->len); + if (node->addr == NULL) { + goto oom; + } + node->addr = sdscatfmt(node->addr, ":%i", port_elem->integer); + if (node->addr == NULL) { + goto oom; + } + node->host = sdsnewlen(host_elem->str, host_elem->len); + if (node->host == NULL) { + goto oom; + } + node->name = NULL; + node->port = (int)port_elem->integer; + node->role = role; + + return node; + +oom: + __redisClusterSetError(cc, REDIS_ERR_OOM, "Out of memory"); + // passthrough + +error: + if (node != NULL) { + sdsfree(node->addr); + sdsfree(node->host); + hi_free(node); + } + return NULL; +} + +/** + * Return a new node with the "cluster nodes" command reply. + */ +static redisClusterNode *node_get_with_nodes(redisClusterContext *cc, + sds *node_infos, int info_count, + uint8_t role) { + char *p = NULL; + redisClusterNode *node = NULL; + + if (info_count < 8) { + return NULL; + } + + node = createRedisClusterNode(); + if (node == NULL) { + goto oom; + } + + if (role == REDIS_ROLE_MASTER) { + node->slots = listCreate(); + if (node->slots == NULL) { + goto oom; + } + + node->slots->free = listClusterSlotDestructor; + } + + /* Handle field */ + node->name = node_infos[0]; + node_infos[0] = NULL; /* Ownership moved */ + + /* Handle field + * Remove @cport... since addr is used as a dict key which should be : */ + if ((p = strchr(node_infos[1], PORT_CPORT_SEPARATOR)) != NULL) { + sdsrange(node_infos[1], 0, p - node_infos[1] - 1 /* skip @ */); + } + node->addr = node_infos[1]; + node_infos[1] = NULL; /* Ownership moved */ + + node->role = role; + + /* Get the ip part */ + if ((p = strrchr(node->addr, IP_PORT_SEPARATOR)) == NULL) { + __redisClusterSetError( + cc, REDIS_ERR_OTHER, + "server address is incorrect, port separator missing."); + goto error; + } + node->host = sdsnewlen(node->addr, p - node->addr); + if (node->host == NULL) { + goto oom; + } + p++; // remove found separator character + + /* Get the port part */ + node->port = hi_atoi(p, strlen(p)); + + return node; + +oom: + __redisClusterSetError(cc, REDIS_ERR_OOM, "Out of memory"); + // passthrough + +error: + freeRedisClusterNode(node); + return NULL; +} + +static void cluster_nodes_swap_ctx(dict *nodes_f, dict *nodes_t) { + dictEntry *de_f, *de_t; + redisClusterNode *node_f, *node_t; + redisContext *c; + redisAsyncContext *ac; + + if (nodes_f == NULL || nodes_t == NULL) { + return; + } + + dictIterator di; + dictInitIterator(&di, nodes_t); + + while ((de_t = dictNext(&di)) != NULL) { + node_t = dictGetEntryVal(de_t); + if (node_t == NULL) { + continue; + } + + de_f = dictFind(nodes_f, node_t->addr); + if (de_f == NULL) { + continue; + } + + node_f = dictGetEntryVal(de_f); + if (node_f->con != NULL) { + c = node_f->con; + node_f->con = node_t->con; + node_t->con = c; + } + + if (node_f->acon != NULL) { + ac = node_f->acon; + node_f->acon = node_t->acon; + node_t->acon = ac; + + node_t->acon->data = node_t; + if (node_f->acon) + node_f->acon->data = node_f; + } + } +} + +static int cluster_master_slave_mapping_with_name(redisClusterContext *cc, + dict **nodes, + redisClusterNode *node, + sds master_name) { + int ret; + dictEntry *di; + redisClusterNode *node_old; + listNode *lnode; + + if (node == NULL || master_name == NULL) { + return REDIS_ERR; + } + + if (*nodes == NULL) { + *nodes = dictCreate(&clusterNodesRefDictType, NULL); + if (*nodes == NULL) { + goto oom; + } + } + + di = dictFind(*nodes, master_name); + if (di == NULL) { + sds key = sdsnewlen(master_name, sdslen(master_name)); + if (key == NULL) { + goto oom; + } + ret = dictAdd(*nodes, key, node); + if (ret != DICT_OK) { + sdsfree(key); + goto oom; + } + + } else { + node_old = dictGetEntryVal(di); + if (node_old == NULL) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, "dict get value null"); + return REDIS_ERR; + } + + if (node->role == REDIS_ROLE_MASTER && + node_old->role == REDIS_ROLE_MASTER) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, + "two masters have the same name"); + return REDIS_ERR; + } else if (node->role == REDIS_ROLE_MASTER && + node_old->role == REDIS_ROLE_SLAVE) { + if (node->slaves == NULL) { + node->slaves = listCreate(); + if (node->slaves == NULL) { + goto oom; + } + + node->slaves->free = listClusterNodeDestructor; + } + + if (node_old->slaves != NULL) { + node_old->slaves->free = NULL; + while (listLength(node_old->slaves) > 0) { + lnode = listFirst(node_old->slaves); + if (listAddNodeHead(node->slaves, lnode->value) == NULL) { + goto oom; + } + listDelNode(node_old->slaves, lnode); + } + listRelease(node_old->slaves); + node_old->slaves = NULL; + } + + if (listAddNodeHead(node->slaves, node_old) == NULL) { + goto oom; + } + dictSetHashVal(*nodes, di, node); + + } else if (node->role == REDIS_ROLE_SLAVE) { + if (node_old->slaves == NULL) { + node_old->slaves = listCreate(); + if (node_old->slaves == NULL) { + goto oom; + } + + node_old->slaves->free = listClusterNodeDestructor; + } + if (listAddNodeTail(node_old->slaves, node) == NULL) { + goto oom; + } + + } else { + NOT_REACHED(); + } + } + + return REDIS_OK; + +oom: + __redisClusterSetError(cc, REDIS_ERR_OOM, "Out of memory"); + return REDIS_ERR; +} + +/** + * Parse the "cluster slots" command reply to nodes dict. + */ +dict *parse_cluster_slots(redisClusterContext *cc, redisReply *reply, + int flags) { + int ret; + cluster_slot *slot = NULL; + dict *nodes = NULL; + dictEntry *den; + redisReply *elem_slots; + redisReply *elem_slots_begin, *elem_slots_end; + redisReply *elem_nodes; + redisReply *elem_ip, *elem_port; + redisClusterNode *master = NULL, *slave; + uint32_t i, idx; + + if (reply == NULL) { + return NULL; + } + + nodes = dictCreate(&clusterNodesDictType, NULL); + if (nodes == NULL) { + goto oom; + } + + if (reply->type != REDIS_REPLY_ARRAY || reply->elements <= 0) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, + "Command(cluster slots) reply error: " + "reply is not an array."); + goto error; + } + + for (i = 0; i < reply->elements; i++) { + elem_slots = reply->element[i]; + if (elem_slots->type != REDIS_REPLY_ARRAY || elem_slots->elements < 3) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, + "Command(cluster slots) reply error: " + "first sub_reply is not an array."); + goto error; + } + + slot = cluster_slot_create(NULL); + if (slot == NULL) { + goto oom; + } + + // one slots region + for (idx = 0; idx < elem_slots->elements; idx++) { + if (idx == 0) { + elem_slots_begin = elem_slots->element[idx]; + if (elem_slots_begin->type != REDIS_REPLY_INTEGER) { + __redisClusterSetError( + cc, REDIS_ERR_OTHER, + "Command(cluster slots) reply error: " + "slot begin is not an integer."); + goto error; + } + slot->start = (int)(elem_slots_begin->integer); + } else if (idx == 1) { + elem_slots_end = elem_slots->element[idx]; + if (elem_slots_end->type != REDIS_REPLY_INTEGER) { + __redisClusterSetError( + cc, REDIS_ERR_OTHER, + "Command(cluster slots) reply error: " + "slot end is not an integer."); + goto error; + } + + slot->end = (int)(elem_slots_end->integer); + + if (slot->start > slot->end) { + __redisClusterSetError( + cc, REDIS_ERR_OTHER, + "Command(cluster slots) reply error: " + "slot begin is bigger than slot end."); + goto error; + } + } else { + elem_nodes = elem_slots->element[idx]; + if (elem_nodes->type != REDIS_REPLY_ARRAY || + elem_nodes->elements < 2) { + __redisClusterSetError( + cc, REDIS_ERR_OTHER, + "Command(cluster slots) reply error: " + "nodes sub_reply is not an correct array."); + goto error; + } + + elem_ip = elem_nodes->element[0]; + elem_port = elem_nodes->element[1]; + + if (elem_ip == NULL || elem_port == NULL || + elem_ip->type != REDIS_REPLY_STRING || + elem_port->type != REDIS_REPLY_INTEGER) { + __redisClusterSetError( + cc, REDIS_ERR_OTHER, + "Command(cluster slots) reply error: " + "master ip or port is not correct."); + goto error; + } + + // this is master. + if (idx == 2) { + sds address = sdsnewlen(elem_ip->str, elem_ip->len); + if (address == NULL) { + goto oom; + } + address = sdscatfmt(address, ":%i", elem_port->integer); + if (address == NULL) { + goto oom; + } + + den = dictFind(nodes, address); + sdsfree(address); + // master already exists, break to the next slots region. + if (den != NULL) { + + master = dictGetEntryVal(den); + ret = cluster_slot_ref_node(slot, master); + if (ret != REDIS_OK) { + goto oom; + } + + slot = NULL; + break; + } + + master = node_get_with_slots(cc, elem_ip, elem_port, + REDIS_ROLE_MASTER); + if (master == NULL) { + goto error; + } + + sds key = sdsnewlen(master->addr, sdslen(master->addr)); + if (key == NULL) { + freeRedisClusterNode(master); + goto oom; + } + + ret = dictAdd(nodes, key, master); + if (ret != DICT_OK) { + sdsfree(key); + freeRedisClusterNode(master); + goto oom; + } + + ret = cluster_slot_ref_node(slot, master); + if (ret != REDIS_OK) { + goto oom; + } + + slot = NULL; + } else if (flags & HIRCLUSTER_FLAG_ADD_SLAVE) { + slave = node_get_with_slots(cc, elem_ip, elem_port, + REDIS_ROLE_SLAVE); + if (slave == NULL) { + goto error; + } + + if (master->slaves == NULL) { + master->slaves = listCreate(); + if (master->slaves == NULL) { + freeRedisClusterNode(slave); + goto oom; + } + + master->slaves->free = listClusterNodeDestructor; + } + + if (listAddNodeTail(master->slaves, slave) == NULL) { + freeRedisClusterNode(slave); + goto oom; + } + } + } + } + } + + return nodes; + +oom: + __redisClusterSetError(cc, REDIS_ERR_OOM, "Out of memory"); + // passthrough + +error: + if (nodes != NULL) { + dictRelease(nodes); + } + if (slot != NULL) { + cluster_slot_destroy(slot); + } + return NULL; +} + +/** + * Parse the "cluster nodes" command reply to nodes dict. + */ +dict *parse_cluster_nodes(redisClusterContext *cc, char *str, int str_len, + int flags) { + int ret; + dict *nodes = NULL; + dict *nodes_name = NULL; + redisClusterNode *master, *slave; + cluster_slot *slot; + char *pos, *start, *end, *line_start, *line_end; + char *role; + int role_len; + int slot_start, slot_end, slot_ranges_found = 0; + sds *part = NULL, *slot_start_end = NULL; + int count_part = 0, count_slot_start_end = 0; + int k; + int len; + + nodes = dictCreate(&clusterNodesDictType, NULL); + if (nodes == NULL) { + goto oom; + } + + start = str; + end = start + str_len; + + line_start = start; + + for (pos = start; pos < end; pos++) { + if (*pos == '\n') { + line_end = pos - 1; + len = line_end - line_start; + + part = sdssplitlen(line_start, len + 1, " ", 1, &count_part); + if (part == NULL) { + goto oom; + } + + if (count_part < 8) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, + "split cluster nodes error"); + goto error; + } + + // if the address string starts with ":0", skip this node. + if (sdslen(part[1]) >= 2 && memcmp(part[1], ":0", 2) == 0) { + sdsfreesplitres(part, count_part); + count_part = 0; + part = NULL; + + start = pos + 1; + line_start = start; + pos = start; + + continue; + } + + if (sdslen(part[2]) >= 7 && memcmp(part[2], "myself,", 7) == 0) { + role_len = sdslen(part[2]) - 7; + role = part[2] + 7; + } else { + role_len = sdslen(part[2]); + role = part[2]; + } + + // add master node + if (role_len >= 6 && memcmp(role, "master", 6) == 0) { + master = node_get_with_nodes(cc, part, count_part, + REDIS_ROLE_MASTER); + if (master == NULL) { + goto error; + } + + sds key = sdsnewlen(master->addr, sdslen(master->addr)); + if (key == NULL) { + freeRedisClusterNode(master); + goto oom; + } + + ret = dictAdd(nodes, key, master); + if (ret != DICT_OK) { + // Key already exists, but possibly an OOM error + __redisClusterSetError( + cc, REDIS_ERR_OTHER, + "The address already exists in the nodes"); + sdsfree(key); + freeRedisClusterNode(master); + goto error; + } + + if (flags & HIRCLUSTER_FLAG_ADD_SLAVE) { + ret = cluster_master_slave_mapping_with_name( + cc, &nodes_name, master, master->name); + if (ret != REDIS_OK) { + freeRedisClusterNode(master); + goto error; + } + } + + for (k = 8; k < count_part; k++) { + slot_start_end = sdssplitlen(part[k], sdslen(part[k]), "-", + 1, &count_slot_start_end); + if (slot_start_end == NULL) { + goto oom; + } + + if (count_slot_start_end == 1) { + slot_start = hi_atoi(slot_start_end[0], + sdslen(slot_start_end[0])); + slot_end = slot_start; + } else if (count_slot_start_end == 2) { + slot_start = hi_atoi(slot_start_end[0], + sdslen(slot_start_end[0])); + ; + slot_end = hi_atoi(slot_start_end[1], + sdslen(slot_start_end[1])); + ; + } else { + // add open slot for master + if (flags & HIRCLUSTER_FLAG_ADD_OPENSLOT && + count_slot_start_end == 3 && + sdslen(slot_start_end[0]) > 1 && + sdslen(slot_start_end[1]) == 1 && + sdslen(slot_start_end[2]) > 1 && + slot_start_end[0][0] == '[' && + slot_start_end[2][sdslen(slot_start_end[2]) - 1] == + ']') { + + copen_slot *oslot, **oslot_elem; + + sdsrange(slot_start_end[0], 1, -1); + sdsrange(slot_start_end[2], 0, -2); + + if (slot_start_end[1][0] == '>') { + oslot = cluster_open_slot_create( + hi_atoi(slot_start_end[0], + sdslen(slot_start_end[0])), + 1, slot_start_end[2], master); + if (oslot == NULL) { + __redisClusterSetError( + cc, REDIS_ERR_OTHER, + "create open slot error"); + goto error; + } + + if (master->migrating == NULL) { + master->migrating = + hiarray_create(1, sizeof(oslot)); + if (master->migrating == NULL) { + cluster_open_slot_destroy(oslot); + goto oom; + } + } + + oslot_elem = hiarray_push(master->migrating); + if (oslot_elem == NULL) { + cluster_open_slot_destroy(oslot); + goto oom; + } + + *oslot_elem = oslot; + } else if (slot_start_end[1][0] == '<') { + oslot = cluster_open_slot_create( + hi_atoi(slot_start_end[0], + sdslen(slot_start_end[0])), + 0, slot_start_end[2], master); + if (oslot == NULL) { + __redisClusterSetError( + cc, REDIS_ERR_OTHER, + "create open slot error"); + goto error; + } + + if (master->importing == NULL) { + master->importing = + hiarray_create(1, sizeof(oslot)); + if (master->importing == NULL) { + cluster_open_slot_destroy(oslot); + goto oom; + } + } + + oslot_elem = hiarray_push(master->importing); + if (oslot_elem == NULL) { + cluster_open_slot_destroy(oslot); + goto oom; + } + + *oslot_elem = oslot; + } + } + + slot_start = -1; + slot_end = -1; + } + + sdsfreesplitres(slot_start_end, count_slot_start_end); + count_slot_start_end = 0; + slot_start_end = NULL; + + if (slot_start < 0 || slot_end < 0 || + slot_start > slot_end || + slot_end >= REDIS_CLUSTER_SLOTS) { + continue; + } + slot_ranges_found += 1; + + slot = cluster_slot_create(master); + if (slot == NULL) { + goto oom; + } + + slot->start = (uint32_t)slot_start; + slot->end = (uint32_t)slot_end; + } + + } + // add slave node + else if ((flags & HIRCLUSTER_FLAG_ADD_SLAVE) && + (role_len >= 5 && memcmp(role, "slave", 5) == 0)) { + slave = + node_get_with_nodes(cc, part, count_part, REDIS_ROLE_SLAVE); + if (slave == NULL) { + goto error; + } + + ret = cluster_master_slave_mapping_with_name(cc, &nodes_name, + slave, part[3]); + if (ret != REDIS_OK) { + freeRedisClusterNode(slave); + goto error; + } + } + + sdsfreesplitres(part, count_part); + count_part = 0; + part = NULL; + + start = pos + 1; + line_start = start; + pos = start; + } + } + + if (slot_ranges_found == 0) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, "No slot information"); + goto error; + } + + if (nodes_name != NULL) { + dictRelease(nodes_name); + } + + return nodes; + +oom: + __redisClusterSetError(cc, REDIS_ERR_OOM, "Out of memory"); + // passthrough + +error: + sdsfreesplitres(part, count_part); + sdsfreesplitres(slot_start_end, count_slot_start_end); + if (nodes != NULL) { + dictRelease(nodes); + } + if (nodes_name != NULL) { + dictRelease(nodes_name); + } + return NULL; +} + +/* Sends CLUSTER SLOTS or CLUSTER NODES to the node with context c. */ +static int clusterUpdateRouteSendCommand(redisClusterContext *cc, + redisContext *c) { + const char *cmd = (cc->flags & HIRCLUSTER_FLAG_ROUTE_USE_SLOTS ? + REDIS_COMMAND_CLUSTER_SLOTS : + REDIS_COMMAND_CLUSTER_NODES); + if (redisAppendCommand(c, cmd) != REDIS_OK) { + const char *msg = (cc->flags & HIRCLUSTER_FLAG_ROUTE_USE_SLOTS ? + "Command (cluster slots) send error." : + "Command (cluster nodes) send error."); + __redisClusterSetError(cc, c->err, msg); + return REDIS_ERR; + } + /* Flush buffer to socket. */ + if (redisBufferWrite(c, NULL) == REDIS_ERR) + return REDIS_ERR; + + return REDIS_OK; +} + +/* Receives and handles a CLUSTER SLOTS reply from node with context c. */ +static int handleClusterSlotsReply(redisClusterContext *cc, redisContext *c) { + redisReply *reply = NULL; + int result = redisGetReply(c, (void **)&reply); + if (result != REDIS_OK) { + if (c->err == REDIS_ERR_TIMEOUT) { + __redisClusterSetError( + cc, c->err, + "Command (cluster slots) reply error (socket timeout)"); + } else { + __redisClusterSetError( + cc, REDIS_ERR_OTHER, + "Command (cluster slots) reply error (NULL)."); + } + return REDIS_ERR; + } else if (reply->type != REDIS_REPLY_ARRAY) { + if (reply->type == REDIS_REPLY_ERROR) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, reply->str); + } else { + __redisClusterSetError( + cc, REDIS_ERR_OTHER, + "Command (cluster slots) reply error: type is not array."); + } + freeReplyObject(reply); + return REDIS_ERR; + } + + dict *nodes = parse_cluster_slots(cc, reply, cc->flags); + freeReplyObject(reply); + return updateNodesAndSlotmap(cc, nodes); +} + +/* Receives and handles a CLUSTER NODES reply from node with context c. */ +static int handleClusterNodesReply(redisClusterContext *cc, redisContext *c) { + redisReply *reply = NULL; + int result = redisGetReply(c, (void **)&reply); + if (result != REDIS_OK) { + if (c->err == REDIS_ERR_TIMEOUT) { + __redisClusterSetError(cc, c->err, + "Command (cluster nodes) reply error " + "(socket timeout)"); + } else { + __redisClusterSetError(cc, REDIS_ERR_OTHER, + "Command (cluster nodes) reply error " + "(NULL)."); + } + return REDIS_ERR; + } else if (reply->type != REDIS_REPLY_STRING) { + if (reply->type == REDIS_REPLY_ERROR) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, reply->str); + } else { + __redisClusterSetError(cc, REDIS_ERR_OTHER, + "Command(cluster nodes) reply error: " + "type is not string."); + } + freeReplyObject(reply); + return REDIS_ERR; + } + + dict *nodes = parse_cluster_nodes(cc, reply->str, reply->len, cc->flags); + freeReplyObject(reply); + return updateNodesAndSlotmap(cc, nodes); +} + +/* Receives and handles a CLUSTER SLOTS or CLUSTER NODES reply from node with + * context c. */ +static int clusterUpdateRouteHandleReply(redisClusterContext *cc, + redisContext *c) { + if (cc->flags & HIRCLUSTER_FLAG_ROUTE_USE_SLOTS) { + return handleClusterSlotsReply(cc, c); + } else { + return handleClusterNodesReply(cc, c); + } +} + +/** + * Update route with the "cluster nodes" or "cluster slots" command reply. + */ +static int cluster_update_route_by_addr(redisClusterContext *cc, const char *ip, + int port) { + redisContext *c = NULL; + + if (cc == NULL) { + return REDIS_ERR; + } + + if (ip == NULL || port <= 0) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, "Ip or port error!"); + goto error; + } + + redisOptions options = {0}; + REDIS_OPTIONS_SET_TCP(&options, ip, port); + options.connect_timeout = cc->connect_timeout; + options.command_timeout = cc->command_timeout; + + c = redisConnectWithOptions(&options); + if (c == NULL) { + __redisClusterSetError(cc, REDIS_ERR_OOM, "Out of memory"); + return REDIS_ERR; + } + + if (cc->on_connect) { + cc->on_connect(c, c->err ? REDIS_ERR : REDIS_OK); + } + + if (c->err) { + __redisClusterSetError(cc, c->err, c->errstr); + goto error; + } + + if (cc->ssl && cc->ssl_init_fn(c, cc->ssl) != REDIS_OK) { + __redisClusterSetError(cc, c->err, c->errstr); + goto error; + } + + if (authenticate(cc, c) != REDIS_OK) { + goto error; + } + + if (clusterUpdateRouteSendCommand(cc, c) != REDIS_OK) { + goto error; + } + + if (clusterUpdateRouteHandleReply(cc, c) != REDIS_OK) { + goto error; + } + + redisFree(c); + return REDIS_OK; + +error: + redisFree(c); + return REDIS_ERR; +} + +/* Update known cluster nodes with a new collection of redisClusterNodes. + * Will also update the slot-to-node lookup table for the new nodes. */ +static int updateNodesAndSlotmap(redisClusterContext *cc, dict *nodes) { + if (nodes == NULL) { + return REDIS_ERR; + } + + /* Create a slot to redisClusterNode lookup table */ + redisClusterNode **table; + table = hi_calloc(REDIS_CLUSTER_SLOTS, sizeof(redisClusterNode *)); + if (table == NULL) { + goto oom; + } + + dictIterator di; + dictInitIterator(&di, nodes); + + dictEntry *de; + while ((de = dictNext(&di))) { + redisClusterNode *master = dictGetEntryVal(de); + if (master->role != REDIS_ROLE_MASTER) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, + "Node role must be master"); + goto error; + } + + if (master->slots == NULL) { + continue; + } + + listIter li; + listRewind(master->slots, &li); + + listNode *ln; + while ((ln = listNext(&li))) { + cluster_slot *slot = listNodeValue(ln); + if (slot->start > slot->end || slot->end >= REDIS_CLUSTER_SLOTS) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, + "Slot region for node is invalid"); + goto error; + } + for (uint32_t i = slot->start; i <= slot->end; i++) { + if (table[i] != NULL) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, + "Different node holds same slot"); + goto error; + } + table[i] = master; + } + } + } + + /* Update slot-to-node table before changing cc->nodes since + * removal of nodes might trigger user callbacks which may + * send commands, which depend on the slot-to-node table. */ + if (cc->table != NULL) { + hi_free(cc->table); + } + cc->table = table; + + cc->route_version++; + + // Move all hiredis contexts in cc->nodes to nodes + cluster_nodes_swap_ctx(cc->nodes, nodes); + + /* Replace cc->nodes before releasing the old dict since + * the release procedure might access cc->nodes. */ + dict *oldnodes = cc->nodes; + cc->nodes = nodes; + if (oldnodes != NULL) { + dictRelease(oldnodes); + } + if (cc->event_callback != NULL) { + cc->event_callback(cc, HIRCLUSTER_EVENT_SLOTMAP_UPDATED, + cc->event_privdata); + if (cc->route_version == 1) { + /* Special event the first time the slotmap was updated. */ + cc->event_callback(cc, HIRCLUSTER_EVENT_READY, cc->event_privdata); + } + } + cc->need_update_route = 0; + return REDIS_OK; + +oom: + __redisClusterSetError(cc, REDIS_ERR_OOM, "Out of memory"); + // passthrough +error: + hi_free(table); + dictRelease(nodes); + return REDIS_ERR; +} + +int redisClusterUpdateSlotmap(redisClusterContext *cc) { + int ret; + int flag_err_not_set = 1; + redisClusterNode *node; + dictEntry *de; + + if (cc == NULL) { + return REDIS_ERR; + } + + if (cc->nodes == NULL) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, "no server address"); + return REDIS_ERR; + } + + dictIterator di; + dictInitIterator(&di, cc->nodes); + + while ((de = dictNext(&di)) != NULL) { + node = dictGetEntryVal(de); + if (node == NULL || node->host == NULL) { + continue; + } + + ret = cluster_update_route_by_addr(cc, node->host, node->port); + if (ret == REDIS_OK) { + if (cc->err) { + cc->err = 0; + memset(cc->errstr, '\0', strlen(cc->errstr)); + } + return REDIS_OK; + } + + flag_err_not_set = 0; + } + + if (flag_err_not_set) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, "no valid server address"); + } + + return REDIS_ERR; +} + +redisClusterContext *redisClusterContextInit(void) { + redisClusterContext *cc; + + cc = hi_calloc(1, sizeof(redisClusterContext)); + if (cc == NULL) + return NULL; + + cc->max_retry_count = CLUSTER_DEFAULT_MAX_RETRY_COUNT; + return cc; +} + +void redisClusterFree(redisClusterContext *cc) { + + if (cc == NULL) + return; + + if (cc->event_callback) { + cc->event_callback(cc, HIRCLUSTER_EVENT_FREE_CONTEXT, + cc->event_privdata); + } + + if (cc->connect_timeout) { + hi_free(cc->connect_timeout); + cc->connect_timeout = NULL; + } + + if (cc->command_timeout) { + hi_free(cc->command_timeout); + cc->command_timeout = NULL; + } + + if (cc->table != NULL) { + hi_free(cc->table); + cc->table = NULL; + } + + if (cc->nodes != NULL) { + /* Clear cc->nodes before releasing the dict since the release procedure + might access cc->nodes. When a node and its hiredis context are freed + all pending callbacks are executed. Clearing cc->nodes prevents a pending + slotmap update command callback to trigger additional slotmap updates. */ + dict *nodes = cc->nodes; + cc->nodes = NULL; + dictRelease(nodes); + } + + if (cc->requests != NULL) { + listRelease(cc->requests); + } + + if (cc->username != NULL) { + hi_free(cc->username); + cc->username = NULL; + } + + if (cc->password != NULL) { + hi_free(cc->password); + cc->password = NULL; + } + + hi_free(cc); +} + +/* Connect to a Redis cluster. 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. */ +static int _redisClusterConnect2(redisClusterContext *cc) { + + if (cc->nodes == NULL || dictSize(cc->nodes) == 0) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, + "servers address does not set up"); + return REDIS_ERR; + } + + return redisClusterUpdateSlotmap(cc); +} + +/* Connect to a Redis cluster. 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. */ +static redisClusterContext *_redisClusterConnect(redisClusterContext *cc, + const char *addrs) { + + int ret; + + ret = redisClusterSetOptionAddNodes(cc, addrs); + if (ret != REDIS_OK) { + return cc; + } + + redisClusterUpdateSlotmap(cc); + + return cc; +} + +redisClusterContext *redisClusterConnect(const char *addrs, int flags) { + redisClusterContext *cc; + + cc = redisClusterContextInit(); + + if (cc == NULL) { + return NULL; + } + + cc->flags = flags; + + return _redisClusterConnect(cc, addrs); +} + +redisClusterContext *redisClusterConnectWithTimeout(const char *addrs, + const struct timeval tv, + int flags) { + redisClusterContext *cc; + + cc = redisClusterContextInit(); + + if (cc == NULL) { + return NULL; + } + + cc->flags = flags; + + if (cc->connect_timeout == NULL) { + cc->connect_timeout = hi_malloc(sizeof(struct timeval)); + if (cc->connect_timeout == NULL) { + return NULL; + } + } + + memcpy(cc->connect_timeout, &tv, sizeof(struct timeval)); + + return _redisClusterConnect(cc, addrs); +} + +int redisClusterSetOptionAddNode(redisClusterContext *cc, const char *addr) { + dictEntry *node_entry; + redisClusterNode *node = NULL; + int port, ret; + sds ip = NULL; + + if (cc == NULL) { + return REDIS_ERR; + } + + if (cc->nodes == NULL) { + cc->nodes = dictCreate(&clusterNodesDictType, NULL); + if (cc->nodes == NULL) { + goto oom; + } + } + + sds addr_sds = sdsnew(addr); + if (addr_sds == NULL) { + goto oom; + } + node_entry = dictFind(cc->nodes, addr_sds); + sdsfree(addr_sds); + if (node_entry == NULL) { + + char *p; + if ((p = strrchr(addr, IP_PORT_SEPARATOR)) == NULL) { + __redisClusterSetError( + cc, REDIS_ERR_OTHER, + "server address is incorrect, port separator missing."); + return REDIS_ERR; + } + // p includes separator + + if (p - addr <= 0) { /* length until separator */ + __redisClusterSetError( + cc, REDIS_ERR_OTHER, + "server address is incorrect, address part missing."); + return REDIS_ERR; + } + + ip = sdsnewlen(addr, p - addr); + if (ip == NULL) { + goto oom; + } + p++; // remove separator character + + if (strlen(p) <= 0) { + __redisClusterSetError( + cc, REDIS_ERR_OTHER, + "server address is incorrect, port part missing."); + goto error; + } + + port = hi_atoi(p, strlen(p)); + if (port <= 0) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, + "server port is incorrect"); + goto error; + } + + node = createRedisClusterNode(); + if (node == NULL) { + goto oom; + } + + node->addr = sdsnew(addr); + if (node->addr == NULL) { + goto oom; + } + + node->host = ip; + node->port = port; + + sds key = sdsnewlen(node->addr, sdslen(node->addr)); + if (key == NULL) { + goto oom; + } + ret = dictAdd(cc->nodes, key, node); + if (ret != DICT_OK) { + sdsfree(key); + goto oom; + } + } + + return REDIS_OK; + +oom: + __redisClusterSetError(cc, REDIS_ERR_OOM, "Out of memory"); + // passthrough + +error: + sdsfree(ip); + if (node != NULL) { + sdsfree(node->addr); + hi_free(node); + } + return REDIS_ERR; +} + +int redisClusterSetOptionAddNodes(redisClusterContext *cc, const char *addrs) { + int ret; + sds *address = NULL; + int address_count = 0; + int i; + + if (cc == NULL) { + return REDIS_ERR; + } + + address = sdssplitlen(addrs, strlen(addrs), CLUSTER_ADDRESS_SEPARATOR, + strlen(CLUSTER_ADDRESS_SEPARATOR), &address_count); + if (address == NULL) { + __redisClusterSetError(cc, REDIS_ERR_OOM, "Out of memory"); + return REDIS_ERR; + } + + if (address_count <= 0) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, + "invalid server addresses (example format: " + "127.0.0.1:1234,127.0.0.2:5678)"); + sdsfreesplitres(address, address_count); + return REDIS_ERR; + } + + for (i = 0; i < address_count; i++) { + ret = redisClusterSetOptionAddNode(cc, address[i]); + if (ret != REDIS_OK) { + sdsfreesplitres(address, address_count); + return REDIS_ERR; + } + } + + sdsfreesplitres(address, address_count); + + return REDIS_OK; +} + +/* Deprecated function, option has no effect. */ +int redisClusterSetOptionConnectBlock(redisClusterContext *cc) { + if (cc == NULL) { + return REDIS_ERR; + } + return REDIS_OK; +} + +/* Deprecated function, option has no effect. */ +int redisClusterSetOptionConnectNonBlock(redisClusterContext *cc) { + if (cc == NULL) { + return REDIS_ERR; + } + return REDIS_OK; +} + +/** + * Configure a username used during authentication, see + * the Redis AUTH command. + * Disabled by default. Can be disabled again by providing an + * empty string or a null pointer. + */ +int redisClusterSetOptionUsername(redisClusterContext *cc, + const char *username) { + if (cc == NULL) { + return REDIS_ERR; + } + + // Disabling option + if (username == NULL || username[0] == '\0') { + hi_free(cc->username); + cc->username = NULL; + return REDIS_OK; + } + + hi_free(cc->username); + cc->username = hi_strdup(username); + if (cc->username == NULL) { + return REDIS_ERR; + } + + return REDIS_OK; +} + +/** + * Configure a password used when connecting to password-protected + * Redis instances. (See Redis AUTH command) + */ +int redisClusterSetOptionPassword(redisClusterContext *cc, + const char *password) { + + if (cc == NULL) { + return REDIS_ERR; + } + + // Disabling use of password + if (password == NULL || password[0] == '\0') { + hi_free(cc->password); + cc->password = NULL; + return REDIS_OK; + } + + hi_free(cc->password); + cc->password = hi_strdup(password); + if (cc->password == NULL) { + return REDIS_ERR; + } + + return REDIS_OK; +} + +int redisClusterSetOptionParseSlaves(redisClusterContext *cc) { + + if (cc == NULL) { + return REDIS_ERR; + } + + cc->flags |= HIRCLUSTER_FLAG_ADD_SLAVE; + + return REDIS_OK; +} + +int redisClusterSetOptionParseOpenSlots(redisClusterContext *cc) { + + if (cc == NULL) { + return REDIS_ERR; + } + + cc->flags |= HIRCLUSTER_FLAG_ADD_OPENSLOT; + + return REDIS_OK; +} + +int redisClusterSetOptionRouteUseSlots(redisClusterContext *cc) { + + if (cc == NULL) { + return REDIS_ERR; + } + + cc->flags |= HIRCLUSTER_FLAG_ROUTE_USE_SLOTS; + + return REDIS_OK; +} + +int redisClusterSetOptionConnectTimeout(redisClusterContext *cc, + const struct timeval tv) { + + if (cc == NULL) { + return REDIS_ERR; + } + + if (cc->connect_timeout == NULL) { + cc->connect_timeout = hi_malloc(sizeof(struct timeval)); + if (cc->connect_timeout == NULL) { + __redisClusterSetError(cc, REDIS_ERR_OOM, "Out of memory"); + return REDIS_ERR; + } + } + + memcpy(cc->connect_timeout, &tv, sizeof(struct timeval)); + + return REDIS_OK; +} + +int redisClusterSetOptionTimeout(redisClusterContext *cc, + const struct timeval tv) { + if (cc == NULL) { + return REDIS_ERR; + } + + if (cc->command_timeout == NULL || + cc->command_timeout->tv_sec != tv.tv_sec || + cc->command_timeout->tv_usec != tv.tv_usec) { + + if (cc->command_timeout == NULL) { + cc->command_timeout = hi_malloc(sizeof(struct timeval)); + if (cc->command_timeout == NULL) { + __redisClusterSetError(cc, REDIS_ERR_OOM, "Out of memory"); + return REDIS_ERR; + } + } + + memcpy(cc->command_timeout, &tv, sizeof(struct timeval)); + + /* Set timeout on already connected nodes */ + if (cc->nodes && dictSize(cc->nodes) > 0) { + dictEntry *de; + redisClusterNode *node; + + dictIterator di; + dictInitIterator(&di, cc->nodes); + + while ((de = dictNext(&di)) != NULL) { + node = dictGetEntryVal(de); + if (node->acon) { + redisAsyncSetTimeout(node->acon, tv); + } + if (node->con && node->con->err == 0) { + redisSetTimeout(node->con, tv); + } + + if (node->slaves && listLength(node->slaves) > 0) { + redisClusterNode *slave; + listNode *ln; + + listIter li; + listRewind(node->slaves, &li); + + while ((ln = listNext(&li)) != NULL) { + slave = listNodeValue(ln); + if (slave->acon) { + redisAsyncSetTimeout(slave->acon, tv); + } + if (slave->con && slave->con->err == 0) { + redisSetTimeout(slave->con, tv); + } + } + } + } + } + } + + return REDIS_OK; +} + +int redisClusterSetOptionMaxRetry(redisClusterContext *cc, + int max_retry_count) { + if (cc == NULL || max_retry_count <= 0) { + return REDIS_ERR; + } + + cc->max_retry_count = max_retry_count; + + return REDIS_OK; +} + +int redisClusterConnect2(redisClusterContext *cc) { + + if (cc == NULL) { + return REDIS_ERR; + } + + return _redisClusterConnect2(cc); +} + +redisContext *ctx_get_by_node(redisClusterContext *cc, redisClusterNode *node) { + redisContext *c = NULL; + if (node == NULL) { + return NULL; + } + + c = node->con; + if (c != NULL) { + if (c->err) { + redisReconnect(c); + + if (cc->on_connect) { + cc->on_connect(c, c->err ? REDIS_ERR : REDIS_OK); + } + + if (cc->ssl && cc->ssl_init_fn(c, cc->ssl) != REDIS_OK) { + __redisClusterSetError(cc, c->err, c->errstr); + } + + authenticate(cc, c); // err and errstr handled in function + } + + return c; + } + + if (node->host == NULL || node->port <= 0) { + return NULL; + } + + redisOptions options = {0}; + REDIS_OPTIONS_SET_TCP(&options, node->host, node->port); + options.connect_timeout = cc->connect_timeout; + options.command_timeout = cc->command_timeout; + + c = redisConnectWithOptions(&options); + if (c == NULL) { + __redisClusterSetError(cc, REDIS_ERR_OOM, "Out of memory"); + return NULL; + } + + if (cc->on_connect) { + cc->on_connect(c, c->err ? REDIS_ERR : REDIS_OK); + } + + if (c->err) { + __redisClusterSetError(cc, c->err, c->errstr); + redisFree(c); + return NULL; + } + + if (cc->ssl && cc->ssl_init_fn(c, cc->ssl) != REDIS_OK) { + __redisClusterSetError(cc, c->err, c->errstr); + redisFree(c); + return NULL; + } + + if (authenticate(cc, c) != REDIS_OK) { + redisFree(c); + return NULL; + } + + node->con = c; + + return c; +} + +static redisClusterNode *node_get_by_table(redisClusterContext *cc, + uint32_t slot_num) { + if (cc == NULL) { + return NULL; + } + + if (slot_num >= REDIS_CLUSTER_SLOTS) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, "invalid slot"); + return NULL; + } + + if (cc->table == NULL) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, "slotmap not available"); + return NULL; + } + + if (cc->table[slot_num] == NULL) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, + "slot not served by any node"); + return NULL; + } + + return cc->table[slot_num]; +} + +/* Helper function for the redisClusterAppendCommand* family of functions. + * + * Write a formatted command to the output buffer. When this family + * is used, you need to call redisGetReply yourself to retrieve + * the reply (or replies in pub/sub). + */ +static int __redisClusterAppendCommand(redisClusterContext *cc, + struct cmd *command) { + + redisClusterNode *node; + redisContext *c = NULL; + + if (cc == NULL || command == NULL) { + return REDIS_ERR; + } + + node = node_get_by_table(cc, (uint32_t)command->slot_num); + if (node == NULL) { + return REDIS_ERR; + } + + c = ctx_get_by_node(cc, node); + if (c == NULL) { + return REDIS_ERR; + } else if (c->err) { + __redisClusterSetError(cc, c->err, c->errstr); + return REDIS_ERR; + } + + if (redisAppendFormattedCommand(c, command->cmd, command->clen) != + REDIS_OK) { + __redisClusterSetError(cc, c->err, c->errstr); + return REDIS_ERR; + } + + return REDIS_OK; +} + +/* Helper functions for the redisClusterGetReply* family of functions. + */ +static int __redisClusterGetReplyFromNode(redisClusterContext *cc, + redisClusterNode *node, + void **reply) { + redisContext *c; + + if (cc == NULL || node == NULL || reply == NULL) + return REDIS_ERR; + + c = node->con; + if (c == NULL) { + return REDIS_ERR; + } else if (c->err) { + if (cc->need_update_route == 0) { + cc->retry_count++; + if (cc->retry_count > cc->max_retry_count) { + cc->need_update_route = 1; + cc->retry_count = 0; + } + } + __redisClusterSetError(cc, c->err, c->errstr); + return REDIS_ERR; + } + + if (redisGetReply(c, reply) != REDIS_OK) { + __redisClusterSetError(cc, c->err, c->errstr); + return REDIS_ERR; + } + + if (cluster_reply_error_type(*reply) == CLUSTER_ERR_MOVED) + cc->need_update_route = 1; + + return REDIS_OK; +} + +static int __redisClusterGetReply(redisClusterContext *cc, int slot_num, + void **reply) { + redisClusterNode *node; + + if (cc == NULL || slot_num < 0 || reply == NULL) + return REDIS_ERR; + + node = node_get_by_table(cc, (uint32_t)slot_num); + if (node == NULL) { + return REDIS_ERR; + } + + return __redisClusterGetReplyFromNode(cc, node, reply); +} + +/* Parses a MOVED or ASK error reply and returns the destination node. The slot + * is returned by pointer, if provided. */ +static redisClusterNode *getNodeFromRedirectReply(redisClusterContext *cc, + redisReply *reply, + int *slotptr) { + redisClusterNode *node = NULL; + sds *part = NULL; + int part_len = 0; + char *p; + + /* Expecting ["ASK" | "MOVED", "", ":"] */ + part = sdssplitlen(reply->str, reply->len, " ", 1, &part_len); + if (part == NULL) { + goto oom; + } + if (part_len != 3) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, "failed to parse redirect"); + goto done; + } + + /* Parse slot if requested. */ + if (slotptr != NULL) { + *slotptr = hi_atoi(part[1], sdslen(part[1])); + } + + /* Find the last occurance of the port separator since + * IPv6 addresses can contain ':' */ + if ((p = strrchr(part[2], IP_PORT_SEPARATOR)) == NULL) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, + "port separator missing in redirect"); + goto done; + } + // p includes separator + + /* Empty endpoint not supported yet */ + if (p - part[2] == 0) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, + "endpoint missing in redirect"); + goto done; + } + + dictEntry *de = dictFind(cc->nodes, part[2]); + if (de != NULL) { + node = de->val; + goto done; + } + + /* Add this node since it was unknown */ + node = createRedisClusterNode(); + if (node == NULL) { + goto oom; + } + node->role = REDIS_ROLE_MASTER; + node->addr = part[2]; + part[2] = NULL; /* Memory ownership moved */ + + node->host = sdsnewlen(node->addr, p - node->addr); + if (node->host == NULL) { + goto oom; + } + p++; // remove found separator character + node->port = hi_atoi(p, strlen(p)); + + sds key = sdsnewlen(node->addr, sdslen(node->addr)); + if (key == NULL) { + goto oom; + } + + if (dictAdd(cc->nodes, key, node) != DICT_OK) { + sdsfree(key); + goto oom; + } + +done: + sdsfreesplitres(part, part_len); + return node; + +oom: + __redisClusterSetError(cc, REDIS_ERR_OOM, "Out of memory"); + sdsfreesplitres(part, part_len); + if (node != NULL) { + sdsfree(node->addr); + sdsfree(node->host); + hi_free(node); + } + + return NULL; +} + +static void *redis_cluster_command_execute(redisClusterContext *cc, + struct cmd *command) { + void *reply = NULL; + redisClusterNode *node; + redisContext *c = NULL; + int error_type; + redisContext *c_updating_route = NULL; + +retry: + + node = node_get_by_table(cc, (uint32_t)command->slot_num); + if (node == NULL) { + /* Update the slotmap since the slot is not served. */ + if (redisClusterUpdateSlotmap(cc) != REDIS_OK) { + goto error; + } + node = node_get_by_table(cc, (uint32_t)command->slot_num); + if (node == NULL) { + /* Return error since the slot is still not served. */ + goto error; + } + } + + c = ctx_get_by_node(cc, node); + if (c == NULL || c->err) { + /* Failed to connect. Maybe there was a failover and this node is gone. + * Update slotmap to find out. */ + if (redisClusterUpdateSlotmap(cc) != REDIS_OK) { + goto error; + } + + node = node_get_by_table(cc, (uint32_t)command->slot_num); + if (node == NULL) { + goto error; + } + c = ctx_get_by_node(cc, node); + if (c == NULL) { + goto error; + } else if (c->err) { + __redisClusterSetError(cc, c->err, c->errstr); + goto error; + } + } + +moved_retry: +ask_retry: + + if (redisAppendFormattedCommand(c, command->cmd, command->clen) != + REDIS_OK) { + __redisClusterSetError(cc, c->err, c->errstr); + goto error; + } + + /* If update slotmap has been scheduled, do that in the same pipeline. */ + if (cc->need_update_route && c_updating_route == NULL) { + if (clusterUpdateRouteSendCommand(cc, c) == REDIS_OK) { + c_updating_route = c; + } + } + + if (redisGetReply(c, &reply) != REDIS_OK) { + __redisClusterSetError(cc, c->err, c->errstr); + /* We may need to update the slotmap if this node is removed from the + * cluster, but the current request may have already timed out so we + * schedule it for later. */ + if (c->err != REDIS_ERR_OOM) + cc->need_update_route = 1; + goto error; + } + + error_type = cluster_reply_error_type(reply); + if (error_type > CLUSTER_NOT_ERR && error_type < CLUSTER_ERR_SENTINEL) { + cc->retry_count++; + if (cc->retry_count > cc->max_retry_count) { + __redisClusterSetError(cc, REDIS_ERR_CLUSTER_TOO_MANY_RETRIES, + "too many cluster retries"); + goto error; + } + + int slot = -1; + switch (error_type) { + case CLUSTER_ERR_MOVED: + node = getNodeFromRedirectReply(cc, reply, &slot); + freeReplyObject(reply); + reply = NULL; + + if (node == NULL) { + /* Failed to parse redirect. Specific error already set. */ + goto error; + } + + /* Update the slot mapping entry for this slot. */ + if (slot >= 0) { + cc->table[slot] = node; + } + + if (c_updating_route == NULL) { + if (clusterUpdateRouteSendCommand(cc, c) == REDIS_OK) { + /* Deferred update route using the node that sent the + * redirect. */ + c_updating_route = c; + } else if (redisClusterUpdateSlotmap(cc) == REDIS_OK) { + /* Synchronous update route successful using new connection. */ + cc->err = 0; + cc->errstr[0] = '\0'; + } else { + /* Failed to update route. Specific error already set. */ + goto error; + } + } + + c = ctx_get_by_node(cc, node); + if (c == NULL) { + goto error; + } else if (c->err) { + __redisClusterSetError(cc, c->err, c->errstr); + goto error; + } + + goto moved_retry; + + break; + case CLUSTER_ERR_ASK: + node = getNodeFromRedirectReply(cc, reply, NULL); + if (node == NULL) { + goto error; + } + + freeReplyObject(reply); + reply = NULL; + + c = ctx_get_by_node(cc, node); + if (c == NULL) { + goto error; + } else if (c->err) { + __redisClusterSetError(cc, c->err, c->errstr); + goto error; + } + + reply = redisCommand(c, REDIS_COMMAND_ASKING); + if (reply == NULL) { + __redisClusterSetError(cc, c->err, c->errstr); + goto error; + } + + freeReplyObject(reply); + reply = NULL; + + goto ask_retry; + + break; + case CLUSTER_ERR_TRYAGAIN: + case CLUSTER_ERR_CLUSTERDOWN: + freeReplyObject(reply); + reply = NULL; + goto retry; + + break; + default: + + break; + } + } + + goto done; + +error: + if (reply) { + freeReplyObject(reply); + reply = NULL; + } + +done: + if (c_updating_route) { + /* Deferred CLUSTER SLOTS or CLUSTER NODES in progress. Wait for the + * reply and handle it. */ + if (clusterUpdateRouteHandleReply(cc, c_updating_route) != REDIS_OK) { + /* Clear error and update synchronously using another node. */ + cc->err = 0; + cc->errstr[0] = '\0'; + if (redisClusterUpdateSlotmap(cc) != REDIS_OK) { + /* Clear the reply to indicate failure. */ + freeReplyObject(reply); + reply = NULL; + } + } + } + + return reply; +} + +static int command_pre_fragment(redisClusterContext *cc, struct cmd *command, + hilist *commands) { + + struct keypos *kp, *sub_kp; + uint32_t key_count; + uint32_t i, j; + uint32_t idx; + uint32_t key_len; + int slot_num = -1; + struct cmd *sub_command; + struct cmd **sub_commands = NULL; + char num_str[12]; + uint8_t num_str_len; + + if (command == NULL || commands == NULL) { + goto done; + } + + key_count = hiarray_n(command->keys); + + sub_commands = hi_calloc(REDIS_CLUSTER_SLOTS, sizeof(*sub_commands)); + if (sub_commands == NULL) { + goto oom; + } + + command->frag_seq = hi_calloc(key_count, sizeof(*command->frag_seq)); + if (command->frag_seq == NULL) { + goto oom; + } + + // Fill sub_command with key, slot and command length (clen, only keylength) + for (i = 0; i < key_count; i++) { + kp = hiarray_get(command->keys, i); + + slot_num = keyHashSlot(kp->start, kp->end - kp->start); + + if (slot_num < 0 || slot_num >= REDIS_CLUSTER_SLOTS) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, + "keyHashSlot return error"); + goto done; + } + + if (sub_commands[slot_num] == NULL) { + sub_commands[slot_num] = command_get(); + if (sub_commands[slot_num] == NULL) { + goto oom; + } + } + + command->frag_seq[i] = sub_command = sub_commands[slot_num]; + + sub_command->narg++; + + sub_kp = hiarray_push(sub_command->keys); + if (sub_kp == NULL) { + goto oom; + } + + sub_kp->start = kp->start; + sub_kp->end = kp->end; + + // Number of characters in key + key_len = (uint32_t)(kp->end - kp->start); + + sub_command->clen += key_len + uint_len(key_len); + + sub_command->slot_num = slot_num; + + if (command->type == CMD_REQ_REDIS_MSET) { + uint32_t len = 0; + char *p; + + for (p = sub_kp->end + 1; !isdigit(*p); p++) { + } + + p = sub_kp->end + 1; + while (!isdigit(*p)) { + p++; + } + + for (; isdigit(*p); p++) { + len = len * 10 + (uint32_t)(*p - '0'); + } + + len += CRLF_LEN * 2; + len += (p - sub_kp->end); + sub_kp->remain_len = len; + sub_command->clen += len; + } + } + + /* prepend command header */ + for (i = 0; i < REDIS_CLUSTER_SLOTS; i++) { + sub_command = sub_commands[i]; + if (sub_command == NULL) { + continue; + } + + idx = 0; + if (command->type == CMD_REQ_REDIS_MGET) { + //"*%d\r\n$4\r\nmget\r\n" + + sub_command->clen += 5 * sub_command->narg; + + sub_command->narg++; + + hi_itoa(num_str, sub_command->narg); + num_str_len = (uint8_t)(strlen(num_str)); + + sub_command->clen += 13 + num_str_len; + + sub_command->cmd = + hi_calloc(sub_command->clen, sizeof(*sub_command->cmd)); + if (sub_command->cmd == NULL) { + goto oom; + } + + sub_command->cmd[idx++] = '*'; + memcpy(sub_command->cmd + idx, num_str, num_str_len); + idx += num_str_len; + memcpy(sub_command->cmd + idx, "\r\n$4\r\nmget\r\n", 12); + idx += 12; + + for (j = 0; j < hiarray_n(sub_command->keys); j++) { + kp = hiarray_get(sub_command->keys, j); + key_len = (uint32_t)(kp->end - kp->start); + hi_itoa(num_str, key_len); + num_str_len = strlen(num_str); + + sub_command->cmd[idx++] = '$'; + memcpy(sub_command->cmd + idx, num_str, num_str_len); + idx += num_str_len; + memcpy(sub_command->cmd + idx, CRLF, CRLF_LEN); + idx += CRLF_LEN; + memcpy(sub_command->cmd + idx, kp->start, key_len); + idx += key_len; + memcpy(sub_command->cmd + idx, CRLF, CRLF_LEN); + idx += CRLF_LEN; + } + } else if (command->type == CMD_REQ_REDIS_DEL) { + //"*%d\r\n$3\r\ndel\r\n" + + sub_command->clen += 5 * sub_command->narg; + + sub_command->narg++; + + hi_itoa(num_str, sub_command->narg); + num_str_len = (uint8_t)strlen(num_str); + + sub_command->clen += 12 + num_str_len; + + sub_command->cmd = + hi_calloc(sub_command->clen, sizeof(*sub_command->cmd)); + if (sub_command->cmd == NULL) { + goto oom; + } + + sub_command->cmd[idx++] = '*'; + memcpy(sub_command->cmd + idx, num_str, num_str_len); + idx += num_str_len; + memcpy(sub_command->cmd + idx, "\r\n$3\r\ndel\r\n", 11); + idx += 11; + + for (j = 0; j < hiarray_n(sub_command->keys); j++) { + kp = hiarray_get(sub_command->keys, j); + key_len = (uint32_t)(kp->end - kp->start); + hi_itoa(num_str, key_len); + num_str_len = strlen(num_str); + + sub_command->cmd[idx++] = '$'; + memcpy(sub_command->cmd + idx, num_str, num_str_len); + idx += num_str_len; + memcpy(sub_command->cmd + idx, CRLF, CRLF_LEN); + idx += CRLF_LEN; + memcpy(sub_command->cmd + idx, kp->start, key_len); + idx += key_len; + memcpy(sub_command->cmd + idx, CRLF, CRLF_LEN); + idx += CRLF_LEN; + } + } else if (command->type == CMD_REQ_REDIS_EXISTS) { + //"*%d\r\n$6\r\nexists\r\n" + + sub_command->clen += 5 * sub_command->narg; + + sub_command->narg++; + + hi_itoa(num_str, sub_command->narg); + num_str_len = (uint8_t)strlen(num_str); + + sub_command->clen += 15 + num_str_len; + + sub_command->cmd = + hi_calloc(sub_command->clen, sizeof(*sub_command->cmd)); + if (sub_command->cmd == NULL) { + goto oom; + } + + sub_command->cmd[idx++] = '*'; + memcpy(sub_command->cmd + idx, num_str, num_str_len); + idx += num_str_len; + memcpy(sub_command->cmd + idx, "\r\n$6\r\nexists\r\n", 14); + idx += 14; + + for (j = 0; j < hiarray_n(sub_command->keys); j++) { + kp = hiarray_get(sub_command->keys, j); + key_len = (uint32_t)(kp->end - kp->start); + hi_itoa(num_str, key_len); + num_str_len = strlen(num_str); + + sub_command->cmd[idx++] = '$'; + memcpy(sub_command->cmd + idx, num_str, num_str_len); + idx += num_str_len; + memcpy(sub_command->cmd + idx, CRLF, CRLF_LEN); + idx += CRLF_LEN; + memcpy(sub_command->cmd + idx, kp->start, key_len); + idx += key_len; + memcpy(sub_command->cmd + idx, CRLF, CRLF_LEN); + idx += CRLF_LEN; + } + } else if (command->type == CMD_REQ_REDIS_MSET) { + //"*%d\r\n$4\r\nmset\r\n" + + sub_command->clen += 3 * sub_command->narg; + + sub_command->narg *= 2; + + sub_command->narg++; + + hi_itoa(num_str, sub_command->narg); + num_str_len = (uint8_t)strlen(num_str); + + sub_command->clen += 13 + num_str_len; + + sub_command->cmd = + hi_calloc(sub_command->clen, sizeof(*sub_command->cmd)); + if (sub_command->cmd == NULL) { + goto oom; + } + + sub_command->cmd[idx++] = '*'; + memcpy(sub_command->cmd + idx, num_str, num_str_len); + idx += num_str_len; + memcpy(sub_command->cmd + idx, "\r\n$4\r\nmset\r\n", 12); + idx += 12; + + for (j = 0; j < hiarray_n(sub_command->keys); j++) { + kp = hiarray_get(sub_command->keys, j); + key_len = (uint32_t)(kp->end - kp->start); + hi_itoa(num_str, key_len); + num_str_len = strlen(num_str); + + sub_command->cmd[idx++] = '$'; + memcpy(sub_command->cmd + idx, num_str, num_str_len); + idx += num_str_len; + memcpy(sub_command->cmd + idx, CRLF, CRLF_LEN); + idx += CRLF_LEN; + memcpy(sub_command->cmd + idx, kp->start, + key_len + kp->remain_len); + idx += key_len + kp->remain_len; + } + } else { + NOT_REACHED(); + } + + sub_command->type = command->type; + + if (listAddNodeTail(commands, sub_command) == NULL) { + goto oom; + } + sub_commands[i] = NULL; + } + +done: + hi_free(sub_commands); + + if (slot_num >= 0 && commands != NULL && listLength(commands) == 1) { + listNode *list_node = listFirst(commands); + listDelNode(commands, list_node); + if (command->frag_seq) { + hi_free(command->frag_seq); + command->frag_seq = NULL; + } + + command->slot_num = slot_num; + } + return slot_num; + +oom: + __redisClusterSetError(cc, REDIS_ERR_OOM, "Out of memory"); + if (sub_commands != NULL) { + for (i = 0; i < REDIS_CLUSTER_SLOTS; i++) { + command_destroy(sub_commands[i]); + } + } + hi_free(sub_commands); + return -1; // failing slot_num +} + +static void *command_post_fragment(redisClusterContext *cc, struct cmd *command, + hilist *commands) { + struct cmd *sub_command; + listNode *list_node; + redisReply *reply = NULL, *sub_reply; + long long count = 0; + + listIter li; + listRewind(commands, &li); + + while ((list_node = listNext(&li)) != NULL) { + sub_command = list_node->value; + reply = sub_command->reply; + if (reply == NULL) { + return NULL; + } else if (reply->type == REDIS_REPLY_ERROR) { + return reply; + } + + if (command->type == CMD_REQ_REDIS_MGET) { + if (reply->type != REDIS_REPLY_ARRAY) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, "reply type error"); + return NULL; + } + } else if (command->type == CMD_REQ_REDIS_DEL) { + if (reply->type != REDIS_REPLY_INTEGER) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, "reply type error"); + return NULL; + } + count += reply->integer; + } else if (command->type == CMD_REQ_REDIS_EXISTS) { + if (reply->type != REDIS_REPLY_INTEGER) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, "reply type error"); + return NULL; + } + count += reply->integer; + } else if (command->type == CMD_REQ_REDIS_MSET) { + if (reply->type != REDIS_REPLY_STATUS || reply->len != 2 || + strcmp(reply->str, REDIS_STATUS_OK) != 0) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, "reply type error"); + return NULL; + } + } else { + NOT_REACHED(); + } + } + + reply = hi_calloc(1, sizeof(*reply)); + if (reply == NULL) { + goto oom; + } + + if (command->type == CMD_REQ_REDIS_MGET) { + int i; + uint32_t key_count; + + reply->type = REDIS_REPLY_ARRAY; + + key_count = hiarray_n(command->keys); + + reply->elements = key_count; + reply->element = hi_calloc(key_count, sizeof(*reply->element)); + if (reply->element == NULL) { + goto oom; + } + + for (i = key_count - 1; i >= 0; i--) { /* for each key */ + sub_reply = command->frag_seq[i]->reply; /* get it's reply */ + if (sub_reply == NULL) { + freeReplyObject(reply); + __redisClusterSetError(cc, REDIS_ERR_OTHER, + "sub reply is null"); + return NULL; + } + + if (sub_reply->type == REDIS_REPLY_STRING) { + reply->element[i] = sub_reply; + } else if (sub_reply->type == REDIS_REPLY_ARRAY) { + if (sub_reply->elements == 0) { + freeReplyObject(reply); + __redisClusterSetError(cc, REDIS_ERR_OTHER, + "sub reply elements error"); + return NULL; + } + + reply->element[i] = sub_reply->element[sub_reply->elements - 1]; + sub_reply->elements--; + } + } + } else if (command->type == CMD_REQ_REDIS_DEL) { + reply->type = REDIS_REPLY_INTEGER; + reply->integer = count; + } else if (command->type == CMD_REQ_REDIS_EXISTS) { + reply->type = REDIS_REPLY_INTEGER; + reply->integer = count; + } else if (command->type == CMD_REQ_REDIS_MSET) { + reply->type = REDIS_REPLY_STATUS; + uint32_t str_len = strlen(REDIS_STATUS_OK); + reply->str = hi_malloc((str_len + 1) * sizeof(char)); + if (reply->str == NULL) { + goto oom; + } + + reply->len = str_len; + memcpy(reply->str, REDIS_STATUS_OK, str_len); + reply->str[str_len] = '\0'; + } else { + NOT_REACHED(); + } + + return reply; + +oom: + freeReplyObject(reply); + __redisClusterSetError(cc, REDIS_ERR_OOM, "Out of memory"); + return NULL; +} + +/* + * Split the command into subcommands by slot + * + * Returns slot_num + * If slot_num < 0 or slot_num >= REDIS_CLUSTER_SLOTS means this function runs + * error; Otherwise if the commands > 1 , slot_num is the last subcommand slot + * number. + */ +static int command_format_by_slot(redisClusterContext *cc, struct cmd *command, + hilist *commands) { + struct keypos *kp; + int key_count; + int slot_num = -1; + + if (cc == NULL || commands == NULL || command == NULL || + command->cmd == NULL || command->clen <= 0) { + goto done; + } + + redis_parse_cmd(command); + if (command->result == CMD_PARSE_ENOMEM) { + __redisClusterSetError(cc, REDIS_ERR_OOM, "Out of memory"); + goto done; + } else if (command->result != CMD_PARSE_OK) { + __redisClusterSetError(cc, REDIS_ERR_PROTOCOL, command->errstr); + goto done; + } + + key_count = hiarray_n(command->keys); + + if (key_count <= 0) { + __redisClusterSetError( + cc, REDIS_ERR_OTHER, + "No keys in command(must have keys for redis cluster mode)"); + goto done; + } else if (key_count == 1) { + kp = hiarray_get(command->keys, 0); + slot_num = keyHashSlot(kp->start, kp->end - kp->start); + command->slot_num = slot_num; + + goto done; + } + + slot_num = command_pre_fragment(cc, command, commands); + +done: + + return slot_num; +} + +/* Deprecated function, replaced with redisClusterSetOptionMaxRetry() */ +void redisClusterSetMaxRedirect(redisClusterContext *cc, int max_retry_count) { + if (cc == NULL || max_retry_count <= 0) { + return; + } + + cc->max_retry_count = max_retry_count; +} + +int redisClusterSetConnectCallback(redisClusterContext *cc, + void(fn)(const redisContext *c, + int status)) { + if (cc->on_connect == NULL) { + cc->on_connect = fn; + return REDIS_OK; + } + return REDIS_ERR; +} + +int redisClusterSetEventCallback(redisClusterContext *cc, + void(fn)(const redisClusterContext *cc, + int event, void *privdata), + void *privdata) { + if (cc->event_callback == NULL) { + cc->event_callback = fn; + cc->event_privdata = privdata; + return REDIS_OK; + } + return REDIS_ERR; +} + +void *redisClusterFormattedCommand(redisClusterContext *cc, char *cmd, + int len) { + redisReply *reply = NULL; + int slot_num; + struct cmd *command = NULL, *sub_command; + hilist *commands = NULL; + listNode *list_node; + + if (cc == NULL) { + return NULL; + } + + if (cc->err) { + cc->err = 0; + memset(cc->errstr, '\0', strlen(cc->errstr)); + } + + command = command_get(); + if (command == NULL) { + goto oom; + } + + command->cmd = cmd; + command->clen = len; + + commands = listCreate(); + if (commands == NULL) { + goto oom; + } + + commands->free = listCommandFree; + + slot_num = command_format_by_slot(cc, command, commands); + + if (slot_num < 0) { + goto error; + } else if (slot_num >= REDIS_CLUSTER_SLOTS) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, "slot_num is out of range"); + goto error; + } + + // all keys belong to one slot + if (listLength(commands) == 0) { + reply = redis_cluster_command_execute(cc, command); + goto done; + } + + ASSERT(listLength(commands) != 1); + + listIter li; + listRewind(commands, &li); + + while ((list_node = listNext(&li)) != NULL) { + sub_command = list_node->value; + + reply = redis_cluster_command_execute(cc, sub_command); + if (reply == NULL) { + goto error; + } else if (reply->type == REDIS_REPLY_ERROR) { + goto done; + } + + sub_command->reply = reply; + } + + reply = command_post_fragment(cc, command, commands); + +done: + + command->cmd = NULL; + command_destroy(command); + + if (commands != NULL) { + listRelease(commands); + } + + cc->retry_count = 0; + return reply; + +oom: + __redisClusterSetError(cc, REDIS_ERR_OOM, "Out of memory"); + // passthrough + +error: + if (command != NULL) { + command->cmd = NULL; + command_destroy(command); + } + if (commands != NULL) { + listRelease(commands); + } + cc->retry_count = 0; + return NULL; +} + +void *redisClustervCommand(redisClusterContext *cc, const char *format, + va_list ap) { + redisReply *reply; + char *cmd; + int len; + + if (cc == NULL) { + return NULL; + } + + len = redisvFormatCommand(&cmd, format, ap); + + if (len == -1) { + __redisClusterSetError(cc, REDIS_ERR_OOM, "Out of memory"); + return NULL; + } else if (len == -2) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, "Invalid format string"); + return NULL; + } + + reply = redisClusterFormattedCommand(cc, cmd, len); + + hi_free(cmd); + + return reply; +} + +void *redisClusterCommand(redisClusterContext *cc, const char *format, ...) { + va_list ap; + redisReply *reply = NULL; + + va_start(ap, format); + reply = redisClustervCommand(cc, format, ap); + va_end(ap); + + return reply; +} + +void *redisClusterCommandToNode(redisClusterContext *cc, redisClusterNode *node, + const char *format, ...) { + redisContext *c; + va_list ap; + int ret; + void *reply; + int updating_slotmap = 0; + + c = ctx_get_by_node(cc, node); + if (c == NULL) { + return NULL; + } else if (c->err) { + __redisClusterSetError(cc, c->err, c->errstr); + return NULL; + } + + if (cc->err) { + cc->err = 0; + memset(cc->errstr, '\0', sizeof(cc->errstr)); + } + + va_start(ap, format); + ret = redisvAppendCommand(c, format, ap); + va_end(ap); + + if (ret != REDIS_OK) { + __redisClusterSetError(cc, c->err, c->errstr); + return NULL; + } + + if (cc->need_update_route) { + /* Pipeline slotmap update on the same connection. */ + if (clusterUpdateRouteSendCommand(cc, c) == REDIS_OK) { + updating_slotmap = 1; + } + } + + if (redisGetReply(c, &reply) != REDIS_OK) { + __redisClusterSetError(cc, c->err, c->errstr); + if (c->err != REDIS_ERR_OOM) + cc->need_update_route = 1; + return NULL; + } + + if (updating_slotmap) { + /* Handle reply from pipelined CLUSTER SLOTS or CLUSTER NODES. */ + if (clusterUpdateRouteHandleReply(cc, c) != REDIS_OK) { + /* Ignore error. Update will be triggered on the next command. */ + cc->err = 0; + cc->errstr[0] = '\0'; + } + } + + return reply; +} + +void *redisClusterCommandArgv(redisClusterContext *cc, int argc, + const char **argv, const size_t *argvlen) { + redisReply *reply = NULL; + char *cmd; + int len; + + len = redisFormatCommandArgv(&cmd, argc, argv, argvlen); + if (len == -1) { + __redisClusterSetError(cc, REDIS_ERR_OOM, "Out of memory"); + return NULL; + } + + reply = redisClusterFormattedCommand(cc, cmd, len); + + hi_free(cmd); + + return reply; +} + +int redisClusterAppendFormattedCommand(redisClusterContext *cc, char *cmd, + int len) { + int slot_num; + struct cmd *command = NULL, *sub_command; + hilist *commands = NULL; + listNode *list_node; + + if (cc->requests == NULL) { + cc->requests = listCreate(); + if (cc->requests == NULL) { + goto oom; + } + cc->requests->free = listCommandFree; + } + + command = command_get(); + if (command == NULL) { + goto oom; + } + + command->cmd = cmd; + command->clen = len; + + commands = listCreate(); + if (commands == NULL) { + goto oom; + } + + commands->free = listCommandFree; + + slot_num = command_format_by_slot(cc, command, commands); + + if (slot_num < 0) { + goto error; + } else if (slot_num >= REDIS_CLUSTER_SLOTS) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, "slot_num is out of range"); + goto error; + } + + // Append command(s) + if (listLength(commands) == 0) { + // All keys belong to one slot + if (__redisClusterAppendCommand(cc, command) != REDIS_OK) { + goto error; + } + } else { + // Keys belongs to different slots + ASSERT(listLength(commands) != 1); + + listIter li; + listRewind(commands, &li); + + while ((list_node = listNext(&li)) != NULL) { + sub_command = list_node->value; + + if (__redisClusterAppendCommand(cc, sub_command) != REDIS_OK) { + goto error; + } + } + } + + if (listLength(commands) > 0) { + command->sub_commands = commands; + } else { + listRelease(commands); + } + commands = NULL; + command->cmd = NULL; + + if (listAddNodeTail(cc->requests, command) == NULL) { + goto oom; + } + return REDIS_OK; + +oom: + __redisClusterSetError(cc, REDIS_ERR_OOM, "Out of memory"); + // passthrough + +error: + if (command != NULL) { + command->cmd = NULL; + command_destroy(command); + } + if (commands != NULL) { + listRelease(commands); + } + + /* Attention: mybe here we must pop the + sub_commands that had append to the nodes. + But now we do not handle it. */ + return REDIS_ERR; +} + +int redisClustervAppendCommand(redisClusterContext *cc, const char *format, + va_list ap) { + int ret; + char *cmd; + int len; + + len = redisvFormatCommand(&cmd, format, ap); + if (len == -1) { + __redisClusterSetError(cc, REDIS_ERR_OOM, "Out of memory"); + return REDIS_ERR; + } else if (len == -2) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, "Invalid format string"); + return REDIS_ERR; + } + + ret = redisClusterAppendFormattedCommand(cc, cmd, len); + + hi_free(cmd); + + return ret; +} + +int redisClusterAppendCommand(redisClusterContext *cc, const char *format, + ...) { + + int ret; + va_list ap; + + if (cc == NULL || format == NULL) { + return REDIS_ERR; + } + + va_start(ap, format); + ret = redisClustervAppendCommand(cc, format, ap); + va_end(ap); + + return ret; +} + +int redisClusterAppendCommandToNode(redisClusterContext *cc, + redisClusterNode *node, const char *format, + ...) { + redisContext *c; + va_list ap; + struct cmd *command = NULL; + char *cmd = NULL; + int len; + + if (cc->requests == NULL) { + cc->requests = listCreate(); + if (cc->requests == NULL) + goto oom; + + cc->requests->free = listCommandFree; + } + + c = ctx_get_by_node(cc, node); + if (c == NULL) { + return REDIS_ERR; + } else if (c->err) { + __redisClusterSetError(cc, c->err, c->errstr); + return REDIS_ERR; + } + + /* Allocate cmd and encode the variadic command */ + va_start(ap, format); + len = redisvFormatCommand(&cmd, format, ap); + va_end(ap); + + if (len == -1) { + goto oom; + } else if (len == -2) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, "Invalid format string"); + return REDIS_ERR; + } + + // Append the command to the outgoing hiredis buffer + if (redisAppendFormattedCommand(c, cmd, len) != REDIS_OK) { + __redisClusterSetError(cc, c->err, c->errstr); + hi_free(cmd); + return REDIS_ERR; + } + + // Keep the command in the outstanding request list + command = command_get(); + if (command == NULL) { + hi_free(cmd); + goto oom; + } + command->cmd = cmd; + command->clen = len; + command->node_addr = sdsnew(node->addr); + if (command->node_addr == NULL) + goto oom; + + if (listAddNodeTail(cc->requests, command) == NULL) + goto oom; + + return REDIS_OK; + +oom: + command_destroy(command); + __redisClusterSetError(cc, REDIS_ERR_OOM, "Out of memory"); + return REDIS_ERR; +} + +int redisClusterAppendCommandArgv(redisClusterContext *cc, int argc, + const char **argv, const size_t *argvlen) { + int ret; + char *cmd; + int len; + + len = redisFormatCommandArgv(&cmd, argc, argv, argvlen); + if (len == -1) { + __redisClusterSetError(cc, REDIS_ERR_OOM, "Out of memory"); + return REDIS_ERR; + } + + ret = redisClusterAppendFormattedCommand(cc, cmd, len); + + hi_free(cmd); + + return ret; +} + +static int redisClusterSendAll(redisClusterContext *cc) { + dictEntry *de; + redisClusterNode *node; + redisContext *c = NULL; + int wdone = 0; + + if (cc == NULL || cc->nodes == NULL) { + return REDIS_ERR; + } + + dictIterator di; + dictInitIterator(&di, cc->nodes); + + while ((de = dictNext(&di)) != NULL) { + node = dictGetEntryVal(de); + if (node == NULL) { + continue; + } + + c = ctx_get_by_node(cc, node); + if (c == NULL) { + continue; + } + + /* Write until done */ + do { + if (redisBufferWrite(c, &wdone) == REDIS_ERR) { + return REDIS_ERR; + } + } while (!wdone); + } + + return REDIS_OK; +} + +static int redisClusterClearAll(redisClusterContext *cc) { + dictEntry *de; + redisClusterNode *node; + redisContext *c = NULL; + + if (cc == NULL) { + return REDIS_ERR; + } + + if (cc->err) { + cc->err = 0; + memset(cc->errstr, '\0', strlen(cc->errstr)); + } + + if (cc->nodes == NULL) { + return REDIS_ERR; + } + + dictIterator di; + dictInitIterator(&di, cc->nodes); + + while ((de = dictNext(&di)) != NULL) { + node = dictGetEntryVal(de); + if (node == NULL) { + continue; + } + + c = node->con; + if (c == NULL) { + continue; + } + + redisFree(c); + node->con = NULL; + } + + return REDIS_OK; +} + +int redisClusterGetReply(redisClusterContext *cc, void **reply) { + + struct cmd *command, *sub_command; + hilist *commands = NULL; + listNode *list_command, *list_sub_command; + int slot_num; + void *sub_reply; + + if (cc == NULL || reply == NULL) + return REDIS_ERR; + + cc->err = 0; + cc->errstr[0] = '\0'; + + *reply = NULL; + + if (cc->requests == NULL) + return REDIS_ERR; // No queued requests + + list_command = listFirst(cc->requests); + + // no more reply + if (list_command == NULL) { + *reply = NULL; + return REDIS_OK; + } + + command = list_command->value; + if (command == NULL) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, + "command in the requests list is null"); + goto error; + } + + slot_num = command->slot_num; + if (slot_num >= 0) { + /* Command was sent via single slot */ + listDelNode(cc->requests, list_command); + return __redisClusterGetReply(cc, slot_num, reply); + + } else if (command->node_addr) { + /* Command was sent to a single node */ + dictEntry *de; + + de = dictFind(cc->nodes, command->node_addr); + if (de != NULL) { + listDelNode(cc->requests, list_command); + return __redisClusterGetReplyFromNode(cc, dictGetEntryVal(de), + reply); + } else { + __redisClusterSetError(cc, REDIS_ERR_OTHER, + "command was sent to a now unknown node"); + goto error; + } + } + + commands = command->sub_commands; + if (commands == NULL) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, + "sub_commands in command is null"); + goto error; + } + + ASSERT(listLength(commands) != 1); + + listIter li; + listRewind(commands, &li); + + while ((list_sub_command = listNext(&li)) != NULL) { + sub_command = list_sub_command->value; + if (sub_command == NULL) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, "sub_command is null"); + goto error; + } + + slot_num = sub_command->slot_num; + if (slot_num < 0) { + __redisClusterSetError(cc, REDIS_ERR_OTHER, + "sub_command slot_num is less then zero"); + goto error; + } + + if (__redisClusterGetReply(cc, slot_num, &sub_reply) != REDIS_OK) { + goto error; + } + + sub_command->reply = sub_reply; + } + + *reply = command_post_fragment(cc, command, commands); + if (*reply == NULL) { + goto error; + } + + listDelNode(cc->requests, list_command); + return REDIS_OK; + +error: + + listDelNode(cc->requests, list_command); + return REDIS_ERR; +} + +/** + * Resets cluster state after pipeline. + * Resets Redis node connections if pipeline commands were not called beforehand. + */ +void redisClusterReset(redisClusterContext *cc) { + int status; + void *reply; + + if (cc == NULL || cc->nodes == NULL) { + return; + } + + if (cc->err) { + redisClusterClearAll(cc); + } else { + /* Write/flush each nodes output buffer to socket */ + redisClusterSendAll(cc); + + /* Expect a reply for each pipelined request */ + do { + status = redisClusterGetReply(cc, &reply); + if (status == REDIS_OK) { + freeReplyObject(reply); + } else { + redisClusterClearAll(cc); + break; + } + } while (reply != NULL); + } + + if (cc->requests) { + listRelease(cc->requests); + cc->requests = NULL; + } + + if (cc->need_update_route) { + status = redisClusterUpdateSlotmap(cc); + if (status != REDIS_OK) { + /* Specific error already set */ + return; + } + cc->need_update_route = 0; + } +} + +/*############redis cluster async############*/ + +static void __redisClusterAsyncSetError(redisClusterAsyncContext *acc, int type, + const char *str) { + + size_t len; + + acc->err = type; + if (str != NULL) { + len = strlen(str); + len = len < (sizeof(acc->errstr) - 1) ? len : (sizeof(acc->errstr) - 1); + memcpy(acc->errstr, str, len); + acc->errstr[len] = '\0'; + } else { + /* Only REDIS_ERR_IO may lack a description! */ + assert(type == REDIS_ERR_IO); + strerror_r(errno, acc->errstr, sizeof(acc->errstr)); + } +} + +static redisClusterAsyncContext * +redisClusterAsyncInitialize(redisClusterContext *cc) { + redisClusterAsyncContext *acc; + + if (cc == NULL) { + return NULL; + } + + acc = hi_calloc(1, sizeof(redisClusterAsyncContext)); + if (acc == NULL) + return NULL; + + acc->cc = cc; + + /* We want the error field to be accessible directly instead of requiring + * an indirection to the redisContext struct. */ + // TODO: really needed? + acc->err = cc->err; + memcpy(acc->errstr, cc->errstr, 128); + + return acc; +} + +static cluster_async_data *cluster_async_data_create(void) { + /* use calloc to guarantee all fields are zeroed */ + return hi_calloc(1, sizeof(cluster_async_data)); +} + +static void cluster_async_data_free(cluster_async_data *cad) { + if (cad == NULL) { + return; + } + + command_destroy(cad->command); + + hi_free(cad); +} + +static void unlinkAsyncContextAndNode(void *data) { + redisClusterNode *node; + + if (data) { + node = (redisClusterNode *)(data); + node->acon = NULL; + } +} + +redisAsyncContext *actx_get_by_node(redisClusterAsyncContext *acc, + redisClusterNode *node) { + redisAsyncContext *ac; + int ret; + + if (node == NULL) { + return NULL; + } + + ac = node->acon; + if (ac != NULL) { + if (ac->c.err == 0) { + return ac; + } else { + /* The cluster node has a hiredis context with errors. Hiredis + * will asynchronously destruct the context and unlink it from + * the cluster node object. Return an error until done. + * An example scenario is when sending a command from a command + * callback, which has a NULL reply due to a disconnect. */ + __redisClusterAsyncSetError(acc, ac->c.err, ac->c.errstr); + return NULL; + } + } + + // No async context exists, perform a connect + + if (node->host == NULL || node->port <= 0) { + __redisClusterAsyncSetError(acc, REDIS_ERR_OTHER, + "node host or port is error"); + return NULL; + } + + redisOptions options = {0}; + REDIS_OPTIONS_SET_TCP(&options, node->host, node->port); + options.connect_timeout = acc->cc->connect_timeout; + options.command_timeout = acc->cc->command_timeout; + + node->lastConnectionAttempt = hi_usec_now(); + + ac = redisAsyncConnectWithOptions(&options); + if (ac == NULL) { + __redisClusterAsyncSetError(acc, REDIS_ERR_OOM, "Out of memory"); + return NULL; + } + + if (ac->err) { + __redisClusterAsyncSetError(acc, ac->err, ac->errstr); + redisAsyncFree(ac); + return NULL; + } + + if (acc->cc->ssl && + acc->cc->ssl_init_fn(&ac->c, acc->cc->ssl) != REDIS_OK) { + __redisClusterAsyncSetError(acc, ac->c.err, ac->c.errstr); + redisAsyncFree(ac); + return NULL; + } + + // Authenticate when needed + if (acc->cc->password != NULL) { + if (acc->cc->username != NULL) { + ret = redisAsyncCommand(ac, NULL, NULL, "AUTH %s %s", + acc->cc->username, acc->cc->password); + } else { + ret = + redisAsyncCommand(ac, NULL, NULL, "AUTH %s", acc->cc->password); + } + + if (ret != REDIS_OK) { + __redisClusterAsyncSetError(acc, ac->c.err, ac->c.errstr); + redisAsyncFree(ac); + return NULL; + } + } + + if (acc->adapter) { + ret = acc->attach_fn(ac, acc->adapter); + if (ret != REDIS_OK) { + __redisClusterAsyncSetError(acc, REDIS_ERR_OTHER, + "Failed to attach event adapter"); + redisAsyncFree(ac); + return NULL; + } + } + + if (acc->onConnect) { + redisAsyncSetConnectCallback(ac, acc->onConnect); + } +#ifndef HIRCLUSTER_NO_NONCONST_CONNECT_CB + else if (acc->onConnectNC) { + redisAsyncSetConnectCallbackNC(ac, acc->onConnectNC); + } +#endif + + if (acc->onDisconnect) { + redisAsyncSetDisconnectCallback(ac, acc->onDisconnect); + } + + ac->data = node; + ac->dataCleanup = unlinkAsyncContextAndNode; + node->acon = ac; + + return ac; +} + +redisClusterAsyncContext *redisClusterAsyncContextInit(void) { + redisClusterContext *cc; + redisClusterAsyncContext *acc; + + cc = redisClusterContextInit(); + if (cc == NULL) { + return NULL; + } + + acc = redisClusterAsyncInitialize(cc); + if (acc == NULL) { + redisClusterFree(cc); + return NULL; + } + + return acc; +} + +redisClusterAsyncContext *redisClusterAsyncConnect(const char *addrs, + int flags) { + + redisClusterContext *cc; + redisClusterAsyncContext *acc; + + cc = redisClusterConnect(addrs, flags); + if (cc == NULL) { + return NULL; + } + + acc = redisClusterAsyncInitialize(cc); + if (acc == NULL) { + redisClusterFree(cc); + return NULL; + } + + return acc; +} + +int redisClusterAsyncConnect2(redisClusterAsyncContext *acc) { + /* An adapter to an async event library is required. */ + if (acc->adapter == NULL) { + return REDIS_ERR; + } + return updateSlotMapAsync(acc, NULL /*any node*/); +} + +int redisClusterAsyncSetConnectCallback(redisClusterAsyncContext *acc, + redisConnectCallback *fn) { + if (acc->onConnect != NULL) + return REDIS_ERR; +#ifndef HIRCLUSTER_NO_NONCONST_CONNECT_CB + if (acc->onConnectNC != NULL) + return REDIS_ERR; +#endif + acc->onConnect = fn; + return REDIS_OK; +} + +#ifndef HIRCLUSTER_NO_NONCONST_CONNECT_CB +int redisClusterAsyncSetConnectCallbackNC(redisClusterAsyncContext *acc, + redisConnectCallbackNC *fn) { + if (acc->onConnectNC != NULL || acc->onConnect != NULL) { + return REDIS_ERR; + } + acc->onConnectNC = fn; + return REDIS_OK; +} +#endif + +int redisClusterAsyncSetDisconnectCallback(redisClusterAsyncContext *acc, + redisDisconnectCallback *fn) { + if (acc->onDisconnect == NULL) { + acc->onDisconnect = fn; + return REDIS_OK; + } + return REDIS_ERR; +} + +/* Reply callback function for CLUSTER SLOTS */ +void clusterSlotsReplyCallback(redisAsyncContext *ac, void *r, void *privdata) { + UNUSED(ac); + redisReply *reply = (redisReply *)r; + redisClusterAsyncContext *acc = (redisClusterAsyncContext *)privdata; + acc->lastSlotmapUpdateAttempt = hi_usec_now(); + + if (reply == NULL) { + /* Retry using available nodes */ + updateSlotMapAsync(acc, NULL); + return; + } + + redisClusterContext *cc = acc->cc; + dict *nodes = parse_cluster_slots(cc, reply, cc->flags); + if (updateNodesAndSlotmap(cc, nodes) != REDIS_OK) { + /* Ignore failures for now */ + } +} + +/* Reply callback function for CLUSTER NODES */ +void clusterNodesReplyCallback(redisAsyncContext *ac, void *r, void *privdata) { + UNUSED(ac); + redisReply *reply = (redisReply *)r; + redisClusterAsyncContext *acc = (redisClusterAsyncContext *)privdata; + acc->lastSlotmapUpdateAttempt = hi_usec_now(); + + if (reply == NULL) { + /* Retry using available nodes */ + updateSlotMapAsync(acc, NULL); + return; + } + + redisClusterContext *cc = acc->cc; + dict *nodes = parse_cluster_nodes(cc, reply->str, reply->len, cc->flags); + if (updateNodesAndSlotmap(cc, nodes) != REDIS_OK) { + /* Ignore failures for now */ + } +} + +#define nodeIsConnected(n) \ + ((n)->acon != NULL && (n)->acon->err == 0 && \ + (n)->acon->c.flags & REDIS_CONNECTED) + +/* Select a node. + * Primarily selects a connected node found close to a randomly picked index of + * all known nodes. The random index should give a more even distribution of + * selected nodes. If no connected node is found while iterating to this index + * the remaining nodes are also checked until a connected node is found. + * If no connected node is found a node for which a connect has not been attempted + * within throttle-time, and is found near the picked index, is selected. + */ +static redisClusterNode *selectNode(dict *nodes) { + redisClusterNode *node, *selected = NULL; + dictIterator di; + dictInitIterator(&di, nodes); + + int64_t throttleLimit = hi_usec_now() - SLOTMAP_UPDATE_THROTTLE_USEC; + unsigned long currentIndex = 0; + unsigned long checkIndex = random() % dictSize(nodes); + + dictEntry *de; + while ((de = dictNext(&di)) != NULL) { + node = dictGetEntryVal(de); + + if (nodeIsConnected(node)) { + /* Keep any connected node */ + selected = node; + } else if (node->lastConnectionAttempt < throttleLimit && + (selected == NULL || (currentIndex < checkIndex && + !nodeIsConnected(selected)))) { + /* Keep an accepted node when none is yet found, or + any accepted node until the chosen index is reached */ + selected = node; + } + + /* Return a found connected node when chosen index is reached. */ + if (currentIndex >= checkIndex && selected != NULL && + nodeIsConnected(selected)) + break; + currentIndex += 1; + } + return selected; +} + +/* Update the slot map by querying a selected cluster node. If ac is NULL, an + * arbitrary connected node is selected. */ +static int updateSlotMapAsync(redisClusterAsyncContext *acc, + redisAsyncContext *ac) { + if (acc->lastSlotmapUpdateAttempt == SLOTMAP_UPDATE_ONGOING) { + /* Don't allow concurrent slot map updates. */ + return REDIS_ERR; + } + + if (ac == NULL) { + if (acc->cc->nodes == NULL) { + __redisClusterAsyncSetError(acc, REDIS_ERR_OTHER, "no nodes added"); + goto error; + } + + redisClusterNode *node = selectNode(acc->cc->nodes); + if (node == NULL) { + goto error; + } + + /* Get hiredis context, connect if needed */ + ac = actx_get_by_node(acc, node); + } + if (ac == NULL) + goto error; /* Specific error already set */ + + /* Send a command depending of config */ + int status; + if (acc->cc->flags & HIRCLUSTER_FLAG_ROUTE_USE_SLOTS) { + status = redisAsyncCommand(ac, clusterSlotsReplyCallback, acc, + REDIS_COMMAND_CLUSTER_SLOTS); + } else { + status = redisAsyncCommand(ac, clusterNodesReplyCallback, acc, + REDIS_COMMAND_CLUSTER_NODES); + } + + if (status == REDIS_OK) { + acc->lastSlotmapUpdateAttempt = SLOTMAP_UPDATE_ONGOING; + return REDIS_OK; + } + +error: + acc->lastSlotmapUpdateAttempt = hi_usec_now(); + return REDIS_ERR; +} + +/* Start a slotmap update if the throttling allows. */ +static void throttledUpdateSlotMapAsync(redisClusterAsyncContext *acc, + redisAsyncContext *ac) { + if (acc->lastSlotmapUpdateAttempt != SLOTMAP_UPDATE_ONGOING && + (acc->lastSlotmapUpdateAttempt + SLOTMAP_UPDATE_THROTTLE_USEC) < + hi_usec_now()) { + updateSlotMapAsync(acc, ac); + } +} + +static void redisClusterAsyncCallback(redisAsyncContext *ac, void *r, + void *privdata) { + int ret; + redisReply *reply = r; + cluster_async_data *cad = privdata; + redisClusterAsyncContext *acc; + redisClusterContext *cc; + redisAsyncContext *ac_retry = NULL; + int error_type; + redisClusterNode *node; + struct cmd *command; + + if (cad == NULL) { + goto error; + } + + acc = cad->acc; + if (acc == NULL) { + goto error; + } + + cc = acc->cc; + if (cc == NULL) { + goto error; + } + + command = cad->command; + if (command == NULL) { + goto error; + } + + if (reply == NULL) { + /* Copy reply specific error from hiredis */ + __redisClusterAsyncSetError(acc, ac->err, ac->errstr); + + node = (redisClusterNode *)ac->data; + if (node == NULL) + goto done; /* Node already removed from topology */ + + /* Start a slotmap update when the throttling allows */ + throttledUpdateSlotMapAsync(acc, NULL); + goto done; + } + + if (cad->retry_count == NO_RETRY) /* Skip retry handling */ + goto done; + + error_type = cluster_reply_error_type(reply); + + if (error_type > CLUSTER_NOT_ERR && error_type < CLUSTER_ERR_SENTINEL) { + cad->retry_count++; + if (cad->retry_count > cc->max_retry_count) { + cad->retry_count = 0; + __redisClusterAsyncSetError(acc, REDIS_ERR_CLUSTER_TOO_MANY_RETRIES, + "too many cluster retries"); + goto done; + } + + int slot = -1; + switch (error_type) { + case CLUSTER_ERR_MOVED: + /* Initiate slot mapping update using the node that sent MOVED. */ + throttledUpdateSlotMapAsync(acc, ac); + + node = getNodeFromRedirectReply(cc, reply, &slot); + if (node == NULL) { + __redisClusterAsyncSetError(acc, cc->err, cc->errstr); + goto done; + } + /* Update the slot mapping entry for this slot. */ + if (slot >= 0) { + cc->table[slot] = node; + } + ac_retry = actx_get_by_node(acc, node); + + break; + case CLUSTER_ERR_ASK: + node = getNodeFromRedirectReply(cc, reply, NULL); + if (node == NULL) { + __redisClusterAsyncSetError(acc, cc->err, cc->errstr); + goto done; + } + + ac_retry = actx_get_by_node(acc, node); + if (ac_retry == NULL) { + /* Specific error already set */ + goto done; + } + + ret = redisAsyncCommand(ac_retry, NULL, NULL, REDIS_COMMAND_ASKING); + if (ret != REDIS_OK) { + goto error; + } + + break; + case CLUSTER_ERR_TRYAGAIN: + case CLUSTER_ERR_CLUSTERDOWN: + ac_retry = ac; + + break; + default: + + goto done; + break; + } + + goto retry; + } + +done: + + if (acc->err) { + cad->callback(acc, NULL, cad->privdata); + } else { + cad->callback(acc, r, cad->privdata); + } + + if (cc->err) { + cc->err = 0; + memset(cc->errstr, '\0', strlen(cc->errstr)); + } + + if (acc->err) { + acc->err = 0; + memset(acc->errstr, '\0', strlen(acc->errstr)); + } + + cluster_async_data_free(cad); + + return; + +retry: + + ret = redisAsyncFormattedCommand(ac_retry, redisClusterAsyncCallback, cad, + command->cmd, command->clen); + if (ret != REDIS_OK) { + goto error; + } + + return; + +error: + + cluster_async_data_free(cad); +} + +int redisClusterAsyncFormattedCommand(redisClusterAsyncContext *acc, + redisClusterCallbackFn *fn, + void *privdata, char *cmd, int len) { + + redisClusterContext *cc; + int status = REDIS_OK; + int slot_num; + redisClusterNode *node; + redisAsyncContext *ac; + struct cmd *command = NULL; + hilist *commands = NULL; + cluster_async_data *cad; + + if (acc == NULL) { + return REDIS_ERR; + } + + cc = acc->cc; + + if (cc->err) { + cc->err = 0; + memset(cc->errstr, '\0', strlen(cc->errstr)); + } + + if (acc->err) { + acc->err = 0; + memset(acc->errstr, '\0', strlen(acc->errstr)); + } + + command = command_get(); + if (command == NULL) { + goto oom; + } + + command->cmd = hi_calloc(len, sizeof(*command->cmd)); + if (command->cmd == NULL) { + goto oom; + } + memcpy(command->cmd, cmd, len); + command->clen = len; + + commands = listCreate(); + if (commands == NULL) { + goto oom; + } + + commands->free = listCommandFree; + + slot_num = command_format_by_slot(cc, command, commands); + + if (slot_num < 0) { + __redisClusterAsyncSetError(acc, cc->err, cc->errstr); + goto error; + } else if (slot_num >= REDIS_CLUSTER_SLOTS) { + __redisClusterAsyncSetError(acc, REDIS_ERR_OTHER, + "slot_num is out of range"); + goto error; + } + + // all keys not belong to one slot + if (listLength(commands) > 0) { + ASSERT(listLength(commands) != 1); + + __redisClusterAsyncSetError( + acc, REDIS_ERR_OTHER, + "Asynchronous API now not support multi-key command"); + goto error; + } + + node = node_get_by_table(cc, (uint32_t)slot_num); + if (node == NULL) { + /* Initiate a slotmap update since the slot is not served. */ + throttledUpdateSlotMapAsync(acc, NULL); + + /* node_get_by_table() has set the error on cc. */ + __redisClusterAsyncSetError(acc, cc->err, cc->errstr); + goto error; + } + + ac = actx_get_by_node(acc, node); + if (ac == NULL) { + /* Specific error already set */ + goto error; + } + + cad = cluster_async_data_create(); + if (cad == NULL) { + goto oom; + } + + cad->acc = acc; + cad->command = command; + cad->callback = fn; + cad->privdata = privdata; + + status = redisAsyncFormattedCommand(ac, redisClusterAsyncCallback, cad, cmd, + len); + if (status != REDIS_OK) { + goto error; + } + + if (commands != NULL) { + listRelease(commands); + } + + return REDIS_OK; + +oom: + __redisClusterAsyncSetError(acc, REDIS_ERR_OOM, "Out of memory"); + // passthrough + +error: + command_destroy(command); + if (commands != NULL) { + listRelease(commands); + } + return REDIS_ERR; +} + +int redisClusterAsyncFormattedCommandToNode(redisClusterAsyncContext *acc, + redisClusterNode *node, + redisClusterCallbackFn *fn, + void *privdata, char *cmd, + int len) { + redisClusterContext *cc; + redisAsyncContext *ac; + int status; + cluster_async_data *cad = NULL; + struct cmd *command = NULL; + + ac = actx_get_by_node(acc, node); + if (ac == NULL) { + /* Specific error already set */ + return REDIS_ERR; + } + + cc = acc->cc; + + if (cc->err) { + cc->err = 0; + memset(cc->errstr, '\0', strlen(cc->errstr)); + } + + if (acc->err) { + acc->err = 0; + memset(acc->errstr, '\0', strlen(acc->errstr)); + } + + command = command_get(); + if (command == NULL) { + goto oom; + } + + command->cmd = hi_calloc(len, sizeof(*command->cmd)); + if (command->cmd == NULL) { + goto oom; + } + memcpy(command->cmd, cmd, len); + command->clen = len; + + cad = cluster_async_data_create(); + if (cad == NULL) + goto oom; + + cad->acc = acc; + cad->command = command; + cad->callback = fn; + cad->privdata = privdata; + cad->retry_count = NO_RETRY; + + status = redisAsyncFormattedCommand(ac, redisClusterAsyncCallback, cad, cmd, + len); + if (status != REDIS_OK) + goto error; + + return REDIS_OK; + +oom: + __redisClusterAsyncSetError(acc, REDIS_ERR_OTHER, "Out of memory"); + // passthrough + +error: + command_destroy(command); + return REDIS_ERR; +} + +int redisClustervAsyncCommand(redisClusterAsyncContext *acc, + redisClusterCallbackFn *fn, void *privdata, + const char *format, va_list ap) { + int ret; + char *cmd; + int len; + + if (acc == NULL) { + return REDIS_ERR; + } + + len = redisvFormatCommand(&cmd, format, ap); + if (len == -1) { + __redisClusterAsyncSetError(acc, REDIS_ERR_OOM, "Out of memory"); + return REDIS_ERR; + } else if (len == -2) { + __redisClusterAsyncSetError(acc, REDIS_ERR_OTHER, + "Invalid format string"); + return REDIS_ERR; + } + + ret = redisClusterAsyncFormattedCommand(acc, fn, privdata, cmd, len); + + hi_free(cmd); + + return ret; +} + +int redisClusterAsyncCommand(redisClusterAsyncContext *acc, + redisClusterCallbackFn *fn, void *privdata, + const char *format, ...) { + int ret; + va_list ap; + + va_start(ap, format); + ret = redisClustervAsyncCommand(acc, fn, privdata, format, ap); + va_end(ap); + + return ret; +} + +int redisClusterAsyncCommandToNode(redisClusterAsyncContext *acc, + redisClusterNode *node, + redisClusterCallbackFn *fn, void *privdata, + const char *format, ...) { + int ret; + va_list ap; + int len; + char *cmd = NULL; + + /* Allocate cmd and encode the variadic command */ + va_start(ap, format); + len = redisvFormatCommand(&cmd, format, ap); + va_end(ap); + + if (len == -1) { + __redisClusterAsyncSetError(acc, REDIS_ERR_OTHER, "Out of memory"); + return REDIS_ERR; + } else if (len == -2) { + __redisClusterAsyncSetError(acc, REDIS_ERR_OTHER, + "Invalid format string"); + return REDIS_ERR; + } + + ret = redisClusterAsyncFormattedCommandToNode(acc, node, fn, privdata, cmd, + len); + hi_free(cmd); + return ret; +} + +int redisClusterAsyncCommandArgv(redisClusterAsyncContext *acc, + redisClusterCallbackFn *fn, void *privdata, + int argc, const char **argv, + const size_t *argvlen) { + int ret; + char *cmd; + int len; + + len = redisFormatCommandArgv(&cmd, argc, argv, argvlen); + if (len == -1) { + __redisClusterAsyncSetError(acc, REDIS_ERR_OOM, "Out of memory"); + return REDIS_ERR; + } + + ret = redisClusterAsyncFormattedCommand(acc, fn, privdata, cmd, len); + + hi_free(cmd); + + return ret; +} + +int redisClusterAsyncCommandArgvToNode(redisClusterAsyncContext *acc, + redisClusterNode *node, + redisClusterCallbackFn *fn, + void *privdata, int argc, + const char **argv, + const size_t *argvlen) { + + int ret; + char *cmd; + int len; + + len = redisFormatCommandArgv(&cmd, argc, argv, argvlen); + if (len == -1) { + __redisClusterAsyncSetError(acc, REDIS_ERR_OOM, "Out of memory"); + return REDIS_ERR; + } + + ret = redisClusterAsyncFormattedCommandToNode(acc, node, fn, privdata, cmd, + len); + + hi_free(cmd); + + return ret; +} + +void redisClusterAsyncDisconnect(redisClusterAsyncContext *acc) { + redisClusterContext *cc; + redisAsyncContext *ac; + dictEntry *de; + redisClusterNode *node; + + if (acc == NULL) { + return; + } + + cc = acc->cc; + + if (cc->nodes == NULL) { + return; + } + + dictIterator di; + dictInitIterator(&di, cc->nodes); + + while ((de = dictNext(&di)) != NULL) { + node = dictGetEntryVal(de); + + ac = node->acon; + + if (ac == NULL) { + continue; + } + + redisAsyncDisconnect(ac); + } +} + +void redisClusterAsyncFree(redisClusterAsyncContext *acc) { + redisClusterContext *cc; + + if (acc == NULL) { + return; + } + + cc = acc->cc; + + redisClusterFree(cc); + + hi_free(acc); +} + +/* Initiate an iterator for iterating over current cluster nodes */ +void redisClusterInitNodeIterator(redisClusterNodeIterator *iter, + redisClusterContext *cc) { + iter->cc = cc; + iter->route_version = cc->route_version; + dictInitIterator(&iter->di, cc->nodes); + iter->retries_left = 1; +} + +/* Get next node from the iterator + * The iterator will restart if the routing table is updated + * before all nodes have been iterated. */ +redisClusterNode *redisClusterNodeNext(redisClusterNodeIterator *iter) { + if (iter->retries_left <= 0) + return NULL; + + if (iter->route_version != iter->cc->route_version) { + // The routing table has changed and current iterator + // is invalid. The nodes dict has been recreated in + // the cluster context. We need to re-init the dictIter. + dictInitIterator(&iter->di, iter->cc->nodes); + iter->route_version = iter->cc->route_version; + iter->retries_left--; + } + + dictEntry *de; + if ((de = dictNext(&iter->di)) != NULL) + return dictGetEntryVal(de); + else + return NULL; +} + +/* Get hash slot for given key string, which can include hash tags */ +unsigned int redisClusterGetSlotByKey(char *key) { + return keyHashSlot(key, strlen(key)); +} + +/* Get node that handles given key string, which can include hash tags */ +redisClusterNode *redisClusterGetNodeByKey(redisClusterContext *cc, char *key) { + return node_get_by_table(cc, keyHashSlot(key, strlen(key))); +} diff --git a/libvalkeycluster/hircluster.h b/libvalkeycluster/hircluster.h new file mode 100644 index 00000000..4a365380 --- /dev/null +++ b/libvalkeycluster/hircluster.h @@ -0,0 +1,371 @@ +/* + * Copyright (c) 2015-2017, Ieshen Zheng + * Copyright (c) 2020, Nick + * Copyright (c) 2020-2021, Bjorn Svensson + * Copyright (c) 2021, Red Hat + * 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 __HIRCLUSTER_H +#define __HIRCLUSTER_H + +#include "dict.h" +#include +#include + +#define UNUSED(x) (void)(x) + +#define HIREDIS_CLUSTER_MAJOR 0 +#define HIREDIS_CLUSTER_MINOR 13 +#define HIREDIS_CLUSTER_PATCH 0 +#define HIREDIS_CLUSTER_SONAME 0.13 + +#define REDIS_CLUSTER_SLOTS 16384 + +#define REDIS_ROLE_NULL 0 +#define REDIS_ROLE_MASTER 1 +#define REDIS_ROLE_SLAVE 2 + +/* Configuration flags */ +#define HIRCLUSTER_FLAG_NULL 0x0 +/* Flag to enable parsing of slave nodes. Currently not used, but the + information is added to its master node structure. */ +#define HIRCLUSTER_FLAG_ADD_SLAVE 0x1000 +/* Flag to enable parsing of importing/migrating slots for master nodes. + * Only applicable when 'cluster nodes' command is used for route updates. */ +#define HIRCLUSTER_FLAG_ADD_OPENSLOT 0x2000 +/* Flag to enable routing table updates using the command 'cluster slots'. + * Default is the 'cluster nodes' command. */ +#define HIRCLUSTER_FLAG_ROUTE_USE_SLOTS 0x4000 + +/* Events, for redisClusterSetEventCallback() */ +#define HIRCLUSTER_EVENT_SLOTMAP_UPDATED 1 +#define HIRCLUSTER_EVENT_READY 2 +#define HIRCLUSTER_EVENT_FREE_CONTEXT 3 + +/* The non-const connect callback API is not available when: + * - using hiredis prior v.1.1.0; or + * - built on Windows since hiredis_cluster.def can't have conditional definitions. */ +#if !(HIREDIS_MAJOR >= 1 && HIREDIS_MINOR >= 1) || _WIN32 +#define HIRCLUSTER_NO_NONCONST_CONNECT_CB +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +struct dict; +struct hilist; +struct redisClusterAsyncContext; + +typedef int(adapterAttachFn)(redisAsyncContext *, void *); +typedef int(sslInitFn)(redisContext *, void *); +typedef void(redisClusterCallbackFn)(struct redisClusterAsyncContext *, void *, + void *); +typedef struct redisClusterNode { + sds name; + sds addr; + sds host; + uint16_t port; + uint8_t role; + uint8_t pad; + int failure_count; /* consecutive failing attempts in async */ + redisContext *con; + redisAsyncContext *acon; + int64_t lastConnectionAttempt; /* Timestamp */ + struct hilist *slots; + struct hilist *slaves; + struct hiarray *migrating; /* copen_slot[] */ + struct hiarray *importing; /* copen_slot[] */ +} redisClusterNode; + +typedef struct cluster_slot { + uint32_t start; + uint32_t end; + redisClusterNode *node; /* master that this slot region belong to */ +} cluster_slot; + +typedef struct copen_slot { + uint32_t slot_num; /* slot number */ + int migrate; /* migrating or importing? */ + sds remote_name; /* name of node this slot migrating to/importing from */ + redisClusterNode *node; /* master that this slot belong to */ +} copen_slot; + +/* Context for accessing a Redis Cluster */ +typedef struct redisClusterContext { + int err; /* Error flags, 0 when there is no error */ + char errstr[128]; /* String representation of error when applicable */ + + /* Configurations */ + int flags; /* Configuration flags */ + struct timeval *connect_timeout; /* TCP connect timeout */ + struct timeval *command_timeout; /* Receive and send timeout */ + int max_retry_count; /* Allowed retry attempts */ + char *username; /* Authenticate using user */ + char *password; /* Authentication password */ + + struct dict *nodes; /* Known redisClusterNode's */ + uint64_t route_version; /* Increased when the node lookup table changes */ + redisClusterNode **table; /* redisClusterNode lookup table */ + + struct hilist *requests; /* Outstanding commands (Pipelining) */ + + int retry_count; /* Current number of failing attempts */ + int need_update_route; /* Indicator for redisClusterReset() (Pipel.) */ + + void *ssl; /* Pointer to a redisSSLContext when using SSL/TLS. */ + sslInitFn *ssl_init_fn; /* Func ptr for SSL context initiation */ + + void (*on_connect)(const struct redisContext *c, int status); + void (*event_callback)(const struct redisClusterContext *cc, int event, + void *privdata); + void *event_privdata; + +} redisClusterContext; + +/* Context for accessing a Redis Cluster asynchronously */ +typedef struct redisClusterAsyncContext { + redisClusterContext *cc; + + int err; /* Error flags, 0 when there is no error */ + char errstr[128]; /* String representation of error when applicable */ + + int64_t lastSlotmapUpdateAttempt; /* Timestamp */ + + void *adapter; /* Adapter to the async event library */ + adapterAttachFn *attach_fn; /* Func ptr for attaching the async library */ + + /* Called when either the connection is terminated due to an error or per + * user request. The status is set accordingly (REDIS_OK, REDIS_ERR). */ + redisDisconnectCallback *onDisconnect; + + /* Called when the first write event was received. */ + redisConnectCallback *onConnect; +#ifndef HIRCLUSTER_NO_NONCONST_CONNECT_CB + redisConnectCallbackNC *onConnectNC; +#endif + +} redisClusterAsyncContext; + +typedef struct redisClusterNodeIterator { + redisClusterContext *cc; + uint64_t route_version; + int retries_left; + dictIterator di; +} redisClusterNodeIterator; + +/* + * Synchronous API + */ + +redisClusterContext *redisClusterConnect(const char *addrs, int flags); +redisClusterContext *redisClusterConnectWithTimeout(const char *addrs, + const struct timeval tv, + int flags); +int redisClusterConnect2(redisClusterContext *cc); + +redisClusterContext *redisClusterContextInit(void); +void redisClusterFree(redisClusterContext *cc); + +/* Configuration options */ +int redisClusterSetOptionAddNode(redisClusterContext *cc, const char *addr); +int redisClusterSetOptionAddNodes(redisClusterContext *cc, const char *addrs); +/* Deprecated function, option has no effect. */ +int redisClusterSetOptionConnectBlock(redisClusterContext *cc); +/* Deprecated function, option has no effect. */ +int redisClusterSetOptionConnectNonBlock(redisClusterContext *cc); +int redisClusterSetOptionUsername(redisClusterContext *cc, + const char *username); +int redisClusterSetOptionPassword(redisClusterContext *cc, + const char *password); +int redisClusterSetOptionParseSlaves(redisClusterContext *cc); +int redisClusterSetOptionParseOpenSlots(redisClusterContext *cc); +int redisClusterSetOptionRouteUseSlots(redisClusterContext *cc); +int redisClusterSetOptionConnectTimeout(redisClusterContext *cc, + const struct timeval tv); +int redisClusterSetOptionTimeout(redisClusterContext *cc, + const struct timeval tv); +int redisClusterSetOptionMaxRetry(redisClusterContext *cc, int max_retry_count); +/* Deprecated function, replaced with redisClusterSetOptionMaxRetry() */ +void redisClusterSetMaxRedirect(redisClusterContext *cc, + int max_redirect_count); +/* A hook for connect and reconnect attempts, e.g. for applying additional + * socket options. This is called just after connect, before TLS handshake and + * Redis authentication. + * + * On successful connection, `status` is set to `REDIS_OK` and the file + * descriptor can be accessed as `c->fd` to apply socket options. + * + * On failed connection attempt, this callback is called with `status` set to + * `REDIS_ERR`. The `err` field in the `redisContext` can be used to find out + * the cause of the error. */ +int redisClusterSetConnectCallback(redisClusterContext *cc, + void(fn)(const redisContext *c, int status)); + +/* A hook for events. */ +int redisClusterSetEventCallback(redisClusterContext *cc, + void(fn)(const redisClusterContext *cc, + int event, void *privdata), + void *privdata); + +/* Blocking + * The following functions will block for a reply, or return NULL if there was + * an error in performing the command. + */ + +/* Variadic commands (like printf) */ +void *redisClusterCommand(redisClusterContext *cc, const char *format, ...); +void *redisClusterCommandToNode(redisClusterContext *cc, redisClusterNode *node, + const char *format, ...); +/* Variadic using va_list */ +void *redisClustervCommand(redisClusterContext *cc, const char *format, + va_list ap); +/* Using argc and argv */ +void *redisClusterCommandArgv(redisClusterContext *cc, int argc, + const char **argv, const size_t *argvlen); +/* Send a Redis protocol encoded string */ +void *redisClusterFormattedCommand(redisClusterContext *cc, char *cmd, int len); + +/* Pipelining + * The following functions will write a command to the output buffer. + * A call to `redisClusterGetReply()` will flush all commands in the output + * buffer and read until it has a reply from the first command in the buffer. + */ + +/* Variadic commands (like printf) */ +int redisClusterAppendCommand(redisClusterContext *cc, const char *format, ...); +int redisClusterAppendCommandToNode(redisClusterContext *cc, + redisClusterNode *node, const char *format, + ...); +/* Variadic using va_list */ +int redisClustervAppendCommand(redisClusterContext *cc, const char *format, + va_list ap); +/* Using argc and argv */ +int redisClusterAppendCommandArgv(redisClusterContext *cc, int argc, + const char **argv, const size_t *argvlen); +/* Use a Redis protocol encoded string as command */ +int redisClusterAppendFormattedCommand(redisClusterContext *cc, char *cmd, + int len); +/* Flush output buffer and return first reply */ +int redisClusterGetReply(redisClusterContext *cc, void **reply); + +/* Reset context after a performed pipelining */ +void redisClusterReset(redisClusterContext *cc); + +/* Update the slotmap by querying any node. */ +int redisClusterUpdateSlotmap(redisClusterContext *cc); + +/* Internal functions */ +redisContext *ctx_get_by_node(redisClusterContext *cc, redisClusterNode *node); +struct dict *parse_cluster_nodes(redisClusterContext *cc, char *str, + int str_len, int flags); +struct dict *parse_cluster_slots(redisClusterContext *cc, redisReply *reply, + int flags); + +/* + * Asynchronous API + */ + +redisClusterAsyncContext *redisClusterAsyncContextInit(void); +void redisClusterAsyncFree(redisClusterAsyncContext *acc); + +int redisClusterAsyncSetConnectCallback(redisClusterAsyncContext *acc, + redisConnectCallback *fn); +#ifndef HIRCLUSTER_NO_NONCONST_CONNECT_CB +int redisClusterAsyncSetConnectCallbackNC(redisClusterAsyncContext *acc, + redisConnectCallbackNC *fn); +#endif +int redisClusterAsyncSetDisconnectCallback(redisClusterAsyncContext *acc, + redisDisconnectCallback *fn); + +/* Connect and update slotmap, will block until complete. */ +redisClusterAsyncContext *redisClusterAsyncConnect(const char *addrs, + int flags); +/* Connect and update slotmap asynchronously using configured event engine. */ +int redisClusterAsyncConnect2(redisClusterAsyncContext *acc); +void redisClusterAsyncDisconnect(redisClusterAsyncContext *acc); + +/* Commands */ +int redisClusterAsyncCommand(redisClusterAsyncContext *acc, + redisClusterCallbackFn *fn, void *privdata, + const char *format, ...); +int redisClusterAsyncCommandToNode(redisClusterAsyncContext *acc, + redisClusterNode *node, + redisClusterCallbackFn *fn, void *privdata, + const char *format, ...); +int redisClustervAsyncCommand(redisClusterAsyncContext *acc, + redisClusterCallbackFn *fn, void *privdata, + const char *format, va_list ap); +int redisClusterAsyncCommandArgv(redisClusterAsyncContext *acc, + redisClusterCallbackFn *fn, void *privdata, + int argc, const char **argv, + const size_t *argvlen); +int redisClusterAsyncCommandArgvToNode(redisClusterAsyncContext *acc, + redisClusterNode *node, + redisClusterCallbackFn *fn, + void *privdata, int argc, + const char **argv, + const size_t *argvlen); + +/* Use a Redis protocol encoded string as command */ +int redisClusterAsyncFormattedCommand(redisClusterAsyncContext *acc, + redisClusterCallbackFn *fn, + void *privdata, char *cmd, int len); +int redisClusterAsyncFormattedCommandToNode(redisClusterAsyncContext *acc, + redisClusterNode *node, + redisClusterCallbackFn *fn, + void *privdata, char *cmd, int len); + +/* Internal functions */ +redisAsyncContext *actx_get_by_node(redisClusterAsyncContext *acc, + redisClusterNode *node); + +/* Cluster node iterator functions */ +void redisClusterInitNodeIterator(redisClusterNodeIterator *iter, + redisClusterContext *cc); +redisClusterNode *redisClusterNodeNext(redisClusterNodeIterator *iter); + +/* Helper functions */ +unsigned int redisClusterGetSlotByKey(char *key); +redisClusterNode *redisClusterGetNodeByKey(redisClusterContext *cc, char *key); + +/* Old names of renamed functions and types, kept for backward compatibility. */ +#ifndef HIRCLUSTER_NO_OLD_NAMES +#define cluster_update_route redisClusterUpdateSlotmap +#define initNodeIterator redisClusterInitNodeIterator +#define nodeNext redisClusterNodeNext +#define redisClusterConnectNonBlock redisClusterConnect +typedef struct redisClusterNode cluster_node; +typedef struct redisClusterNodeIterator nodeIterator; +#endif + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/libvalkeycluster/hircluster_ssl.c b/libvalkeycluster/hircluster_ssl.c new file mode 100644 index 00000000..323ff22e --- /dev/null +++ b/libvalkeycluster/hircluster_ssl.c @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2022, Bjorn Svensson + * 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 "hircluster_ssl.h" + +static int redisClusterInitiateSSLWithContext(redisContext *c, + void *redis_ssl_ctx) { + return redisInitiateSSLWithContext(c, redis_ssl_ctx); +} + +int redisClusterSetOptionEnableSSL(redisClusterContext *cc, + redisSSLContext *ssl) { + if (cc == NULL || ssl == NULL) { + return REDIS_ERR; + } + + cc->ssl = ssl; + cc->ssl_init_fn = &redisClusterInitiateSSLWithContext; + + return REDIS_OK; +} diff --git a/libvalkeycluster/hircluster_ssl.h b/libvalkeycluster/hircluster_ssl.h new file mode 100644 index 00000000..9ab1f785 --- /dev/null +++ b/libvalkeycluster/hircluster_ssl.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2022, Bjorn Svensson + * 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 __HIRCLUSTER_SSL_H +#define __HIRCLUSTER_SSL_H + +#include "hircluster.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Configuration option to enable SSL/TLS negotiation on a context. + */ +int redisClusterSetOptionEnableSSL(redisClusterContext *cc, + redisSSLContext *ssl); + +#ifdef __cplusplus +} +#endif + +#endif /* __HIRCLUSTER_SSL_H */ diff --git a/libvalkeycluster/hiredis_cluster-config.cmake.in b/libvalkeycluster/hiredis_cluster-config.cmake.in new file mode 100644 index 00000000..be9a588f --- /dev/null +++ b/libvalkeycluster/hiredis_cluster-config.cmake.in @@ -0,0 +1,13 @@ +@PACKAGE_INIT@ + +set_and_check(hiredis_cluster_INCLUDEDIR "@PACKAGE_INCLUDE_INSTALL_DIR@") + +if(NOT TARGET hiredis_cluster::hiredis_cluster) + include(${CMAKE_CURRENT_LIST_DIR}/hiredis_cluster-targets.cmake) +endif() + +set(hiredis_cluster_LIBRARIES hiredis_cluster::hiredis_cluster) +set(hiredis_cluster_INCLUDE_DIRS ${hiredis_cluster_INCLUDEDIR}) + +check_required_components(hiredis_cluster) + diff --git a/libvalkeycluster/hiredis_cluster.def b/libvalkeycluster/hiredis_cluster.def new file mode 100644 index 00000000..a50b3b1b --- /dev/null +++ b/libvalkeycluster/hiredis_cluster.def @@ -0,0 +1,62 @@ + EXPORTS + actx_get_by_node + command_destroy + command_get + ctx_get_by_node + dictInitIterator + dictNext + hiarray_get + parse_cluster_nodes + parse_cluster_slots + redisClusterAppendCommand + redisClusterAppendCommandArgv + redisClusterAppendCommandToNode + redisClusterAppendFormattedCommand + redisClusterAsyncContextInit + redisClusterAsyncCommand + redisClusterAsyncCommandArgv + redisClusterAsyncCommandArgvToNode + redisClusterAsyncCommandToNode + redisClusterAsyncConnect + redisClusterAsyncConnect2 + redisClusterAsyncDisconnect + redisClusterAsyncFormattedCommand + redisClusterAsyncFormattedCommandToNode + redisClusterAsyncFree + redisClusterAsyncSetConnectCallback + redisClusterAsyncSetDisconnectCallback + redisClusterCommand + redisClusterCommandArgv + redisClusterCommandToNode + redisClusterConnect + redisClusterConnect2 + redisClusterConnectWithTimeout + redisClusterContextInit + redisClusterFormattedCommand + redisClusterFree + redisClusterGetNodeByKey + redisClusterGetReply + redisClusterGetSlotByKey + redisClusterInitNodeIterator + redisClusterNodeNext + redisClusterReset + redisClusterSetConnectCallback + redisClusterSetEventCallback + redisClusterSetMaxRedirect + redisClusterSetOptionAddNode + redisClusterSetOptionAddNodes + redisClusterSetOptionConnectBlock + redisClusterSetOptionConnectNonBlock + redisClusterSetOptionConnectTimeout + redisClusterSetOptionMaxRetry + redisClusterSetOptionParseOpenSlots + redisClusterSetOptionParseSlaves + redisClusterSetOptionRouteUseSlots + redisClusterSetOptionTimeout + redisClusterSetOptionUsername + redisClusterSetOptionPassword + redisClusterUpdateSlotmap + redisClustervAppendCommand + redisClustervAsyncCommand + redisClustervCommand + redis_parse_cmd diff --git a/libvalkeycluster/hiredis_cluster.pc.in b/libvalkeycluster/hiredis_cluster.pc.in new file mode 100644 index 00000000..0857071b --- /dev/null +++ b/libvalkeycluster/hiredis_cluster.pc.in @@ -0,0 +1,11 @@ +prefix=@CMAKE_INSTALL_PREFIX@ +exec_prefix=${prefix} +libdir=${exec_prefix}/lib +includedir=${prefix}/include +pkgincludedir=${includedir}/hiredis_cluster + +Name: hiredis_cluster +Description: Minimalistic C client library for Redis with cluster support. +Version: @PROJECT_VERSION@ +Libs: -L${libdir} -lhiredis_cluster +Cflags: -I${pkgincludedir} -D_FILE_OFFSET_BITS=64 diff --git a/libvalkeycluster/hiredis_cluster_ssl-config.cmake.in b/libvalkeycluster/hiredis_cluster_ssl-config.cmake.in new file mode 100644 index 00000000..1c0509b3 --- /dev/null +++ b/libvalkeycluster/hiredis_cluster_ssl-config.cmake.in @@ -0,0 +1,12 @@ +@PACKAGE_INIT@ + +set_and_check(hiredis_cluster_ssl_INCLUDEDIR "@PACKAGE_INCLUDE_INSTALL_DIR@") + +if(NOT TARGET hiredis_cluster::hiredis_cluster_ssl) + include(${CMAKE_CURRENT_LIST_DIR}/hiredis_cluster_ssl-targets.cmake) +endif() + +set(hiredis_cluster_ssl_LIBRARIES hiredis_cluster::hiredis_cluster_ssl) +set(hiredis_cluster_ssl_INCLUDE_DIRS ${hiredis_cluster_ssl_INCLUDEDIR}) + +check_required_components(hiredis_cluster_ssl) diff --git a/libvalkeycluster/hiredis_cluster_ssl.pc.in b/libvalkeycluster/hiredis_cluster_ssl.pc.in new file mode 100644 index 00000000..61feb7fb --- /dev/null +++ b/libvalkeycluster/hiredis_cluster_ssl.pc.in @@ -0,0 +1,12 @@ +prefix=@CMAKE_INSTALL_PREFIX@ +exec_prefix=${prefix} +libdir=${exec_prefix}/lib +includedir=${prefix}/include +pkgincludedir=${includedir}/hiredis_cluster + +Name: hiredis_cluster_ssl +Description: SSL support for hiredis-cluster +Version: @PROJECT_VERSION@ +Requires: hiredis_cluster +Libs: -L${libdir} -lhiredis_cluster_ssl +Libs.private: -lhiredis_ssl -lssl -lcrypto diff --git a/libvalkeycluster/hiutil.c b/libvalkeycluster/hiutil.c new file mode 100644 index 00000000..4659bf00 --- /dev/null +++ b/libvalkeycluster/hiutil.c @@ -0,0 +1,403 @@ +/* + * Copyright (c) 2015-2017, Ieshen Zheng + * Copyright (c) 2020, Nick + * Copyright (c) 2020-2021, Bjorn Svensson + * 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 +#include +#include +#include +#include +#include +#include +#include + +#ifndef _WIN32 +#include +#include +#include +#include +#endif + +#ifdef HI_HAVE_BACKTRACE +#include +#endif + +#include "hiutil.h" +#include "win32.h" + +#ifndef _WIN32 +int hi_set_blocking(int sd) { + int flags; + + flags = fcntl(sd, F_GETFL, 0); + if (flags < 0) { + return flags; + } + + return fcntl(sd, F_SETFL, flags & ~O_NONBLOCK); +} + +int hi_set_nonblocking(int sd) { + int flags; + + flags = fcntl(sd, F_GETFL, 0); + if (flags < 0) { + return flags; + } + + return fcntl(sd, F_SETFL, flags | O_NONBLOCK); +} + +int hi_set_reuseaddr(int sd) { + int reuse; + socklen_t len; + + reuse = 1; + len = sizeof(reuse); + + return setsockopt(sd, SOL_SOCKET, SO_REUSEADDR, &reuse, len); +} + +/* + * Disable Nagle algorithm on TCP socket. + * + * This option helps to minimize transmit latency by disabling coalescing + * of data to fill up a TCP segment inside the kernel. Sockets with this + * option must use readv() or writev() to do data transfer in bulk and + * hence avoid the overhead of small packets. + */ +int hi_set_tcpnodelay(int sd) { + int nodelay; + socklen_t len; + + nodelay = 1; + len = sizeof(nodelay); + + return setsockopt(sd, IPPROTO_TCP, TCP_NODELAY, &nodelay, len); +} + +int hi_set_linger(int sd, int timeout) { + struct linger linger; + socklen_t len; + + linger.l_onoff = 1; + linger.l_linger = timeout; + + len = sizeof(linger); + + return setsockopt(sd, SOL_SOCKET, SO_LINGER, &linger, len); +} + +int hi_set_sndbuf(int sd, int size) { + socklen_t len; + + len = sizeof(size); + + return setsockopt(sd, SOL_SOCKET, SO_SNDBUF, &size, len); +} + +int hi_set_rcvbuf(int sd, int size) { + socklen_t len; + + len = sizeof(size); + + return setsockopt(sd, SOL_SOCKET, SO_RCVBUF, &size, len); +} + +int hi_get_soerror(int sd) { + int status, err; + socklen_t len; + + err = 0; + len = sizeof(err); + + status = getsockopt(sd, SOL_SOCKET, SO_ERROR, &err, &len); + if (status == 0) { + errno = err; + } + + return status; +} + +int hi_get_sndbuf(int sd) { + int status, size; + socklen_t len; + + size = 0; + len = sizeof(size); + + status = getsockopt(sd, SOL_SOCKET, SO_SNDBUF, &size, &len); + if (status < 0) { + return status; + } + + return size; +} + +int hi_get_rcvbuf(int sd) { + int status, size; + socklen_t len; + + size = 0; + len = sizeof(size); + + status = getsockopt(sd, SOL_SOCKET, SO_RCVBUF, &size, &len); + if (status < 0) { + return status; + } + + return size; +} +#endif + +int _hi_atoi(uint8_t *line, size_t n) { + int value; + + if (n == 0) { + return -1; + } + + for (value = 0; n--; line++) { + if (*line < '0' || *line > '9') { + return -1; + } + + value = value * 10 + (*line - '0'); + } + + if (value < 0) { + return -1; + } + + return value; +} + +void _hi_itoa(uint8_t *s, int num) { + uint8_t c; + uint8_t sign = 0; + + if (s == NULL) { + return; + } + + uint32_t len, i; + len = 0; + + if (num < 0) { + sign = 1; + num = abs(num); + } else if (num == 0) { + s[len++] = '0'; + return; + } + + while (num % 10 || num / 10) { + c = num % 10 + '0'; + num = num / 10; + s[len + 1] = s[len]; + s[len] = c; + len++; + } + + if (sign == 1) { + s[len++] = '-'; + } + + s[len] = '\0'; + + for (i = 0; i < len / 2; i++) { + c = s[i]; + s[i] = s[len - i - 1]; + s[len - i - 1] = c; + } +} + +int hi_valid_port(int n) { + if (n < 1 || n > UINT16_MAX) { + return 0; + } + + return 1; +} + +int _uint_len(uint32_t num) { + int n = 0; + + if (num == 0) { + return 1; + } + + while (num != 0) { + n++; + num /= 10; + } + + return n; +} + +void hi_stacktrace(int skip_count) { +#ifdef HI_HAVE_BACKTRACE + void *stack[64]; + char **symbols; + int size, i, j; + + size = backtrace(stack, 64); + symbols = backtrace_symbols(stack, size); + if (symbols == NULL) { + return; + } + + skip_count++; /* skip the current frame also */ + + for (i = skip_count, j = 0; i < size; i++, j++) { + printf("[%d] %s\n", j, symbols[i]); + } + + hi_free(symbols); +#else + (void)skip_count; +#endif +} + +void hi_stacktrace_fd(int fd) { +#ifdef HI_HAVE_BACKTRACE + void *stack[64]; + int size; + + size = backtrace(stack, 64); + backtrace_symbols_fd(stack, size, fd); +#else + (void)fd; +#endif +} + +void hi_assert(const char *cond, const char *file, int line, int panic) { + + printf("File: %s Line: %d: %s\n", file, line, cond); + + if (panic) { + hi_stacktrace(1); + abort(); + } + abort(); +} + +#ifndef _WIN32 +/* + * Send n bytes on a blocking descriptor + */ +ssize_t _hi_sendn(int sd, const void *vptr, size_t n) { + size_t nleft; + ssize_t nsend; + const char *ptr; + + ptr = vptr; + nleft = n; + while (nleft > 0) { + nsend = send(sd, ptr, nleft, 0); + if (nsend < 0) { + if (errno == EINTR) { + continue; + } + return nsend; + } + if (nsend == 0) { + return -1; + } + + nleft -= (size_t)nsend; + ptr += nsend; + } + + return (ssize_t)n; +} + +/* + * Recv n bytes from a blocking descriptor + */ +ssize_t _hi_recvn(int sd, void *vptr, size_t n) { + size_t nleft; + ssize_t nrecv; + char *ptr; + + ptr = vptr; + nleft = n; + while (nleft > 0) { + nrecv = recv(sd, ptr, nleft, 0); + if (nrecv < 0) { + if (errno == EINTR) { + continue; + } + return nrecv; + } + if (nrecv == 0) { + break; + } + + nleft -= (size_t)nrecv; + ptr += nrecv; + } + + return (ssize_t)(n - nleft); +} +#endif + +/* + * Return the current time in microseconds since Epoch + */ +int64_t hi_usec_now(void) { + int64_t usec; +#ifdef _WIN32 + LARGE_INTEGER counter, frequency; + + if (!QueryPerformanceCounter(&counter) || + !QueryPerformanceFrequency(&frequency)) { + return -1; + } + + usec = counter.QuadPart * 1000000 / frequency.QuadPart; +#else + struct timeval now; + int status; + + status = gettimeofday(&now, NULL); + if (status < 0) { + return -1; + } + + usec = (int64_t)now.tv_sec * 1000000LL + (int64_t)now.tv_usec; +#endif + + return usec; +} + +/* + * Return the current time in milliseconds since Epoch + */ +int64_t hi_msec_now(void) { return hi_usec_now() / 1000LL; } diff --git a/libvalkeycluster/hiutil.h b/libvalkeycluster/hiutil.h new file mode 100644 index 00000000..83a2f858 --- /dev/null +++ b/libvalkeycluster/hiutil.h @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2015-2017, Ieshen Zheng + * Copyright (c) 2020, Nick + * Copyright (c) 2020-2021, Bjorn Svensson + * Copyright (c) 2020-2021, Viktor Söderqvist + * 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 __HIUTIL_H_ +#define __HIUTIL_H_ + +#include +#include + +#define HI_OK 0 +#define HI_ERROR -1 +#define HI_EAGAIN -2 + +typedef int rstatus_t; /* return type */ + +#define HI_INET4_ADDRSTRLEN (sizeof("255.255.255.255") - 1) +#define HI_INET6_ADDRSTRLEN \ + (sizeof("ffff:ffff:ffff:ffff:ffff:ffff:255.255.255.255") - 1) +#define HI_INET_ADDRSTRLEN MAX(HI_INET4_ADDRSTRLEN, HI_INET6_ADDRSTRLEN) +#define HI_UNIX_ADDRSTRLEN \ + (sizeof(struct sockaddr_un) - offsetof(struct sockaddr_un, sun_path)) + +#define HI_MAXHOSTNAMELEN 256 + +/* + * Length of 1 byte, 2 bytes, 4 bytes, 8 bytes and largest integral + * type (uintmax_t) in ascii, including the null terminator '\0' + * + * From stdint.h, we have: + * # define UINT8_MAX (255) + * # define UINT16_MAX (65535) + * # define UINT32_MAX (4294967295U) + * # define UINT64_MAX (__UINT64_C(18446744073709551615)) + */ +#define HI_UINT8_MAXLEN (3 + 1) +#define HI_UINT16_MAXLEN (5 + 1) +#define HI_UINT32_MAXLEN (10 + 1) +#define HI_UINT64_MAXLEN (20 + 1) +#define HI_UINTMAX_MAXLEN HI_UINT64_MAXLEN + +/* + * Make data 'd' or pointer 'p', n-byte aligned, where n is a power of 2 + * of 2. + */ +#define HI_ALIGNMENT sizeof(unsigned long) /* platform word */ +#define HI_ALIGN(d, n) (((d) + (n - 1)) & ~(n - 1)) +#define HI_ALIGN_PTR(p, n) \ + (void *)(((uintptr_t)(p) + ((uintptr_t)n - 1)) & ~((uintptr_t)n - 1)) + +/* + * Wrapper to workaround well known, safe, implicit type conversion when + * invoking system calls. + */ +#define hi_gethostname(_name, _len) gethostname((char *)_name, (size_t)_len) + +#define hi_atoi(_line, _n) _hi_atoi((uint8_t *)_line, (size_t)_n) +#define hi_itoa(_line, _n) _hi_itoa((uint8_t *)_line, (int)_n) + +#define uint_len(_n) _uint_len((uint32_t)_n) + +#ifndef _WIN32 +int hi_set_blocking(int sd); +int hi_set_nonblocking(int sd); +int hi_set_reuseaddr(int sd); +int hi_set_tcpnodelay(int sd); +int hi_set_linger(int sd, int timeout); +int hi_set_sndbuf(int sd, int size); +int hi_set_rcvbuf(int sd, int size); +int hi_get_soerror(int sd); +int hi_get_sndbuf(int sd); +int hi_get_rcvbuf(int sd); +#endif + +int _hi_atoi(uint8_t *line, size_t n); +void _hi_itoa(uint8_t *s, int num); + +int hi_valid_port(int n); + +int _uint_len(uint32_t num); + +#ifndef _WIN32 +/* + * Wrappers to send or receive n byte message on a blocking + * socket descriptor. + */ +#define hi_sendn(_s, _b, _n) _hi_sendn(_s, _b, (size_t)(_n)) + +#define hi_recvn(_s, _b, _n) _hi_recvn(_s, _b, (size_t)(_n)) +#endif + +/* + * Wrappers to read or write data to/from (multiple) buffers + * to a file or socket descriptor. + */ +#define hi_read(_d, _b, _n) read(_d, _b, (size_t)(_n)) + +#define hi_readv(_d, _b, _n) readv(_d, _b, (int)(_n)) + +#define hi_write(_d, _b, _n) write(_d, _b, (size_t)(_n)) + +#define hi_writev(_d, _b, _n) writev(_d, _b, (int)(_n)) + +#ifndef _WIN32 +ssize_t _hi_sendn(int sd, const void *vptr, size_t n); +ssize_t _hi_recvn(int sd, void *vptr, size_t n); +#endif + +/* + * Wrappers for defining custom assert based on whether macro + * HI_ASSERT_PANIC or HI_ASSERT_LOG was defined at the moment + * ASSERT was called. + */ +#ifdef HI_ASSERT_PANIC + +#define ASSERT(_x) \ + do { \ + if (!(_x)) { \ + hi_assert(#_x, __FILE__, __LINE__, 1); \ + } \ + } while (0) + +#define NOT_REACHED() ASSERT(0) + +#elif HI_ASSERT_LOG + +#define ASSERT(_x) \ + do { \ + if (!(_x)) { \ + hi_assert(#_x, __FILE__, __LINE__, 0); \ + } \ + } while (0) + +#define NOT_REACHED() ASSERT(0) + +#else + +#define ASSERT(_x) + +#define NOT_REACHED() + +#endif + +void hi_assert(const char *cond, const char *file, int line, int panic); +void hi_stacktrace(int skip_count); +void hi_stacktrace_fd(int fd); + +int64_t hi_usec_now(void); +int64_t hi_msec_now(void); + +uint16_t crc16(const char *buf, int len); + +#endif diff --git a/libvalkeycluster/tests/CMakeLists.txt b/libvalkeycluster/tests/CMakeLists.txt new file mode 100644 index 00000000..f20ffde0 --- /dev/null +++ b/libvalkeycluster/tests/CMakeLists.txt @@ -0,0 +1,276 @@ +SET(TEST_WITH_REDIS_VERSION "6.2.1" CACHE STRING "Redis version used when running tests.") + +# Re-enable `-std=gnu99` for tests only, this avoids the need to sprinkle +# `#define _XOPEN_SOURCE 600` in test code for e.g. strdup() +set(CMAKE_C_EXTENSIONS ON) + +if(ENABLE_SSL) + # Generate SSL certs and keys when needed + set(SSL_CONFIGS ca.crt ca.key ca.txt redis.crt redis.key client.crt client.key) + add_custom_command( + OUTPUT ${SSL_CONFIGS} + COMMAND openssl genrsa -out ca.key 4096 + COMMAND openssl req -x509 -new -nodes -sha256 -key ca.key -days 3650 -subj '/CN=Redis Test CA' -out ca.crt + COMMAND openssl genrsa -out redis.key 2048 + COMMAND openssl req -new -sha256 -key redis.key -subj '/CN=Redis Server Test Cert' | openssl x509 -req -sha256 -CA ca.crt -CAkey ca.key -CAserial ca.txt -CAcreateserial -days 365 -out redis.crt + COMMAND openssl genrsa -out client.key 2048 + COMMAND openssl req -new -sha256 -key client.key -subj '/CN=Redis Client Test Cert' | openssl x509 -req -sha256 -CA ca.crt -CAkey ca.key -CAserial ca.txt -CAcreateserial -days 365 -out client.crt + ) + add_custom_target(generate_tls_configs DEPENDS ${SSL_CONFIGS}) + + set(SSL_LIBRARY hiredis_cluster_ssl) +endif() + +# Targets to setup Redis Clusters for testing +if(ENABLE_IPV6_TESTS) + set(NO_IPV6 "") +else() + set(NO_IPV6 "true") # Ignore command +endif() + +if(TEST_WITH_VALKEY_VERSION) + set(CLUSTER_SCRIPT "${CMAKE_SOURCE_DIR}/tests/scripts/valkey-cluster") + set(CLUSTER_VERSION "VALKEY_VERSION=${TEST_WITH_VALKEY_VERSION}") +else() + set(CLUSTER_SCRIPT "${CMAKE_SOURCE_DIR}/tests/scripts/redis-cluster") + set(CLUSTER_VERSION "REDIS_VERSION=${TEST_WITH_REDIS_VERSION}") +endif() + +add_custom_target(start + COMMAND PORT=7000 ${CLUSTER_VERSION} ${CLUSTER_SCRIPT} start + COMMAND PORT=7100 ${CLUSTER_VERSION} ADDITIONAL_OPTIONS='--requirepass secretword --masterauth secretword' ADDITIONAL_CLI_OPTIONS='-a secretword' ${CLUSTER_SCRIPT} start + COMMAND ${NO_IPV6} PORT=7200 ${CLUSTER_VERSION} CLUSTER_HOST=::1 ADDITIONAL_OPTIONS='--bind ::1' ADDITIONAL_CLI_OPTIONS='-h ::1' ${CLUSTER_SCRIPT} start +) +add_custom_target(stop + COMMAND PORT=7000 ${CLUSTER_SCRIPT} stop + COMMAND PORT=7100 ${CLUSTER_SCRIPT} stop + COMMAND ${NO_IPV6} PORT=7200 ${CLUSTER_SCRIPT} stop +) + +# Find dependencies +find_package(PkgConfig REQUIRED) +pkg_check_modules(GLIB_LIBRARY IMPORTED_TARGET glib-2.0) +find_library(LIBUV_LIBRARY uv HINTS /usr/lib/x86_64-linux-gnu) +find_library(LIBEV_LIBRARY ev HINTS /usr/lib/x86_64-linux-gnu) +find_library(LIBEVENT_LIBRARY event HINTS /usr/lib/x86_64-linux-gnu) +find_path(LIBEVENT_INCLUDES event2/event.h) +include_directories(${LIBEVENT_INCLUDES}) + +if(MSVC OR MINGW) + find_library(LIBEVENT_LIBRARY Libevent) +else() + add_compile_options(-Wall -Wextra -pedantic -Werror) + # Debug mode for tests + set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "" FORCE) +endif() + +# Make sure ctest gives the output when tests fail +list(APPEND CMAKE_CTEST_ARGUMENTS "--output-on-failure") + +add_executable(ct_async ct_async.c) +target_link_libraries(ct_async hiredis_cluster ${SSL_LIBRARY} ${LIBEVENT_LIBRARY}) +add_test(NAME ct_async COMMAND "$") +set_tests_properties(ct_async PROPERTIES LABELS "CT") + +add_executable(ct_commands ct_commands.c test_utils.c) +target_link_libraries(ct_commands hiredis_cluster ${SSL_LIBRARY}) +add_test(NAME ct_commands COMMAND "$") +set_tests_properties(ct_commands PROPERTIES LABELS "CT") + +add_executable(ct_connection ct_connection.c test_utils.c) +target_link_libraries(ct_connection hiredis_cluster ${SSL_LIBRARY} ${LIBEVENT_LIBRARY}) +add_test(NAME ct_connection COMMAND "$") +set_tests_properties(ct_connection PROPERTIES LABELS "CT") + +add_executable(ct_pipeline ct_pipeline.c) +target_link_libraries(ct_pipeline hiredis_cluster ${SSL_LIBRARY} ${LIBEVENT_LIBRARY}) +add_test(NAME ct_pipeline COMMAND "$") +set_tests_properties(ct_pipeline PROPERTIES LABELS "CT") + +add_executable(ct_connection_ipv6 ct_connection_ipv6.c) +target_link_libraries(ct_connection_ipv6 hiredis_cluster ${SSL_LIBRARY} ${LIBEVENT_LIBRARY}) +add_test(NAME ct_connection_ipv6 COMMAND "$") +set_tests_properties(ct_connection_ipv6 PROPERTIES LABELS "CT") +if(NOT ENABLE_IPV6_TESTS) + set_tests_properties(ct_connection_ipv6 PROPERTIES DISABLED True) +endif() + +add_executable(ct_out_of_memory_handling ct_out_of_memory_handling.c) +target_link_libraries(ct_out_of_memory_handling hiredis_cluster ${SSL_LIBRARY} ${LIBEVENT_LIBRARY}) +add_test(NAME ct_out_of_memory_handling COMMAND "$") +set_tests_properties(ct_out_of_memory_handling PROPERTIES LABELS "CT") + +add_executable(ct_specific_nodes ct_specific_nodes.c test_utils.c) +target_link_libraries(ct_specific_nodes hiredis_cluster ${SSL_LIBRARY} ${LIBEVENT_LIBRARY}) +add_test(NAME ct_specific_nodes COMMAND "$") +set_tests_properties(ct_specific_nodes PROPERTIES LABELS "CT") + +add_executable(ut_parse_cmd ut_parse_cmd.c test_utils.c) +target_link_libraries(ut_parse_cmd hiredis_cluster ${SSL_LIBRARY}) +add_test(NAME ut_parse_cmd COMMAND "$") +set_tests_properties(ut_parse_cmd PROPERTIES LABELS "UT") + +if(ENABLE_SSL) + # Executable: tls + add_executable(example_tls main_tls.c) + target_link_libraries(example_tls hiredis_cluster ${SSL_LIBRARY}) + add_dependencies(example_tls generate_tls_configs) + + # Executable: async tls + add_executable(example_async_tls main_async_tls.c) + target_link_libraries(example_async_tls hiredis_cluster ${SSL_LIBRARY} ${LIBEVENT_LIBRARY}) + add_dependencies(example_async_tls generate_tls_configs) +endif() + +if(LIBUV_LIBRARY) + add_executable(ct_async_libuv ct_async_libuv.c) + target_link_libraries(ct_async_libuv hiredis_cluster ${SSL_LIBRARY} ${LIBUV_LIBRARY}) + add_test(NAME ct_async_libuv COMMAND "$") + set_tests_properties(ct_async_libuv PROPERTIES LABELS "CT") +else() + add_test(NAME ct_async_libuv COMMAND "") + set_tests_properties(ct_async_libuv PROPERTIES DISABLED True) +endif() + +if(LIBEV_LIBRARY) + add_executable(ct_async_libev ct_async_libev.c) + # Temporary remove warning of unused parameter due to an issue in hiredis libev adapter + target_compile_options(ct_async_libev PRIVATE -Wno-unused-parameter) + target_link_libraries(ct_async_libev hiredis_cluster ${SSL_LIBRARY} ${LIBEV_LIBRARY}) + add_test(NAME ct_async_libev COMMAND "$") + set_tests_properties(ct_async_libev PROPERTIES LABELS "CT") +else() + add_test(NAME ct_async_libev COMMAND "") + set_tests_properties(ct_async_libev PROPERTIES DISABLED True) +endif() + +if(GLIB_LIBRARY_FOUND) + add_executable(ct_async_glib ct_async_glib.c) + target_link_libraries(ct_async_glib hiredis_cluster ${SSL_LIBRARY} PkgConfig::GLIB_LIBRARY) + add_test(NAME ct_async_glib COMMAND "$") + set_tests_properties(ct_async_glib PROPERTIES LABELS "CT") +else() + add_test(NAME ct_async_glib COMMAND "") + set_tests_properties(ct_async_glib PROPERTIES DISABLED True) +endif() + +# Tests using simulated redis node +add_executable(clusterclient clusterclient.c) +target_link_libraries(clusterclient hiredis_cluster ${SSL_LIBRARY}) +add_executable(clusterclient_async clusterclient_async.c) +target_link_libraries(clusterclient_async hiredis_cluster ${SSL_LIBRARY} ${LIBEVENT_LIBRARY}) +add_executable(clusterclient_reconnect_async clusterclient_reconnect_async.c) +target_link_libraries(clusterclient_reconnect_async hiredis_cluster ${SSL_LIBRARY} ${LIBEVENT_LIBRARY}) +add_test(NAME set-get-test + COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/set-get-test.sh" + "$" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/") +add_test(NAME set-get-test-async + COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/set-get-test.sh" + "$" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/") +add_test(NAME ask-redirect-test + COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/ask-redirect-test.sh" + "$" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/") +add_test(NAME ask-redirect-test-async + COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/ask-redirect-test.sh" + "$" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/") +add_test(NAME ask-redirect-using-cluster-nodes-test-async + COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/ask-redirect-using-cluster-nodes-test.sh" + "$" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/") +add_test(NAME moved-redirect-test + COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/moved-redirect-test.sh" + "$" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/") +add_test(NAME moved-redirect-test-async + COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/moved-redirect-test.sh" + "$" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/") +add_test(NAME moved-redirect-using-cluster-nodes-test-async + COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/moved-redirect-using-cluster-nodes-test.sh" + "$" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/") +add_test(NAME dbsize-to-all-nodes-test + COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/dbsize-to-all-nodes-test.sh" + "$" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/") +add_test(NAME dbsize-to-all-nodes-test-async + COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/dbsize-to-all-nodes-test.sh" + "$" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/") +add_test(NAME dbsize-to-all-nodes-during-scaledown-test + COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/dbsize-to-all-nodes-during-scaledown-test.sh" + "$" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/") +add_test(NAME dbsize-to-all-nodes-during-scaledown-test-async + COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/dbsize-to-all-nodes-during-scaledown-test-async.sh" + "$" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/") +add_test(NAME reconnect-test + COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/reconnect-test.sh" + "$" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/") +add_test(NAME timeout-handling-test + COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/timeout-handling-test.sh" + "$" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/") +add_test(NAME connect-error-using-cluster-nodes-test + COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/connect-error-using-cluster-nodes-test.sh" + "$" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/") +add_test(NAME command-from-callback-test + COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/command-from-callback-test.sh" + "$" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/") +add_test(NAME ask-redirect-connection-error-test + COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/ask-redirect-connection-error-test.sh" + "$" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/") +add_test(NAME cluster-down-test + COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/cluster-down-test.sh" + "$" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/") +add_test(NAME connection-error-test + COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/connection-error-test.sh" + "$" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/") +add_test(NAME redirect-with-hostname-test + COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/redirect-with-hostname-test.sh" + "$" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/") +add_test(NAME redirect-with-ipv6-test + COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/redirect-with-ipv6-test.sh" + "$" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/") +add_test(NAME redirect-with-ipv6-async-test + COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/redirect-with-ipv6-test.sh" + "$" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/") +if(NOT ENABLE_IPV6_TESTS) + set_tests_properties(redirect-with-ipv6-test PROPERTIES DISABLED True) + set_tests_properties(redirect-with-ipv6-async-test PROPERTIES DISABLED True) +endif() +# This test can't be run on hiredis v1.1.0 due to hiredis issue #1171. +# Disabling the testcase if hiredis contains the issue or if the version is unknown. +add_test(NAME redirect-with-hostname-test-async + COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/redirect-with-hostname-test.sh" + "$" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/") +if(hiredis_VERSION VERSION_EQUAL "1.1.0" OR hiredis_VERSION VERSION_EQUAL "0") + set_tests_properties(redirect-with-hostname-test-async PROPERTIES DISABLED True) +endif() +add_test(NAME cluster-scale-down-test + COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/cluster-scale-down-test.sh" + "$" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/") +add_test(NAME slots-not-served-test + COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/slots-not-served-test.sh" + "$" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/") +add_test(NAME slots-not-served-test-async + COMMAND "${CMAKE_SOURCE_DIR}/tests/scripts/slots-not-served-test-async.sh" + "$" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/tests/scripts/") diff --git a/libvalkeycluster/tests/clusterclient.c b/libvalkeycluster/tests/clusterclient.c new file mode 100644 index 00000000..35657a9a --- /dev/null +++ b/libvalkeycluster/tests/clusterclient.c @@ -0,0 +1,150 @@ +/* + * This program connects to a cluster and then reads commands from stdin, such + * as "SET foo bar", one per line and prints the results to stdout. + * + * The behaviour of the client can be altered by following action commands: + * + * !all - Send each command to all nodes in the cluster. + * Will send following commands using the `..ToNode()` API and a + * cluster node iterator to send each command to all known nodes. + * + * Exit statuses this program can return: + * 0 - Successful execution of program. + * 1 - Bad arguments. + * 2 - Client failed to get initial slotmap from given "HOST:PORT". + */ + +#include "hircluster.h" +#include "win32.h" +#include +#include +#include + +void printReply(const redisReply *reply) { + switch (reply->type) { + case REDIS_REPLY_ERROR: + case REDIS_REPLY_STATUS: + case REDIS_REPLY_STRING: + case REDIS_REPLY_VERB: + case REDIS_REPLY_BIGNUM: + printf("%s\n", reply->str); + break; + case REDIS_REPLY_INTEGER: + printf("%lld\n", reply->integer); + break; + default: + printf("Unhandled reply type: %d\n", reply->type); + } +} + +void eventCallback(const redisClusterContext *cc, int event, void *privdata) { + (void)cc; + (void)privdata; + char *e = NULL; + switch (event) { + case HIRCLUSTER_EVENT_SLOTMAP_UPDATED: + e = "slotmap-updated"; + break; + case HIRCLUSTER_EVENT_READY: + e = "ready"; + break; + case HIRCLUSTER_EVENT_FREE_CONTEXT: + e = "free-context"; + break; + default: + e = "unknown"; + } + printf("Event: %s\n", e); +} + +int main(int argc, char **argv) { + int show_events = 0; + int use_cluster_slots = 1; + int send_to_all = 0; + + int argindex; + for (argindex = 1; argindex < argc && argv[argindex][0] == '-'; + argindex++) { + if (strcmp(argv[argindex], "--events") == 0) { + show_events = 1; + } else if (strcmp(argv[argindex], "--use-cluster-nodes") == 0) { + use_cluster_slots = 0; + } else { + fprintf(stderr, "Unknown argument: '%s'\n", argv[argindex]); + exit(1); + } + } + + if (argindex >= argc) { + fprintf(stderr, "Usage: clusterclient [--events] [--use-cluster-nodes] " + "HOST:PORT\n"); + exit(1); + } + const char *initnode = argv[argindex]; + + struct timeval timeout = {1, 500000}; // 1.5s + + redisClusterContext *cc = redisClusterContextInit(); + redisClusterSetOptionAddNodes(cc, initnode); + redisClusterSetOptionConnectTimeout(cc, timeout); + if (use_cluster_slots) { + redisClusterSetOptionRouteUseSlots(cc); + } + if (show_events) { + redisClusterSetEventCallback(cc, eventCallback, NULL); + } + + if (redisClusterConnect2(cc) != REDIS_OK) { + printf("Connect error: %s\n", cc->errstr); + exit(2); + } + + char command[256]; + while (fgets(command, 256, stdin)) { + size_t len = strlen(command); + if (command[len - 1] == '\n') // Chop trailing line break + command[len - 1] = '\0'; + + if (command[0] == '\0') /* Skip empty lines */ + continue; + if (command[0] == '#') /* Skip comments */ + continue; + if (command[0] == '!') { + if (strcmp(command, "!all") == 0) /* Enable send to all nodes */ + send_to_all = 1; + continue; + } + + if (send_to_all) { + nodeIterator ni; + initNodeIterator(&ni, cc); + + redisClusterNode *node; + while ((node = nodeNext(&ni)) != NULL) { + redisReply *reply = + redisClusterCommandToNode(cc, node, command); + if (!reply || cc->err) { + printf("error: %s\n", cc->errstr); + } else { + printReply(reply); + } + freeReplyObject(reply); + if (ni.route_version != cc->route_version) { + /* Updated slotmap resets the iterator. Abort iteration. */ + break; + } + } + } else { + redisReply *reply = redisClusterCommand(cc, command); + if (!reply || cc->err) { + printf("error: %s\n", cc->errstr); + } else { + printReply(reply); + } + freeReplyObject(reply); + } + } + + redisClusterFree(cc); + return 0; +} diff --git a/libvalkeycluster/tests/clusterclient_async.c b/libvalkeycluster/tests/clusterclient_async.c new file mode 100644 index 00000000..f6f5bd68 --- /dev/null +++ b/libvalkeycluster/tests/clusterclient_async.c @@ -0,0 +1,254 @@ +/* + * This program connects to a cluster and then reads commands from stdin, such + * as "SET foo bar", one per line and prints the results to stdout. + * + * The behaviour is the same as that of clusterclient.c, but the asynchronous + * API of the library is used rather than the synchronous API. + * The following action commands can alter the default behaviour: + * + * !async - Send multiple commands and then wait for their responses. + * Will send all following commands until EOF or the command `!sync` + * + * !sync - Send a single command and wait for its response before sending next + * command. This is the default behaviour. + * + * !resend - Resend a failed command from its reply callback. + * Will resend all following failed commands until EOF. + * + * !sleep - Sleep a second. Can be used to allow timers to timeout. + * Currently not supported while in !async mode. + * + * !all - Send each command to all nodes in the cluster. + * Will send following commands using the `..ToNode()` API and a + * cluster node iterator to send each command to all known nodes. + * + * An example input of first sending 2 commands and waiting for their responses, + * before sending a single command and waiting for its response: + * + * !async + * SET dual-1 command + * SET dual-2 command + * !sync + * SET single command + * + */ + +#include "adapters/libevent.h" +#include "hircluster.h" +#include "test_utils.h" +#include +#include +#include +#include + +#define CMD_SIZE 256 +#define HISTORY_DEPTH 16 + +char cmd_history[HISTORY_DEPTH][CMD_SIZE]; + +int num_running = 0; +int resend_failed_cmd = 0; +int send_to_all = 0; + +void sendNextCommand(int, short, void *); + +void printReply(const redisReply *reply) { + switch (reply->type) { + case REDIS_REPLY_ERROR: + case REDIS_REPLY_STATUS: + case REDIS_REPLY_STRING: + case REDIS_REPLY_VERB: + case REDIS_REPLY_BIGNUM: + printf("%s\n", reply->str); + break; + case REDIS_REPLY_INTEGER: + printf("%lld\n", reply->integer); + break; + default: + printf("Unhandled reply type: %d\n", reply->type); + } +} + +void replyCallback(redisClusterAsyncContext *acc, void *r, void *privdata) { + redisReply *reply = (redisReply *)r; + intptr_t cmd_id = (intptr_t)privdata; /* Id to corresponding cmd */ + + if (reply == NULL) { + if (acc->err) { + printf("error: %s\n", acc->errstr); + } else { + printf("unknown error\n"); + } + + if (resend_failed_cmd) { + printf("resend '%s'\n", cmd_history[cmd_id]); + if (redisClusterAsyncCommand(acc, replyCallback, (void *)cmd_id, + cmd_history[cmd_id]) != REDIS_OK) + printf("send error\n"); + return; + } + } else { + printReply(reply); + } + + if (--num_running == 0) { + /* Schedule a read from stdin and send next command */ + event_base_once(acc->adapter, -1, EV_TIMEOUT, sendNextCommand, acc, + NULL); + } +} + +void sendNextCommand(int fd, short kind, void *arg) { + UNUSED(fd); + UNUSED(kind); + redisClusterAsyncContext *acc = arg; + int async = 0; + + char cmd[CMD_SIZE]; + while (fgets(cmd, CMD_SIZE, stdin)) { + size_t len = strlen(cmd); + if (cmd[len - 1] == '\n') /* Chop trailing line break */ + cmd[len - 1] = '\0'; + + if (cmd[0] == '\0') /* Skip empty lines */ + continue; + if (cmd[0] == '#') /* Skip comments */ + continue; + if (cmd[0] == '!') { + if (strcmp(cmd, "!sleep") == 0) { + ASSERT_MSG(async == 0, "!sleep in !async not supported"); + struct timeval timeout = {1, 0}; + event_base_once(acc->adapter, -1, EV_TIMEOUT, sendNextCommand, + acc, &timeout); + return; + } + if (strcmp(cmd, "!async") == 0) /* Enable async send */ + async = 1; + if (strcmp(cmd, "!sync") == 0) { /* Disable async send */ + if (async) + return; /* We are done sending commands */ + } + if (strcmp(cmd, "!resend") == 0) /* Enable resend of failed cmd */ + resend_failed_cmd = 1; + if (strcmp(cmd, "!all") == 0) { /* Enable send to all nodes */ + ASSERT_MSG(resend_failed_cmd == 0, + "!all in !resend not supported"); + send_to_all = 1; + } + continue; /* Skip line */ + } + + /* Copy command string to history buffer */ + assert(num_running < HISTORY_DEPTH); + strcpy(cmd_history[num_running], cmd); + + if (send_to_all) { + nodeIterator ni; + initNodeIterator(&ni, acc->cc); + + redisClusterNode *node; + while ((node = nodeNext(&ni)) != NULL) { + int status = redisClusterAsyncCommandToNode( + acc, node, replyCallback, (void *)((intptr_t)num_running), + cmd); + ASSERT_MSG(status == REDIS_OK, acc->errstr); + num_running++; + } + } else { + int status = redisClusterAsyncCommand( + acc, replyCallback, (void *)((intptr_t)num_running), cmd); + if (status == REDIS_OK) { + num_running++; + } else { + printf("error: %s\n", acc->errstr); + + /* Schedule a read from stdin and handle next command. */ + event_base_once(acc->adapter, -1, EV_TIMEOUT, sendNextCommand, + acc, NULL); + } + } + + if (async) + continue; /* Send next command as well */ + + return; + } + + /* Disconnect if nothing is left to read from stdin */ + redisClusterAsyncDisconnect(acc); +} + +void eventCallback(const redisClusterContext *cc, int event, void *privdata) { + (void)cc; + (void)privdata; + char *e = NULL; + switch (event) { + case HIRCLUSTER_EVENT_SLOTMAP_UPDATED: + e = "slotmap-updated"; + break; + case HIRCLUSTER_EVENT_READY: + e = "ready"; + break; + case HIRCLUSTER_EVENT_FREE_CONTEXT: + e = "free-context"; + break; + default: + e = "unknown"; + } + printf("Event: %s\n", e); +} + +int main(int argc, char **argv) { + int use_cluster_slots = 1; // Get topology via CLUSTER SLOTS + int show_events = 0; + + int optind; + for (optind = 1; optind < argc && argv[optind][0] == '-'; optind++) { + if (strcmp(argv[optind], "--use-cluster-nodes") == 0) { + use_cluster_slots = 0; // Use the default CLUSTER NODES instead + } else if (strcmp(argv[optind], "--events") == 0) { + show_events = 1; + } else { + fprintf(stderr, "Unknown argument: '%s'\n", argv[optind]); + } + } + + if (optind >= argc) { + fprintf(stderr, + "Usage: clusterclient_async [--use-cluster-nodes] HOST:PORT\n"); + exit(1); + } + const char *initnode = argv[optind]; + struct timeval timeout = {0, 500000}; + + redisClusterAsyncContext *acc = redisClusterAsyncContextInit(); + assert(acc); + redisClusterSetOptionAddNodes(acc->cc, initnode); + redisClusterSetOptionTimeout(acc->cc, timeout); + redisClusterSetOptionConnectTimeout(acc->cc, timeout); + redisClusterSetOptionMaxRetry(acc->cc, 1); + if (use_cluster_slots) { + redisClusterSetOptionRouteUseSlots(acc->cc); + } + if (show_events) { + redisClusterSetEventCallback(acc->cc, eventCallback, NULL); + } + + if (redisClusterConnect2(acc->cc) != REDIS_OK) { + printf("Connect error: %s\n", acc->cc->errstr); + exit(2); + } + + struct event_base *base = event_base_new(); + int status = redisClusterLibeventAttach(acc, base); + assert(status == REDIS_OK); + + /* Schedule a read from stdin and send next command */ + event_base_once(acc->adapter, -1, EV_TIMEOUT, sendNextCommand, acc, NULL); + + event_base_dispatch(base); + + redisClusterAsyncFree(acc); + event_base_free(base); + return 0; +} diff --git a/libvalkeycluster/tests/clusterclient_reconnect_async.c b/libvalkeycluster/tests/clusterclient_reconnect_async.c new file mode 100644 index 00000000..dc852d94 --- /dev/null +++ b/libvalkeycluster/tests/clusterclient_reconnect_async.c @@ -0,0 +1,117 @@ +/* + * This program connects to a Redis node and then reads commands from stdin, such + * as "SET foo bar", one per line and prints the results to stdout. + * + * The behaviour is similar to that of clusterclient_async.c, but it sends the + * next command after receiving a reply from the previous command. It also works + * for standalone Redis nodes (without cluster mode), and uses the + * redisClusterAsyncCommandToNode function to send the command to the first node. + * If it receives any I/O error, the program performs a reconnect. + */ + +#include "adapters/libevent.h" +#include "hircluster.h" +#include "test_utils.h" +#include +#include +#include +#include + +/* Unfortunately there is no error code for this error to match */ +#define REDIS_ENOCLUSTER "ERR This instance has cluster support disabled" + +void sendNextCommand(int, short, void *); + +void connectToRedis(redisClusterAsyncContext *acc) { + /* reset Redis context in case of reconnect */ + redisClusterAsyncDisconnect(acc); + + int status = redisClusterConnect2(acc->cc); + if (status == REDIS_OK) { + // cluster mode + } else if (acc->cc->err && strcmp(acc->cc->errstr, REDIS_ENOCLUSTER) == 0) { + printf("[no cluster]\n"); + acc->cc->err = 0; + memset(acc->cc->errstr, '\0', strlen(acc->cc->errstr)); + } else { + printf("Connect error: %s\n", acc->cc->errstr); + exit(-1); + } +} + +void replyCallback(redisClusterAsyncContext *acc, void *r, void *privdata) { + UNUSED(privdata); + redisReply *reply = (redisReply *)r; + + if (reply == NULL) { + if (acc->err == REDIS_ERR_IO || acc->err == REDIS_ERR_EOF) { + printf("[reconnect]\n"); + connectToRedis(acc); + } else if (acc->err) { + printf("error: %s\n", acc->errstr); + } else { + printf("unknown error\n"); + } + } else { + printf("%s\n", reply->str); + } + + // schedule reading from stdin and sending next command + event_base_once(acc->adapter, -1, EV_TIMEOUT, sendNextCommand, acc, NULL); +} + +void sendNextCommand(int fd, short kind, void *arg) { + UNUSED(fd); + UNUSED(kind); + redisClusterAsyncContext *acc = arg; + + char command[256]; + if (fgets(command, 256, stdin)) { + size_t len = strlen(command); + if (command[len - 1] == '\n') // Chop trailing line break + command[len - 1] = '\0'; + + dictIterator di; + dictInitIterator(&di, acc->cc->nodes); + + dictEntry *de = dictNext(&di); + assert(de); + redisClusterNode *node = dictGetEntryVal(de); + assert(node); + + // coverity[tainted_scalar] + int status = redisClusterAsyncCommandToNode(acc, node, replyCallback, + NULL, command); + ASSERT_MSG(status == REDIS_OK, acc->errstr); + } else { + // disconnect if nothing is left to read from stdin + redisClusterAsyncDisconnect(acc); + } +} + +int main(int argc, char **argv) { + if (argc <= 1) { + fprintf(stderr, "Usage: %s HOST:PORT\n", argv[0]); + exit(1); + } + const char *initnode = argv[1]; + + redisClusterAsyncContext *acc = redisClusterAsyncContextInit(); + assert(acc); + redisClusterSetOptionAddNodes(acc->cc, initnode); + redisClusterSetOptionRouteUseSlots(acc->cc); + + struct event_base *base = event_base_new(); + int status = redisClusterLibeventAttach(acc, base); + assert(status == REDIS_OK); + + connectToRedis(acc); + // schedule reading from stdin and sending next command + event_base_once(acc->adapter, -1, EV_TIMEOUT, sendNextCommand, acc, NULL); + + event_base_dispatch(base); + + redisClusterAsyncFree(acc); + event_base_free(base); + return 0; +} diff --git a/libvalkeycluster/tests/ct_async.c b/libvalkeycluster/tests/ct_async.c new file mode 100644 index 00000000..008e5683 --- /dev/null +++ b/libvalkeycluster/tests/ct_async.c @@ -0,0 +1,110 @@ +#include "adapters/libevent.h" +#include "hircluster.h" +#include "test_utils.h" +#include +#include +#include + +#define CLUSTER_NODE "127.0.0.1:7000" + +void getCallback(redisClusterAsyncContext *acc, void *r, void *privdata) { + UNUSED(privdata); + redisReply *reply = (redisReply *)r; + ASSERT_MSG(reply != NULL, acc->errstr); + + /* Disconnect after receiving the first reply to GET */ + redisClusterAsyncDisconnect(acc); +} + +void setCallback(redisClusterAsyncContext *acc, void *r, void *privdata) { + UNUSED(privdata); + redisReply *reply = (redisReply *)r; + ASSERT_MSG(reply != NULL, acc->errstr); +} + +void connectCallback(const redisAsyncContext *ac, int status) { + ASSERT_MSG(status == REDIS_OK, ac->errstr); + printf("Connected to %s:%d\n", ac->c.tcp.host, ac->c.tcp.port); +} + +#ifndef HIRCLUSTER_NO_NONCONST_CONNECT_CB +void connectCallbackNC(redisAsyncContext *ac, int status) { + UNUSED(ac); + UNUSED(status); + /* The testcase expects a failure during registration of this + non-const connect callback and it should never be called. */ + assert(0); +} +#endif + +void disconnectCallback(const redisAsyncContext *ac, int status) { + ASSERT_MSG(status == REDIS_OK, ac->errstr); + printf("Disconnected from %s:%d\n", ac->c.tcp.host, ac->c.tcp.port); +} + +void eventCallback(const redisClusterContext *cc, int event, void *privdata) { + (void)cc; + redisClusterAsyncContext *acc = (redisClusterAsyncContext *)privdata; + + /* We send our commands when the client is ready to accept commands. */ + if (event == HIRCLUSTER_EVENT_READY) { + int status; + status = redisClusterAsyncCommand(acc, setCallback, (char *)"ID", + "SET key12345 value"); + ASSERT_MSG(status == REDIS_OK, acc->errstr); + + /* This command will trigger a disconnect in its reply callback. */ + status = redisClusterAsyncCommand(acc, getCallback, (char *)"ID", + "GET key12345"); + ASSERT_MSG(status == REDIS_OK, acc->errstr); + + status = redisClusterAsyncCommand(acc, setCallback, (char *)"ID", + "SET key23456 value2"); + ASSERT_MSG(status == REDIS_OK, acc->errstr); + + status = redisClusterAsyncCommand(acc, getCallback, (char *)"ID", + "GET key23456"); + ASSERT_MSG(status == REDIS_OK, acc->errstr); + } +} + +int main(void) { + + redisClusterAsyncContext *acc = redisClusterAsyncContextInit(); + assert(acc); + + int status; + status = redisClusterAsyncSetConnectCallback(acc, connectCallback); + assert(status == REDIS_OK); + status = redisClusterAsyncSetConnectCallback(acc, connectCallback); + assert(status == REDIS_ERR); /* Re-registration not accepted */ + +#ifndef HIRCLUSTER_NO_NONCONST_CONNECT_CB + status = redisClusterAsyncSetConnectCallbackNC(acc, connectCallbackNC); + assert(status == REDIS_ERR); /* Re-registration not accepted */ +#endif + + status = redisClusterAsyncSetDisconnectCallback(acc, disconnectCallback); + assert(status == REDIS_OK); + status = redisClusterSetEventCallback(acc->cc, eventCallback, acc); + assert(status == REDIS_OK); + status = redisClusterSetOptionAddNodes(acc->cc, CLUSTER_NODE); + assert(status == REDIS_OK); + + /* Expect error when connecting without an attached event library. */ + status = redisClusterAsyncConnect2(acc); + assert(status == REDIS_ERR); + + struct event_base *base = event_base_new(); + status = redisClusterLibeventAttach(acc, base); + assert(status == REDIS_OK); + + status = redisClusterAsyncConnect2(acc); + assert(status == REDIS_OK); + + event_base_dispatch(base); + + redisClusterAsyncFree(acc); + event_base_free(base); + return 0; +} diff --git a/libvalkeycluster/tests/ct_async_glib.c b/libvalkeycluster/tests/ct_async_glib.c new file mode 100644 index 00000000..6eed4a32 --- /dev/null +++ b/libvalkeycluster/tests/ct_async_glib.c @@ -0,0 +1,66 @@ +#include "adapters/glib.h" +#include "hircluster.h" +#include "test_utils.h" +#include + +#define CLUSTER_NODE "127.0.0.1:7000" + +static GMainLoop *mainloop; + +void setCallback(redisClusterAsyncContext *acc, void *r, void *privdata) { + UNUSED(privdata); + redisReply *reply = (redisReply *)r; + ASSERT_MSG(reply != NULL, acc->errstr); +} + +void getCallback(redisClusterAsyncContext *acc, void *r, void *privdata) { + UNUSED(privdata); + redisReply *reply = (redisReply *)r; + ASSERT_MSG(reply != NULL, acc->errstr); + + /* Disconnect after receiving the first reply to GET */ + redisClusterAsyncDisconnect(acc); + g_main_loop_quit(mainloop); +} + +void connectCallback(const redisAsyncContext *ac, int status) { + ASSERT_MSG(status == REDIS_OK, ac->errstr); + printf("Connected to %s:%d\n", ac->c.tcp.host, ac->c.tcp.port); +} + +void disconnectCallback(const redisAsyncContext *ac, int status) { + ASSERT_MSG(status == REDIS_OK, ac->errstr); + printf("Disconnected from %s:%d\n", ac->c.tcp.host, ac->c.tcp.port); +} + +int main(int argc, char **argv) { + UNUSED(argc); + UNUSED(argv); + + GMainContext *context = NULL; + mainloop = g_main_loop_new(context, FALSE); + + redisClusterAsyncContext *acc = + redisClusterAsyncConnect(CLUSTER_NODE, HIRCLUSTER_FLAG_NULL); + assert(acc); + ASSERT_MSG(acc->err == 0, acc->errstr); + + int status; + redisClusterGlibAdapter adapter = {.context = context}; + status = redisClusterGlibAttach(acc, &adapter); + assert(status == REDIS_OK); + + redisClusterAsyncSetConnectCallback(acc, connectCallback); + redisClusterAsyncSetDisconnectCallback(acc, disconnectCallback); + + status = redisClusterAsyncCommand(acc, setCallback, "id", "SET key value"); + ASSERT_MSG(status == REDIS_OK, acc->errstr); + + status = redisClusterAsyncCommand(acc, getCallback, "id", "GET key"); + ASSERT_MSG(status == REDIS_OK, acc->errstr); + + g_main_loop_run(mainloop); + + redisClusterAsyncFree(acc); + return 0; +} diff --git a/libvalkeycluster/tests/ct_async_libev.c b/libvalkeycluster/tests/ct_async_libev.c new file mode 100644 index 00000000..83cae20e --- /dev/null +++ b/libvalkeycluster/tests/ct_async_libev.c @@ -0,0 +1,61 @@ +#include "adapters/libev.h" +#include "hircluster.h" +#include "test_utils.h" +#include + +#define CLUSTER_NODE "127.0.0.1:7000" + +void setCallback(redisClusterAsyncContext *acc, void *r, void *privdata) { + UNUSED(privdata); + redisReply *reply = (redisReply *)r; + ASSERT_MSG(reply != NULL, acc->errstr); +} + +void getCallback(redisClusterAsyncContext *acc, void *r, void *privdata) { + UNUSED(privdata); + redisReply *reply = (redisReply *)r; + ASSERT_MSG(reply != NULL, acc->errstr); + + /* Disconnect after receiving the first reply to GET */ + redisClusterAsyncDisconnect(acc); +} + +void connectCallback(const redisAsyncContext *ac, int status) { + ASSERT_MSG(status == REDIS_OK, ac->errstr); + printf("Connected to %s:%d\n", ac->c.tcp.host, ac->c.tcp.port); +} + +void disconnectCallback(const redisAsyncContext *ac, int status) { + ASSERT_MSG(status == REDIS_OK, ac->errstr); + printf("Disconnected from %s:%d\n", ac->c.tcp.host, ac->c.tcp.port); +} + +int main(int argc, char **argv) { + UNUSED(argc); + UNUSED(argv); + + redisClusterAsyncContext *acc = + redisClusterAsyncConnect(CLUSTER_NODE, HIRCLUSTER_FLAG_NULL); + assert(acc); + ASSERT_MSG(acc->err == 0, acc->errstr); + + int status; + status = redisClusterLibevAttach(acc, EV_DEFAULT); + assert(status == REDIS_OK); + + redisClusterAsyncSetConnectCallback(acc, connectCallback); + redisClusterAsyncSetDisconnectCallback(acc, disconnectCallback); + + status = redisClusterAsyncCommand(acc, setCallback, (char *)"ID", + "SET key value"); + ASSERT_MSG(status == REDIS_OK, acc->errstr); + + status = + redisClusterAsyncCommand(acc, getCallback, (char *)"ID", "GET key"); + ASSERT_MSG(status == REDIS_OK, acc->errstr); + + ev_loop(EV_DEFAULT_ 0); + + redisClusterAsyncFree(acc); + return 0; +} diff --git a/libvalkeycluster/tests/ct_async_libuv.c b/libvalkeycluster/tests/ct_async_libuv.c new file mode 100644 index 00000000..3ba8b6e7 --- /dev/null +++ b/libvalkeycluster/tests/ct_async_libuv.c @@ -0,0 +1,63 @@ +#include "adapters/libuv.h" +#include "hircluster.h" +#include "test_utils.h" +#include + +#define CLUSTER_NODE "127.0.0.1:7000" + +void setCallback(redisClusterAsyncContext *acc, void *r, void *privdata) { + UNUSED(privdata); + redisReply *reply = (redisReply *)r; + ASSERT_MSG(reply != NULL, acc->errstr); +} + +void getCallback(redisClusterAsyncContext *acc, void *r, void *privdata) { + UNUSED(privdata); + redisReply *reply = (redisReply *)r; + ASSERT_MSG(reply != NULL, acc->errstr); + + /* Disconnect after receiving the first reply to GET */ + redisClusterAsyncDisconnect(acc); +} + +void connectCallback(const redisAsyncContext *ac, int status) { + ASSERT_MSG(status == REDIS_OK, ac->errstr); + printf("Connected to %s:%d\n", ac->c.tcp.host, ac->c.tcp.port); +} + +void disconnectCallback(const redisAsyncContext *ac, int status) { + ASSERT_MSG(status == REDIS_OK, ac->errstr); + printf("Disconnected from %s:%d\n", ac->c.tcp.host, ac->c.tcp.port); +} + +int main(int argc, char **argv) { + UNUSED(argc); + UNUSED(argv); + + redisClusterAsyncContext *acc = + redisClusterAsyncConnect(CLUSTER_NODE, HIRCLUSTER_FLAG_NULL); + assert(acc); + ASSERT_MSG(acc->err == 0, acc->errstr); + + int status; + uv_loop_t *loop = uv_default_loop(); + status = redisClusterLibuvAttach(acc, loop); + assert(status == REDIS_OK); + + redisClusterAsyncSetConnectCallback(acc, connectCallback); + redisClusterAsyncSetDisconnectCallback(acc, disconnectCallback); + + status = redisClusterAsyncCommand(acc, setCallback, (char *)"ID", + "SET key value"); + ASSERT_MSG(status == REDIS_OK, acc->errstr); + + status = + redisClusterAsyncCommand(acc, getCallback, (char *)"ID", "GET key"); + ASSERT_MSG(status == REDIS_OK, acc->errstr); + + uv_run(loop, UV_RUN_DEFAULT); + + redisClusterAsyncFree(acc); + uv_loop_delete(loop); + return 0; +} diff --git a/libvalkeycluster/tests/ct_commands.c b/libvalkeycluster/tests/ct_commands.c new file mode 100644 index 00000000..44607950 --- /dev/null +++ b/libvalkeycluster/tests/ct_commands.c @@ -0,0 +1,495 @@ +#include "hircluster.h" +#include "test_utils.h" +#include "win32.h" +#include +#include +#include +#include + +#define CLUSTER_NODE "127.0.0.1:7000" + +void test_exists(redisClusterContext *cc) { + redisReply *reply; + reply = (redisReply *)redisClusterCommand(cc, "SET key1 Hello"); + CHECK_REPLY_OK(cc, reply); + freeReplyObject(reply); + + reply = (redisReply *)redisClusterCommand(cc, "EXISTS key1"); + CHECK_REPLY_INT(cc, reply, 1); + freeReplyObject(reply); + + reply = (redisReply *)redisClusterCommand(cc, "EXISTS nosuchkey"); + CHECK_REPLY_INT(cc, reply, 0); + freeReplyObject(reply); + + reply = (redisReply *)redisClusterCommand(cc, "SET key2 World"); + CHECK_REPLY_OK(cc, reply); + freeReplyObject(reply); + + reply = (redisReply *)redisClusterCommand(cc, "EXISTS key1 key2 nosuchkey"); + CHECK_REPLY_INT(cc, reply, 2); + freeReplyObject(reply); +} + +void test_bitfield(redisClusterContext *cc) { + redisReply *reply; + + reply = (redisReply *)redisClusterCommand( + cc, "BITFIELD bkey1 SET u32 #0 255 GET u32 #0"); + CHECK_REPLY_ARRAY(cc, reply, 2); + CHECK_REPLY_INT(cc, reply->element[1], 255); + freeReplyObject(reply); +} + +void test_bitfield_ro(redisClusterContext *cc) { + if (redis_version_less_than(6, 0)) + return; /* Skip test, command not available. */ + + redisReply *reply; + + reply = (redisReply *)redisClusterCommand(cc, "SET bkey2 a"); // 97 + CHECK_REPLY_OK(cc, reply); + freeReplyObject(reply); + + reply = + (redisReply *)redisClusterCommand(cc, "BITFIELD_RO bkey2 GET u8 #0"); + CHECK_REPLY_ARRAY(cc, reply, 1); + CHECK_REPLY_INT(cc, reply->element[0], 97); + freeReplyObject(reply); +} + +void test_mset(redisClusterContext *cc) { + redisReply *reply; + reply = (redisReply *)redisClusterCommand( + cc, "MSET key1 mset1 key2 mset2 key3 mset3"); + CHECK_REPLY_OK(cc, reply); + freeReplyObject(reply); + + reply = (redisReply *)redisClusterCommand(cc, "GET key1"); + CHECK_REPLY_STR(cc, reply, "mset1"); + freeReplyObject(reply); + + reply = (redisReply *)redisClusterCommand(cc, "GET key2"); + CHECK_REPLY_STR(cc, reply, "mset2"); + freeReplyObject(reply); + + reply = (redisReply *)redisClusterCommand(cc, "GET key3"); + CHECK_REPLY_STR(cc, reply, "mset3"); + freeReplyObject(reply); +} + +void test_mget(redisClusterContext *cc) { + redisReply *reply; + reply = (redisReply *)redisClusterCommand(cc, "SET key1 mget1"); + CHECK_REPLY_OK(cc, reply); + freeReplyObject(reply); + + reply = (redisReply *)redisClusterCommand(cc, "SET key2 mget2"); + CHECK_REPLY_OK(cc, reply); + freeReplyObject(reply); + + reply = (redisReply *)redisClusterCommand(cc, "SET key3 mget3"); + CHECK_REPLY_OK(cc, reply); + freeReplyObject(reply); + + reply = (redisReply *)redisClusterCommand(cc, "MGET key1 key2 key3"); + CHECK_REPLY_ARRAY(cc, reply, 3); + CHECK_REPLY_STR(cc, reply->element[0], "mget1"); + CHECK_REPLY_STR(cc, reply->element[1], "mget2"); + CHECK_REPLY_STR(cc, reply->element[2], "mget3"); + freeReplyObject(reply); +} + +void test_hset_hget_hdel_hexists(redisClusterContext *cc) { + redisReply *reply; + + // Prepare + reply = (redisReply *)redisClusterCommand(cc, "HDEL myhash field1"); + CHECK_REPLY(cc, reply); + freeReplyObject(reply); + reply = (redisReply *)redisClusterCommand(cc, "HDEL myhash field2"); + CHECK_REPLY(cc, reply); + freeReplyObject(reply); + + // Set hash field + reply = + (redisReply *)redisClusterCommand(cc, "HSET myhash field1 hsetvalue"); + CHECK_REPLY_INT(cc, reply, 1); // Set 1 field + freeReplyObject(reply); + + // Set second hash field + reply = + (redisReply *)redisClusterCommand(cc, "HSET myhash field3 hsetvalue3"); + CHECK_REPLY_INT(cc, reply, 1); // Set 1 field + freeReplyObject(reply); + + // Get field value + reply = (redisReply *)redisClusterCommand(cc, "HGET myhash field1"); + CHECK_REPLY_STR(cc, reply, "hsetvalue"); + freeReplyObject(reply); + + // Get field that is not present + reply = (redisReply *)redisClusterCommand(cc, "HGET myhash field2"); + CHECK_REPLY_NIL(cc, reply); + freeReplyObject(reply); + + // Delete a field + reply = (redisReply *)redisClusterCommand(cc, "HDEL myhash field1"); + CHECK_REPLY_INT(cc, reply, 1); // Delete 1 field + freeReplyObject(reply); + + // Delete a field that is not present + reply = (redisReply *)redisClusterCommand(cc, "HDEL myhash field2"); + CHECK_REPLY_INT(cc, reply, 0); // Nothing to delete + freeReplyObject(reply); + + // Check if field exists + reply = (redisReply *)redisClusterCommand(cc, "HEXISTS myhash field3"); + CHECK_REPLY_INT(cc, reply, 1); // exists + freeReplyObject(reply); + + // Delete multiple fields at once + reply = (redisReply *)redisClusterCommand( + cc, "HDEL myhash field1 field2 field3"); + CHECK_REPLY_INT(cc, reply, 1); // field3 deleted + freeReplyObject(reply); + + // Make sure field3 is deleted now + reply = (redisReply *)redisClusterCommand(cc, "HEXISTS myhash field3"); + CHECK_REPLY_INT(cc, reply, 0); // no field + freeReplyObject(reply); + + // Set multiple fields at once + reply = (redisReply *)redisClusterCommand( + cc, "HSET myhash field1 hsetvalue1 field2 hsetvalue2"); + CHECK_REPLY_INT(cc, reply, 2); + freeReplyObject(reply); +} + +// Command layout: +// eval