diff --git a/.cmake-format.yaml b/.cmake-format.yaml new file mode 100644 index 0000000000..98ab11753a --- /dev/null +++ b/.cmake-format.yaml @@ -0,0 +1,76 @@ +format: + _help_line_width: + - How wide to allow formatted cmake files + line_width: 120 + _help_tab_size: + - How many spaces to tab for indent + tab_size: 4 + _help_use_tabchars: + - If true, lines are indented using tab characters (utf-8 + - 0x09) instead of space characters (utf-8 0x20). + - In cases where the layout would require a fractional tab + - character, the behavior of the fractional indentation is + - governed by + use_tabchars: false + _help_separate_ctrl_name_with_space: + - If true, separate flow control names from their parentheses + - with a space + separate_ctrl_name_with_space: true + _help_min_prefix_chars: + - If the statement spelling length (including space and + - parenthesis) is smaller than this amount, then force reject + - nested layouts. + min_prefix_chars: 4 + _help_max_prefix_chars: + - If the statement spelling length (including space and + - parenthesis) is larger than the tab width by more than this + - amount, then force reject un-nested layouts. + max_prefix_chars: 10 + _help_max_lines_hwrap: + - If a candidate layout is wrapped horizontally but it exceeds + - this many lines, then reject the layout. + max_lines_hwrap: 2 + _help_line_ending: + - What style line endings to use in the output. + line_ending: unix + _help_command_case: + - Format command names consistently as 'lower' or 'upper' case + command_case: lower + _help_keyword_case: + - Format keywords consistently as 'lower' or 'upper' case + keyword_case: unchanged + _help_always_wrap: + - A list of command names which should always be wrapped + always_wrap: [] + _help_enable_sort: + - If true, the argument lists which are known to be sortable + - will be sorted lexicographicall + enable_sort: true + _help_autosort: + - If true, the parsers may infer whether or not an argument + - list is sortable (without annotation). + autosort: false + _help_require_valid_layout: + - By default, if cmake-format cannot successfully fit + - everything into the desired linewidth it will apply the + - last, most agressive attempt that it made. If this flag is + - True, however, cmake-format will print error, exit with non- + - zero status code, and write-out nothing + require_valid_layout: false + _help_layout_passes: + - A dictionary mapping layout nodes to a list of wrap + - decisions. See the documentation for more information. + layout_passes: {} +encode: + _help_emit_byteorder_mark: + - If true, emit the unicode byte-order mark (BOM) at the start + - of the file + emit_byteorder_mark: false + _help_input_encoding: + - Specify the encoding of the input file. Defaults to utf-8 + input_encoding: utf-8 + _help_output_encoding: + - Specify the encoding of the output file. Defaults to utf-8. + - Note that cmake only claims to support utf-8 so be careful + - when using anything else + output_encoding: utf-8 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 48a94ef984..bc946b7193 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,31 @@ jobs: run: | ./src/valkey-unit-tests + test-ubuntu-latest-cmake: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: cmake and make + run: | + sudo apt-get install -y cmake libssl-dev + mkdir -p build-release + cd build-release + cmake -DCMAKE_BUILD_TYPE=Release .. -DBUILD_TLS=yes -DBUILD_UNIT_TESTS=yes + make -j$(nproc) + - name: test + run: | + sudo apt-get install -y tcl8.6 tclx + ln -sf $(pwd)/build-release/bin/valkey-server $(pwd)/src/valkey-server + ln -sf $(pwd)/build-release/bin/valkey-cli $(pwd)/src/valkey-cli + ln -sf $(pwd)/build-release/bin/valkey-benchmark $(pwd)/src/valkey-benchmark + ln -sf $(pwd)/build-release/bin/valkey-server $(pwd)/src/valkey-check-aof + ln -sf $(pwd)/build-release/bin/valkey-server $(pwd)/src/valkey-check-rdb + ln -sf $(pwd)/build-release/bin/valkey-server $(pwd)/src/valkey-sentinel + ./runtest --verbose --tags -slow --dump-logs + - name: unit tests + run: | + ./build-release/bin/valkey-unit-tests + test-sanitizer-address: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index bcfa35c939..8bdbc8d4c2 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -60,7 +60,7 @@ jobs: repository: ${{ env.GITHUB_REPOSITORY }} ref: ${{ env.GITHUB_HEAD_REF }} - name: make - run: make all-with-unit-tests SERVER_CFLAGS='-Werror -DSERVER_TEST' + run: make all-with-unit-tests SERVER_CFLAGS='-Werror' - name: testprep run: sudo apt-get install tcl8.6 tclx - name: test @@ -75,10 +75,7 @@ jobs: - name: cluster tests if: true && !contains(github.event.inputs.skiptests, 'cluster') run: ./runtest-cluster ${{github.event.inputs.cluster_test_args}} - - name: legacy unit tests - if: true && !contains(github.event.inputs.skiptests, 'unittest') - run: ./src/valkey-server test all --accurate - - name: new unit tests + - name: unittest if: true && !contains(github.event.inputs.skiptests, 'unittest') run: ./src/valkey-unit-tests --accurate @@ -109,7 +106,7 @@ jobs: run: | apt-get update && apt-get install -y make gcc-13 update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 100 - make all-with-unit-tests CC=gcc OPT=-O3 SERVER_CFLAGS='-Werror -DSERVER_TEST -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3' + make all-with-unit-tests CC=gcc OPT=-O3 SERVER_CFLAGS='-Werror -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3' - name: testprep run: apt-get install -y tcl8.6 tclx procps - name: test @@ -124,10 +121,7 @@ jobs: - name: cluster tests if: true && !contains(github.event.inputs.skiptests, 'cluster') run: ./runtest-cluster ${{github.event.inputs.cluster_test_args}} - - name: legacy unit tests - if: true && !contains(github.event.inputs.skiptests, 'unittest') - run: ./src/valkey-server test all --accurate - - name: new unit tests + - name: unittest if: true && !contains(github.event.inputs.skiptests, 'unittest') run: ./src/valkey-unit-tests --accurate @@ -234,7 +228,7 @@ jobs: - name: make run: | sudo apt-get update && sudo apt-get install libc6-dev-i386 - make 32bit SERVER_CFLAGS='-Werror -DSERVER_TEST' + make 32bit SERVER_CFLAGS='-Werror' - name: testprep run: sudo apt-get install tcl8.6 tclx - name: test @@ -251,10 +245,7 @@ jobs: - name: cluster tests if: true && !contains(github.event.inputs.skiptests, 'cluster') run: ./runtest-cluster ${{github.event.inputs.cluster_test_args}} - - name: legacy unit tests - if: true && !contains(github.event.inputs.skiptests, 'unittest') - run: ./src/valkey-server test all --accurate - - name: new unit tests + - name: unittest if: true && !contains(github.event.inputs.skiptests, 'unittest') run: ./src/valkey-unit-tests --accurate @@ -483,7 +474,7 @@ jobs: repository: ${{ env.GITHUB_REPOSITORY }} ref: ${{ env.GITHUB_HEAD_REF }} - name: make - run: make valgrind SERVER_CFLAGS='-Werror -DSERVER_TEST' + run: make valgrind SERVER_CFLAGS='-Werror' - name: testprep run: | sudo apt-get update @@ -515,7 +506,7 @@ jobs: repository: ${{ env.GITHUB_REPOSITORY }} ref: ${{ env.GITHUB_HEAD_REF }} - name: make - run: make valgrind SERVER_CFLAGS='-Werror -DSERVER_TEST' + run: make valgrind all-with-unit-tests SERVER_CFLAGS='-Werror' - name: testprep run: | sudo apt-get update @@ -526,7 +517,7 @@ jobs: - name: unittest if: true && !contains(github.event.inputs.skiptests, 'unittest') run: | - valgrind --track-origins=yes --suppressions=./src/valgrind.sup --show-reachable=no --show-possibly-lost=no --leak-check=full --log-file=err.txt ./src/valkey-server test all --valgrind + valgrind --track-origins=yes --suppressions=./src/valgrind.sup --show-reachable=no --show-possibly-lost=no --leak-check=full --log-file=err.txt ./src/valkey-unit-tests --valgrind if grep -q 0x err.txt; then cat err.txt; exit 1; fi test-valgrind-no-malloc-usable-size-test: @@ -552,7 +543,7 @@ jobs: repository: ${{ env.GITHUB_REPOSITORY }} ref: ${{ env.GITHUB_HEAD_REF }} - name: make - run: make valgrind CFLAGS="-DNO_MALLOC_USABLE_SIZE -DSERVER_TEST" SERVER_CFLAGS='-Werror' + run: make valgrind CFLAGS="-DNO_MALLOC_USABLE_SIZE" SERVER_CFLAGS='-Werror' - name: testprep run: | sudo apt-get update @@ -584,7 +575,7 @@ jobs: repository: ${{ env.GITHUB_REPOSITORY }} ref: ${{ env.GITHUB_HEAD_REF }} - name: make - run: make valgrind CFLAGS="-DNO_MALLOC_USABLE_SIZE -DSERVER_TEST" SERVER_CFLAGS='-Werror' + run: make valgrind all-with-unit-tests CFLAGS="-DNO_MALLOC_USABLE_SIZE" SERVER_CFLAGS='-Werror' - name: testprep run: | sudo apt-get update @@ -595,7 +586,7 @@ jobs: - name: unittest if: true && !contains(github.event.inputs.skiptests, 'unittest') run: | - valgrind --track-origins=yes --suppressions=./src/valgrind.sup --show-reachable=no --show-possibly-lost=no --leak-check=full --log-file=err.txt ./src/valkey-server test all --valgrind + valgrind --track-origins=yes --suppressions=./src/valgrind.sup --show-reachable=no --show-possibly-lost=no --leak-check=full --log-file=err.txt ./src/valkey-unit-tests --valgrind if grep -q 0x err.txt; then cat err.txt; exit 1; fi test-sanitizer-address: @@ -627,7 +618,7 @@ jobs: repository: ${{ env.GITHUB_REPOSITORY }} ref: ${{ env.GITHUB_HEAD_REF }} - name: make - run: make all-with-unit-tests OPT=-O3 SANITIZER=address SERVER_CFLAGS='-DSERVER_TEST -Werror' + run: make all-with-unit-tests OPT=-O3 SANITIZER=address SERVER_CFLAGS='-Werror' - name: testprep run: | sudo apt-get update @@ -644,10 +635,7 @@ jobs: - name: cluster tests if: true && !contains(github.event.inputs.skiptests, 'cluster') run: ./runtest-cluster ${{github.event.inputs.cluster_test_args}} - - name: legacy unit tests - if: true && !contains(github.event.inputs.skiptests, 'unittest') - run: ./src/valkey-server test all - - name: new unit tests + - name: unittest if: true && !contains(github.event.inputs.skiptests, 'unittest') run: ./src/valkey-unit-tests @@ -680,7 +668,7 @@ jobs: repository: ${{ env.GITHUB_REPOSITORY }} ref: ${{ env.GITHUB_HEAD_REF }} - name: make - run: make all-with-unit-tests OPT=-O3 SANITIZER=undefined SERVER_CFLAGS='-DSERVER_TEST -Werror' LUA_DEBUG=yes # we (ab)use this flow to also check Lua C API violations + run: make all-with-unit-tests OPT=-O3 SANITIZER=undefined SERVER_CFLAGS='-Werror' LUA_DEBUG=yes # we (ab)use this flow to also check Lua C API violations - name: testprep run: | sudo apt-get update @@ -697,10 +685,7 @@ jobs: - name: cluster tests if: true && !contains(github.event.inputs.skiptests, 'cluster') run: ./runtest-cluster ${{github.event.inputs.cluster_test_args}} - - name: legacy unit tests - if: true && !contains(github.event.inputs.skiptests, 'unittest') - run: ./src/valkey-server test all --accurate - - name: new unit tests + - name: unittest if: true && !contains(github.event.inputs.skiptests, 'unittest') run: ./src/valkey-unit-tests --accurate @@ -1005,7 +990,7 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-12, macos-14] + os: [macos-13, macos-14] runs-on: ${{ matrix.os }} if: | (github.event_name == 'workflow_dispatch' || @@ -1031,10 +1016,10 @@ jobs: repository: ${{ env.GITHUB_REPOSITORY }} ref: ${{ env.GITHUB_HEAD_REF }} - name: make - run: make SERVER_CFLAGS='-Werror -DSERVER_TEST' + run: make SERVER_CFLAGS='-Werror' test-freebsd: - runs-on: macos-12 + runs-on: macos-13 if: | (github.event_name == 'workflow_dispatch' || (github.event_name == 'schedule' && github.repository == 'valkey-io/valkey') || diff --git a/.gitignore b/.gitignore index e448e23f7e..b108b4bb92 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,5 @@ nodes*.conf tests/cluster/tmp/* tests/rdma/rdma-test tags +build-debug/ +build-release/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000000..ad0bab8896 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,43 @@ +cmake_minimum_required(VERSION 3.20) + +# Must be done first +if (APPLE) + # Force clang compiler on macOS + find_program(CLANGPP "clang++") + find_program(CLANG "clang") + if (CLANG AND CLANGPP) + message(STATUS "Found ${CLANGPP}, ${CLANG}") + set(CMAKE_CXX_COMPILER ${CLANGPP}) + set(CMAKE_C_COMPILER ${CLANG}) + endif () +endif () + +# Options +option(BUILD_UNIT_TESTS "Build valkey-unit-tests" OFF) +option(BUILD_TEST_MODULES "Build all test modules" OFF) +option(BUILD_EXAMPLE_MODULES "Build example modules" OFF) + +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/Modules/") +project("valkey") + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS ON) + +include(ValkeySetup) +add_subdirectory(src) +add_subdirectory(tests) + +# Include the packaging module +include(Packaging) + +# Clear cached variables from the cache +unset(BUILD_TESTS CACHE) +unset(CLANGPP CACHE) +unset(CLANG CACHE) +unset(BUILD_RDMA_MODULE CACHE) +unset(BUILD_TLS_MODULE CACHE) +unset(BUILD_UNIT_TESTS CACHE) +unset(BUILD_TEST_MODULES CACHE) +unset(BUILD_EXAMPLE_MODULES CACHE) +unset(USE_TLS CACHE) diff --git a/README.md b/README.md index 1a8ce1a4db..94f38bccf7 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,12 @@ This project was forked from the open source Redis project right before the tran This README is just a fast *quick start* document. More details can be found under [valkey.io](https://valkey.io/) -What is Valkey? --------------- +# What is Valkey? + Valkey is a high-performance data structure server that primarily serves key/value workloads. It supports a wide range of native structures and an extensible plugin system for adding new data structures and access patterns. -Building Valkey --------------- +# Building Valkey using `Makefile` Valkey can be compiled and used on Linux, OSX, OpenBSD, NetBSD, FreeBSD. We support big endian and little endian architectures, and both 32 bit @@ -43,7 +42,7 @@ supports RDMA as connection module mode. Run: % make BUILD_RDMA=module -To build with systemd support, you'll need systemd development libraries (such +To build with systemd support, you'll need systemd development libraries (such as libsystemd-dev on Debian/Ubuntu or systemd-devel on CentOS) and run: % make USE_SYSTEMD=yes @@ -71,8 +70,7 @@ More about running the integration tests can be found in [tests/README.md](tests/README.md) and for unit tests, see [src/unit/README.md](src/unit/README.md). -Fixing build problems with dependencies or cached build options ---------- +## Fixing build problems with dependencies or cached build options Valkey has some dependencies which are included in the `deps` directory. `make` does not automatically rebuild dependencies even if something in @@ -91,8 +89,7 @@ optimizations (for debugging purposes), and other similar build time options, those options are cached indefinitely until you issue a `make distclean` command. -Fixing problems building 32 bit binaries ---------- +## Fixing problems building 32 bit binaries If after building Valkey with a 32 bit target you need to rebuild it with a 64 bit target, or the other way around, you need to perform a @@ -105,8 +102,7 @@ the following steps: * Try using the following command line instead of `make 32bit`: `make CFLAGS="-m32 -march=native" LDFLAGS="-m32"` -Allocator ---------- +## Allocator Selecting a non-default memory allocator when building Valkey is done by setting the `MALLOC` environment variable. Valkey is compiled and linked against libc @@ -122,28 +118,25 @@ To compile against jemalloc on Mac OS X systems, use: % make MALLOC=jemalloc -Monotonic clock ---------------- +## Monotonic clock By default, Valkey will build using the POSIX clock_gettime function as the monotonic clock source. On most modern systems, the internal processor clock -can be used to improve performance. Cautions can be found here: +can be used to improve performance. Cautions can be found here: http://oliveryang.net/2015/09/pitfalls-of-TSC-usage/ To build with support for the processor's internal instruction clock, use: % make CFLAGS="-DUSE_PROCESSOR_CLOCK" -Verbose build -------------- +## Verbose build Valkey will build with a user-friendly colorized output by default. If you want to see a more verbose output, use the following: % make V=1 -Running Valkey -------------- +# Running Valkey To run Valkey with the default configuration, just type: @@ -165,10 +158,10 @@ as options using the command line. Examples: All the options in valkey.conf are also supported as options using the command line, with exactly the same name. -Running Valkey with TLS: ------------------- +# Running Valkey with TLS: + +## Running manually -### Running manually To manually run a Valkey server with TLS mode (assuming `./gen-test-certs.sh` was invoked so sample certificates/keys are available): * TLS built-in mode: @@ -204,8 +197,7 @@ Specifying `--tls-replication yes` makes a replica connect to the primary. Using `--tls-cluster yes` makes Valkey Cluster use TLS across nodes. -Running Valkey with RDMA: ------------------- +# Running Valkey with RDMA: Note that Valkey Over RDMA is an experimental feature. It may be changed or removed in any minor or major version. @@ -236,8 +228,7 @@ Or: % ibv_devices -Playing with Valkey ------------------- +# Playing with Valkey You can use valkey-cli to play with Valkey. Start a valkey-server instance, then in another terminal try the following: @@ -256,8 +247,7 @@ then in another terminal try the following: (integer) 2 valkey> -Installing Valkey ------------------ +# Installing Valkey In order to install Valkey binaries into /usr/local/bin, just use: @@ -289,16 +279,82 @@ system reboots. You'll be able to stop and start Valkey using the script named `/etc/init.d/valkey_`, for instance `/etc/init.d/valkey_6379`. -Code contributions ------------------ +# Building using `CMake` + +In addition to the traditional `Makefile` build, Valkey supports an alternative, **experimental**, build system using `CMake`. + +To build and install `Valkey`, in `Release` mode (an optimized build), type this into your terminal: + +```bash +mkdir build-release +cd $_ +cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/opt/valkey +sudo make install +# Valkey is now installed under /opt/valkey +``` + +Other options supported by Valkey's `CMake` build system: + +## Special build flags + +- `-DBUILD_TLS=` enable TLS build for Valkey +- `-DBUILD_RDMA=` enable RDMA module build (only module mode supported) +- `-DBUILD_MALLOC=` choose the allocator to use. Default on Linux: `jemalloc`, for other OS: `libc` +- `-DBUILD_SANITIZER=` build with address sanitizer enabled +- `-DBUILD_UNIT_TESTS=[1|0]` when set, the build will produce the executable `valkey-unit-tests` +- `-DBUILD_TEST_MODULES=[1|0]` when set, the build will include the modules located under the `tests/modules` folder +- `-DBUILD_EXAMPLE_MODULES=[1|0]` when set, the build will include the example modules located under the `src/modules` folder + +## Common flags + +- `-DCMAKE_BUILD_TYPE=` define the build type, see CMake manual for more details +- `-DCMAKE_INSTALL_PREFIX=/installation/path` override this value to define a custom install prefix. Default: `/usr/local` +- `-G` generate build files for "Generator Name". By default, CMake will generate `Makefile`s. + +## Verbose build + +`CMake` generates a user-friendly colorized output by default. +If you want to see a more verbose output, use the following: + +```bash +make VERBOSE=1 +``` + +## Troubleshooting + +During the `CMake` stage, `CMake` caches variables in a local file named `CMakeCache.txt`. All variables generated by Valkey +are removed from the cache once consumed (this is done by calling to `unset(VAR-NAME CACHE)`). However, some variables, +like the compiler path, are kept in cache. To start a fresh build either remove the cache file `CMakeCache.txt` from the +build folder, or delete the build folder completely. + +**It is important to re-run `CMake` when adding new source files.** + +## Integration with IDE + +During the `CMake` stage of the build, `CMake` generates a JSON file named `compile_commands.json` and places it under the +build folder. This file is used by many IDEs and text editors for providing code completion (via `clangd`). + +A small caveat is that these tools will look for `compile_commands.json` under the Valkey's top folder. +A common workaround is to create a symbolic link to it: + +```bash +cd /path/to/valkey/ +# We assume here that your build folder is `build-release` +ln -sf $(pwd)/build-release/compile_commands.json $(pwd)/compile_commands.json +``` + +Restart your IDE and voila + +# Code contributions + Please see the [CONTRIBUTING.md][2]. For security bugs and vulnerabilities, please see [SECURITY.md][3]. -[1]: https://github.com/valkey-io/valkey/blob/unstable/COPYING -[2]: https://github.com/valkey-io/valkey/blob/unstable/CONTRIBUTING.md -[3]: https://github.com/valkey-io/valkey/blob/unstable/SECURITY.md +# Valkey is an open community project under LF Projects -Valkey is an open community project under LF Projects ------------------ Valkey a Series of LF Projects, LLC 2810 N Church St, PMB 57274 Wilmington, Delaware 19802-4447 + +[1]: https://github.com/valkey-io/valkey/blob/unstable/COPYING +[2]: https://github.com/valkey-io/valkey/blob/unstable/CONTRIBUTING.md +[3]: https://github.com/valkey-io/valkey/blob/unstable/SECURITY.md diff --git a/cmake/Modules/Packaging.cmake b/cmake/Modules/Packaging.cmake new file mode 100644 index 0000000000..c7ed5c426b --- /dev/null +++ b/cmake/Modules/Packaging.cmake @@ -0,0 +1,44 @@ +set(CPACK_PACKAGE_NAME "valkey") + +valkey_parse_version(CPACK_PACKAGE_VERSION_MAJOR CPACK_PACKAGE_VERSION_MINOR CPACK_PACKAGE_VERSION_PATCH) + +set(CPACK_PACKAGE_CONTACT "maintainers@lists.valkey.io") +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Valkey is an open source (BSD) high-performance key/value datastore") +set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/COPYING") +set(CPACK_RESOURCE_FILE_README "${CMAKE_SOURCE_DIR}/README.md") +set(CPACK_STRIP_FILES TRUE) + +valkey_get_distro_name(DISTRO_NAME) +message(STATUS "Current host distro: ${DISTRO_NAME}") + +if (DISTRO_NAME MATCHES ubuntu + OR DISTRO_NAME MATCHES debian + OR DISTRO_NAME MATCHES mint) + message(STATUS "Adding target package for ${DISTRO_NAME}") + set(CPACK_PACKAGING_INSTALL_PREFIX "/opt/valkey") + # Debian related parameters + set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Valkey contributors") + set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) + set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT) + set(CPACK_GENERATOR "DEB") +endif () + +include(CPack) +unset(DISTRO_NAME CACHE) + +# --------------------------------------------------- +# Create a helper script for creating symbolic links +# --------------------------------------------------- +write_file( + ${CMAKE_BINARY_DIR}/CreateSymlink.sh + "\ +#!/bin/bash \n\ +if [ -z \${DESTDIR} ]; then \n\ + # Script is called during 'make install' \n\ + PREFIX=${CMAKE_INSTALL_PREFIX}/bin \n\ +else \n\ + # Script is called during 'make package' \n\ + PREFIX=\${DESTDIR}${CPACK_PACKAGING_INSTALL_PREFIX}/bin \n\ +fi \n\ +cd \$PREFIX \n\ +ln -sf \$1 \$2") diff --git a/cmake/Modules/SourceFiles.cmake b/cmake/Modules/SourceFiles.cmake new file mode 100644 index 0000000000..d76f17625e --- /dev/null +++ b/cmake/Modules/SourceFiles.cmake @@ -0,0 +1,153 @@ +# ------------------------------------------------- +# Define the sources to be built +# ------------------------------------------------- + +# valkey-server source files +set(VALKEY_SERVER_SRCS + ${CMAKE_SOURCE_DIR}/src/threads_mngr.c + ${CMAKE_SOURCE_DIR}/src/adlist.c + ${CMAKE_SOURCE_DIR}/src/quicklist.c + ${CMAKE_SOURCE_DIR}/src/ae.c + ${CMAKE_SOURCE_DIR}/src/anet.c + ${CMAKE_SOURCE_DIR}/src/dict.c + ${CMAKE_SOURCE_DIR}/src/kvstore.c + ${CMAKE_SOURCE_DIR}/src/sds.c + ${CMAKE_SOURCE_DIR}/src/zmalloc.c + ${CMAKE_SOURCE_DIR}/src/lzf_c.c + ${CMAKE_SOURCE_DIR}/src/lzf_d.c + ${CMAKE_SOURCE_DIR}/src/pqsort.c + ${CMAKE_SOURCE_DIR}/src/zipmap.c + ${CMAKE_SOURCE_DIR}/src/sha1.c + ${CMAKE_SOURCE_DIR}/src/ziplist.c + ${CMAKE_SOURCE_DIR}/src/release.c + ${CMAKE_SOURCE_DIR}/src/memory_prefetch.c + ${CMAKE_SOURCE_DIR}/src/io_threads.c + ${CMAKE_SOURCE_DIR}/src/networking.c + ${CMAKE_SOURCE_DIR}/src/util.c + ${CMAKE_SOURCE_DIR}/src/object.c + ${CMAKE_SOURCE_DIR}/src/db.c + ${CMAKE_SOURCE_DIR}/src/replication.c + ${CMAKE_SOURCE_DIR}/src/rdb.c + ${CMAKE_SOURCE_DIR}/src/t_string.c + ${CMAKE_SOURCE_DIR}/src/t_list.c + ${CMAKE_SOURCE_DIR}/src/t_set.c + ${CMAKE_SOURCE_DIR}/src/t_zset.c + ${CMAKE_SOURCE_DIR}/src/t_hash.c + ${CMAKE_SOURCE_DIR}/src/config.c + ${CMAKE_SOURCE_DIR}/src/aof.c + ${CMAKE_SOURCE_DIR}/src/pubsub.c + ${CMAKE_SOURCE_DIR}/src/multi.c + ${CMAKE_SOURCE_DIR}/src/debug.c + ${CMAKE_SOURCE_DIR}/src/sort.c + ${CMAKE_SOURCE_DIR}/src/intset.c + ${CMAKE_SOURCE_DIR}/src/syncio.c + ${CMAKE_SOURCE_DIR}/src/cluster.c + ${CMAKE_SOURCE_DIR}/src/cluster_legacy.c + ${CMAKE_SOURCE_DIR}/src/cluster_slot_stats.c + ${CMAKE_SOURCE_DIR}/src/crc16.c + ${CMAKE_SOURCE_DIR}/src/endianconv.c + ${CMAKE_SOURCE_DIR}/src/slowlog.c + ${CMAKE_SOURCE_DIR}/src/eval.c + ${CMAKE_SOURCE_DIR}/src/bio.c + ${CMAKE_SOURCE_DIR}/src/rio.c + ${CMAKE_SOURCE_DIR}/src/rand.c + ${CMAKE_SOURCE_DIR}/src/memtest.c + ${CMAKE_SOURCE_DIR}/src/syscheck.c + ${CMAKE_SOURCE_DIR}/src/crcspeed.c + ${CMAKE_SOURCE_DIR}/src/crccombine.c + ${CMAKE_SOURCE_DIR}/src/crc64.c + ${CMAKE_SOURCE_DIR}/src/bitops.c + ${CMAKE_SOURCE_DIR}/src/sentinel.c + ${CMAKE_SOURCE_DIR}/src/notify.c + ${CMAKE_SOURCE_DIR}/src/setproctitle.c + ${CMAKE_SOURCE_DIR}/src/blocked.c + ${CMAKE_SOURCE_DIR}/src/hyperloglog.c + ${CMAKE_SOURCE_DIR}/src/latency.c + ${CMAKE_SOURCE_DIR}/src/sparkline.c + ${CMAKE_SOURCE_DIR}/src/valkey-check-rdb.c + ${CMAKE_SOURCE_DIR}/src/valkey-check-aof.c + ${CMAKE_SOURCE_DIR}/src/geo.c + ${CMAKE_SOURCE_DIR}/src/lazyfree.c + ${CMAKE_SOURCE_DIR}/src/module.c + ${CMAKE_SOURCE_DIR}/src/evict.c + ${CMAKE_SOURCE_DIR}/src/expire.c + ${CMAKE_SOURCE_DIR}/src/geohash.c + ${CMAKE_SOURCE_DIR}/src/geohash_helper.c + ${CMAKE_SOURCE_DIR}/src/childinfo.c + ${CMAKE_SOURCE_DIR}/src/defrag.c + ${CMAKE_SOURCE_DIR}/src/siphash.c + ${CMAKE_SOURCE_DIR}/src/rax.c + ${CMAKE_SOURCE_DIR}/src/t_stream.c + ${CMAKE_SOURCE_DIR}/src/listpack.c + ${CMAKE_SOURCE_DIR}/src/localtime.c + ${CMAKE_SOURCE_DIR}/src/lolwut.c + ${CMAKE_SOURCE_DIR}/src/lolwut5.c + ${CMAKE_SOURCE_DIR}/src/lolwut6.c + ${CMAKE_SOURCE_DIR}/src/acl.c + ${CMAKE_SOURCE_DIR}/src/tracking.c + ${CMAKE_SOURCE_DIR}/src/socket.c + ${CMAKE_SOURCE_DIR}/src/tls.c + ${CMAKE_SOURCE_DIR}/src/sha256.c + ${CMAKE_SOURCE_DIR}/src/timeout.c + ${CMAKE_SOURCE_DIR}/src/setcpuaffinity.c + ${CMAKE_SOURCE_DIR}/src/monotonic.c + ${CMAKE_SOURCE_DIR}/src/mt19937-64.c + ${CMAKE_SOURCE_DIR}/src/resp_parser.c + ${CMAKE_SOURCE_DIR}/src/call_reply.c + ${CMAKE_SOURCE_DIR}/src/script_lua.c + ${CMAKE_SOURCE_DIR}/src/script.c + ${CMAKE_SOURCE_DIR}/src/functions.c + ${CMAKE_SOURCE_DIR}/src/function_lua.c + ${CMAKE_SOURCE_DIR}/src/commands.c + ${CMAKE_SOURCE_DIR}/src/strl.c + ${CMAKE_SOURCE_DIR}/src/connection.c + ${CMAKE_SOURCE_DIR}/src/unix.c + ${CMAKE_SOURCE_DIR}/src/server.c + ${CMAKE_SOURCE_DIR}/src/logreqres.c) + +# valkey-cli +set(VALKEY_CLI_SRCS + ${CMAKE_SOURCE_DIR}/src/anet.c + ${CMAKE_SOURCE_DIR}/src/adlist.c + ${CMAKE_SOURCE_DIR}/src/dict.c + ${CMAKE_SOURCE_DIR}/src/valkey-cli.c + ${CMAKE_SOURCE_DIR}/src/zmalloc.c + ${CMAKE_SOURCE_DIR}/src/release.c + ${CMAKE_SOURCE_DIR}/src/ae.c + ${CMAKE_SOURCE_DIR}/src/serverassert.c + ${CMAKE_SOURCE_DIR}/src/crcspeed.c + ${CMAKE_SOURCE_DIR}/src/crccombine.c + ${CMAKE_SOURCE_DIR}/src/crc64.c + ${CMAKE_SOURCE_DIR}/src/siphash.c + ${CMAKE_SOURCE_DIR}/src/crc16.c + ${CMAKE_SOURCE_DIR}/src/monotonic.c + ${CMAKE_SOURCE_DIR}/src/cli_common.c + ${CMAKE_SOURCE_DIR}/src/mt19937-64.c + ${CMAKE_SOURCE_DIR}/src/strl.c + ${CMAKE_SOURCE_DIR}/src/cli_commands.c) + +# valkey-benchmark +set(VALKEY_BENCHMARK_SRCS + ${CMAKE_SOURCE_DIR}/src/ae.c + ${CMAKE_SOURCE_DIR}/src/anet.c + ${CMAKE_SOURCE_DIR}/src/valkey-benchmark.c + ${CMAKE_SOURCE_DIR}/src/adlist.c + ${CMAKE_SOURCE_DIR}/src/dict.c + ${CMAKE_SOURCE_DIR}/src/zmalloc.c + ${CMAKE_SOURCE_DIR}/src/serverassert.c + ${CMAKE_SOURCE_DIR}/src/release.c + ${CMAKE_SOURCE_DIR}/src/crcspeed.c + ${CMAKE_SOURCE_DIR}/src/crccombine.c + ${CMAKE_SOURCE_DIR}/src/crc64.c + ${CMAKE_SOURCE_DIR}/src/siphash.c + ${CMAKE_SOURCE_DIR}/src/crc16.c + ${CMAKE_SOURCE_DIR}/src/monotonic.c + ${CMAKE_SOURCE_DIR}/src/cli_common.c + ${CMAKE_SOURCE_DIR}/src/mt19937-64.c + ${CMAKE_SOURCE_DIR}/src/strl.c) + +# valkey-rdma module +set(VALKEY_RDMA_MODULE_SRCS ${CMAKE_SOURCE_DIR}/src/rdma.c) + +# valkey-tls module +set(VALKEY_TLS_MODULE_SRCS ${CMAKE_SOURCE_DIR}/src/tls.c) diff --git a/cmake/Modules/Utils.cmake b/cmake/Modules/Utils.cmake new file mode 100644 index 0000000000..304f39fb2c --- /dev/null +++ b/cmake/Modules/Utils.cmake @@ -0,0 +1,102 @@ +# Return the current host distro name. For example: ubuntu, debian, amzn etc +function (valkey_get_distro_name DISTRO_NAME) + if (LINUX AND NOT APPLE) + execute_process( + COMMAND /bin/bash "-c" "cat /etc/os-release |grep ^ID=|cut -d = -f 2" + OUTPUT_VARIABLE _OUT_VAR + OUTPUT_STRIP_TRAILING_WHITESPACE) + # clean the output + string(REPLACE "\"" "" _OUT_VAR "${_OUT_VAR}") + string(REPLACE "." "" _OUT_VAR "${_OUT_VAR}") + set(${DISTRO_NAME} + "${_OUT_VAR}" + PARENT_SCOPE) + elseif (APPLE) + set(${DISTRO_NAME} + "darwin" + PARENT_SCOPE) + elseif (IS_FREEBSD) + set(${DISTRO_NAME} + "freebsd" + PARENT_SCOPE) + else () + set(${DISTRO_NAME} + "unknown" + PARENT_SCOPE) + endif () +endfunction () + +function (valkey_parse_version OUT_MAJOR OUT_MINOR OUT_PATCH) + # Read and parse package version from version.h file + file(STRINGS ${CMAKE_SOURCE_DIR}/src/version.h VERSION_LINES) + foreach (LINE ${VERSION_LINES}) + string(FIND "${LINE}" "#define VALKEY_VERSION " VERSION_STR_POS) + if (VERSION_STR_POS GREATER -1) + string(REPLACE "#define VALKEY_VERSION " "" LINE "${LINE}") + string(REPLACE "\"" "" LINE "${LINE}") + # Change "." to ";" to make it a list + string(REPLACE "." ";" LINE "${LINE}") + list(GET LINE 0 _MAJOR) + list(GET LINE 1 _MINOR) + list(GET LINE 2 _PATCH) + message(STATUS "Valkey version: ${_MAJOR}.${_MINOR}.${_PATCH}") + # Set the output variables + set(${OUT_MAJOR} + ${_MAJOR} + PARENT_SCOPE) + set(${OUT_MINOR} + ${_MINOR} + PARENT_SCOPE) + set(${OUT_PATCH} + ${_PATCH} + PARENT_SCOPE) + endif () + endforeach () +endfunction () + +# Given input argument `OPTION_VALUE`, check that the `OPTION_VALUE` is from the allowed values (one of: +# module/yes/no/1/0/true/false) +# +# Return value: +# +# If ARG is valid, return its number where: +# +# ~~~ +# - `no` | `0` | `off` => return `0` +# - `yes` | `1` | `on` => return `1` +# - `module` => return `2` +# ~~~ +function (valkey_parse_build_option OPTION_VALUE OUT_ARG_ENUM) + list(APPEND VALID_OPTIONS "yes") + list(APPEND VALID_OPTIONS "1") + list(APPEND VALID_OPTIONS "on") + list(APPEND VALID_OPTIONS "no") + list(APPEND VALID_OPTIONS "0") + list(APPEND VALID_OPTIONS "off") + list(APPEND VALID_OPTIONS "module") + + string(TOLOWER "${OPTION_VALUE}" OPTION_VALUE) + list(FIND VALID_OPTIONS "${ARG}" OPT_INDEX) + if (VERSION_STR_POS GREATER -1) + message(FATAL_ERROR "Invalid value passed ''${OPTION_VALUE}'") + endif () + + if ("${OPTION_VALUE}" STREQUAL "yes" + OR "${OPTION_VALUE}" STREQUAL "1" + OR "${OPTION_VALUE}" STREQUAL "on") + set(${OUT_ARG_ENUM} + 1 + PARENT_SCOPE) + elseif ( + "${OPTION_VALUE}" STREQUAL "no" + OR "${OPTION_VALUE}" STREQUAL "0" + OR "${OPTION_VALUE}" STREQUAL "off") + set(${OUT_ARG_ENUM} + 0 + PARENT_SCOPE) + else () + set(${OUT_ARG_ENUM} + 2 + PARENT_SCOPE) + endif () +endfunction () diff --git a/cmake/Modules/ValkeySetup.cmake b/cmake/Modules/ValkeySetup.cmake new file mode 100644 index 0000000000..e935c3b308 --- /dev/null +++ b/cmake/Modules/ValkeySetup.cmake @@ -0,0 +1,381 @@ +include(CheckIncludeFiles) +include(ProcessorCount) +include(Utils) + +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib") +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib") + +# Generate compile_commands.json file for IDEs code completion support +set(CMAKE_EXPORT_COMPILE_COMMANDS 1) + +processorcount(VALKEY_PROCESSOR_COUNT) +message(STATUS "Processor count: ${VALKEY_PROCESSOR_COUNT}") + +# Installed executables will have this permissions +set(VALKEY_EXE_PERMISSIONS + OWNER_EXECUTE + OWNER_WRITE + OWNER_READ + GROUP_EXECUTE + GROUP_READ + WORLD_EXECUTE + WORLD_READ) + +set(VALKEY_SERVER_CFLAGS "") +set(VALKEY_SERVER_LDFLAGS "") + +# ---------------------------------------------------- +# Helper functions & macros +# ---------------------------------------------------- +macro (add_valkey_server_compiler_options value) + set(VALKEY_SERVER_CFLAGS "${VALKEY_SERVER_CFLAGS} ${value}") +endmacro () + +macro (add_valkey_server_linker_option value) + list(APPEND VALKEY_SERVER_LDFLAGS ${value}) +endmacro () + +macro (get_valkey_server_linker_option return_value) + list(JOIN VALKEY_SERVER_LDFLAGS " " ${value} ${return_value}) +endmacro () + +set(IS_FREEBSD 0) +if (CMAKE_SYSTEM_NAME MATCHES "^.*BSD$|DragonFly") + message(STATUS "Building for FreeBSD compatible system") + set(IS_FREEBSD 1) + include_directories("/usr/local/include") + add_valkey_server_compiler_options("-DUSE_BACKTRACE") +endif () + +# Helper function for creating symbolic link so that: link -> source +macro (valkey_create_symlink source link) + install( + CODE "execute_process( \ + COMMAND /bin/bash ${CMAKE_BINARY_DIR}/CreateSymlink.sh \ + ${source} \ + ${link} \ + )" + COMPONENT "valkey") +endmacro () + +# Install a binary +macro (valkey_install_bin target) + # Install cli tool and create a redis symbolic link + install( + TARGETS ${target} + DESTINATION ${CMAKE_INSTALL_BINDIR} + PERMISSIONS ${VALKEY_EXE_PERMISSIONS} + COMPONENT "valkey") +endmacro () + +# Helper function that defines, builds and installs `target` In addition, it creates a symbolic link between the target +# and `link_name` +macro (valkey_build_and_install_bin target sources ld_flags libs link_name) + add_executable(${target} ${sources}) + + if (USE_JEMALLOC) + # Using jemalloc + target_link_libraries(${target} jemalloc) + endif () + + # Place this line last to ensure that ${ld_flags} is placed last on the linker line + target_link_libraries(${target} ${libs} ${ld_flags}) + target_link_libraries(${target} hiredis) + if (USE_TLS) + # Add required libraries needed for TLS + target_link_libraries(${target} OpenSSL::SSL hiredis_ssl) + endif () + + if (IS_FREEBSD) + target_link_libraries(${target} execinfo) + endif () + + # Install cli tool and create a redis symbolic link + valkey_install_bin(${target}) + valkey_create_symlink(${target} ${link_name}) +endmacro () + +# Helper function that defines, builds and installs `target` module. +macro (valkey_build_and_install_module target sources ld_flags libs) + add_library(${target} SHARED ${sources}) + + if (USE_JEMALLOC) + # Using jemalloc + target_link_libraries(${target} jemalloc) + endif () + + # Place this line last to ensure that ${ld_flags} is placed last on the linker line + target_link_libraries(${target} ${libs} ${ld_flags}) + if (USE_TLS) + # Add required libraries needed for TLS + target_link_libraries(${target} OpenSSL::SSL hiredis_ssl) + endif () + + if (IS_FREEBSD) + target_link_libraries(${target} execinfo) + endif () + + # Install cli tool and create a redis symbolic link + valkey_install_bin(${target}) +endmacro () + +# Determine if we are building in Release or Debug mode +if (CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DebugFull) + set(VALKEY_DEBUG_BUILD 1) + set(VALKEY_RELEASE_BUILD 0) + message(STATUS "Building in debug mode") +else () + set(VALKEY_DEBUG_BUILD 0) + set(VALKEY_RELEASE_BUILD 1) + message(STATUS "Building in release mode") +endif () + +# ---------------------------------------------------- +# Helper functions - end +# ---------------------------------------------------- + +# ---------------------------------------------------- +# Build options (allocator, tls, rdma et al) +# ---------------------------------------------------- + +if (NOT BUILD_MALLOC) + if (APPLE) + set(BUILD_MALLOC "libc") + elseif (UNIX) + set(BUILD_MALLOC "jemalloc") + endif () +endif () + +# User may pass different allocator library. Using -DBUILD_MALLOC=, make sure it is a valid value +if (BUILD_MALLOC) + if ("${BUILD_MALLOC}" STREQUAL "jemalloc") + set(MALLOC_LIB "jemalloc") + add_valkey_server_compiler_options("-DUSE_JEMALLOC") + set(USE_JEMALLOC 1) + elseif ("${BUILD_MALLOC}" STREQUAL "libc") + set(MALLOC_LIB "libc") + elseif ("${BUILD_MALLOC}" STREQUAL "tcmalloc") + set(MALLOC_LIB "tcmalloc") + add_valkey_server_compiler_options("-DUSE_TCMALLOC") + elseif ("${BUILD_MALLOC}" STREQUAL "tcmalloc_minimal") + set(MALLOC_LIB "tcmalloc_minimal") + add_valkey_server_compiler_options("-DUSE_TCMALLOC") + else () + message(FATAL_ERROR "BUILD_MALLOC can be one of: jemalloc, libc, tcmalloc or tcmalloc_minimal") + endif () +endif () + +message(STATUS "Using ${MALLOC_LIB}") + +# TLS support +if (BUILD_TLS) + valkey_parse_build_option(${BUILD_TLS} USE_TLS) + if (USE_TLS EQUAL 1) + # Only search for OpenSSL if needed + find_package(OpenSSL REQUIRED) + message(STATUS "OpenSSL include dir: ${OPENSSL_INCLUDE_DIR}") + message(STATUS "OpenSSL libraries: ${OPENSSL_LIBRARIES}") + include_directories(${OPENSSL_INCLUDE_DIR}) + endif () + + if (USE_TLS EQUAL 1) + add_valkey_server_compiler_options("-DUSE_OPENSSL=1") + add_valkey_server_compiler_options("-DBUILD_TLS_MODULE=0") + else () + # Build TLS as a module RDMA can only be built as a module. So disable it + message(WARNING "BUILD_TLS can be one of: [ON | OFF | 1 | 0], but '${BUILD_TLS}' was provided") + message(STATUS "TLS support is disabled") + set(USE_TLS 0) + endif () +else () + # By default, TLS is disabled + message(STATUS "TLS is disabled") + set(USE_TLS 0) +endif () + +if (BUILD_RDMA) + set(BUILD_RDMA_MODULE 0) + # RDMA support (Linux only) + if (LINUX AND NOT APPLE) + valkey_parse_build_option(${BUILD_RDMA} USE_RDMA) + if (USE_RDMA EQUAL 2) # Module + message(STATUS "Building RDMA as module") + add_valkey_server_compiler_options("-DUSE_RDMA=2") + find_package(PkgConfig REQUIRED) + + # Locate librdmacm & libibverbs, fail if we can't find them + pkg_check_modules(RDMACM REQUIRED librdmacm) + pkg_check_modules(IBVERBS REQUIRED libibverbs) + + message(STATUS "${RDMACM_LINK_LIBRARIES};${IBVERBS_LINK_LIBRARIES}") + list(APPEND RDMA_LIBS "${RDMACM_LIBRARIES};${IBVERBS_LIBRARIES}") + unset(RDMACM_LINK_LIBRARIES CACHE) + unset(IBVERBS_LINK_LIBRARIES CACHE) + set(BUILD_RDMA_MODULE 1) + elseif (USE_RDMA EQUAL 1) + # RDMA can only be built as a module. So disable it + message(WARNING "BUILD_RDMA can be one of: [NO | 0 | MODULE], but '${BUILD_RDMA}' was provided") + message(STATUS "RDMA build is disabled") + set(USE_RDMA 0) + endif () + else () + message(WARNING "RDMA is only supported on Linux platforms") + endif () +endif () + +set(BUILDING_ARM64 0) +set(BUILDING_ARM32 0) + +if ("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "arm64") + set(BUILDING_ARM64 1) +endif () + +if ("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "arm") + set(BUILDING_ARM32 1) +endif () + +message(STATUS "Building on ${CMAKE_HOST_SYSTEM_NAME}") +if (BUILDING_ARM64) + message(STATUS "Compiling valkey for ARM64") + add_valkey_server_linker_option("-funwind-tables") +endif () + +if (APPLE) + add_valkey_server_linker_option("-rdynamic") + add_valkey_server_linker_option("-ldl") +elseif (UNIX) + add_valkey_server_linker_option("-rdynamic") + add_valkey_server_linker_option("-pthread") + add_valkey_server_linker_option("-ldl") + add_valkey_server_linker_option("-lm") +endif () + +if (VALKEY_DEBUG_BUILD) + # Debug build, use enable "-fno-omit-frame-pointer" + add_valkey_server_compiler_options("-fno-omit-frame-pointer") +endif () + +# Check for Atomic +check_include_files(stdatomic.h HAVE_C11_ATOMIC) +if (HAVE_C11_ATOMIC) + add_valkey_server_compiler_options("-std=gnu11") +else () + add_valkey_server_compiler_options("-std=c99") +endif () + +# Sanitizer +if (BUILD_SANITIZER) + # For best results, force libc + set(MALLOC_LIB, "libc") + if ("${BUILD_SANITIZER}" STREQUAL "address") + add_valkey_server_compiler_options("-fsanitize=address -fno-sanitize-recover=all -fno-omit-frame-pointer") + add_valkey_server_linker_option("-fsanitize=address") + elseif ("${BUILD_SANITIZER}" STREQUAL "thread") + add_valkey_server_compiler_options("-fsanitize=thread -fno-sanitize-recover=all -fno-omit-frame-pointer") + add_valkey_server_linker_option("-fsanitize=thread") + elseif ("${BUILD_SANITIZER}" STREQUAL "undefined") + add_valkey_server_compiler_options("-fsanitize=undefined -fno-sanitize-recover=all -fno-omit-frame-pointer") + add_valkey_server_linker_option("-fsanitize=undefined") + else () + message(FATAL_ERROR "Unknown sanitizer: ${BUILD_SANITIZER}") + endif () +endif () + +include_directories("${CMAKE_SOURCE_DIR}/deps/hiredis") +include_directories("${CMAKE_SOURCE_DIR}/deps/linenoise") +include_directories("${CMAKE_SOURCE_DIR}/deps/lua/src") +include_directories("${CMAKE_SOURCE_DIR}/deps/hdr_histogram") +include_directories("${CMAKE_SOURCE_DIR}/deps/fpconv") + +add_subdirectory("${CMAKE_SOURCE_DIR}/deps") + +# Update linker flags for the allocator +if (USE_JEMALLOC) + include_directories("${CMAKE_SOURCE_DIR}/deps/jemalloc/include") +endif () + +# Common compiler flags +add_valkey_server_compiler_options("-pedantic") + +# ---------------------------------------------------- +# Build options (allocator, tls, rdma et al) - end +# ---------------------------------------------------- + +# ------------------------------------------------- +# Code Generation section +# ------------------------------------------------- +find_program(PYTHON_EXE python3) +if (PYTHON_EXE) + # Python based code generation + message(STATUS "Found python3: ${PYTHON_EXE}") + # Rule for generating commands.def file from json files + message(STATUS "Adding target generate_commands_def") + file(GLOB COMMAND_FILES_JSON "${CMAKE_SOURCE_DIR}/src/commands/*.json") + add_custom_command( + OUTPUT ${CMAKE_BINARY_DIR}/commands_def_generated + DEPENDS ${COMMAND_FILES_JSON} + COMMAND ${PYTHON_EXE} ${CMAKE_SOURCE_DIR}/utils/generate-command-code.py + COMMAND touch ${CMAKE_BINARY_DIR}/commands_def_generated + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/src") + add_custom_target(generate_commands_def DEPENDS ${CMAKE_BINARY_DIR}/commands_def_generated) + + # Rule for generating fmtargs.h + message(STATUS "Adding target generate_fmtargs_h") + add_custom_command( + OUTPUT ${CMAKE_BINARY_DIR}/fmtargs_generated + DEPENDS ${CMAKE_SOURCE_DIR}/utils/generate-fmtargs.py + COMMAND sed '/Everything/,$$d' fmtargs.h > fmtargs.h.tmp + COMMAND ${PYTHON_EXE} ${CMAKE_SOURCE_DIR}/utils/generate-fmtargs.py >> fmtargs.h.tmp + COMMAND mv fmtargs.h.tmp fmtargs.h + COMMAND touch ${CMAKE_BINARY_DIR}/fmtargs_generated + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/src") + add_custom_target(generate_fmtargs_h DEPENDS ${CMAKE_BINARY_DIR}/fmtargs_generated) + + # Rule for generating test_files.h + message(STATUS "Adding target generate_test_files_h") + file(GLOB UNIT_TEST_SRCS "${CMAKE_SOURCE_DIR}/src/unit/*.c") + add_custom_command( + OUTPUT ${CMAKE_BINARY_DIR}/test_files_generated + DEPENDS "${UNIT_TEST_SRCS};${CMAKE_SOURCE_DIR}/utils/generate-unit-test-header.py" + COMMAND ${PYTHON_EXE} ${CMAKE_SOURCE_DIR}/utils/generate-unit-test-header.py + COMMAND touch ${CMAKE_BINARY_DIR}/test_files_generated + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/src") + add_custom_target(generate_test_files_h DEPENDS ${CMAKE_BINARY_DIR}/test_files_generated) +else () + # Fake targets + add_custom_target(generate_commands_def) + add_custom_target(generate_fmtargs_h) + add_custom_target(generate_test_files_h) +endif () + +# Generate release.h file (always) +add_custom_target( + release_header + COMMAND sh -c '${CMAKE_SOURCE_DIR}/src/mkreleasehdr.sh' + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/src") + +# ------------------------------------------------- +# Code Generation section - end +# ------------------------------------------------- + +# ---------------------------------------------------------- +# All our source files are defined in SourceFiles.cmake file +# ---------------------------------------------------------- +include(SourceFiles) + +# Clear the below variables from the cache +unset(CMAKE_C_FLAGS CACHE) +unset(BUILD_SANITIZER CACHE) +unset(VALKEY_SERVER_LDFLAGS CACHE) +unset(VALKEY_SERVER_CFLAGS CACHE) +unset(PYTHON_EXE CACHE) +unset(HAVE_C11_ATOMIC CACHE) +unset(USE_TLS CACHE) +unset(USE_RDMA CACHE) +unset(BUILD_TLS CACHE) +unset(BUILD_RDMA CACHE) +unset(BUILD_MALLOC CACHE) +unset(USE_JEMALLOC CACHE) +unset(BUILD_TLS_MODULE CACHE) +unset(BUILD_TLS_BUILTIN CACHE) diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt new file mode 100644 index 0000000000..c904b94031 --- /dev/null +++ b/deps/CMakeLists.txt @@ -0,0 +1,26 @@ +add_subdirectory(jemalloc) +add_subdirectory(lua) + +# Set hiredis options. We need to disable the defaults set in the OPTION(..) we do this by setting them in the CACHE +set(BUILD_SHARED_LIBS + OFF + CACHE BOOL "Build shared libraries") +set(DISABLE_TESTS + ON + CACHE BOOL "If tests should be compiled or not") +if (USE_TLS) # Module or no module + message(STATUS "Building hiredis_ssl") + set(ENABLE_SSL + ON + CACHE BOOL "Should we test SSL connections") +endif () + +add_subdirectory(hiredis) +add_subdirectory(linenoise) +add_subdirectory(fpconv) +add_subdirectory(hdr_histogram) + +# Clear any cached variables passed to hiredis from the cache +unset(BUILD_SHARED_LIBS CACHE) +unset(DISABLE_TESTS CACHE) +unset(ENABLE_SSL CACHE) diff --git a/deps/fpconv/CMakeLists.txt b/deps/fpconv/CMakeLists.txt new file mode 100644 index 0000000000..c586aa650a --- /dev/null +++ b/deps/fpconv/CMakeLists.txt @@ -0,0 +1,4 @@ +project(fpconv) + +set(SRCS "${CMAKE_CURRENT_LIST_DIR}/fpconv_dtoa.c" "${CMAKE_CURRENT_LIST_DIR}/fpconv_dtoa.h") +add_library(fpconv STATIC ${SRCS}) diff --git a/deps/hdr_histogram/CMakeLists.txt b/deps/hdr_histogram/CMakeLists.txt new file mode 100644 index 0000000000..7b45bd76ba --- /dev/null +++ b/deps/hdr_histogram/CMakeLists.txt @@ -0,0 +1,7 @@ +project(hdr_histogram) + +set(SRCS "${CMAKE_CURRENT_LIST_DIR}/hdr_histogram.c" "${CMAKE_CURRENT_LIST_DIR}/hdr_histogram.h" + "${CMAKE_CURRENT_LIST_DIR}/hdr_atomic.h" "${CMAKE_CURRENT_LIST_DIR}/hdr_redis_malloc.h") + +add_library(hdr_histogram STATIC ${SRCS}) +target_compile_definitions(hdr_histogram PRIVATE HDR_MALLOC_INCLUDE=\"hdr_redis_malloc.h\") diff --git a/deps/hiredis/.github/workflows/build.yml b/deps/hiredis/.github/workflows/build.yml index 581800b4f7..048ee51cd4 100644 --- a/deps/hiredis/.github/workflows/build.yml +++ b/deps/hiredis/.github/workflows/build.yml @@ -112,7 +112,7 @@ jobs: run: $GITHUB_WORKSPACE/test.sh freebsd: - runs-on: macos-12 + runs-on: macos-13 name: FreeBSD steps: - uses: actions/checkout@v3 diff --git a/deps/jemalloc/CMakeLists.txt b/deps/jemalloc/CMakeLists.txt new file mode 100644 index 0000000000..e79e960ec2 --- /dev/null +++ b/deps/jemalloc/CMakeLists.txt @@ -0,0 +1,23 @@ +project(jemalloc) + +# Build jemalloc using configure && make install +set(JEMALLOC_INSTALL_DIR ${CMAKE_BINARY_DIR}/jemalloc-build) +set(JEMALLOC_SRC_DIR ${CMAKE_CURRENT_LIST_DIR}) +if (NOT EXISTS ${JEMALLOC_INSTALL_DIR}/lib/libjemalloc.a) + message(STATUS "Building jemalloc (custom build)") + message(STATUS "JEMALLOC_SRC_DIR = ${JEMALLOC_SRC_DIR}") + message(STATUS "JEMALLOC_INSTALL_DIR = ${JEMALLOC_INSTALL_DIR}") + + execute_process( + COMMAND sh -c "${JEMALLOC_SRC_DIR}/configure --disable-cxx \ + --with-version=5.3.0-0-g0 --with-lg-quantum=3 --disable-cache-oblivious --with-jemalloc-prefix=je_ \ + --enable-static --disable-shared --prefix=${JEMALLOC_INSTALL_DIR}" + WORKING_DIRECTORY ${JEMALLOC_SRC_DIR} COMMAND_ERROR_IS_FATAL ANY) + execute_process(COMMAND make -j${VALKEY_PROCESSOR_COUNT} lib/libjemalloc.a install + WORKING_DIRECTORY "${JEMALLOC_SRC_DIR}") +endif () + +# Import the compiled library as a CMake target +add_library(jemalloc STATIC IMPORTED GLOBAL) +set_target_properties(jemalloc PROPERTIES IMPORTED_LOCATION "${JEMALLOC_INSTALL_DIR}/lib/libjemalloc.a" + INCLUDE_DIRECTORIES "${JEMALLOC_INSTALL_DIR}/include") diff --git a/deps/linenoise/CMakeLists.txt b/deps/linenoise/CMakeLists.txt new file mode 100644 index 0000000000..f801e4abf1 --- /dev/null +++ b/deps/linenoise/CMakeLists.txt @@ -0,0 +1,4 @@ +project(linenoise) + +set(SRCS "${CMAKE_CURRENT_LIST_DIR}/linenoise.c" "${CMAKE_CURRENT_LIST_DIR}/linenoise.h") +add_library(linenoise STATIC ${SRCS}) diff --git a/deps/lua/CMakeLists.txt b/deps/lua/CMakeLists.txt new file mode 100644 index 0000000000..e911de9232 --- /dev/null +++ b/deps/lua/CMakeLists.txt @@ -0,0 +1,44 @@ +project(lualib) + +set(LUA_SRC_DIR "${CMAKE_CURRENT_LIST_DIR}/src") +set(LUA_SRCS + ${LUA_SRC_DIR}/fpconv.c + ${LUA_SRC_DIR}/lbaselib.c + ${LUA_SRC_DIR}/lmathlib.c + ${LUA_SRC_DIR}/lstring.c + ${LUA_SRC_DIR}/lparser.c + ${LUA_SRC_DIR}/ldo.c + ${LUA_SRC_DIR}/lzio.c + ${LUA_SRC_DIR}/lmem.c + ${LUA_SRC_DIR}/strbuf.c + ${LUA_SRC_DIR}/lstrlib.c + ${LUA_SRC_DIR}/lundump.c + ${LUA_SRC_DIR}/lua_cmsgpack.c + ${LUA_SRC_DIR}/loslib.c + ${LUA_SRC_DIR}/lua_struct.c + ${LUA_SRC_DIR}/ldebug.c + ${LUA_SRC_DIR}/lobject.c + ${LUA_SRC_DIR}/ldump.c + ${LUA_SRC_DIR}/lua_cjson.c + ${LUA_SRC_DIR}/ldblib.c + ${LUA_SRC_DIR}/ltm.c + ${LUA_SRC_DIR}/ltable.c + ${LUA_SRC_DIR}/lstate.c + ${LUA_SRC_DIR}/lua_bit.c + ${LUA_SRC_DIR}/lua.c + ${LUA_SRC_DIR}/loadlib.c + ${LUA_SRC_DIR}/lcode.c + ${LUA_SRC_DIR}/lapi.c + ${LUA_SRC_DIR}/lgc.c + ${LUA_SRC_DIR}/lvm.c + ${LUA_SRC_DIR}/lfunc.c + ${LUA_SRC_DIR}/lauxlib.c + ${LUA_SRC_DIR}/ltablib.c + ${LUA_SRC_DIR}/linit.c + ${LUA_SRC_DIR}/lopcodes.c + ${LUA_SRC_DIR}/llex.c + ${LUA_SRC_DIR}/liolib.c) + +add_library(lualib STATIC "${LUA_SRCS}") +target_include_directories(lualib PUBLIC "${LUA_SRC_DIR}") +target_compile_definitions(lualib PRIVATE ENABLE_CJSON_GLOBAL) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000000..b7e328163b --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,77 @@ +project(valkey-server) + +set(INSTALL_BIN_PATH ${CMAKE_INSTALL_PREFIX}/bin) +set_directory_properties(PROPERTIES CLEAN_NO_CUSTOM 1) + +# Target: valkey-server +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${VALKEY_SERVER_CFLAGS}") +message(STATUS "CFLAGS: ${CMAKE_C_FLAGS}") + +get_valkey_server_linker_option(VALKEY_SERVER_LDFLAGS) +list(APPEND SERVER_LIBS "fpconv") +list(APPEND SERVER_LIBS "lualib") +list(APPEND SERVER_LIBS "hdr_histogram") +valkey_build_and_install_bin(valkey-server "${VALKEY_SERVER_SRCS}" "${VALKEY_SERVER_LDFLAGS}" "${SERVER_LIBS}" + "redis-server") +add_dependencies(valkey-server generate_commands_def) +add_dependencies(valkey-server generate_fmtargs_h) +add_dependencies(valkey-server release_header) + +if (VALKEY_RELEASE_BUILD) + # Enable LTO for Release build + set_property(TARGET valkey-server PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE) +endif () + +# Target: valkey-cli +list(APPEND CLI_LIBS "linenoise") +valkey_build_and_install_bin(valkey-cli "${VALKEY_CLI_SRCS}" "${VALKEY_SERVER_LDFLAGS}" "${CLI_LIBS}" "redis-cli") +add_dependencies(valkey-cli generate_commands_def) +add_dependencies(valkey-cli generate_fmtargs_h) + +# Target: valkey-benchmark +list(APPEND BENCH_LIBS "hdr_histogram") +valkey_build_and_install_bin(valkey-benchmark "${VALKEY_BENCHMARK_SRCS}" "${VALKEY_SERVER_LDFLAGS}" "${BENCH_LIBS}" + "redis-benchmark") +add_dependencies(valkey-benchmark generate_commands_def) +add_dependencies(valkey-benchmark generate_fmtargs_h) + +# Targets: valkey-sentinel, valkey-check-aof and valkey-check-rdb are just symbolic links +valkey_create_symlink("valkey-server" "valkey-sentinel") +valkey_create_symlink("valkey-server" "valkey-check-rdb") +valkey_create_symlink("valkey-server" "valkey-check-aof") + +# Target valkey-rdma +if (BUILD_RDMA_MODULE) + set(MODULE_NAME "valkey-rdma") + message(STATUS "Building RDMA module") + add_library(${MODULE_NAME} SHARED "${VALKEY_RDMA_MODULE_SRCS}") + target_compile_options(${MODULE_NAME} PRIVATE -DBUILD_RDMA_MODULE -DUSE_RDMA=1) + target_link_libraries(${MODULE_NAME} "${RDMA_LIBS}") + # remove the "lib" prefix from the module + set_target_properties(${MODULE_NAME} PROPERTIES PREFIX "") + valkey_install_bin(${MODULE_NAME}) +endif () + +# Target valkey-tls (a module) +if (BUILD_TLS_MODULE) + message(STATUS "Building TLS as a module") + set(MODULE_NAME "valkey-tls") + add_library(${MODULE_NAME} SHARED ${VALKEY_TLS_MODULE_SRCS}) + target_compile_options(${MODULE_NAME} PRIVATE -DUSE_OPENSSL=2 -DBUILD_TLS_MODULE=2) + if (APPLE) + # Some symbols can only be resolved during runtime (they exist in the executable) + target_link_options(${MODULE_NAME} PRIVATE -undefined dynamic_lookup) + endif () + target_link_libraries(${MODULE_NAME} hiredis_ssl OpenSSL::SSL) + set_target_properties(${MODULE_NAME} PROPERTIES PREFIX "") +endif () + +if (BUILD_EXAMPLE_MODULES) + # Include the modules ("hello*") + message(STATUS "Building example modules") + add_subdirectory(modules) +endif () + +if (BUILD_UNIT_TESTS) + add_subdirectory(unit) +endif () diff --git a/src/Makefile b/src/Makefile index ae2de1c626..a76356e9d5 100644 --- a/src/Makefile +++ b/src/Makefile @@ -25,7 +25,7 @@ ifeq ($(OPTIMIZATION),-O3) ifeq (clang,$(CLANG)) OPTIMIZATION+=-flto else - OPTIMIZATION+=-flto=auto + OPTIMIZATION+=-flto=auto -ffat-lto-objects endif endif ifneq ($(OPTIMIZATION),-O0) @@ -131,9 +131,6 @@ ifdef REDIS_LDFLAGS endif FINAL_CFLAGS=$(STD) $(WARN) $(OPT) $(DEBUG) $(CFLAGS) $(SERVER_CFLAGS) -ifeq ($(SERVER_TEST),yes) - FINAL_CFLAGS +=-DSERVER_TEST=1 -endif FINAL_LDFLAGS=$(LDFLAGS) $(OPT) $(SERVER_LDFLAGS) $(DEBUG) FINAL_LIBS=-lm DEBUG=-g -ggdb diff --git a/src/cluster_legacy.c b/src/cluster_legacy.c index 43d56b9a09..7b3384ee9f 100644 --- a/src/cluster_legacy.c +++ b/src/cluster_legacy.c @@ -2451,6 +2451,7 @@ void clusterUpdateSlotsConfigWith(clusterNode *sender, uint64_t senderConfigEpoc * need to delete all the keys in the slots we lost ownership. */ uint16_t dirty_slots[CLUSTER_SLOTS]; int dirty_slots_count = 0; + int delete_dirty_slots = 0; /* We should detect if sender is new primary of our shard. * We will know it if all our slots were migrated to sender, and sender @@ -2677,6 +2678,12 @@ void clusterUpdateSlotsConfigWith(clusterNode *sender, uint64_t senderConfigEpoc serverLog(LL_NOTICE, "My last slot was migrated to node %.40s (%s) in shard %.40s. I am now an empty primary.", sender->name, sender->human_nodename, sender->shard_id); + /* We may still have dirty slots when we became a empty primary due to + * a bad migration. + * + * In order to maintain a consistent state between keys and slots + * we need to remove all the keys from the slots we lost. */ + delete_dirty_slots = 1; } } else if (dirty_slots_count) { /* If we are here, we received an update message which removed @@ -2686,6 +2693,10 @@ void clusterUpdateSlotsConfigWith(clusterNode *sender, uint64_t senderConfigEpoc * * In order to maintain a consistent state between keys and slots * we need to remove all the keys from the slots we lost. */ + delete_dirty_slots = 1; + } + + if (delete_dirty_slots) { for (int j = 0; j < dirty_slots_count; j++) { serverLog(LL_NOTICE, "Deleting keys in dirty slot %d on node %.40s (%s) in shard %.40s", dirty_slots[j], myself->name, myself->human_nodename, myself->shard_id); @@ -2981,7 +2992,7 @@ int clusterIsValidPacket(clusterLink *link) { return 0; } - if (type == server.cluster_drop_packet_filter) { + if (type == server.cluster_drop_packet_filter || server.cluster_drop_packet_filter == -2) { serverLog(LL_WARNING, "Dropping packet that matches debug drop filter"); return 0; } @@ -3070,7 +3081,8 @@ int clusterProcessPacket(clusterLink *link) { if (!clusterIsValidPacket(link)) { clusterMsg *hdr = (clusterMsg *)link->rcvbuf; uint16_t type = ntohs(hdr->type); - if (server.debug_cluster_close_link_on_packet_drop && type == server.cluster_drop_packet_filter) { + if (server.debug_cluster_close_link_on_packet_drop && + (type == server.cluster_drop_packet_filter || server.cluster_drop_packet_filter == -2)) { freeClusterLink(link); serverLog(LL_WARNING, "Closing link for matching packet type %hu", type); return 0; @@ -3134,6 +3146,24 @@ int clusterProcessPacket(clusterLink *link) { if (sender_claims_to_be_primary && sender_claimed_config_epoch > sender->configEpoch) { sender->configEpoch = sender_claimed_config_epoch; clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG | CLUSTER_TODO_FSYNC_CONFIG); + + if (server.cluster->failover_auth_time && sender->configEpoch >= server.cluster->failover_auth_epoch) { + /* Another node has claimed an epoch greater than or equal to ours. + * If we have an ongoing election, reset it because we cannot win + * with an epoch smaller than or equal to the incoming claim. This + * allows us to start a new election as soon as possible. */ + server.cluster->failover_auth_time = 0; + serverLog(LL_WARNING, + "Failover election in progress for epoch %llu, but received a claim from " + "node %.40s (%s) with an equal or higher epoch %llu. Resetting the election " + "since we cannot win an election in the past.", + (unsigned long long)server.cluster->failover_auth_epoch, + sender->name, sender->human_nodename, + (unsigned long long)sender->configEpoch); + /* Maybe we could start a new election, set a flag here to make sure + * we check as soon as possible, instead of waiting for a cron. */ + clusterDoBeforeSleep(CLUSTER_TODO_HANDLE_FAILOVER); + } } /* Update the replication offset info for this node. */ sender->repl_offset = ntohu64(hdr->offset); @@ -4333,12 +4363,17 @@ void clusterSendFailoverAuthIfNeeded(clusterNode *node, clusterMsg *request) { /* We did not voted for a replica about this primary for two * times the node timeout. This is not strictly needed for correctness - * of the algorithm but makes the base case more linear. */ - if (mstime() - node->replicaof->voted_time < server.cluster_node_timeout * 2) { + * of the algorithm but makes the base case more linear. + * + * This limitation does not restrict manual failover. If a user initiates + * a manual failover, we need to allow it to vote, otherwise the manual + * failover may time out. */ + if (!force_ack && mstime() - node->replicaof->voted_time < server.cluster_node_timeout * 2) { serverLog(LL_WARNING, - "Failover auth denied to %.40s %s: " - "can't vote about this primary before %lld milliseconds", + "Failover auth denied to %.40s (%s): " + "can't vote for any replica of %.40s (%s) within %lld milliseconds", node->name, node->human_nodename, + node->replicaof->name, node->replicaof->human_nodename, (long long)((server.cluster_node_timeout * 2) - (mstime() - node->replicaof->voted_time))); return; } @@ -4364,7 +4399,7 @@ void clusterSendFailoverAuthIfNeeded(clusterNode *node, clusterMsg *request) { /* We can vote for this replica. */ server.cluster->lastVoteEpoch = server.cluster->currentEpoch; - node->replicaof->voted_time = mstime(); + if (!force_ack) node->replicaof->voted_time = mstime(); clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG | CLUSTER_TODO_FSYNC_CONFIG); clusterSendFailoverAuth(node); serverLog(LL_NOTICE, "Failover auth granted to %.40s (%s) for epoch %llu", node->name, node->human_nodename, @@ -4518,8 +4553,9 @@ void clusterFailoverReplaceYourPrimary(void) { * 3) Perform the failover informing all the other nodes. */ void clusterHandleReplicaFailover(void) { + mstime_t now = mstime(); mstime_t data_age; - mstime_t auth_age = mstime() - server.cluster->failover_auth_time; + mstime_t auth_age = now - server.cluster->failover_auth_time; int needed_quorum = (server.cluster->size / 2) + 1; int manual_failover = server.cluster->mf_end != 0 && server.cluster->mf_can_start; mstime_t auth_timeout, auth_retry_time; @@ -4581,7 +4617,7 @@ void clusterHandleReplicaFailover(void) { /* If the previous failover attempt timeout and the retry time has * elapsed, we can setup a new one. */ if (auth_age > auth_retry_time) { - server.cluster->failover_auth_time = mstime() + + server.cluster->failover_auth_time = now + 500 + /* Fixed delay of 500 milliseconds, let FAIL msg propagate. */ random() % 500; /* Random delay between 0 and 500 milliseconds. */ server.cluster->failover_auth_count = 0; @@ -4593,20 +4629,26 @@ void clusterHandleReplicaFailover(void) { server.cluster->failover_auth_time += server.cluster->failover_auth_rank * 1000; /* However if this is a manual failover, no delay is needed. */ if (server.cluster->mf_end) { - server.cluster->failover_auth_time = mstime(); + server.cluster->failover_auth_time = now; server.cluster->failover_auth_rank = 0; - clusterDoBeforeSleep(CLUSTER_TODO_HANDLE_FAILOVER); + /* Reset auth_age since it is outdated now and we can bypass the auth_timeout + * check in the next state and start the election ASAP. */ + auth_age = 0; } serverLog(LL_NOTICE, "Start of election delayed for %lld milliseconds " "(rank #%d, offset %lld).", - server.cluster->failover_auth_time - mstime(), server.cluster->failover_auth_rank, + server.cluster->failover_auth_time - now, server.cluster->failover_auth_rank, replicationGetReplicaOffset()); /* Now that we have a scheduled election, broadcast our offset * to all the other replicas so that they'll updated their offsets * if our offset is better. */ clusterBroadcastPong(CLUSTER_BROADCAST_LOCAL_REPLICAS); - return; + + /* Return ASAP if we can't start the election now. In a manual failover, + * we can start the election immediately, so in this case we continue to + * the next state without waiting for the next beforeSleep. */ + if (now < server.cluster->failover_auth_time) return; } /* It is possible that we received more updated offsets from other @@ -4626,7 +4668,7 @@ void clusterHandleReplicaFailover(void) { } /* Return ASAP if we can't still start the election. */ - if (mstime() < server.cluster->failover_auth_time) { + if (now < server.cluster->failover_auth_time) { clusterLogCantFailover(CLUSTER_CANT_FAILOVER_WAITING_DELAY); return; } @@ -4872,6 +4914,8 @@ static int clusterNodeCronHandleReconnect(clusterNode *node, mstime_t handshake_ /* A Node in HANDSHAKE state has a limited lifespan equal to the * configured node timeout. */ if (nodeInHandshake(node) && now - node->ctime > handshake_timeout) { + serverLog(LL_WARNING, "Clusterbus handshake timeout %s:%d after %lldms", node->ip, + node->cport, handshake_timeout); clusterDelNode(node); return 1; } @@ -6043,8 +6087,11 @@ void removeChannelsInSlot(unsigned int slot) { /* Remove all the keys in the specified hash slot. * The number of removed items is returned. */ unsigned int delKeysInSlot(unsigned int hashslot) { - if (!kvstoreDictSize(server.db->keys, hashslot)) return 0; + if (!countKeysInSlot(hashslot)) return 0; + /* We may lose a slot during the pause. We need to track this + * state so that we don't assert in propagateNow(). */ + server.server_del_keys_in_slot = 1; unsigned int j = 0; kvstoreDictIterator *kvs_di = NULL; @@ -6069,6 +6116,8 @@ unsigned int delKeysInSlot(unsigned int hashslot) { } kvstoreReleaseDictIterator(kvs_di); + server.server_del_keys_in_slot = 0; + serverAssert(server.execution_nesting == 0); return j; } diff --git a/src/cluster_legacy.h b/src/cluster_legacy.h index 5280644e6e..2c3e1d83c8 100644 --- a/src/cluster_legacy.h +++ b/src/cluster_legacy.h @@ -338,7 +338,8 @@ struct _clusterNode { mstime_t pong_received; /* Unix time we received the pong */ mstime_t data_received; /* Unix time we received any data */ mstime_t fail_time; /* Unix time when FAIL flag was set */ - mstime_t voted_time; /* Last time we voted for a replica of this primary */ + mstime_t voted_time; /* Last time we voted for a replica of this primary in non manual + * failover scenarios. */ mstime_t repl_offset_time; /* Unix time we received offset for this node */ mstime_t orphaned_time; /* Starting time of orphaned primary condition */ long long repl_offset; /* Last known repl offset for this node. */ diff --git a/src/commands.def b/src/commands.def index 791b30d540..ecc77126af 100644 --- a/src/commands.def +++ b/src/commands.def @@ -1230,6 +1230,34 @@ struct COMMAND_ARG CLIENT_CAPA_Args[] = { #define CLIENT_ID_Keyspecs NULL #endif +/********** CLIENT IMPORT_SOURCE ********************/ + +#ifndef SKIP_CMD_HISTORY_TABLE +/* CLIENT IMPORT_SOURCE history */ +#define CLIENT_IMPORT_SOURCE_History NULL +#endif + +#ifndef SKIP_CMD_TIPS_TABLE +/* CLIENT IMPORT_SOURCE tips */ +#define CLIENT_IMPORT_SOURCE_Tips NULL +#endif + +#ifndef SKIP_CMD_KEY_SPECS_TABLE +/* CLIENT IMPORT_SOURCE key specs */ +#define CLIENT_IMPORT_SOURCE_Keyspecs NULL +#endif + +/* CLIENT IMPORT_SOURCE enabled argument table */ +struct COMMAND_ARG CLIENT_IMPORT_SOURCE_enabled_Subargs[] = { +{MAKE_ARG("on",ARG_TYPE_PURE_TOKEN,-1,"ON",NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{MAKE_ARG("off",ARG_TYPE_PURE_TOKEN,-1,"OFF",NULL,NULL,CMD_ARG_NONE,0,NULL)}, +}; + +/* CLIENT IMPORT_SOURCE argument table */ +struct COMMAND_ARG CLIENT_IMPORT_SOURCE_Args[] = { +{MAKE_ARG("enabled",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_NONE,2,NULL),.subargs=CLIENT_IMPORT_SOURCE_enabled_Subargs}, +}; + /********** CLIENT INFO ********************/ #ifndef SKIP_CMD_HISTORY_TABLE @@ -1630,6 +1658,7 @@ struct COMMAND_STRUCT CLIENT_Subcommands[] = { {MAKE_CMD("getredir","Returns the client ID to which the connection's tracking notifications are redirected.","O(1)","6.0.0",CMD_DOC_NONE,NULL,NULL,"connection",COMMAND_GROUP_CONNECTION,CLIENT_GETREDIR_History,0,CLIENT_GETREDIR_Tips,0,clientCommand,2,CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,CLIENT_GETREDIR_Keyspecs,0,NULL,0)}, {MAKE_CMD("help","Returns helpful text about the different subcommands.","O(1)","5.0.0",CMD_DOC_NONE,NULL,NULL,"connection",COMMAND_GROUP_CONNECTION,CLIENT_HELP_History,0,CLIENT_HELP_Tips,0,clientCommand,2,CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,CLIENT_HELP_Keyspecs,0,NULL,0)}, {MAKE_CMD("id","Returns the unique client ID of the connection.","O(1)","5.0.0",CMD_DOC_NONE,NULL,NULL,"connection",COMMAND_GROUP_CONNECTION,CLIENT_ID_History,0,CLIENT_ID_Tips,0,clientCommand,2,CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,CLIENT_ID_Keyspecs,0,NULL,0)}, +{MAKE_CMD("import-source","Mark this client as an import source when server is in import mode.","O(1)","8.1.0",CMD_DOC_NONE,NULL,NULL,"connection",COMMAND_GROUP_CONNECTION,CLIENT_IMPORT_SOURCE_History,0,CLIENT_IMPORT_SOURCE_Tips,0,clientCommand,3,CMD_NOSCRIPT|CMD_LOADING|CMD_STALE,ACL_CATEGORY_CONNECTION,CLIENT_IMPORT_SOURCE_Keyspecs,0,NULL,1),.args=CLIENT_IMPORT_SOURCE_Args}, {MAKE_CMD("info","Returns information about the connection.","O(1)","6.2.0",CMD_DOC_NONE,NULL,NULL,"connection",COMMAND_GROUP_CONNECTION,CLIENT_INFO_History,0,CLIENT_INFO_Tips,1,clientCommand,2,CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,CLIENT_INFO_Keyspecs,0,NULL,0)}, {MAKE_CMD("kill","Terminates open connections.","O(N) where N is the number of client connections","2.4.0",CMD_DOC_NONE,NULL,NULL,"connection",COMMAND_GROUP_CONNECTION,CLIENT_KILL_History,7,CLIENT_KILL_Tips,0,clientCommand,-3,CMD_ADMIN|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,CLIENT_KILL_Keyspecs,0,NULL,1),.args=CLIENT_KILL_Args}, {MAKE_CMD("list","Lists open connections.","O(N) where N is the number of client connections","2.4.0",CMD_DOC_NONE,NULL,NULL,"connection",COMMAND_GROUP_CONNECTION,CLIENT_LIST_History,7,CLIENT_LIST_Tips,1,clientCommand,-2,CMD_ADMIN|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,ACL_CATEGORY_CONNECTION,CLIENT_LIST_Keyspecs,0,NULL,2),.args=CLIENT_LIST_Args}, diff --git a/src/commands/client-import-source.json b/src/commands/client-import-source.json new file mode 100644 index 0000000000..113c07d70a --- /dev/null +++ b/src/commands/client-import-source.json @@ -0,0 +1,40 @@ +{ + "IMPORT-SOURCE": { + "summary": "Mark this client as an import source when server is in import mode.", + "complexity": "O(1)", + "group": "connection", + "since": "8.1.0", + "arity": 3, + "container": "CLIENT", + "function": "clientCommand", + "command_flags": [ + "NOSCRIPT", + "LOADING", + "STALE" + ], + "acl_categories": [ + "CONNECTION" + ], + "reply_schema": { + "const": "OK" + }, + "arguments": [ + { + "name": "enabled", + "type": "oneof", + "arguments": [ + { + "name": "on", + "type": "pure-token", + "token": "ON" + }, + { + "name": "off", + "type": "pure-token", + "token": "OFF" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/src/config.c b/src/config.c index f718543c39..c4009adefa 100644 --- a/src/config.c +++ b/src/config.c @@ -1013,15 +1013,14 @@ void configGetCommand(client *c) { #define CONFIG_REWRITE_SIGNATURE "# Generated by CONFIG REWRITE" -/* We use the following dictionary type to store where a configuration - * option is mentioned in the old configuration file, so it's - * like "maxmemory" -> list of line numbers (first line is zero). */ -void dictListDestructor(dict *d, void *val); - /* Sentinel config rewriting is implemented inside sentinel.c by * rewriteConfigSentinelOption(). */ void rewriteConfigSentinelOption(struct rewriteConfigState *state); +/* We use the following dictionary type to store where a configuration + * option is mentioned in the old configuration file, so it's + * like "maxmemory" -> list of line numbers (first line is zero). + */ dictType optionToLineDictType = { dictSdsCaseHash, /* hash function */ NULL, /* key dup */ @@ -3140,6 +3139,7 @@ standardConfig static_configs[] = { createBoolConfig("enable-debug-assert", NULL, IMMUTABLE_CONFIG | HIDDEN_CONFIG, server.enable_debug_assert, 0, NULL, NULL), createBoolConfig("cluster-slot-stats-enabled", NULL, MODIFIABLE_CONFIG, server.cluster_slot_stats_enabled, 0, NULL, NULL), createBoolConfig("hide-user-data-from-log", NULL, MODIFIABLE_CONFIG, server.hide_user_data_from_log, 1, NULL, NULL), + createBoolConfig("import-mode", NULL, MODIFIABLE_CONFIG, server.import_mode, 0, NULL, NULL), /* String Configs */ createStringConfig("aclfile", NULL, IMMUTABLE_CONFIG, ALLOW_EMPTY_STRING, server.acl_filename, "", NULL, NULL), diff --git a/src/db.c b/src/db.c index ceb3105f9b..10d4a04091 100644 --- a/src/db.c +++ b/src/db.c @@ -385,7 +385,7 @@ robj *dbRandomKey(serverDb *db) { key = dictGetKey(de); keyobj = createStringObject(key, sdslen(key)); if (dbFindExpiresWithDictIndex(db, key, randomDictIndex)) { - if (allvolatile && server.primary_host && --maxtries == 0) { + if (allvolatile && (server.primary_host || server.import_mode) && --maxtries == 0) { /* If the DB is composed only of keys with an expire set, * it could happen that all the keys are already logically * expired in the repilca, so the function cannot stop because @@ -830,11 +830,6 @@ void keysCommand(client *c) { kvstoreDictIterator *kvs_di = NULL; kvstoreIterator *kvs_it = NULL; if (pslot != -1) { - if (!kvstoreDictSize(c->db->keys, pslot)) { - /* Requested slot is empty */ - setDeferredArrayLen(c, replylen, 0); - return; - } kvs_di = kvstoreGetDictSafeIterator(c->db->keys, pslot); } else { kvs_it = kvstoreIteratorInit(c->db->keys); @@ -1826,6 +1821,25 @@ keyStatus expireIfNeededWithDictIndex(serverDb *db, robj *key, int flags, int di if (server.primary_host != NULL) { if (server.current_client && (server.current_client->flag.primary)) return KEY_VALID; if (!(flags & EXPIRE_FORCE_DELETE_EXPIRED)) return KEY_EXPIRED; + } else if (server.import_mode) { + /* If we are running in the import mode on a primary, instead of + * evicting the expired key from the database, we return ASAP: + * the key expiration is controlled by the import source that will + * send us synthesized DEL operations for expired keys. The + * exception is when write operations are performed on this server + * because it's a primary. + * + * Notice: other clients, apart from the import source, should not access + * the data imported by import source. + * + * Still we try to return the right information to the caller, + * that is, KEY_VALID if we think the key should still be valid, + * KEY_EXPIRED if we think the key is expired but don't want to delete it at this time. + * + * When receiving commands from the import source, keys are never considered + * expired. */ + if (server.current_client && (server.current_client->flag.import_source)) return KEY_VALID; + if (!(flags & EXPIRE_FORCE_DELETE_EXPIRED)) return KEY_EXPIRED; } /* In some cases we're explicitly instructed to return an indication of a @@ -1889,7 +1903,7 @@ keyStatus expireIfNeeded(serverDb *db, robj *key, int flags) { * The purpose is to skip expansion of unused dicts in cluster mode (all * dicts not mapped to *my* slots) */ static int dbExpandSkipSlot(int slot) { - return !clusterNodeCoversSlot(getMyClusterNode(), slot); + return !clusterNodeCoversSlot(clusterNodeGetPrimary(getMyClusterNode()), slot); } /* diff --git a/src/debug.c b/src/debug.c index d221a884ee..13da7bcc93 100644 --- a/src/debug.c +++ b/src/debug.c @@ -432,7 +432,7 @@ void debugCommand(client *c) { " Some fields of the default behavior may be time consuming to fetch,", " and `fast` can be passed to avoid fetching them.", "DROP-CLUSTER-PACKET-FILTER ", - " Drop all packets that match the filtered type. Set to -1 allow all packets.", + " Drop all packets that match the filtered type. Set to -1 allow all packets or -2 to drop all packets.", "CLOSE-CLUSTER-LINK-ON-PACKET-DROP <0|1>", " This is valid only when DROP-CLUSTER-PACKET-FILTER is set to a valid packet type.", " When set to 1, the cluster link is closed after dropping a packet based on the filter.", diff --git a/src/dict.c b/src/dict.c index f164820584..48c0f815bb 100644 --- a/src/dict.c +++ b/src/dict.c @@ -576,7 +576,7 @@ dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing) { if (!position) return NULL; /* Dup the key if necessary. */ - if (d->type->keyDup) key = d->type->keyDup(d, key); + if (d->type->keyDup) key = d->type->keyDup(key); return dictInsertAtPosition(d, key, position); } @@ -640,7 +640,7 @@ int dictReplace(dict *d, void *key, void *val) { * reverse. */ void *oldval = dictGetVal(existing); dictSetVal(d, existing, val); - if (d->type->valDestructor) d->type->valDestructor(d, oldval); + if (d->type->valDestructor) d->type->valDestructor(oldval); return 0; } @@ -742,6 +742,18 @@ dictEntry *dictUnlink(dict *d, const void *key) { return dictGenericDelete(d, key, 1); } +inline static void dictFreeKey(dict *d, dictEntry *entry) { + if (d->type->keyDestructor) { + d->type->keyDestructor(dictGetKey(entry)); + } +} + +inline static void dictFreeVal(dict *d, dictEntry *entry) { + if (d->type->valDestructor) { + d->type->valDestructor(dictGetVal(entry)); + } +} + /* You need to call this function to really free the entry after a call * to dictUnlink(). It's safe to call this function with 'he' = NULL. */ void dictFreeUnlinkedEntry(dict *d, dictEntry *he) { @@ -919,7 +931,7 @@ void dictTwoPhaseUnlinkFree(dict *d, dictEntry *he, dictEntry **plink, int table : (entryIsEmbedded(de) ? &decodeEntryEmbedded(de)->field : (panic("Entry type not supported"), NULL))) void dictSetKey(dict *d, dictEntry *de, void *key) { - void *k = d->type->keyDup ? d->type->keyDup(d, key) : key; + void *k = d->type->keyDup ? d->type->keyDup(key) : key; if (entryIsNormal(de)) { dictEntryNormal *_de = decodeEntryNormal(de); _de->key = k; diff --git a/src/dict.h b/src/dict.h index 1c9e059baa..88ebd7bf99 100644 --- a/src/dict.h +++ b/src/dict.h @@ -53,10 +53,10 @@ typedef struct dict dict; typedef struct dictType { /* Callbacks */ uint64_t (*hashFunction)(const void *key); - void *(*keyDup)(dict *d, const void *key); - int (*keyCompare)(dict *d, const void *key1, const void *key2); - void (*keyDestructor)(dict *d, void *key); - void (*valDestructor)(dict *d, void *obj); + void *(*keyDup)(const void *key); + int (*keyCompare)(const void *key1, const void *key2); + void (*keyDestructor)(void *key); + void (*valDestructor)(void *obj); int (*resizeAllowed)(size_t moreMem, double usedRatio); /* Invoked at the start of dict initialization/rehashing (old and new ht are already created) */ void (*rehashingStarted)(dict *d); @@ -144,16 +144,13 @@ typedef struct { #define DICT_HT_INITIAL_SIZE (1 << (DICT_HT_INITIAL_EXP)) /* ------------------------------- Macros ------------------------------------*/ -#define dictFreeVal(d, entry) \ - do { \ - if ((d)->type->valDestructor) (d)->type->valDestructor((d), dictGetVal(entry)); \ - } while (0) - -#define dictFreeKey(d, entry) \ - if ((d)->type->keyDestructor) (d)->type->keyDestructor((d), dictGetKey(entry)) - -#define dictCompareKeys(d, key1, key2) \ - (((d)->type->keyCompare) ? (d)->type->keyCompare((d), key1, key2) : (key1) == (key2)) +static inline int dictCompareKeys(dict *d, const void *key1, const void *key2) { + if (d->type->keyCompare) { + return d->type->keyCompare(key1, key2); + } else { + return (key1 == key2); + } +} #define dictMetadata(d) (&(d)->metadata) #define dictMetadataSize(d) ((d)->type->dictMetadataBytes ? (d)->type->dictMetadataBytes(d) : 0) diff --git a/src/eval.c b/src/eval.c index fd12e40ad2..a9c50cdf90 100644 --- a/src/eval.c +++ b/src/eval.c @@ -57,8 +57,7 @@ void evalGenericCommandWithDebugging(client *c, int evalsha); sds ldbCatStackValue(sds s, lua_State *lua, int idx); listNode *luaScriptsLRUAdd(client *c, sds sha, int evalsha); -static void dictLuaScriptDestructor(dict *d, void *val) { - UNUSED(d); +static void dictLuaScriptDestructor(void *val) { if (val == NULL) return; /* Lazy freeing will set value to NULL. */ decrRefCount(((luaScript *)val)->body); zfree(val); @@ -200,10 +199,12 @@ void scriptingInit(int setup) { } /* Initialize a dictionary we use to map SHAs to scripts. - * Initialize a list we use for lua script evictions, it shares the - * sha with the dictionary, so free fn is not set. */ + * Initialize a list we use for lua script evictions. + * Note that we duplicate the sha when adding to the lru list due to defrag, + * and we need to free them respectively. */ lctx.lua_scripts = dictCreate(&shaScriptObjectDictType); lctx.lua_scripts_lru_list = listCreate(); + listSetFreeMethod(lctx.lua_scripts_lru_list, (void (*)(void *))sdsfree); lctx.lua_scripts_mem = 0; luaRegisterServerAPI(lua); @@ -519,9 +520,6 @@ void luaDeleteFunction(client *c, sds sha) { dictEntry *de = dictUnlink(lctx.lua_scripts, sha); serverAssertWithInfo(c ? c : lctx.lua_client, NULL, de); luaScript *l = dictGetVal(de); - /* We only delete `EVAL` scripts, which must exist in the LRU list. */ - serverAssert(l->node); - listDelNode(lctx.lua_scripts_lru_list, l->node); lctx.lua_scripts_mem -= sdsAllocSize(sha) + getStringObjectSdsUsedMemory(l->body); dictFreeUnlinkedEntry(lctx.lua_scripts, de); } @@ -550,11 +548,12 @@ listNode *luaScriptsLRUAdd(client *c, sds sha, int evalsha) { listNode *ln = listFirst(lctx.lua_scripts_lru_list); sds oldest = listNodeValue(ln); luaDeleteFunction(c, oldest); + listDelNode(lctx.lua_scripts_lru_list, ln); server.stat_evictedscripts++; } /* Add current. */ - listAddNodeTail(lctx.lua_scripts_lru_list, sha); + listAddNodeTail(lctx.lua_scripts_lru_list, sdsdup(sha)); return listLast(lctx.lua_scripts_lru_list); } diff --git a/src/evict.c b/src/evict.c index 5e4b6220eb..5208328b32 100644 --- a/src/evict.c +++ b/src/evict.c @@ -546,8 +546,8 @@ int performEvictions(void) { goto update_metrics; } - if (server.maxmemory_policy == MAXMEMORY_NO_EVICTION) { - result = EVICT_FAIL; /* We need to free memory, but policy forbids. */ + if (server.maxmemory_policy == MAXMEMORY_NO_EVICTION || (iAmPrimary() && server.import_mode)) { + result = EVICT_FAIL; /* We need to free memory, but policy forbids or we are in import mode. */ goto update_metrics; } diff --git a/src/expire.c b/src/expire.c index 928bb58d86..c22df1ef86 100644 --- a/src/expire.c +++ b/src/expire.c @@ -520,8 +520,11 @@ int checkAlreadyExpired(long long when) { * of a replica instance. * * Instead we add the already expired key to the database with expire time - * (possibly in the past) and wait for an explicit DEL from the primary. */ - return (when <= commandTimeSnapshot() && !server.loading && !server.primary_host); + * (possibly in the past) and wait for an explicit DEL from the primary. + * + * If the server is a primary and in the import mode, we also add the already + * expired key and wait for an explicit DEL from the import source. */ + return (when <= commandTimeSnapshot() && !server.loading && !server.primary_host && !server.import_mode); } #define EXPIRE_NX (1 << 0) diff --git a/src/functions.c b/src/functions.c index 268c90dbc8..d5bab4128b 100644 --- a/src/functions.c +++ b/src/functions.c @@ -43,9 +43,9 @@ typedef enum { static size_t engine_cache_memory = 0; /* Forward declaration */ -static void engineFunctionDispose(dict *d, void *obj); -static void engineStatsDispose(dict *d, void *obj); -static void engineLibraryDispose(dict *d, void *obj); +static void engineFunctionDispose(void *obj); +static void engineStatsDispose(void *obj); +static void engineLibraryDispose(void *obj); static int functionsVerifyName(sds name); typedef struct functionsLibEngineStats { @@ -126,15 +126,13 @@ static size_t libraryMallocSize(functionLibInfo *li) { return zmalloc_size(li) + sdsAllocSize(li->name) + sdsAllocSize(li->code); } -static void engineStatsDispose(dict *d, void *obj) { - UNUSED(d); +static void engineStatsDispose(void *obj) { functionsLibEngineStats *stats = obj; zfree(stats); } /* Dispose function memory */ -static void engineFunctionDispose(dict *d, void *obj) { - UNUSED(d); +static void engineFunctionDispose(void *obj) { if (!obj) { return; } @@ -158,8 +156,7 @@ static void engineLibraryFree(functionLibInfo *li) { zfree(li); } -static void engineLibraryDispose(dict *d, void *obj) { - UNUSED(d); +static void engineLibraryDispose(void *obj) { engineLibraryFree(obj); } @@ -175,7 +172,7 @@ void functionsLibCtxClear(functionsLibCtx *lib_ctx) { stats->n_lib = 0; } dictReleaseIterator(iter); - curr_functions_lib_ctx->cache_memory = 0; + lib_ctx->cache_memory = 0; } void functionsLibCtxClearCurrent(int async) { diff --git a/src/kvstore.c b/src/kvstore.c index 7142fa0f61..49662f330a 100644 --- a/src/kvstore.c +++ b/src/kvstore.c @@ -423,9 +423,11 @@ unsigned long long kvstoreScan(kvstore *kvs, * `dictTryExpand` call and in case of `dictExpand` call it signifies no expansion was performed. */ int kvstoreExpand(kvstore *kvs, uint64_t newsize, int try_expand, kvstoreExpandShouldSkipDictIndex *skip_cb) { + if (newsize == 0) return 1; for (int i = 0; i < kvs->num_dicts; i++) { - dict *d = kvstoreGetDict(kvs, i); - if (!d || (skip_cb && skip_cb(i))) continue; + if (skip_cb && skip_cb(i)) continue; + /* If the dictionary doesn't exist, create it */ + dict *d = createDictIfNeeded(kvs, i); int result = try_expand ? dictTryExpand(d, newsize) : dictExpand(d, newsize); if (try_expand && result == DICT_ERR) return 0; } diff --git a/src/latency.c b/src/latency.c index eef1532d03..783f04b197 100644 --- a/src/latency.c +++ b/src/latency.c @@ -37,8 +37,7 @@ #include "hdr_histogram.h" /* Dictionary type for latency events. */ -int dictStringKeyCompare(dict *d, const void *key1, const void *key2) { - UNUSED(d); +int dictStringKeyCompare(const void *key1, const void *key2) { return strcmp(key1, key2) == 0; } diff --git a/src/module.c b/src/module.c index 2884239200..1e98b36f30 100644 --- a/src/module.c +++ b/src/module.c @@ -11814,8 +11814,7 @@ uint64_t dictCStringKeyHash(const void *key) { return dictGenHashFunction((unsigned char *)key, strlen((char *)key)); } -int dictCStringKeyCompare(dict *d, const void *key1, const void *key2) { - UNUSED(d); +int dictCStringKeyCompare(const void *key1, const void *key2) { return strcmp(key1, key2) == 0; } diff --git a/src/modules/CMakeLists.txt b/src/modules/CMakeLists.txt new file mode 100644 index 0000000000..958796232f --- /dev/null +++ b/src/modules/CMakeLists.txt @@ -0,0 +1,21 @@ +# Build modules +list(APPEND MODULES_LIST "helloacl") +list(APPEND MODULES_LIST "helloblock") +list(APPEND MODULES_LIST "hellocluster") +list(APPEND MODULES_LIST "hellodict") +list(APPEND MODULES_LIST "hellohook") +list(APPEND MODULES_LIST "hellotimer") +list(APPEND MODULES_LIST "hellotype") +list(APPEND MODULES_LIST "helloworld") + +foreach (MODULE_NAME ${MODULES_LIST}) + message(STATUS "Building module: ${MODULE_NAME}") + add_library(${MODULE_NAME} SHARED "${CMAKE_CURRENT_LIST_DIR}/${MODULE_NAME}.c") + target_include_directories(${MODULE_NAME} PRIVATE "${CMAKE_SOURCE_DIR}/src") + set_target_properties(${MODULE_NAME} PROPERTIES PREFIX "") + valkey_install_bin(${MODULE_NAME}) + if (APPLE) + # Some symbols can only be resolved during runtime (they exist in the executable) + target_link_options(${MODULE_NAME} PRIVATE -undefined dynamic_lookup) + endif () +endforeach () diff --git a/src/networking.c b/src/networking.c index 96dd05d505..9558780f39 100644 --- a/src/networking.c +++ b/src/networking.c @@ -1555,12 +1555,17 @@ void unlinkClient(client *c) { * in which case it needs to be cleaned from that list */ if (c->flag.replica && c->repl_state == REPLICA_STATE_WAIT_BGSAVE_END && server.rdb_pipe_conns) { int i; + int still_alive = 0; for (i = 0; i < server.rdb_pipe_numconns; i++) { if (server.rdb_pipe_conns[i] == c->conn) { rdbPipeWriteHandlerConnRemoved(c->conn); server.rdb_pipe_conns[i] = NULL; - break; } + if (server.rdb_pipe_conns[i]) still_alive++; + } + if (still_alive == 0) { + serverLog(LL_NOTICE, "Diskless rdb transfer, last replica dropped, killing fork child."); + killRDBChild(); } } /* Only use shutdown when the fork is active and we are the parent. */ @@ -1731,6 +1736,7 @@ void freeClient(client *c) { /* UNWATCH all the keys */ unwatchAllKeys(c); listRelease(c->watched_keys); + c->watched_keys = NULL; /* Unsubscribe from all the pubsub channels */ pubsubUnsubscribeAllChannels(c, 0); @@ -1738,16 +1744,22 @@ void freeClient(client *c) { pubsubUnsubscribeAllPatterns(c, 0); unmarkClientAsPubSub(c); dictRelease(c->pubsub_channels); + c->pubsub_channels = NULL; dictRelease(c->pubsub_patterns); + c->pubsub_patterns = NULL; dictRelease(c->pubsubshard_channels); + c->pubsubshard_channels = NULL; /* Free data structures. */ listRelease(c->reply); + c->reply = NULL; zfree(c->buf); + c->buf = NULL; freeReplicaReferencedReplBuffer(c); freeClientArgv(c); freeClientOriginalArgv(c); if (c->deferred_reply_errors) listRelease(c->deferred_reply_errors); + c->deferred_reply_errors = NULL; #ifdef LOG_REQ_RES reqresReset(c, 1); #endif @@ -1774,6 +1786,7 @@ void freeClient(client *c) { if (server.saveparamslen == 0 && c->repl_state == REPLICA_STATE_WAIT_BGSAVE_END && server.child_type == CHILD_TYPE_RDB && server.rdb_child_type == RDB_CHILD_TYPE_DISK && anyOtherReplicaWaitRdb(c) == 0) { + serverLog(LL_NOTICE, "Background saving, persistence disabled, last replica dropped, killing fork child."); killRDBChild(); } if (c->repl_state == REPLICA_STATE_SEND_BULK) { @@ -3572,6 +3585,10 @@ void clientCommand(client *c) { " Protect current client connection from eviction.", "NO-TOUCH (ON|OFF)", " Will not touch LRU/LFU stats when this mode is on.", + "IMPORT-SOURCE (ON|OFF)", + " Mark this connection as an import source if server.import_mode is true.", + " Sync tools can set their connections into 'import-source' state to visit", + " expired keys.", NULL}; addReplyHelp(c, help); } else if (!strcasecmp(c->argv[1]->ptr, "id") && c->argc == 2) { @@ -4045,6 +4062,22 @@ void clientCommand(client *c) { } } addReply(c, shared.ok); + } else if (!strcasecmp(c->argv[1]->ptr, "import-source")) { + /* CLIENT IMPORT-SOURCE ON|OFF */ + if (!server.import_mode) { + addReplyError(c, "Server is not in import mode"); + return; + } + if (!strcasecmp(c->argv[2]->ptr, "on")) { + c->flag.import_source = 1; + addReply(c, shared.ok); + } else if (!strcasecmp(c->argv[2]->ptr, "off")) { + c->flag.import_source = 0; + addReply(c, shared.ok); + } else { + addReplyErrorObject(c, shared.syntaxerr); + return; + } } else { addReplySubcommandSyntaxError(c); } @@ -4558,7 +4591,7 @@ static void pauseClientsByClient(mstime_t endTime, int isPauseClientAll) { } /* Pause actions up to the specified unixtime (in ms) for a given type of - * commands. + * purpose. * * A main use case of this function is to allow pausing replication traffic * so that a failover without data loss to occur. Replicas will continue to receive diff --git a/src/quicklist.c b/src/quicklist.c index 617d21cd8c..225fac6fdf 100644 --- a/src/quicklist.c +++ b/src/quicklist.c @@ -210,9 +210,7 @@ void quicklistRelease(quicklist *quicklist) { * Returns 1 if listpack compressed successfully. * Returns 0 if compression failed or if listpack too small to compress. */ static int __quicklistCompressNode(quicklistNode *node) { -#ifdef SERVER_TEST node->attempted_compress = 1; -#endif if (node->dont_compress) return 0; /* validate that the node is neither @@ -250,9 +248,7 @@ static int __quicklistCompressNode(quicklistNode *node) { /* Uncompress the listpack in 'node' and update encoding details. * Returns 1 on successful decode, 0 on failure to decode. */ static int __quicklistDecompressNode(quicklistNode *node) { -#ifdef SERVER_TEST node->attempted_compress = 0; -#endif node->recompress = 0; void *decompressed = zmalloc(node->sz); @@ -1692,1419 +1688,3 @@ void quicklistBookmarksClear(quicklist *ql) { /* NOTE: We do not shrink (realloc) the quick list. main use case for this * function is just before releasing the allocation. */ } - -/* The rest of this file is test cases and test helpers. */ -#ifdef SERVER_TEST -#include -#include -#include "testhelp.h" -#include - -#define yell(str, ...) printf("ERROR! " str "\n\n", __VA_ARGS__) - -#define ERROR \ - do { \ - printf("\tERROR!\n"); \ - err++; \ - } while (0) - -#define ERR(x, ...) \ - do { \ - printf("%s:%s:%d:\t", __FILE__, __func__, __LINE__); \ - printf("ERROR! " x "\n", __VA_ARGS__); \ - err++; \ - } while (0) - -#define TEST(name) printf("test — %s\n", name); -#define TEST_DESC(name, ...) printf("test — " name "\n", __VA_ARGS__); - -#define QL_TEST_VERBOSE 0 - -#define UNUSED(x) (void)(x) -static void ql_info(quicklist *ql) { -#if QL_TEST_VERBOSE - printf("Container length: %lu\n", ql->len); - printf("Container size: %lu\n", ql->count); - if (ql->head) printf("\t(zsize head: %lu)\n", lpLength(ql->head->entry)); - if (ql->tail) printf("\t(zsize tail: %lu)\n", lpLength(ql->tail->entry)); - printf("\n"); -#else - UNUSED(ql); -#endif -} - -/* Return the UNIX time in microseconds */ -static long long ustime(void) { - struct timeval tv; - long long ust; - - gettimeofday(&tv, NULL); - ust = ((long long)tv.tv_sec) * 1000000; - ust += tv.tv_usec; - return ust; -} - -/* Return the UNIX time in milliseconds */ -static long long mstime(void) { - return ustime() / 1000; -} - -/* Iterate over an entire quicklist. - * Print the list if 'print' == 1. - * - * Returns physical count of elements found by iterating over the list. */ -static int _itrprintr(quicklist *ql, int print, int forward) { - quicklistIter *iter = quicklistGetIterator(ql, forward ? AL_START_HEAD : AL_START_TAIL); - quicklistEntry entry; - int i = 0; - int p = 0; - quicklistNode *prev = NULL; - while (quicklistNext(iter, &entry)) { - if (entry.node != prev) { - /* Count the number of list nodes too */ - p++; - prev = entry.node; - } - if (print) { - int size = (entry.sz > (1 << 20)) ? 1 << 20 : entry.sz; - printf("[%3d (%2d)]: [%.*s] (%lld)\n", i, p, size, (char *)entry.value, entry.longval); - } - i++; - } - quicklistReleaseIterator(iter); - return i; -} -static int itrprintr(quicklist *ql, int print) { - return _itrprintr(ql, print, 1); -} - -static int itrprintr_rev(quicklist *ql, int print) { - return _itrprintr(ql, print, 0); -} - -#define ql_verify(a, b, c, d, e) \ - do { \ - err += _ql_verify((a), (b), (c), (d), (e)); \ - } while (0) - -static int _ql_verify_compress(quicklist *ql) { - int errors = 0; - if (quicklistAllowsCompression(ql)) { - quicklistNode *node = ql->head; - unsigned int low_raw = ql->compress; - unsigned int high_raw = ql->len - ql->compress; - - for (unsigned int at = 0; at < ql->len; at++, node = node->next) { - if (node && (at < low_raw || at >= high_raw)) { - if (node->encoding != QUICKLIST_NODE_ENCODING_RAW) { - yell("Incorrect compression: node %d is " - "compressed at depth %d ((%u, %u); total " - "nodes: %lu; size: %zu; recompress: %d)", - at, ql->compress, low_raw, high_raw, ql->len, node->sz, node->recompress); - errors++; - } - } else { - if (node->encoding != QUICKLIST_NODE_ENCODING_LZF && !node->attempted_compress) { - yell("Incorrect non-compression: node %d is NOT " - "compressed at depth %d ((%u, %u); total " - "nodes: %lu; size: %zu; recompress: %d; attempted: %d)", - at, ql->compress, low_raw, high_raw, ql->len, node->sz, node->recompress, - node->attempted_compress); - errors++; - } - } - } - } - return errors; -} - -/* Verify list metadata matches physical list contents. */ -static int _ql_verify(quicklist *ql, uint32_t len, uint32_t count, uint32_t head_count, uint32_t tail_count) { - int errors = 0; - - ql_info(ql); - if (len != ql->len) { - yell("quicklist length wrong: expected %d, got %lu", len, ql->len); - errors++; - } - - if (count != ql->count) { - yell("quicklist count wrong: expected %d, got %lu", count, ql->count); - errors++; - } - - int loopr = itrprintr(ql, 0); - if (loopr != (int)ql->count) { - yell("quicklist cached count not match actual count: expected %lu, got " - "%d", - ql->count, loopr); - errors++; - } - - int rloopr = itrprintr_rev(ql, 0); - if (loopr != rloopr) { - yell("quicklist has different forward count than reverse count! " - "Forward count is %d, reverse count is %d.", - loopr, rloopr); - errors++; - } - - if (ql->len == 0 && !errors) { - return errors; - } - - if (ql->head && head_count != ql->head->count && head_count != lpLength(ql->head->entry)) { - yell("quicklist head count wrong: expected %d, " - "got cached %d vs. actual %lu", - head_count, ql->head->count, lpLength(ql->head->entry)); - errors++; - } - - if (ql->tail && tail_count != ql->tail->count && tail_count != lpLength(ql->tail->entry)) { - yell("quicklist tail count wrong: expected %d, " - "got cached %u vs. actual %lu", - tail_count, ql->tail->count, lpLength(ql->tail->entry)); - errors++; - } - - errors += _ql_verify_compress(ql); - return errors; -} - -/* Release iterator and verify compress correctly. */ -static void ql_release_iterator(quicklistIter *iter) { - quicklist *ql = NULL; - if (iter) ql = iter->quicklist; - quicklistReleaseIterator(iter); - if (ql) assert(!_ql_verify_compress(ql)); -} - -/* Generate new string concatenating integer i against string 'prefix' */ -static char *genstr(char *prefix, int i) { - static char result[64] = {0}; - snprintf(result, sizeof(result), "%s%d", prefix, i); - return result; -} - -static void randstring(unsigned char *target, size_t sz) { - size_t p = 0; - int minval, maxval; - switch (rand() % 3) { - case 0: - minval = 'a'; - maxval = 'z'; - break; - case 1: - minval = '0'; - maxval = '9'; - break; - case 2: - minval = 'A'; - maxval = 'Z'; - break; - default: assert(NULL); - } - - while (p < sz) target[p++] = minval + rand() % (maxval - minval + 1); -} - -/* main test, but callable from other files */ -int quicklistTest(int argc, char *argv[], int flags) { - UNUSED(argc); - UNUSED(argv); - - int accurate = (flags & TEST_ACCURATE); - unsigned int err = 0; - int optimize_start = -(int)(sizeof(optimization_level) / sizeof(*optimization_level)); - - printf("Starting optimization offset at: %d\n", optimize_start); - - int options[] = {0, 1, 2, 3, 4, 5, 6, 10}; - int fills[] = {-5, -4, -3, -2, -1, 0, 1, 2, 32, 66, 128, 999}; - size_t option_count = sizeof(options) / sizeof(*options); - int fill_count = (int)(sizeof(fills) / sizeof(*fills)); - long long runtime[option_count]; - - for (int _i = 0; _i < (int)option_count; _i++) { - printf("Testing Compression option %d\n", options[_i]); - long long start = mstime(); - quicklistIter *iter; - - TEST("create list") { - quicklist *ql = quicklistNew(-2, options[_i]); - ql_verify(ql, 0, 0, 0, 0); - quicklistRelease(ql); - } - - TEST("add to tail of empty list") { - quicklist *ql = quicklistNew(-2, options[_i]); - quicklistPushTail(ql, "hello", 6); - /* 1 for head and 1 for tail because 1 node = head = tail */ - ql_verify(ql, 1, 1, 1, 1); - quicklistRelease(ql); - } - - TEST("add to head of empty list") { - quicklist *ql = quicklistNew(-2, options[_i]); - quicklistPushHead(ql, "hello", 6); - /* 1 for head and 1 for tail because 1 node = head = tail */ - ql_verify(ql, 1, 1, 1, 1); - quicklistRelease(ql); - } - - TEST_DESC("add to tail 5x at compress %d", options[_i]) { - for (int f = 0; f < fill_count; f++) { - quicklist *ql = quicklistNew(fills[f], options[_i]); - for (int i = 0; i < 5; i++) quicklistPushTail(ql, genstr("hello", i), 32); - if (ql->count != 5) ERROR; - if (fills[f] == 32) ql_verify(ql, 1, 5, 5, 5); - quicklistRelease(ql); - } - } - - TEST_DESC("add to head 5x at compress %d", options[_i]) { - for (int f = 0; f < fill_count; f++) { - quicklist *ql = quicklistNew(fills[f], options[_i]); - for (int i = 0; i < 5; i++) quicklistPushHead(ql, genstr("hello", i), 32); - if (ql->count != 5) ERROR; - if (fills[f] == 32) ql_verify(ql, 1, 5, 5, 5); - quicklistRelease(ql); - } - } - - TEST_DESC("add to tail 500x at compress %d", options[_i]) { - for (int f = 0; f < fill_count; f++) { - quicklist *ql = quicklistNew(fills[f], options[_i]); - for (int i = 0; i < 500; i++) quicklistPushTail(ql, genstr("hello", i), 64); - if (ql->count != 500) ERROR; - if (fills[f] == 32) ql_verify(ql, 16, 500, 32, 20); - quicklistRelease(ql); - } - } - - TEST_DESC("add to head 500x at compress %d", options[_i]) { - for (int f = 0; f < fill_count; f++) { - quicklist *ql = quicklistNew(fills[f], options[_i]); - for (int i = 0; i < 500; i++) quicklistPushHead(ql, genstr("hello", i), 32); - if (ql->count != 500) ERROR; - if (fills[f] == 32) ql_verify(ql, 16, 500, 20, 32); - quicklistRelease(ql); - } - } - - TEST("rotate empty") { - quicklist *ql = quicklistNew(-2, options[_i]); - quicklistRotate(ql); - ql_verify(ql, 0, 0, 0, 0); - quicklistRelease(ql); - } - - TEST("Comprassion Plain node") { - for (int f = 0; f < fill_count; f++) { - size_t large_limit = (fills[f] < 0) ? quicklistNodeNegFillLimit(fills[f]) + 1 : SIZE_SAFETY_LIMIT + 1; - - char buf[large_limit]; - quicklist *ql = quicklistNew(fills[f], 1); - for (int i = 0; i < 500; i++) { - /* Set to 256 to allow the node to be triggered to compress, - * if it is less than 48(nocompress), the test will be successful. */ - snprintf(buf, sizeof(buf), "hello%d", i); - quicklistPushHead(ql, buf, large_limit); - } - - quicklistIter *iter = quicklistGetIterator(ql, AL_START_TAIL); - quicklistEntry entry; - int i = 0; - while (quicklistNext(iter, &entry)) { - assert(QL_NODE_IS_PLAIN(entry.node)); - snprintf(buf, sizeof(buf), "hello%d", i); - if (strcmp((char *)entry.value, buf)) - ERR("value [%s] didn't match [%s] at position %d", entry.value, buf, i); - i++; - } - ql_release_iterator(iter); - quicklistRelease(ql); - } - } - - TEST("NEXT plain node") { - for (int f = 0; f < fill_count; f++) { - size_t large_limit = (fills[f] < 0) ? quicklistNodeNegFillLimit(fills[f]) + 1 : SIZE_SAFETY_LIMIT + 1; - quicklist *ql = quicklistNew(fills[f], options[_i]); - - char buf[large_limit]; - memcpy(buf, "plain", 5); - quicklistPushHead(ql, buf, large_limit); - quicklistPushHead(ql, buf, large_limit); - quicklistPushHead(ql, "packed3", 7); - quicklistPushHead(ql, "packed4", 7); - quicklistPushHead(ql, buf, large_limit); - - quicklistEntry entry; - quicklistIter *iter = quicklistGetIterator(ql, AL_START_TAIL); - - while (quicklistNext(iter, &entry) != 0) { - if (QL_NODE_IS_PLAIN(entry.node)) - assert(!memcmp(entry.value, "plain", 5)); - else - assert(!memcmp(entry.value, "packed", 6)); - } - ql_release_iterator(iter); - quicklistRelease(ql); - } - } - - TEST("rotate plain node ") { - for (int f = 0; f < fill_count; f++) { - size_t large_limit = (fills[f] < 0) ? quicklistNodeNegFillLimit(fills[f]) + 1 : SIZE_SAFETY_LIMIT + 1; - - unsigned char *data = NULL; - size_t sz; - long long lv; - int i = 0; - quicklist *ql = quicklistNew(fills[f], options[_i]); - char buf[large_limit]; - memcpy(buf, "hello1", 6); - quicklistPushHead(ql, buf, large_limit); - memcpy(buf, "hello4", 6); - quicklistPushHead(ql, buf, large_limit); - memcpy(buf, "hello3", 6); - quicklistPushHead(ql, buf, large_limit); - memcpy(buf, "hello2", 6); - quicklistPushHead(ql, buf, large_limit); - quicklistRotate(ql); - - for (i = 1; i < 5; i++) { - assert(QL_NODE_IS_PLAIN(ql->tail)); - quicklistPop(ql, QUICKLIST_HEAD, &data, &sz, &lv); - int temp_char = data[5]; - zfree(data); - assert(temp_char == ('0' + i)); - } - - ql_verify(ql, 0, 0, 0, 0); - quicklistRelease(ql); - } - } - - TEST("rotate one val once") { - for (int f = 0; f < fill_count; f++) { - quicklist *ql = quicklistNew(fills[f], options[_i]); - quicklistPushHead(ql, "hello", 6); - quicklistRotate(ql); - /* Ignore compression verify because listpack is - * too small to compress. */ - ql_verify(ql, 1, 1, 1, 1); - quicklistRelease(ql); - } - } - - TEST_DESC("rotate 500 val 5000 times at compress %d", options[_i]) { - for (int f = 0; f < fill_count; f++) { - quicklist *ql = quicklistNew(fills[f], options[_i]); - quicklistPushHead(ql, "900", 3); - quicklistPushHead(ql, "7000", 4); - quicklistPushHead(ql, "-1200", 5); - quicklistPushHead(ql, "42", 2); - for (int i = 0; i < 500; i++) quicklistPushHead(ql, genstr("hello", i), 64); - ql_info(ql); - for (int i = 0; i < 5000; i++) { - ql_info(ql); - quicklistRotate(ql); - } - if (fills[f] == 1) - ql_verify(ql, 504, 504, 1, 1); - else if (fills[f] == 2) - ql_verify(ql, 252, 504, 2, 2); - else if (fills[f] == 32) - ql_verify(ql, 16, 504, 32, 24); - quicklistRelease(ql); - } - } - - TEST("pop empty") { - quicklist *ql = quicklistNew(-2, options[_i]); - quicklistPop(ql, QUICKLIST_HEAD, NULL, NULL, NULL); - ql_verify(ql, 0, 0, 0, 0); - quicklistRelease(ql); - } - - TEST("pop 1 string from 1") { - quicklist *ql = quicklistNew(-2, options[_i]); - char *populate = genstr("hello", 331); - quicklistPushHead(ql, populate, 32); - unsigned char *data; - size_t sz; - long long lv; - ql_info(ql); - assert(quicklistPop(ql, QUICKLIST_HEAD, &data, &sz, &lv)); - assert(data != NULL); - assert(sz == 32); - if (strcmp(populate, (char *)data)) { - int size = sz; - ERR("Pop'd value (%.*s) didn't equal original value (%s)", size, data, populate); - } - zfree(data); - ql_verify(ql, 0, 0, 0, 0); - quicklistRelease(ql); - } - - TEST("pop head 1 number from 1") { - quicklist *ql = quicklistNew(-2, options[_i]); - quicklistPushHead(ql, "55513", 5); - unsigned char *data; - size_t sz; - long long lv; - ql_info(ql); - assert(quicklistPop(ql, QUICKLIST_HEAD, &data, &sz, &lv)); - assert(data == NULL); - assert(lv == 55513); - ql_verify(ql, 0, 0, 0, 0); - quicklistRelease(ql); - } - - TEST("pop head 500 from 500") { - quicklist *ql = quicklistNew(-2, options[_i]); - for (int i = 0; i < 500; i++) quicklistPushHead(ql, genstr("hello", i), 32); - ql_info(ql); - for (int i = 0; i < 500; i++) { - unsigned char *data; - size_t sz; - long long lv; - int ret = quicklistPop(ql, QUICKLIST_HEAD, &data, &sz, &lv); - assert(ret == 1); - assert(data != NULL); - assert(sz == 32); - if (strcmp(genstr("hello", 499 - i), (char *)data)) { - int size = sz; - ERR("Pop'd value (%.*s) didn't equal original value (%s)", size, data, genstr("hello", 499 - i)); - } - zfree(data); - } - ql_verify(ql, 0, 0, 0, 0); - quicklistRelease(ql); - } - - TEST("pop head 5000 from 500") { - quicklist *ql = quicklistNew(-2, options[_i]); - for (int i = 0; i < 500; i++) quicklistPushHead(ql, genstr("hello", i), 32); - for (int i = 0; i < 5000; i++) { - unsigned char *data; - size_t sz; - long long lv; - int ret = quicklistPop(ql, QUICKLIST_HEAD, &data, &sz, &lv); - if (i < 500) { - assert(ret == 1); - assert(data != NULL); - assert(sz == 32); - if (strcmp(genstr("hello", 499 - i), (char *)data)) { - int size = sz; - ERR("Pop'd value (%.*s) didn't equal original value " - "(%s)", - size, data, genstr("hello", 499 - i)); - } - zfree(data); - } else { - assert(ret == 0); - } - } - ql_verify(ql, 0, 0, 0, 0); - quicklistRelease(ql); - } - - TEST("iterate forward over 500 list") { - quicklist *ql = quicklistNew(-2, options[_i]); - quicklistSetFill(ql, 32); - for (int i = 0; i < 500; i++) quicklistPushHead(ql, genstr("hello", i), 32); - quicklistIter *iter = quicklistGetIterator(ql, AL_START_HEAD); - quicklistEntry entry; - int i = 499, count = 0; - while (quicklistNext(iter, &entry)) { - char *h = genstr("hello", i); - if (strcmp((char *)entry.value, h)) - ERR("value [%s] didn't match [%s] at position %d", entry.value, h, i); - i--; - count++; - } - if (count != 500) ERR("Didn't iterate over exactly 500 elements (%d)", i); - ql_verify(ql, 16, 500, 20, 32); - ql_release_iterator(iter); - quicklistRelease(ql); - } - - TEST("iterate reverse over 500 list") { - quicklist *ql = quicklistNew(-2, options[_i]); - quicklistSetFill(ql, 32); - for (int i = 0; i < 500; i++) quicklistPushHead(ql, genstr("hello", i), 32); - quicklistIter *iter = quicklistGetIterator(ql, AL_START_TAIL); - quicklistEntry entry; - int i = 0; - while (quicklistNext(iter, &entry)) { - char *h = genstr("hello", i); - if (strcmp((char *)entry.value, h)) - ERR("value [%s] didn't match [%s] at position %d", entry.value, h, i); - i++; - } - if (i != 500) ERR("Didn't iterate over exactly 500 elements (%d)", i); - ql_verify(ql, 16, 500, 20, 32); - ql_release_iterator(iter); - quicklistRelease(ql); - } - - TEST("insert after 1 element") { - quicklist *ql = quicklistNew(-2, options[_i]); - quicklistPushHead(ql, "hello", 6); - quicklistEntry entry; - iter = quicklistGetIteratorEntryAtIdx(ql, 0, &entry); - quicklistInsertAfter(iter, &entry, "abc", 4); - ql_release_iterator(iter); - ql_verify(ql, 1, 2, 2, 2); - - /* verify results */ - iter = quicklistGetIteratorEntryAtIdx(ql, 0, &entry); - int sz = entry.sz; - if (strncmp((char *)entry.value, "hello", 5)) { - ERR("Value 0 didn't match, instead got: %.*s", sz, entry.value); - } - ql_release_iterator(iter); - - iter = quicklistGetIteratorEntryAtIdx(ql, 1, &entry); - sz = entry.sz; - if (strncmp((char *)entry.value, "abc", 3)) { - ERR("Value 1 didn't match, instead got: %.*s", sz, entry.value); - } - ql_release_iterator(iter); - quicklistRelease(ql); - } - - TEST("insert before 1 element") { - quicklist *ql = quicklistNew(-2, options[_i]); - quicklistPushHead(ql, "hello", 6); - quicklistEntry entry; - iter = quicklistGetIteratorEntryAtIdx(ql, 0, &entry); - quicklistInsertBefore(iter, &entry, "abc", 4); - ql_release_iterator(iter); - ql_verify(ql, 1, 2, 2, 2); - - /* verify results */ - iter = quicklistGetIteratorEntryAtIdx(ql, 0, &entry); - int sz = entry.sz; - if (strncmp((char *)entry.value, "abc", 3)) { - ERR("Value 0 didn't match, instead got: %.*s", sz, entry.value); - } - ql_release_iterator(iter); - - iter = quicklistGetIteratorEntryAtIdx(ql, 1, &entry); - sz = entry.sz; - if (strncmp((char *)entry.value, "hello", 5)) { - ERR("Value 1 didn't match, instead got: %.*s", sz, entry.value); - } - ql_release_iterator(iter); - quicklistRelease(ql); - } - - TEST("insert head while head node is full") { - quicklist *ql = quicklistNew(4, options[_i]); - for (int i = 0; i < 10; i++) quicklistPushTail(ql, genstr("hello", i), 6); - quicklistSetFill(ql, -1); - quicklistEntry entry; - iter = quicklistGetIteratorEntryAtIdx(ql, -10, &entry); - char buf[4096] = {0}; - quicklistInsertBefore(iter, &entry, buf, 4096); - ql_release_iterator(iter); - ql_verify(ql, 4, 11, 1, 2); - quicklistRelease(ql); - } - - TEST("insert tail while tail node is full") { - quicklist *ql = quicklistNew(4, options[_i]); - for (int i = 0; i < 10; i++) quicklistPushHead(ql, genstr("hello", i), 6); - quicklistSetFill(ql, -1); - quicklistEntry entry; - iter = quicklistGetIteratorEntryAtIdx(ql, -1, &entry); - char buf[4096] = {0}; - quicklistInsertAfter(iter, &entry, buf, 4096); - ql_release_iterator(iter); - ql_verify(ql, 4, 11, 2, 1); - quicklistRelease(ql); - } - - TEST_DESC("insert once in elements while iterating at compress %d", options[_i]) { - for (int f = 0; f < fill_count; f++) { - quicklist *ql = quicklistNew(fills[f], options[_i]); - quicklistPushTail(ql, "abc", 3); - quicklistSetFill(ql, 1); - quicklistPushTail(ql, "def", 3); /* force to unique node */ - quicklistSetFill(ql, f); - quicklistPushTail(ql, "bob", 3); /* force to reset for +3 */ - quicklistPushTail(ql, "foo", 3); - quicklistPushTail(ql, "zoo", 3); - - itrprintr(ql, 0); - /* insert "bar" before "bob" while iterating over list. */ - quicklistIter *iter = quicklistGetIterator(ql, AL_START_HEAD); - quicklistEntry entry; - while (quicklistNext(iter, &entry)) { - if (!strncmp((char *)entry.value, "bob", 3)) { - /* Insert as fill = 1 so it spills into new node. */ - quicklistInsertBefore(iter, &entry, "bar", 3); - break; /* didn't we fix insert-while-iterating? */ - } - } - ql_release_iterator(iter); - itrprintr(ql, 0); - - /* verify results */ - iter = quicklistGetIteratorEntryAtIdx(ql, 0, &entry); - int sz = entry.sz; - - if (strncmp((char *)entry.value, "abc", 3)) - ERR("Value 0 didn't match, instead got: %.*s", sz, entry.value); - ql_release_iterator(iter); - - iter = quicklistGetIteratorEntryAtIdx(ql, 1, &entry); - if (strncmp((char *)entry.value, "def", 3)) - ERR("Value 1 didn't match, instead got: %.*s", sz, entry.value); - ql_release_iterator(iter); - - iter = quicklistGetIteratorEntryAtIdx(ql, 2, &entry); - if (strncmp((char *)entry.value, "bar", 3)) - ERR("Value 2 didn't match, instead got: %.*s", sz, entry.value); - ql_release_iterator(iter); - - iter = quicklistGetIteratorEntryAtIdx(ql, 3, &entry); - if (strncmp((char *)entry.value, "bob", 3)) - ERR("Value 3 didn't match, instead got: %.*s", sz, entry.value); - ql_release_iterator(iter); - - iter = quicklistGetIteratorEntryAtIdx(ql, 4, &entry); - if (strncmp((char *)entry.value, "foo", 3)) - ERR("Value 4 didn't match, instead got: %.*s", sz, entry.value); - ql_release_iterator(iter); - - iter = quicklistGetIteratorEntryAtIdx(ql, 5, &entry); - if (strncmp((char *)entry.value, "zoo", 3)) - ERR("Value 5 didn't match, instead got: %.*s", sz, entry.value); - ql_release_iterator(iter); - quicklistRelease(ql); - } - } - - TEST_DESC("insert [before] 250 new in middle of 500 elements at compress %d", options[_i]) { - for (int f = 0; f < fill_count; f++) { - quicklist *ql = quicklistNew(fills[f], options[_i]); - for (int i = 0; i < 500; i++) quicklistPushTail(ql, genstr("hello", i), 32); - for (int i = 0; i < 250; i++) { - quicklistEntry entry; - iter = quicklistGetIteratorEntryAtIdx(ql, 250, &entry); - quicklistInsertBefore(iter, &entry, genstr("abc", i), 32); - ql_release_iterator(iter); - } - if (fills[f] == 32) ql_verify(ql, 25, 750, 32, 20); - quicklistRelease(ql); - } - } - - TEST_DESC("insert [after] 250 new in middle of 500 elements at compress %d", options[_i]) { - for (int f = 0; f < fill_count; f++) { - quicklist *ql = quicklistNew(fills[f], options[_i]); - for (int i = 0; i < 500; i++) quicklistPushHead(ql, genstr("hello", i), 32); - for (int i = 0; i < 250; i++) { - quicklistEntry entry; - iter = quicklistGetIteratorEntryAtIdx(ql, 250, &entry); - quicklistInsertAfter(iter, &entry, genstr("abc", i), 32); - ql_release_iterator(iter); - } - - if (ql->count != 750) ERR("List size not 750, but rather %ld", ql->count); - - if (fills[f] == 32) ql_verify(ql, 26, 750, 20, 32); - quicklistRelease(ql); - } - } - - TEST("duplicate empty list") { - quicklist *ql = quicklistNew(-2, options[_i]); - ql_verify(ql, 0, 0, 0, 0); - quicklist *copy = quicklistDup(ql); - ql_verify(copy, 0, 0, 0, 0); - quicklistRelease(ql); - quicklistRelease(copy); - } - - TEST("duplicate list of 1 element") { - quicklist *ql = quicklistNew(-2, options[_i]); - quicklistPushHead(ql, genstr("hello", 3), 32); - ql_verify(ql, 1, 1, 1, 1); - quicklist *copy = quicklistDup(ql); - ql_verify(copy, 1, 1, 1, 1); - quicklistRelease(ql); - quicklistRelease(copy); - } - - TEST("duplicate list of 500") { - quicklist *ql = quicklistNew(-2, options[_i]); - quicklistSetFill(ql, 32); - for (int i = 0; i < 500; i++) quicklistPushHead(ql, genstr("hello", i), 32); - ql_verify(ql, 16, 500, 20, 32); - - quicklist *copy = quicklistDup(ql); - ql_verify(copy, 16, 500, 20, 32); - quicklistRelease(ql); - quicklistRelease(copy); - } - - for (int f = 0; f < fill_count; f++) { - TEST_DESC("index 1,200 from 500 list at fill %d at compress %d", f, options[_i]) { - quicklist *ql = quicklistNew(fills[f], options[_i]); - for (int i = 0; i < 500; i++) quicklistPushTail(ql, genstr("hello", i + 1), 32); - quicklistEntry entry; - iter = quicklistGetIteratorEntryAtIdx(ql, 1, &entry); - if (strcmp((char *)entry.value, "hello2") != 0) ERR("Value: %s", entry.value); - ql_release_iterator(iter); - - iter = quicklistGetIteratorEntryAtIdx(ql, 200, &entry); - if (strcmp((char *)entry.value, "hello201") != 0) ERR("Value: %s", entry.value); - ql_release_iterator(iter); - quicklistRelease(ql); - } - - TEST_DESC("index -1,-2 from 500 list at fill %d at compress %d", fills[f], options[_i]) { - quicklist *ql = quicklistNew(fills[f], options[_i]); - for (int i = 0; i < 500; i++) quicklistPushTail(ql, genstr("hello", i + 1), 32); - quicklistEntry entry; - iter = quicklistGetIteratorEntryAtIdx(ql, -1, &entry); - if (strcmp((char *)entry.value, "hello500") != 0) ERR("Value: %s", entry.value); - ql_release_iterator(iter); - - iter = quicklistGetIteratorEntryAtIdx(ql, -2, &entry); - if (strcmp((char *)entry.value, "hello499") != 0) ERR("Value: %s", entry.value); - ql_release_iterator(iter); - quicklistRelease(ql); - } - - TEST_DESC("index -100 from 500 list at fill %d at compress %d", fills[f], options[_i]) { - quicklist *ql = quicklistNew(fills[f], options[_i]); - for (int i = 0; i < 500; i++) quicklistPushTail(ql, genstr("hello", i + 1), 32); - quicklistEntry entry; - iter = quicklistGetIteratorEntryAtIdx(ql, -100, &entry); - if (strcmp((char *)entry.value, "hello401") != 0) ERR("Value: %s", entry.value); - ql_release_iterator(iter); - quicklistRelease(ql); - } - - TEST_DESC("index too big +1 from 50 list at fill %d at compress %d", fills[f], options[_i]) { - quicklist *ql = quicklistNew(fills[f], options[_i]); - for (int i = 0; i < 50; i++) quicklistPushTail(ql, genstr("hello", i + 1), 32); - quicklistEntry entry; - int sz = entry.sz; - iter = quicklistGetIteratorEntryAtIdx(ql, 50, &entry); - if (iter) ERR("Index found at 50 with 50 list: %.*s", sz, entry.value); - ql_release_iterator(iter); - quicklistRelease(ql); - } - } - - TEST("delete range empty list") { - quicklist *ql = quicklistNew(-2, options[_i]); - quicklistDelRange(ql, 5, 20); - ql_verify(ql, 0, 0, 0, 0); - quicklistRelease(ql); - } - - TEST("delete range of entire node in list of one node") { - quicklist *ql = quicklistNew(-2, options[_i]); - for (int i = 0; i < 32; i++) quicklistPushHead(ql, genstr("hello", i), 32); - ql_verify(ql, 1, 32, 32, 32); - quicklistDelRange(ql, 0, 32); - ql_verify(ql, 0, 0, 0, 0); - quicklistRelease(ql); - } - - TEST("delete range of entire node with overflow counts") { - quicklist *ql = quicklistNew(-2, options[_i]); - for (int i = 0; i < 32; i++) quicklistPushHead(ql, genstr("hello", i), 32); - ql_verify(ql, 1, 32, 32, 32); - quicklistDelRange(ql, 0, 128); - ql_verify(ql, 0, 0, 0, 0); - quicklistRelease(ql); - } - - TEST("delete middle 100 of 500 list") { - quicklist *ql = quicklistNew(-2, options[_i]); - quicklistSetFill(ql, 32); - for (int i = 0; i < 500; i++) quicklistPushTail(ql, genstr("hello", i + 1), 32); - ql_verify(ql, 16, 500, 32, 20); - quicklistDelRange(ql, 200, 100); - ql_verify(ql, 14, 400, 32, 20); - quicklistRelease(ql); - } - - TEST("delete less than fill but across nodes") { - quicklist *ql = quicklistNew(-2, options[_i]); - quicklistSetFill(ql, 32); - for (int i = 0; i < 500; i++) quicklistPushTail(ql, genstr("hello", i + 1), 32); - ql_verify(ql, 16, 500, 32, 20); - quicklistDelRange(ql, 60, 10); - ql_verify(ql, 16, 490, 32, 20); - quicklistRelease(ql); - } - - TEST("delete negative 1 from 500 list") { - quicklist *ql = quicklistNew(-2, options[_i]); - quicklistSetFill(ql, 32); - for (int i = 0; i < 500; i++) quicklistPushTail(ql, genstr("hello", i + 1), 32); - ql_verify(ql, 16, 500, 32, 20); - quicklistDelRange(ql, -1, 1); - ql_verify(ql, 16, 499, 32, 19); - quicklistRelease(ql); - } - - TEST("delete negative 1 from 500 list with overflow counts") { - quicklist *ql = quicklistNew(-2, options[_i]); - quicklistSetFill(ql, 32); - for (int i = 0; i < 500; i++) quicklistPushTail(ql, genstr("hello", i + 1), 32); - ql_verify(ql, 16, 500, 32, 20); - quicklistDelRange(ql, -1, 128); - ql_verify(ql, 16, 499, 32, 19); - quicklistRelease(ql); - } - - TEST("delete negative 100 from 500 list") { - quicklist *ql = quicklistNew(-2, options[_i]); - quicklistSetFill(ql, 32); - for (int i = 0; i < 500; i++) quicklistPushTail(ql, genstr("hello", i + 1), 32); - quicklistDelRange(ql, -100, 100); - ql_verify(ql, 13, 400, 32, 16); - quicklistRelease(ql); - } - - TEST("delete -10 count 5 from 50 list") { - quicklist *ql = quicklistNew(-2, options[_i]); - quicklistSetFill(ql, 32); - for (int i = 0; i < 50; i++) quicklistPushTail(ql, genstr("hello", i + 1), 32); - ql_verify(ql, 2, 50, 32, 18); - quicklistDelRange(ql, -10, 5); - ql_verify(ql, 2, 45, 32, 13); - quicklistRelease(ql); - } - - TEST("numbers only list read") { - quicklist *ql = quicklistNew(-2, options[_i]); - quicklistPushTail(ql, "1111", 4); - quicklistPushTail(ql, "2222", 4); - quicklistPushTail(ql, "3333", 4); - quicklistPushTail(ql, "4444", 4); - ql_verify(ql, 1, 4, 4, 4); - quicklistEntry entry; - iter = quicklistGetIteratorEntryAtIdx(ql, 0, &entry); - if (entry.longval != 1111) ERR("Not 1111, %lld", entry.longval); - ql_release_iterator(iter); - - iter = quicklistGetIteratorEntryAtIdx(ql, 1, &entry); - if (entry.longval != 2222) ERR("Not 2222, %lld", entry.longval); - ql_release_iterator(iter); - - iter = quicklistGetIteratorEntryAtIdx(ql, 2, &entry); - if (entry.longval != 3333) ERR("Not 3333, %lld", entry.longval); - ql_release_iterator(iter); - - iter = quicklistGetIteratorEntryAtIdx(ql, 3, &entry); - if (entry.longval != 4444) ERR("Not 4444, %lld", entry.longval); - ql_release_iterator(iter); - - iter = quicklistGetIteratorEntryAtIdx(ql, 4, &entry); - if (iter) ERR("Index past elements: %lld", entry.longval); - ql_release_iterator(iter); - - iter = quicklistGetIteratorEntryAtIdx(ql, -1, &entry); - if (entry.longval != 4444) ERR("Not 4444 (reverse), %lld", entry.longval); - ql_release_iterator(iter); - - iter = quicklistGetIteratorEntryAtIdx(ql, -2, &entry); - if (entry.longval != 3333) ERR("Not 3333 (reverse), %lld", entry.longval); - ql_release_iterator(iter); - - iter = quicklistGetIteratorEntryAtIdx(ql, -3, &entry); - if (entry.longval != 2222) ERR("Not 2222 (reverse), %lld", entry.longval); - ql_release_iterator(iter); - - iter = quicklistGetIteratorEntryAtIdx(ql, -4, &entry); - if (entry.longval != 1111) ERR("Not 1111 (reverse), %lld", entry.longval); - ql_release_iterator(iter); - - iter = quicklistGetIteratorEntryAtIdx(ql, -5, &entry); - if (iter) ERR("Index past elements (reverse), %lld", entry.longval); - ql_release_iterator(iter); - quicklistRelease(ql); - } - - TEST("numbers larger list read") { - quicklist *ql = quicklistNew(-2, options[_i]); - quicklistSetFill(ql, 32); - char num[32]; - long long nums[5000]; - for (int i = 0; i < 5000; i++) { - nums[i] = -5157318210846258176 + i; - int sz = ll2string(num, sizeof(num), nums[i]); - quicklistPushTail(ql, num, sz); - } - quicklistPushTail(ql, "xxxxxxxxxxxxxxxxxxxx", 20); - quicklistEntry entry; - for (int i = 0; i < 5000; i++) { - iter = quicklistGetIteratorEntryAtIdx(ql, i, &entry); - if (entry.longval != nums[i]) ERR("[%d] Not longval %lld but rather %lld", i, nums[i], entry.longval); - entry.longval = 0xdeadbeef; - ql_release_iterator(iter); - } - iter = quicklistGetIteratorEntryAtIdx(ql, 5000, &entry); - if (strncmp((char *)entry.value, "xxxxxxxxxxxxxxxxxxxx", 20)) ERR("String val not match: %s", entry.value); - ql_verify(ql, 157, 5001, 32, 9); - ql_release_iterator(iter); - quicklistRelease(ql); - } - - TEST("numbers larger list read B") { - quicklist *ql = quicklistNew(-2, options[_i]); - quicklistPushTail(ql, "99", 2); - quicklistPushTail(ql, "98", 2); - quicklistPushTail(ql, "xxxxxxxxxxxxxxxxxxxx", 20); - quicklistPushTail(ql, "96", 2); - quicklistPushTail(ql, "95", 2); - quicklistReplaceAtIndex(ql, 1, "foo", 3); - quicklistReplaceAtIndex(ql, -1, "bar", 3); - quicklistRelease(ql); - } - - TEST_DESC("lrem test at compress %d", options[_i]) { - for (int f = 0; f < fill_count; f++) { - quicklist *ql = quicklistNew(fills[f], options[_i]); - char *words[] = {"abc", "foo", "bar", "foobar", "foobared", "zap", "bar", "test", "foo"}; - char *result[] = {"abc", "foo", "foobar", "foobared", "zap", "test", "foo"}; - char *resultB[] = {"abc", "foo", "foobar", "foobared", "zap", "test"}; - for (int i = 0; i < 9; i++) quicklistPushTail(ql, words[i], strlen(words[i])); - - /* lrem 0 bar */ - quicklistIter *iter = quicklistGetIterator(ql, AL_START_HEAD); - quicklistEntry entry; - int i = 0; - while (quicklistNext(iter, &entry)) { - if (quicklistCompare(&entry, (unsigned char *)"bar", 3)) { - quicklistDelEntry(iter, &entry); - } - i++; - } - ql_release_iterator(iter); - - /* check result of lrem 0 bar */ - iter = quicklistGetIterator(ql, AL_START_HEAD); - i = 0; - while (quicklistNext(iter, &entry)) { - /* Result must be: abc, foo, foobar, foobared, zap, test, - * foo */ - int sz = entry.sz; - if (strncmp((char *)entry.value, result[i], entry.sz)) { - ERR("No match at position %d, got %.*s instead of %s", i, sz, entry.value, result[i]); - } - i++; - } - ql_release_iterator(iter); - - quicklistPushTail(ql, "foo", 3); - - /* lrem -2 foo */ - iter = quicklistGetIterator(ql, AL_START_TAIL); - i = 0; - int del = 2; - while (quicklistNext(iter, &entry)) { - if (quicklistCompare(&entry, (unsigned char *)"foo", 3)) { - quicklistDelEntry(iter, &entry); - del--; - } - if (!del) break; - i++; - } - ql_release_iterator(iter); - - /* check result of lrem -2 foo */ - /* (we're ignoring the '2' part and still deleting all foo - * because - * we only have two foo) */ - iter = quicklistGetIterator(ql, AL_START_TAIL); - i = 0; - size_t resB = sizeof(resultB) / sizeof(*resultB); - while (quicklistNext(iter, &entry)) { - /* Result must be: abc, foo, foobar, foobared, zap, test, - * foo */ - int sz = entry.sz; - if (strncmp((char *)entry.value, resultB[resB - 1 - i], sz)) { - ERR("No match at position %d, got %.*s instead of %s", i, sz, entry.value, - resultB[resB - 1 - i]); - } - i++; - } - - ql_release_iterator(iter); - quicklistRelease(ql); - } - } - - TEST_DESC("iterate reverse + delete at compress %d", options[_i]) { - for (int f = 0; f < fill_count; f++) { - quicklist *ql = quicklistNew(fills[f], options[_i]); - quicklistPushTail(ql, "abc", 3); - quicklistPushTail(ql, "def", 3); - quicklistPushTail(ql, "hij", 3); - quicklistPushTail(ql, "jkl", 3); - quicklistPushTail(ql, "oop", 3); - - quicklistEntry entry; - quicklistIter *iter = quicklistGetIterator(ql, AL_START_TAIL); - int i = 0; - while (quicklistNext(iter, &entry)) { - if (quicklistCompare(&entry, (unsigned char *)"hij", 3)) { - quicklistDelEntry(iter, &entry); - } - i++; - } - ql_release_iterator(iter); - - if (i != 5) ERR("Didn't iterate 5 times, iterated %d times.", i); - - /* Check results after deletion of "hij" */ - iter = quicklistGetIterator(ql, AL_START_HEAD); - i = 0; - char *vals[] = {"abc", "def", "jkl", "oop"}; - while (quicklistNext(iter, &entry)) { - if (!quicklistCompare(&entry, (unsigned char *)vals[i], 3)) { - ERR("Value at %d didn't match %s\n", i, vals[i]); - } - i++; - } - ql_release_iterator(iter); - quicklistRelease(ql); - } - } - - TEST_DESC("iterator at index test at compress %d", options[_i]) { - for (int f = 0; f < fill_count; f++) { - quicklist *ql = quicklistNew(fills[f], options[_i]); - char num[32]; - long long nums[5000]; - for (int i = 0; i < 760; i++) { - nums[i] = -5157318210846258176 + i; - int sz = ll2string(num, sizeof(num), nums[i]); - quicklistPushTail(ql, num, sz); - } - - quicklistEntry entry; - quicklistIter *iter = quicklistGetIteratorAtIdx(ql, AL_START_HEAD, 437); - int i = 437; - while (quicklistNext(iter, &entry)) { - if (entry.longval != nums[i]) ERR("Expected %lld, but got %lld", entry.longval, nums[i]); - i++; - } - ql_release_iterator(iter); - quicklistRelease(ql); - } - } - - TEST_DESC("ltrim test A at compress %d", options[_i]) { - for (int f = 0; f < fill_count; f++) { - quicklist *ql = quicklistNew(fills[f], options[_i]); - char num[32]; - long long nums[5000]; - for (int i = 0; i < 32; i++) { - nums[i] = -5157318210846258176 + i; - int sz = ll2string(num, sizeof(num), nums[i]); - quicklistPushTail(ql, num, sz); - } - if (fills[f] == 32) ql_verify(ql, 1, 32, 32, 32); - /* ltrim 25 53 (keep [25,32] inclusive = 7 remaining) */ - quicklistDelRange(ql, 0, 25); - quicklistDelRange(ql, 0, 0); - quicklistEntry entry; - for (int i = 0; i < 7; i++) { - iter = quicklistGetIteratorEntryAtIdx(ql, i, &entry); - if (entry.longval != nums[25 + i]) - ERR("Deleted invalid range! Expected %lld but got " - "%lld", - entry.longval, nums[25 + i]); - ql_release_iterator(iter); - } - if (fills[f] == 32) ql_verify(ql, 1, 7, 7, 7); - quicklistRelease(ql); - } - } - - TEST_DESC("ltrim test B at compress %d", options[_i]) { - for (int f = 0; f < fill_count; f++) { - /* Force-disable compression because our 33 sequential - * integers don't compress and the check always fails. */ - quicklist *ql = quicklistNew(fills[f], QUICKLIST_NOCOMPRESS); - char num[32]; - long long nums[5000]; - for (int i = 0; i < 33; i++) { - nums[i] = i; - int sz = ll2string(num, sizeof(num), nums[i]); - quicklistPushTail(ql, num, sz); - } - if (fills[f] == 32) ql_verify(ql, 2, 33, 32, 1); - /* ltrim 5 16 (keep [5,16] inclusive = 12 remaining) */ - quicklistDelRange(ql, 0, 5); - quicklistDelRange(ql, -16, 16); - if (fills[f] == 32) ql_verify(ql, 1, 12, 12, 12); - quicklistEntry entry; - - iter = quicklistGetIteratorEntryAtIdx(ql, 0, &entry); - if (entry.longval != 5) ERR("A: longval not 5, but %lld", entry.longval); - ql_release_iterator(iter); - - iter = quicklistGetIteratorEntryAtIdx(ql, -1, &entry); - if (entry.longval != 16) ERR("B! got instead: %lld", entry.longval); - quicklistPushTail(ql, "bobobob", 7); - ql_release_iterator(iter); - - iter = quicklistGetIteratorEntryAtIdx(ql, -1, &entry); - int sz = entry.sz; - if (strncmp((char *)entry.value, "bobobob", 7)) - ERR("Tail doesn't match bobobob, it's %.*s instead", sz, entry.value); - ql_release_iterator(iter); - - for (int i = 0; i < 12; i++) { - iter = quicklistGetIteratorEntryAtIdx(ql, i, &entry); - if (entry.longval != nums[5 + i]) - ERR("Deleted invalid range! Expected %lld but got " - "%lld", - entry.longval, nums[5 + i]); - ql_release_iterator(iter); - } - quicklistRelease(ql); - } - } - - TEST_DESC("ltrim test C at compress %d", options[_i]) { - for (int f = 0; f < fill_count; f++) { - quicklist *ql = quicklistNew(fills[f], options[_i]); - char num[32]; - long long nums[5000]; - for (int i = 0; i < 33; i++) { - nums[i] = -5157318210846258176 + i; - int sz = ll2string(num, sizeof(num), nums[i]); - quicklistPushTail(ql, num, sz); - } - if (fills[f] == 32) ql_verify(ql, 2, 33, 32, 1); - /* ltrim 3 3 (keep [3,3] inclusive = 1 remaining) */ - quicklistDelRange(ql, 0, 3); - quicklistDelRange(ql, -29, 4000); /* make sure not loop forever */ - if (fills[f] == 32) ql_verify(ql, 1, 1, 1, 1); - quicklistEntry entry; - iter = quicklistGetIteratorEntryAtIdx(ql, 0, &entry); - if (entry.longval != -5157318210846258173) ERROR; - ql_release_iterator(iter); - quicklistRelease(ql); - } - } - - TEST_DESC("ltrim test D at compress %d", options[_i]) { - for (int f = 0; f < fill_count; f++) { - quicklist *ql = quicklistNew(fills[f], options[_i]); - char num[32]; - long long nums[5000]; - for (int i = 0; i < 33; i++) { - nums[i] = -5157318210846258176 + i; - int sz = ll2string(num, sizeof(num), nums[i]); - quicklistPushTail(ql, num, sz); - } - if (fills[f] == 32) ql_verify(ql, 2, 33, 32, 1); - quicklistDelRange(ql, -12, 3); - if (ql->count != 30) ERR("Didn't delete exactly three elements! Count is: %lu", ql->count); - quicklistRelease(ql); - } - } - - long long stop = mstime(); - runtime[_i] = stop - start; - } - - /* Run a longer test of compression depth outside of primary test loop. */ - int list_sizes[] = {250, 251, 500, 999, 1000}; - long long start = mstime(); - int list_count = accurate ? (int)(sizeof(list_sizes) / sizeof(*list_sizes)) : 1; - for (int list = 0; list < list_count; list++) { - TEST_DESC("verify specific compression of interior nodes with %d list ", list_sizes[list]) { - for (int f = 0; f < fill_count; f++) { - for (int depth = 1; depth < 40; depth++) { - /* skip over many redundant test cases */ - quicklist *ql = quicklistNew(fills[f], depth); - for (int i = 0; i < list_sizes[list]; i++) { - quicklistPushTail(ql, genstr("hello TAIL", i + 1), 64); - quicklistPushHead(ql, genstr("hello HEAD", i + 1), 64); - } - - for (int step = 0; step < 2; step++) { - /* test remove node */ - if (step == 1) { - for (int i = 0; i < list_sizes[list] / 2; i++) { - unsigned char *data; - assert(quicklistPop(ql, QUICKLIST_HEAD, &data, NULL, NULL)); - zfree(data); - assert(quicklistPop(ql, QUICKLIST_TAIL, &data, NULL, NULL)); - zfree(data); - } - } - quicklistNode *node = ql->head; - unsigned int low_raw = ql->compress; - unsigned int high_raw = ql->len - ql->compress; - - for (unsigned int at = 0; at < ql->len; at++, node = node->next) { - if (at < low_raw || at >= high_raw) { - if (node->encoding != QUICKLIST_NODE_ENCODING_RAW) { - ERR("Incorrect compression: node %d is " - "compressed at depth %d ((%u, %u); total " - "nodes: %lu; size: %zu)", - at, depth, low_raw, high_raw, ql->len, node->sz); - } - } else { - if (node->encoding != QUICKLIST_NODE_ENCODING_LZF) { - ERR("Incorrect non-compression: node %d is NOT " - "compressed at depth %d ((%u, %u); total " - "nodes: %lu; size: %zu; attempted: %d)", - at, depth, low_raw, high_raw, ql->len, node->sz, node->attempted_compress); - } - } - } - } - - quicklistRelease(ql); - } - } - } - } - long long stop = mstime(); - - printf("\n"); - for (size_t i = 0; i < option_count; i++) - printf("Test Loop %02d: %0.2f seconds.\n", options[i], (float)runtime[i] / 1000); - printf("Compressions: %0.2f seconds.\n", (float)(stop - start) / 1000); - printf("\n"); - - TEST("bookmark get updated to next item") { - quicklist *ql = quicklistNew(1, 0); - quicklistPushTail(ql, "1", 1); - quicklistPushTail(ql, "2", 1); - quicklistPushTail(ql, "3", 1); - quicklistPushTail(ql, "4", 1); - quicklistPushTail(ql, "5", 1); - assert(ql->len == 5); - /* add two bookmarks, one pointing to the node before the last. */ - assert(quicklistBookmarkCreate(&ql, "_dummy", ql->head->next)); - assert(quicklistBookmarkCreate(&ql, "_test", ql->tail->prev)); - /* test that the bookmark returns the right node, delete it and see that the bookmark points to the last node */ - assert(quicklistBookmarkFind(ql, "_test") == ql->tail->prev); - assert(quicklistDelRange(ql, -2, 1)); - assert(quicklistBookmarkFind(ql, "_test") == ql->tail); - /* delete the last node, and see that the bookmark was deleted. */ - assert(quicklistDelRange(ql, -1, 1)); - assert(quicklistBookmarkFind(ql, "_test") == NULL); - /* test that other bookmarks aren't affected */ - assert(quicklistBookmarkFind(ql, "_dummy") == ql->head->next); - assert(quicklistBookmarkFind(ql, "_missing") == NULL); - assert(ql->len == 3); - quicklistBookmarksClear(ql); /* for coverage */ - assert(quicklistBookmarkFind(ql, "_dummy") == NULL); - quicklistRelease(ql); - } - - TEST("bookmark limit") { - int i; - quicklist *ql = quicklistNew(1, 0); - quicklistPushHead(ql, "1", 1); - for (i = 0; i < QL_MAX_BM; i++) assert(quicklistBookmarkCreate(&ql, genstr("", i), ql->head)); - /* when all bookmarks are used, creation fails */ - assert(!quicklistBookmarkCreate(&ql, "_test", ql->head)); - /* delete one and see that we can now create another */ - assert(quicklistBookmarkDelete(ql, "0")); - assert(quicklistBookmarkCreate(&ql, "_test", ql->head)); - /* delete one and see that the rest survive */ - assert(quicklistBookmarkDelete(ql, "_test")); - for (i = 1; i < QL_MAX_BM; i++) assert(quicklistBookmarkFind(ql, genstr("", i)) == ql->head); - /* make sure the deleted ones are indeed gone */ - assert(!quicklistBookmarkFind(ql, "0")); - assert(!quicklistBookmarkFind(ql, "_test")); - quicklistRelease(ql); - } - - if (flags & TEST_LARGE_MEMORY) { - TEST("compress and decompress quicklist listpack node") { - quicklistNode *node = quicklistCreateNode(); - node->entry = lpNew(0); - - /* Just to avoid triggering the assertion in __quicklistCompressNode(), - * it disables the passing of quicklist head or tail node. */ - node->prev = quicklistCreateNode(); - node->next = quicklistCreateNode(); - - /* Create a rand string */ - size_t sz = (1 << 25); /* 32MB per one entry */ - unsigned char *s = zmalloc(sz); - randstring(s, sz); - - /* Keep filling the node, until it reaches 1GB */ - for (int i = 0; i < 32; i++) { - node->entry = lpAppend(node->entry, s, sz); - quicklistNodeUpdateSz(node); - - long long start = mstime(); - assert(__quicklistCompressNode(node)); - assert(__quicklistDecompressNode(node)); - printf("Compress and decompress: %zu MB in %.2f seconds.\n", node->sz / 1024 / 1024, - (float)(mstime() - start) / 1000); - } - - zfree(s); - zfree(node->prev); - zfree(node->next); - zfree(node->entry); - zfree(node); - } - -#if ULONG_MAX >= 0xffffffffffffffff - TEST("compress and decomress quicklist plain node large than UINT32_MAX") { - size_t sz = (1ull << 32); - unsigned char *s = zmalloc(sz); - randstring(s, sz); - memcpy(s, "helloworld", 10); - memcpy(s + sz - 10, "1234567890", 10); - - quicklistNode *node = __quicklistCreateNode(QUICKLIST_NODE_CONTAINER_PLAIN, s, sz); - - /* Just to avoid triggering the assertion in __quicklistCompressNode(), - * it disables the passing of quicklist head or tail node. */ - node->prev = quicklistCreateNode(); - node->next = quicklistCreateNode(); - - long long start = mstime(); - assert(__quicklistCompressNode(node)); - assert(__quicklistDecompressNode(node)); - printf("Compress and decompress: %zu MB in %.2f seconds.\n", node->sz / 1024 / 1024, - (float)(mstime() - start) / 1000); - - assert(memcmp(node->entry, "helloworld", 10) == 0); - assert(memcmp(node->entry + sz - 10, "1234567890", 10) == 0); - zfree(node->prev); - zfree(node->next); - zfree(node->entry); - zfree(node); - } -#endif - } - - if (!err) - printf("ALL TESTS PASSED!\n"); - else - ERR("Sorry, not all tests passed! In fact, %d tests failed.", err); - - return err; -} -#endif diff --git a/src/quicklist.h b/src/quicklist.h index bb94807913..4411f823b0 100644 --- a/src/quicklist.h +++ b/src/quicklist.h @@ -198,10 +198,6 @@ quicklistNode *quicklistBookmarkFind(quicklist *ql, const char *name); void quicklistBookmarksClear(quicklist *ql); int quicklistSetPackedThreshold(size_t sz); -#ifdef SERVER_TEST -int quicklistTest(int argc, char *argv[], int flags); -#endif - /* Directions for iterators */ #define AL_START_HEAD 0 #define AL_START_TAIL 1 diff --git a/src/rdma.c b/src/rdma.c index bb38baa0f1..7cdcb24913 100644 --- a/src/rdma.c +++ b/src/rdma.c @@ -1199,6 +1199,14 @@ static void connRdmaClose(connection *conn) { conn->fd = -1; } + /* If called from within a handler, schedule the close but + * keep the connection until the handler returns. + */ + if (connHasRefs(conn)) { + conn->flags |= CONN_FLAG_CLOSE_SCHEDULED; + return; + } + if (!cm_id) { return; } @@ -1689,7 +1697,6 @@ static int rdmaProcessPendingData(void) { listNode *ln; rdma_connection *rdma_conn; connection *conn; - listNode *node; int processed; processed = listLength(pending_list); @@ -1697,17 +1704,17 @@ static int rdmaProcessPendingData(void) { while ((ln = listNext(&li))) { rdma_conn = listNodeValue(ln); conn = &rdma_conn->c; - node = rdma_conn->pending_list_node; /* a connection can be disconnected by remote peer, CM event mark state as CONN_STATE_CLOSED, kick connection * read/write handler to close connection */ if (conn->state == CONN_STATE_ERROR || conn->state == CONN_STATE_CLOSED) { - listDelNode(pending_list, node); - /* do NOT call callHandler(conn, conn->read_handler) here, conn is freed in handler! */ - if (conn->read_handler) { - conn->read_handler(conn); - } else if (conn->write_handler) { - conn->write_handler(conn); + listDelNode(pending_list, rdma_conn->pending_list_node); + rdma_conn->pending_list_node = NULL; + /* Invoke both read_handler and write_handler, unless read_handler + returns 0, indicating the connection has closed, in which case + write_handler will be skipped. */ + if (callHandler(conn, conn->read_handler)) { + callHandler(conn, conn->write_handler); } continue; diff --git a/src/replication.c b/src/replication.c index 5f644c9a44..96c87a7497 100644 --- a/src/replication.c +++ b/src/replication.c @@ -1669,7 +1669,9 @@ void rdbPipeReadHandler(struct aeEventLoop *eventLoop, int fd, void *clientData, if (!conn) continue; stillUp++; } - serverLog(LL_NOTICE, "Diskless rdb transfer, done reading from pipe, %d replicas still up.", stillUp); + if (stillUp) { + serverLog(LL_NOTICE, "Diskless rdb transfer, done reading from pipe, %d replicas still up.", stillUp); + } /* Now that the replicas have finished reading, notify the child that it's safe to exit. * When the server detects the child has exited, it can mark the replica as online, and * start streaming the replication buffers. */ @@ -1678,7 +1680,6 @@ void rdbPipeReadHandler(struct aeEventLoop *eventLoop, int fd, void *clientData, return; } - int stillAlive = 0; for (i = 0; i < server.rdb_pipe_numconns; i++) { ssize_t nwritten; connection *conn = server.rdb_pipe_conns[i]; @@ -1708,15 +1709,10 @@ void rdbPipeReadHandler(struct aeEventLoop *eventLoop, int fd, void *clientData, server.rdb_pipe_numconns_writing++; connSetWriteHandler(conn, rdbPipeWriteHandler); } - stillAlive++; } - if (stillAlive == 0) { - serverLog(LL_WARNING, "Diskless rdb transfer, last replica dropped, killing fork child."); - killRDBChild(); - } /* Remove the pipe read handler if at least one write handler was set. */ - if (server.rdb_pipe_numconns_writing || stillAlive == 0) { + if (server.rdb_pipe_numconns_writing) { aeDeleteFileEvent(server.el, server.rdb_pipe_read, AE_READABLE); break; } @@ -1745,6 +1741,8 @@ void updateReplicasWaitingBgsave(int bgsaveerr, int type) { struct valkey_stat buf; if (bgsaveerr != C_OK) { + /* If bgsaveerr is error, there is no need to protect the rdb channel. */ + replica->flag.protected_rdb_channel = 0; freeClientAsync(replica); serverLog(LL_WARNING, "SYNC failed. BGSAVE child returned an error"); continue; @@ -2697,7 +2695,7 @@ static int dualChannelReplHandleEndOffsetResponse(connection *conn, sds *err) { /* Initiate repl_provisional_primary to act as this replica temp primary until RDB is loaded */ server.repl_provisional_primary.conn = server.repl_transfer_s; - memcpy(server.repl_provisional_primary.replid, primary_replid, CONFIG_RUN_ID_SIZE); + memcpy(server.repl_provisional_primary.replid, primary_replid, sizeof(server.repl_provisional_primary.replid)); server.repl_provisional_primary.reploff = reploffset; server.repl_provisional_primary.read_reploff = reploffset; server.repl_provisional_primary.dbid = dbid; @@ -3247,7 +3245,6 @@ int dualChannelReplMainConnSendHandshake(connection *conn, sds *err) { ull2string(llstr, sizeof(llstr), server.rdb_client_id); *err = sendCommand(conn, "REPLCONF", "set-rdb-client-id", llstr, NULL); if (*err) return C_ERR; - server.repl_state = REPL_STATE_RECEIVE_CAPA_REPLY; return C_OK; } @@ -3258,7 +3255,6 @@ int dualChannelReplMainConnRecvCapaReply(connection *conn, sds *err) { serverLog(LL_NOTICE, "Primary does not understand REPLCONF identify: %s", *err); return C_ERR; } - server.repl_state = REPL_STATE_SEND_PSYNC; return C_OK; } @@ -3269,7 +3265,6 @@ int dualChannelReplMainConnSendPsync(connection *conn, sds *err) { *err = sdsnew(connGetLastError(conn)); return C_ERR; } - server.repl_state = REPL_STATE_RECEIVE_PSYNC_REPLY; return C_OK; } @@ -3384,15 +3379,15 @@ void dualChannelSetupMainConnForPsync(connection *conn) { * ┌────────▼──────────┐ │ │ │DUAL_CHANNEL_RECEIVE_ENDOFF│ │ │by the primary │ * │RECEIVE_IP_REPLY │ │ │ └───────┬───────────────────┘ │ ┌──▼────────────────┐ │ * └────────┬──────────┘ │ │ │$ENDOFF │ │RECEIVE_PSYNC_REPLY│ │ - * │ │ │ ├─────────────────────────┘ └──┬────────────────┘ │ - * │ │ │ │ │+CONTINUE │ - * │ │ │ ┌───────▼───────────────┐ ┌──▼────────────────┐ │ - * │ │ │ │DUAL_CHANNEL_RDB_LOAD │ │TRANSFER │ │ + * │+OK │ │ ├─────────────────────────┘ └──┬────────────────┘ │ + * ┌────────▼──────────┐ │ │ │ │+CONTINUE │ + * │RECEIVE_CAPA_REPLY │ │ │ ┌───────▼───────────────┐ ┌──▼────────────────┐ │ + * └────────┬──────────┘ │ │ │DUAL_CHANNEL_RDB_LOAD │ │TRANSFER │ │ * │+OK │ │ └───────┬───────────────┘ └─────┬─────────────┘ │ - * ┌────────▼──────────┐ │ │ │Done loading │ │ - * │RECEIVE_CAPA_REPLY │ │ │ ┌───────▼───────────────┐ │ │ - * └────────┬──────────┘ │ │ │DUAL_CHANNEL_RDB_LOADED│ │ │ - * │ │ │ └───────┬───────────────┘ │ │ + * ┌────────▼─────────────┐ │ │ │Done loading │ │ + * │RECEIVE_VERSION_REPLY │ │ │ ┌───────▼───────────────┐ │ │ + * └────────┬─────────────┘ │ │ │DUAL_CHANNEL_RDB_LOADED│ │ │ + * │+OK │ │ └───────┬───────────────┘ │ │ * ┌────────▼───┐ │ │ │ │ │ * │SEND_PSYNC │ │ │ │Replica loads local replication │ │ * └─┬──────────┘ │ │ │buffer into memory │ │ @@ -3594,6 +3589,7 @@ void syncWithPrimary(connection *conn) { sdsfree(err); err = NULL; server.repl_state = REPL_STATE_RECEIVE_VERSION_REPLY; + return; } /* Receive VERSION reply. */ @@ -4272,7 +4268,7 @@ void replicationResurrectProvisionalPrimary(void) { /* Create a primary client, but do not initialize the read handler yet, as this replica still has a local buffer to * drain. */ replicationCreatePrimaryClientWithHandler(server.repl_transfer_s, server.repl_provisional_primary.dbid, NULL); - memcpy(server.primary->replid, server.repl_provisional_primary.replid, CONFIG_RUN_ID_SIZE); + memcpy(server.primary->replid, server.repl_provisional_primary.replid, sizeof(server.repl_provisional_primary.replid)); server.primary->reploff = server.repl_provisional_primary.reploff; server.primary->read_reploff = server.repl_provisional_primary.read_reploff; server.primary_repl_offset = server.primary->reploff; diff --git a/src/sentinel.c b/src/sentinel.c index 711c4aea3e..ccd3ccbdca 100644 --- a/src/sentinel.c +++ b/src/sentinel.c @@ -416,8 +416,7 @@ void sentinelSimFailureCrash(void); void releaseSentinelValkeyInstance(sentinelValkeyInstance *ri); -void dictInstancesValDestructor(dict *d, void *obj) { - UNUSED(d); +void dictInstancesValDestructor(void *obj) { releaseSentinelValkeyInstance(obj); } @@ -4259,7 +4258,7 @@ void sentinelSetCommand(client *c) { /* If the target name is the same as the source name there * is no need to add an entry mapping to itself. */ - if (!dictSdsKeyCaseCompare(ri->renamed_commands, oldname, newname)) { + if (!dictSdsKeyCaseCompare(oldname, newname)) { oldname = sdsdup(oldname); newname = sdsdup(newname); dictAdd(ri->renamed_commands, oldname, newname); diff --git a/src/server.c b/src/server.c index 508edc7112..aebbb57a93 100644 --- a/src/server.c +++ b/src/server.c @@ -360,25 +360,20 @@ void exitFromChild(int retcode) { * keys and Objects as values (Objects can hold SDS strings, * lists, sets). */ -void dictVanillaFree(dict *d, void *val) { - UNUSED(d); +void dictVanillaFree(void *val) { zfree(val); } -void dictListDestructor(dict *d, void *val) { - UNUSED(d); +void dictListDestructor(void *val) { listRelease((list *)val); } -void dictDictDestructor(dict *d, void *val) { - UNUSED(d); +void dictDictDestructor(void *val) { dictRelease((dict *)val); } -int dictSdsKeyCompare(dict *d, const void *key1, const void *key2) { +int dictSdsKeyCompare(const void *key1, const void *key2) { int l1, l2; - UNUSED(d); - l1 = sdslen((sds)key1); l2 = sdslen((sds)key2); if (l1 != l2) return 0; @@ -391,30 +386,26 @@ size_t dictSdsEmbedKey(unsigned char *buf, size_t buf_len, const void *key, uint /* A case insensitive version used for the command lookup table and other * places where case insensitive non binary-safe comparison is needed. */ -int dictSdsKeyCaseCompare(dict *d, const void *key1, const void *key2) { - UNUSED(d); +int dictSdsKeyCaseCompare(const void *key1, const void *key2) { return strcasecmp(key1, key2) == 0; } -void dictObjectDestructor(dict *d, void *val) { - UNUSED(d); +void dictObjectDestructor(void *val) { if (val == NULL) return; /* Lazy freeing will set value to NULL. */ decrRefCount(val); } -void dictSdsDestructor(dict *d, void *val) { - UNUSED(d); +void dictSdsDestructor(void *val) { sdsfree(val); } -void *dictSdsDup(dict *d, const void *key) { - UNUSED(d); +void *dictSdsDup(const void *key) { return sdsdup((const sds)key); } -int dictObjKeyCompare(dict *d, const void *key1, const void *key2) { +int dictObjKeyCompare(const void *key1, const void *key2) { const robj *o1 = key1, *o2 = key2; - return dictSdsKeyCompare(d, o1->ptr, o2->ptr); + return dictSdsKeyCompare(o1->ptr, o2->ptr); } uint64_t dictObjHash(const void *key) { @@ -446,16 +437,13 @@ uint64_t dictClientHash(const void *key) { } /* Dict compare function for client */ -int dictClientKeyCompare(dict *d, const void *key1, const void *key2) { - UNUSED(d); +int dictClientKeyCompare(const void *key1, const void *key2) { return ((client *)key1)->id == ((client *)key2)->id; } /* Dict compare function for null terminated string */ -int dictCStrKeyCompare(dict *d, const void *key1, const void *key2) { +int dictCStrKeyCompare(const void *key1, const void *key2) { int l1, l2; - UNUSED(d); - l1 = strlen((char *)key1); l2 = strlen((char *)key2); if (l1 != l2) return 0; @@ -463,12 +451,11 @@ int dictCStrKeyCompare(dict *d, const void *key1, const void *key2) { } /* Dict case insensitive compare function for null terminated string */ -int dictCStrKeyCaseCompare(dict *d, const void *key1, const void *key2) { - UNUSED(d); +int dictCStrKeyCaseCompare(const void *key1, const void *key2) { return strcasecmp(key1, key2) == 0; } -int dictEncObjKeyCompare(dict *d, const void *key1, const void *key2) { +int dictEncObjKeyCompare(const void *key1, const void *key2) { robj *o1 = (robj *)key1, *o2 = (robj *)key2; int cmp; @@ -480,7 +467,7 @@ int dictEncObjKeyCompare(dict *d, const void *key1, const void *key2) { * objects as well. */ if (o1->refcount != OBJ_STATIC_REFCOUNT) o1 = getDecodedObject(o1); if (o2->refcount != OBJ_STATIC_REFCOUNT) o2 = getDecodedObject(o2); - cmp = dictSdsKeyCompare(d, o1->ptr, o2->ptr); + cmp = dictSdsKeyCompare(o1->ptr, o2->ptr); if (o1->refcount != OBJ_STATIC_REFCOUNT) decrRefCount(o1); if (o2->refcount != OBJ_STATIC_REFCOUNT) decrRefCount(o2); return cmp; @@ -1144,10 +1131,10 @@ void databasesCron(void) { /* Expire keys by random sampling. Not required for replicas * as primary will synthesize DELs for us. */ if (server.active_expire_enabled) { - if (iAmPrimary()) { - activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW); - } else { + if (!iAmPrimary()) { expireReplicaKeys(); + } else if (!server.import_mode) { + activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW); } } @@ -1740,7 +1727,7 @@ void beforeSleep(struct aeEventLoop *eventLoop) { /* Run a fast expire cycle (the called function will return * ASAP if a fast cycle is not needed). */ - if (server.active_expire_enabled && iAmPrimary()) activeExpireCycle(ACTIVE_EXPIRE_CYCLE_FAST); + if (server.active_expire_enabled && !server.import_mode && iAmPrimary()) activeExpireCycle(ACTIVE_EXPIRE_CYCLE_FAST); if (moduleCount()) { moduleFireServerEvent(VALKEYMODULE_EVENT_EVENTLOOP, VALKEYMODULE_SUBEVENT_EVENTLOOP_BEFORE_SLEEP, NULL); @@ -2146,6 +2133,7 @@ void initServerConfig(void) { server.extended_redis_compat = 0; server.pause_cron = 0; server.dict_resizing = 1; + server.import_mode = 0; server.latency_tracking_info_percentiles_len = 3; server.latency_tracking_info_percentiles = zmalloc(sizeof(double) * (server.latency_tracking_info_percentiles_len)); @@ -2670,7 +2658,6 @@ void initServer(void) { server.aof_state = server.aof_enabled ? AOF_ON : AOF_OFF; server.fsynced_reploff = server.aof_enabled ? 0 : -1; server.hz = server.config_hz; - server.pid = getpid(); server.in_fork_child = CHILD_TYPE_NONE; server.rdb_pipe_read = -1; server.rdb_child_exit_pipe = -1; @@ -3329,8 +3316,28 @@ static void propagateNow(int dbid, robj **argv, int argc, int target) { if (!shouldPropagate(target)) return; /* This needs to be unreachable since the dataset should be fixed during - * replica pause (otherwise data may be lost during a failover) */ - serverAssert(!(isPausedActions(PAUSE_ACTION_REPLICA) && (!server.client_pause_in_transaction))); + * replica pause (otherwise data may be lost during a failover). + * + * Though, there are exceptions: + * + * 1. We allow write commands that were queued up before and after to + * execute, if a CLIENT PAUSE executed during a transaction, we will + * track the state, the CLIENT PAUSE takes effect only after a transaction + * has finished. + * 2. Primary loses a slot during the pause, deletes all keys and replicates + * DEL to its replicas. In this case, we will track the state, the dirty + * slots will be deleted in the end without affecting the data consistency. + * + * Note that case 2 can happen in one of the following scenarios: + * 1) The primary waits for the replica to replicate before exiting, see + * shutdown-timeout in conf for more details. In this case, primary lost + * a slot during the SIGTERM waiting. + * 2) The primary waits for the replica to replicate during a manual failover. + * In this case, primary lost a slot during the pausing. + * 3) The primary was paused by CLIENT PAUSE, and lost a slot during the + * pausing. */ + serverAssert(!isPausedActions(PAUSE_ACTION_REPLICA) || server.client_pause_in_transaction || + server.server_del_keys_in_slot); if (server.aof_state != AOF_OFF && target & PROPAGATE_AOF) feedAppendOnlyFile(dbid, argv, argc); if (target & PROPAGATE_REPL) replicationFeedReplicas(dbid, argv, argc); @@ -4165,12 +4172,6 @@ int processCommand(client *c) { return C_OK; } - /* Not allow several UNSUBSCRIBE commands executed under non-pubsub mode */ - if (!c->flag.pubsub && (c->cmd->proc == unsubscribeCommand || c->cmd->proc == sunsubscribeCommand || - c->cmd->proc == punsubscribeCommand)) { - rejectCommandFormat(c, "-NOSUB '%s' command executed not in subscribed mode", c->cmd->fullname); - return C_OK; - } /* Only allow commands with flag "t", such as INFO, REPLICAOF and so on, * when replica-serve-stale-data is no and we are a replica with a broken * link with primary. */ @@ -6396,7 +6397,7 @@ void closeChildUnusedResourceAfterFork(void) { int serverFork(int purpose) { if (isMutuallyExclusiveChildType(purpose)) { if (hasActiveChildProcess()) { - errno = EEXIST; + errno = EALREADY; return -1; } @@ -6781,85 +6782,12 @@ int iAmPrimary(void) { (server.cluster_enabled && clusterNodeIsPrimary(getMyClusterNode()))); } -#ifdef SERVER_TEST -#include "testhelp.h" -#include "intset.h" /* Compact integer set structure */ - -int __failed_tests = 0; -int __test_num = 0; - -/* The flags are the following: - * --accurate: Runs tests with more iterations. - * --large-memory: Enables tests that consume more than 100mb. */ -typedef int serverTestProc(int argc, char **argv, int flags); -struct serverTest { - char *name; - serverTestProc *proc; - int failed; -} serverTests[] = { - {"quicklist", quicklistTest}, -}; -serverTestProc *getTestProcByName(const char *name) { - int numtests = sizeof(serverTests) / sizeof(struct serverTest); - for (int j = 0; j < numtests; j++) { - if (!strcasecmp(name, serverTests[j].name)) { - return serverTests[j].proc; - } - } - return NULL; -} -#endif - /* Main is marked as weak so that unit tests can use their own main function. */ __attribute__((weak)) int main(int argc, char **argv) { struct timeval tv; int j; char config_from_stdin = 0; -#ifdef SERVER_TEST - monotonicInit(); /* Required for dict tests, that are relying on monotime during dict rehashing. */ - if (argc >= 3 && !strcasecmp(argv[1], "test")) { - int flags = 0; - for (j = 3; j < argc; j++) { - char *arg = argv[j]; - if (!strcasecmp(arg, "--accurate")) - flags |= TEST_ACCURATE; - else if (!strcasecmp(arg, "--large-memory")) - flags |= TEST_LARGE_MEMORY; - else if (!strcasecmp(arg, "--valgrind")) - flags |= TEST_VALGRIND; - } - - if (!strcasecmp(argv[2], "all")) { - int numtests = sizeof(serverTests) / sizeof(struct serverTest); - for (j = 0; j < numtests; j++) { - serverTests[j].failed = (serverTests[j].proc(argc, argv, flags) != 0); - } - - /* Report tests result */ - int failed_num = 0; - for (j = 0; j < numtests; j++) { - if (serverTests[j].failed) { - failed_num++; - printf("[failed] Test - %s\n", serverTests[j].name); - } else { - printf("[ok] Test - %s\n", serverTests[j].name); - } - } - - printf("%d tests, %d passed, %d failed\n", numtests, numtests - failed_num, failed_num); - - return failed_num == 0 ? 0 : 1; - } else { - serverTestProc *proc = getTestProcByName(argv[2]); - if (!proc) return -1; /* test not found */ - return proc(argc, argv, flags); - } - - return 0; - } -#endif - /* We need to initialize our libraries, and the server configuration. */ #ifdef INIT_SETPROCTITLE_REPLACEMENT spt_init(argc, argv); @@ -6889,6 +6817,7 @@ __attribute__((weak)) int main(int argc, char **argv) { if (exec_name == NULL) exec_name = argv[0]; server.sentinel_mode = checkForSentinelMode(argc, argv, exec_name); initServerConfig(); + server.pid = getpid(); ACLInit(); /* The ACL subsystem must be initialized ASAP because the basic networking code and client creation depends on it. */ moduleInitModulesSystem(); @@ -7070,7 +6999,12 @@ __attribute__((weak)) int main(int argc, char **argv) { /* Daemonize if needed */ server.supervised = serverIsSupervised(server.supervised_mode); int background = server.daemonize && !server.supervised; - if (background) daemonize(); + if (background) { + /* We need to reset server.pid after daemonize(), otherwise the + * log printing role will always be the child. */ + daemonize(); + server.pid = getpid(); + } serverLog(LL_NOTICE, "oO0OoO0OoO0Oo Valkey is starting oO0OoO0OoO0Oo"); serverLog(LL_NOTICE, "Valkey version=%s, bits=%d, commit=%s, modified=%d, pid=%d, just started", VALKEY_VERSION, @@ -7154,5 +7088,4 @@ __attribute__((weak)) int main(int argc, char **argv) { aeDeleteEventLoop(server.el); return 0; } - /* The End */ diff --git a/src/server.h b/src/server.h index 5cf56e9c86..531ca8e7c8 100644 --- a/src/server.h +++ b/src/server.h @@ -1233,7 +1233,8 @@ typedef struct ClientFlags { * knows that it does not need the cache and required a full sync. With this * flag, we won't cache the primary in freeClient. */ uint64_t fake : 1; /* This is a fake client without a real connection. */ - uint64_t reserved : 5; /* Reserved for future use */ + uint64_t import_source : 1; /* This client is importing data to server and can visit expired key. */ + uint64_t reserved : 4; /* Reserved for future use */ } ClientFlags; typedef struct client { @@ -1701,6 +1702,7 @@ struct valkeyServer { const char *busy_module_yield_reply; /* When non-null, we are inside RM_Yield. */ char *ignore_warnings; /* Config: warnings that should be ignored. */ int client_pause_in_transaction; /* Was a client pause executed during this Exec? */ + int server_del_keys_in_slot; /* The server is deleting the keys in the dirty slot. */ int thp_enabled; /* If true, THP is enabled. */ size_t page_size; /* The page size of OS. */ /* Modules */ @@ -2088,6 +2090,8 @@ struct valkeyServer { char primary_replid[CONFIG_RUN_ID_SIZE + 1]; /* Primary PSYNC runid. */ long long primary_initial_offset; /* Primary PSYNC offset. */ int repl_replica_lazy_flush; /* Lazy FLUSHALL before loading DB? */ + /* Import Mode */ + int import_mode; /* If true, server is in import mode and forbid expiration and eviction. */ /* Synchronous replication. */ list *clients_waiting_acks; /* Clients waiting in WAIT or WAITAOF. */ int get_ack_from_replicas; /* If true we send REPLCONF GETACK. */ @@ -2730,7 +2734,7 @@ int serverSetProcTitle(char *title); int validateProcTitleTemplate(const char *template); int serverCommunicateSystemd(const char *sd_notify_msg); void serverSetCpuAffinity(const char *cpulist); -void dictVanillaFree(dict *d, void *val); +void dictVanillaFree(void *val); /* ERROR STATS constants */ @@ -2863,7 +2867,7 @@ void flushReplicasOutputBuffers(void); void disconnectReplicas(void); void evictClients(void); int listenToPort(connListener *fds); -void pauseActions(pause_purpose purpose, mstime_t end, uint32_t actions_bitmask); +void pauseActions(pause_purpose purpose, mstime_t end, uint32_t actions); void unpauseActions(pause_purpose purpose); uint32_t isPausedActions(uint32_t action_bitmask); uint32_t isPausedActionsWithUpdate(uint32_t action_bitmask); @@ -3717,11 +3721,11 @@ void startEvictionTimeProc(void); /* Keys hashing / comparison functions for dict.c hash tables. */ uint64_t dictSdsHash(const void *key); uint64_t dictSdsCaseHash(const void *key); -int dictSdsKeyCompare(dict *d, const void *key1, const void *key2); -int dictSdsKeyCaseCompare(dict *d, const void *key1, const void *key2); -void dictSdsDestructor(dict *d, void *val); -void dictListDestructor(dict *d, void *val); -void *dictSdsDup(dict *d, const void *key); +int dictSdsKeyCompare(const void *key1, const void *key2); +int dictSdsKeyCaseCompare(const void *key1, const void *key2); +void dictSdsDestructor(void *val); +void dictListDestructor(void *val); +void *dictSdsDup(const void *key); /* Git SHA1 */ char *serverGitSHA1(void); diff --git a/src/tls.c b/src/tls.c index f1c82d35e4..a1fda2a7ae 100644 --- a/src/tls.c +++ b/src/tls.c @@ -446,6 +446,7 @@ typedef enum { #define TLS_CONN_FLAG_WRITE_WANT_READ (1 << 1) #define TLS_CONN_FLAG_FD_SET (1 << 2) #define TLS_CONN_FLAG_POSTPONE_UPDATE_STATE (1 << 3) +#define TLS_CONN_FLAG_HAS_PENDING (1 << 4) typedef struct tls_connection { connection c; @@ -614,7 +615,7 @@ static void updatePendingData(tls_connection *conn) { /* If SSL has pending data, already read from the socket, we're at risk of not calling the read handler again, make * sure to add it to a list of pending connection that should be handled anyway. */ - if (SSL_pending(conn->ssl) > 0) { + if (conn->flags & TLS_CONN_FLAG_HAS_PENDING) { if (!conn->pending_list_node) { listAddNodeTail(pending_list, conn); conn->pending_list_node = listLast(pending_list); @@ -625,6 +626,14 @@ static void updatePendingData(tls_connection *conn) { } } +void updateSSLPendingFlag(tls_connection *conn) { + if (SSL_pending(conn->ssl) > 0) { + conn->flags |= TLS_CONN_FLAG_HAS_PENDING; + } else { + conn->flags &= ~TLS_CONN_FLAG_HAS_PENDING; + } +} + static void updateSSLEvent(tls_connection *conn) { if (conn->flags & TLS_CONN_FLAG_POSTPONE_UPDATE_STATE) return; @@ -653,8 +662,6 @@ static void tlsHandleEvent(tls_connection *conn, int mask) { TLSCONN_DEBUG("tlsEventHandler(): fd=%d, state=%d, mask=%d, r=%d, w=%d, flags=%d", fd, conn->c.state, mask, conn->c.read_handler != NULL, conn->c.write_handler != NULL, conn->flags); - ERR_clear_error(); - switch (conn->c.state) { case CONN_STATE_CONNECTING: conn_error = anetGetError(conn->c.fd); @@ -662,6 +669,7 @@ static void tlsHandleEvent(tls_connection *conn, int mask) { conn->c.last_errno = conn_error; conn->c.state = CONN_STATE_ERROR; } else { + ERR_clear_error(); if (!(conn->flags & TLS_CONN_FLAG_FD_SET)) { SSL_set_fd(conn->ssl, conn->c.fd); conn->flags |= TLS_CONN_FLAG_FD_SET; @@ -690,6 +698,7 @@ static void tlsHandleEvent(tls_connection *conn, int mask) { conn->c.conn_handler = NULL; break; case CONN_STATE_ACCEPTING: + ERR_clear_error(); ret = SSL_accept(conn->ssl); if (ret <= 0) { WantIOType want = 0; @@ -747,10 +756,7 @@ static void tlsHandleEvent(tls_connection *conn, int mask) { conn->flags &= ~TLS_CONN_FLAG_READ_WANT_WRITE; if (!callHandler((connection *)conn, conn->c.read_handler)) return; } - - if (mask & AE_READABLE) { - updatePendingData(conn); - } + updatePendingData(conn); break; } @@ -941,6 +947,7 @@ static int connTLSRead(connection *conn_, void *buf, size_t buf_len) { if (conn->c.state != CONN_STATE_CONNECTED) return -1; ERR_clear_error(); ret = SSL_read(conn->ssl, buf, buf_len); + updateSSLPendingFlag(conn); return updateStateAfterSSLIO(conn, ret, 1); } @@ -992,7 +999,7 @@ static int connTLSBlockingConnect(connection *conn_, const char *addr, int port, * which means the specified timeout will not be enforced accurately. */ SSL_set_fd(conn->ssl, conn->c.fd); setBlockingTimeout(conn, timeout); - + ERR_clear_error(); if ((ret = SSL_connect(conn->ssl)) <= 0) { conn->c.state = CONN_STATE_ERROR; return C_ERR; @@ -1023,6 +1030,7 @@ static ssize_t connTLSSyncRead(connection *conn_, char *ptr, ssize_t size, long setBlockingTimeout(conn, timeout); ERR_clear_error(); int ret = SSL_read(conn->ssl, ptr, size); + updateSSLPendingFlag(conn); ret = updateStateAfterSSLIO(conn, ret, 0); unsetBlockingTimeout(conn); @@ -1041,6 +1049,7 @@ static ssize_t connTLSSyncReadLine(connection *conn_, char *ptr, ssize_t size, l ERR_clear_error(); int ret = SSL_read(conn->ssl, &c, 1); + updateSSLPendingFlag(conn); ret = updateStateAfterSSLIO(conn, ret, 0); if (ret <= 0) { nread = -1; diff --git a/src/unit/CMakeLists.txt b/src/unit/CMakeLists.txt new file mode 100644 index 0000000000..7d80c533cf --- /dev/null +++ b/src/unit/CMakeLists.txt @@ -0,0 +1,58 @@ +project(valkey-unit-tests) + +file(GLOB UNIT_TEST_SRCS "${CMAKE_CURRENT_LIST_DIR}/*.c") +set(UNIT_TEST_SRCS "${UNIT_TEST_SRCS}") + +get_valkey_server_linker_option(VALKEY_SERVER_LDFLAGS) + +# Build unit tests only +message(STATUS "Building unit tests") +list(APPEND COMPILE_DEFINITIONS "SERVER_TEST=1") +if (USE_TLS) + if (BUILD_TLS_MODULE) + # TLS as a module + list(APPEND COMPILE_DEFINITIONS "USE_OPENSSL=2") + else (BUILD_TLS_MODULE) + # Built-in TLS support + list(APPEND COMPILE_DEFINITIONS "USE_OPENSSL=1") + list(APPEND COMPILE_DEFINITIONS "BUILD_TLS_MODULE=0") + endif () +endif () + +# Build Valkey sources as a static library for the test +add_library(valkeylib STATIC ${VALKEY_SERVER_SRCS}) +target_compile_options(valkeylib PRIVATE "${COMPILE_FLAGS}") +target_compile_definitions(valkeylib PRIVATE "${COMPILE_DEFINITIONS}") + +add_executable(valkey-unit-tests ${UNIT_TEST_SRCS}) +target_compile_options(valkey-unit-tests PRIVATE "${COMPILE_FLAGS}") +target_compile_definitions(valkey-unit-tests PRIVATE "${COMPILE_DEFINITIONS}") +add_dependencies(valkey-unit-tests generate_test_files_h) + +if (UNIX AND NOT APPLE) + # Avoid duplicate symbols on non macOS + target_link_options(valkey-unit-tests PRIVATE "-Wl,--allow-multiple-definition") +endif () + +if (USE_JEMALLOC) + # Using jemalloc + target_link_libraries(valkey-unit-tests jemalloc) +endif () + +if (IS_FREEBSD) + target_link_libraries(valkey-unit-tests execinfo) +endif () + +target_link_libraries( + valkey-unit-tests + valkeylib + fpconv + lualib + hdr_histogram + hiredis + ${VALKEY_SERVER_LDFLAGS}) + +if (USE_TLS) + # Add required libraries needed for TLS + target_link_libraries(valkey-unit-tests OpenSSL::SSL hiredis_ssl) +endif () diff --git a/src/unit/test_dict.c b/src/unit/test_dict.c index a5af4eef79..b03d252c74 100644 --- a/src/unit/test_dict.c +++ b/src/unit/test_dict.c @@ -5,19 +5,15 @@ uint64_t hashCallback(const void *key) { return dictGenHashFunction((unsigned char *)key, strlen((char *)key)); } -int compareCallback(dict *d, const void *key1, const void *key2) { +int compareCallback(const void *key1, const void *key2) { int l1, l2; - UNUSED(d); - l1 = strlen((char *)key1); l2 = strlen((char *)key2); if (l1 != l2) return 0; return memcmp(key1, key2, l1) == 0; } -void freeCallback(dict *d, void *val) { - UNUSED(d); - +void freeCallback(void *val) { zfree(val); } diff --git a/src/unit/test_files.h b/src/unit/test_files.h index c2b062039a..87bc031fb4 100644 --- a/src/unit/test_files.h +++ b/src/unit/test_files.h @@ -84,6 +84,64 @@ int test_listpackBenchmarkLpValidateIntegrity(int argc, char **argv, int flags); int test_listpackBenchmarkLpCompareWithString(int argc, char **argv, int flags); int test_listpackBenchmarkLpCompareWithNumber(int argc, char **argv, int flags); int test_listpackBenchmarkFree(int argc, char **argv, int flags); +int test_quicklistCreateList(int argc, char **argv, int flags); +int test_quicklistAddToTailOfEmptyList(int argc, char **argv, int flags); +int test_quicklistAddToHeadOfEmptyList(int argc, char **argv, int flags); +int test_quicklistAddToTail5xAtCompress(int argc, char **argv, int flags); +int test_quicklistAddToHead5xAtCompress(int argc, char **argv, int flags); +int test_quicklistAddToTail500xAtCompress(int argc, char **argv, int flags); +int test_quicklistAddToHead500xAtCompress(int argc, char **argv, int flags); +int test_quicklistRotateEmpty(int argc, char **argv, int flags); +int test_quicklistComprassionPlainNode(int argc, char **argv, int flags); +int test_quicklistNextPlainNode(int argc, char **argv, int flags); +int test_quicklistRotatePlainNode(int argc, char **argv, int flags); +int test_quicklistRotateOneValOnce(int argc, char **argv, int flags); +int test_quicklistRotate500Val5000TimesAtCompress(int argc, char **argv, int flags); +int test_quicklistPopEmpty(int argc, char **argv, int flags); +int test_quicklistPop1StringFrom1(int argc, char **argv, int flags); +int test_quicklistPopHead1NumberFrom1(int argc, char **argv, int flags); +int test_quicklistPopHead500From500(int argc, char **argv, int flags); +int test_quicklistPopHead5000From500(int argc, char **argv, int flags); +int test_quicklistIterateForwardOver500List(int argc, char **argv, int flags); +int test_quicklistIterateReverseOver500List(int argc, char **argv, int flags); +int test_quicklistInsertAfter1Element(int argc, char **argv, int flags); +int test_quicklistInsertBefore1Element(int argc, char **argv, int flags); +int test_quicklistInsertHeadWhileHeadNodeIsFull(int argc, char **argv, int flags); +int test_quicklistInsertTailWhileTailNodeIsFull(int argc, char **argv, int flags); +int test_quicklistInsertOnceInElementsWhileIteratingAtCompress(int argc, char **argv, int flags); +int test_quicklistInsertBefore250NewInMiddleOf500ElementsAtCompress(int argc, char **argv, int flags); +int test_quicklistInsertAfter250NewInMiddleOf500ElementsAtCompress(int argc, char **argv, int flags); +int test_quicklistDuplicateEmptyList(int argc, char **argv, int flags); +int test_quicklistDuplicateListOf1Element(int argc, char **argv, int flags); +int test_quicklistDuplicateListOf500(int argc, char **argv, int flags); +int test_quicklistIndex1200From500ListAtFill(int argc, char **argv, int flags); +int test_quicklistIndex12From500ListAtFill(int argc, char **argv, int flags); +int test_quicklistIndex100From500ListAtFill(int argc, char **argv, int flags); +int test_quicklistIndexTooBig1From50ListAtFill(int argc, char **argv, int flags); +int test_quicklistDeleteRangeEmptyList(int argc, char **argv, int flags); +int test_quicklistDeleteRangeOfEntireNodeInListOfOneNode(int argc, char **argv, int flags); +int test_quicklistDeleteRangeOfEntireNodeWithOverflowCounts(int argc, char **argv, int flags); +int test_quicklistDeleteMiddle100Of500List(int argc, char **argv, int flags); +int test_quicklistDeleteLessThanFillButAcrossNodes(int argc, char **argv, int flags); +int test_quicklistDeleteNegative1From500List(int argc, char **argv, int flags); +int test_quicklistDeleteNegative1From500ListWithOverflowCounts(int argc, char **argv, int flags); +int test_quicklistDeleteNegative100From500List(int argc, char **argv, int flags); +int test_quicklistDelete10Count5From50List(int argc, char **argv, int flags); +int test_quicklistNumbersOnlyListRead(int argc, char **argv, int flags); +int test_quicklistNumbersLargerListRead(int argc, char **argv, int flags); +int test_quicklistNumbersLargerListReadB(int argc, char **argv, int flags); +int test_quicklistLremTestAtCompress(int argc, char **argv, int flags); +int test_quicklistIterateReverseDeleteAtCompress(int argc, char **argv, int flags); +int test_quicklistIteratorAtIndexTestAtCompress(int argc, char **argv, int flags); +int test_quicklistLtrimTestAAtCompress(int argc, char **argv, int flags); +int test_quicklistLtrimTestBAtCompress(int argc, char **argv, int flags); +int test_quicklistLtrimTestCAtCompress(int argc, char **argv, int flags); +int test_quicklistLtrimTestDAtCompress(int argc, char **argv, int flags); +int test_quicklistVerifySpecificCompressionOfInteriorNodes(int argc, char **argv, int flags); +int test_quicklistBookmarkGetUpdatedToNextItem(int argc, char **argv, int flags); +int test_quicklistBookmarkLimit(int argc, char **argv, int flags); +int test_quicklistCompressAndDecompressQuicklistListpackNode(int argc, char **argv, int flags); +int test_quicklistCompressAndDecomressQuicklistPlainNodeLargeThanUINT32MAX(int argc, char **argv, int flags); int test_raxRandomWalk(int argc, char **argv, int flags); int test_raxIteratorUnitTests(int argc, char **argv, int flags); int test_raxTryInsertUnitTests(int argc, char **argv, int flags); @@ -157,6 +215,7 @@ unitTest __test_endianconv_c[] = {{"test_endianconv", test_endianconv}, {NULL, N unitTest __test_intset_c[] = {{"test_intsetValueEncodings", test_intsetValueEncodings}, {"test_intsetBasicAdding", test_intsetBasicAdding}, {"test_intsetLargeNumberRandomAdd", test_intsetLargeNumberRandomAdd}, {"test_intsetUpgradeFromint16Toint32", test_intsetUpgradeFromint16Toint32}, {"test_intsetUpgradeFromint16Toint64", test_intsetUpgradeFromint16Toint64}, {"test_intsetUpgradeFromint32Toint64", test_intsetUpgradeFromint32Toint64}, {"test_intsetStressLookups", test_intsetStressLookups}, {"test_intsetStressAddDelete", test_intsetStressAddDelete}, {NULL, NULL}}; unitTest __test_kvstore_c[] = {{"test_kvstoreAdd16Keys", test_kvstoreAdd16Keys}, {"test_kvstoreIteratorRemoveAllKeysNoDeleteEmptyDict", test_kvstoreIteratorRemoveAllKeysNoDeleteEmptyDict}, {"test_kvstoreIteratorRemoveAllKeysDeleteEmptyDict", test_kvstoreIteratorRemoveAllKeysDeleteEmptyDict}, {"test_kvstoreDictIteratorRemoveAllKeysNoDeleteEmptyDict", test_kvstoreDictIteratorRemoveAllKeysNoDeleteEmptyDict}, {"test_kvstoreDictIteratorRemoveAllKeysDeleteEmptyDict", test_kvstoreDictIteratorRemoveAllKeysDeleteEmptyDict}, {NULL, NULL}}; unitTest __test_listpack_c[] = {{"test_listpackCreateIntList", test_listpackCreateIntList}, {"test_listpackCreateList", test_listpackCreateList}, {"test_listpackLpPrepend", test_listpackLpPrepend}, {"test_listpackLpPrependInteger", test_listpackLpPrependInteger}, {"test_listpackGetELementAtIndex", test_listpackGetELementAtIndex}, {"test_listpackPop", test_listpackPop}, {"test_listpackGetELementAtIndex2", test_listpackGetELementAtIndex2}, {"test_listpackIterate0toEnd", test_listpackIterate0toEnd}, {"test_listpackIterate1toEnd", test_listpackIterate1toEnd}, {"test_listpackIterate2toEnd", test_listpackIterate2toEnd}, {"test_listpackIterateBackToFront", test_listpackIterateBackToFront}, {"test_listpackIterateBackToFrontWithDelete", test_listpackIterateBackToFrontWithDelete}, {"test_listpackDeleteWhenNumIsMinusOne", test_listpackDeleteWhenNumIsMinusOne}, {"test_listpackDeleteWithNegativeIndex", test_listpackDeleteWithNegativeIndex}, {"test_listpackDeleteInclusiveRange0_0", test_listpackDeleteInclusiveRange0_0}, {"test_listpackDeleteInclusiveRange0_1", test_listpackDeleteInclusiveRange0_1}, {"test_listpackDeleteInclusiveRange1_2", test_listpackDeleteInclusiveRange1_2}, {"test_listpackDeleteWitStartIndexOutOfRange", test_listpackDeleteWitStartIndexOutOfRange}, {"test_listpackDeleteWitNumOverflow", test_listpackDeleteWitNumOverflow}, {"test_listpackBatchDelete", test_listpackBatchDelete}, {"test_listpackDeleteFooWhileIterating", test_listpackDeleteFooWhileIterating}, {"test_listpackReplaceWithSameSize", test_listpackReplaceWithSameSize}, {"test_listpackReplaceWithDifferentSize", test_listpackReplaceWithDifferentSize}, {"test_listpackRegressionGt255Bytes", test_listpackRegressionGt255Bytes}, {"test_listpackCreateLongListAndCheckIndices", test_listpackCreateLongListAndCheckIndices}, {"test_listpackCompareStrsWithLpEntries", test_listpackCompareStrsWithLpEntries}, {"test_listpackLpMergeEmptyLps", test_listpackLpMergeEmptyLps}, {"test_listpackLpMergeLp1Larger", test_listpackLpMergeLp1Larger}, {"test_listpackLpMergeLp2Larger", test_listpackLpMergeLp2Larger}, {"test_listpackLpNextRandom", test_listpackLpNextRandom}, {"test_listpackLpNextRandomCC", test_listpackLpNextRandomCC}, {"test_listpackRandomPairWithOneElement", test_listpackRandomPairWithOneElement}, {"test_listpackRandomPairWithManyElements", test_listpackRandomPairWithManyElements}, {"test_listpackRandomPairsWithOneElement", test_listpackRandomPairsWithOneElement}, {"test_listpackRandomPairsWithManyElements", test_listpackRandomPairsWithManyElements}, {"test_listpackRandomPairsUniqueWithOneElement", test_listpackRandomPairsUniqueWithOneElement}, {"test_listpackRandomPairsUniqueWithManyElements", test_listpackRandomPairsUniqueWithManyElements}, {"test_listpackPushVariousEncodings", test_listpackPushVariousEncodings}, {"test_listpackLpFind", test_listpackLpFind}, {"test_listpackLpValidateIntegrity", test_listpackLpValidateIntegrity}, {"test_listpackNumberOfElementsExceedsLP_HDR_NUMELE_UNKNOWN", test_listpackNumberOfElementsExceedsLP_HDR_NUMELE_UNKNOWN}, {"test_listpackStressWithRandom", test_listpackStressWithRandom}, {"test_listpackSTressWithVariableSize", test_listpackSTressWithVariableSize}, {"test_listpackBenchmarkInit", test_listpackBenchmarkInit}, {"test_listpackBenchmarkLpAppend", test_listpackBenchmarkLpAppend}, {"test_listpackBenchmarkLpFindString", test_listpackBenchmarkLpFindString}, {"test_listpackBenchmarkLpFindNumber", test_listpackBenchmarkLpFindNumber}, {"test_listpackBenchmarkLpSeek", test_listpackBenchmarkLpSeek}, {"test_listpackBenchmarkLpValidateIntegrity", test_listpackBenchmarkLpValidateIntegrity}, {"test_listpackBenchmarkLpCompareWithString", test_listpackBenchmarkLpCompareWithString}, {"test_listpackBenchmarkLpCompareWithNumber", test_listpackBenchmarkLpCompareWithNumber}, {"test_listpackBenchmarkFree", test_listpackBenchmarkFree}, {NULL, NULL}}; +unitTest __test_quicklist_c[] = {{"test_quicklistCreateList", test_quicklistCreateList}, {"test_quicklistAddToTailOfEmptyList", test_quicklistAddToTailOfEmptyList}, {"test_quicklistAddToHeadOfEmptyList", test_quicklistAddToHeadOfEmptyList}, {"test_quicklistAddToTail5xAtCompress", test_quicklistAddToTail5xAtCompress}, {"test_quicklistAddToHead5xAtCompress", test_quicklistAddToHead5xAtCompress}, {"test_quicklistAddToTail500xAtCompress", test_quicklistAddToTail500xAtCompress}, {"test_quicklistAddToHead500xAtCompress", test_quicklistAddToHead500xAtCompress}, {"test_quicklistRotateEmpty", test_quicklistRotateEmpty}, {"test_quicklistComprassionPlainNode", test_quicklistComprassionPlainNode}, {"test_quicklistNextPlainNode", test_quicklistNextPlainNode}, {"test_quicklistRotatePlainNode", test_quicklistRotatePlainNode}, {"test_quicklistRotateOneValOnce", test_quicklistRotateOneValOnce}, {"test_quicklistRotate500Val5000TimesAtCompress", test_quicklistRotate500Val5000TimesAtCompress}, {"test_quicklistPopEmpty", test_quicklistPopEmpty}, {"test_quicklistPop1StringFrom1", test_quicklistPop1StringFrom1}, {"test_quicklistPopHead1NumberFrom1", test_quicklistPopHead1NumberFrom1}, {"test_quicklistPopHead500From500", test_quicklistPopHead500From500}, {"test_quicklistPopHead5000From500", test_quicklistPopHead5000From500}, {"test_quicklistIterateForwardOver500List", test_quicklistIterateForwardOver500List}, {"test_quicklistIterateReverseOver500List", test_quicklistIterateReverseOver500List}, {"test_quicklistInsertAfter1Element", test_quicklistInsertAfter1Element}, {"test_quicklistInsertBefore1Element", test_quicklistInsertBefore1Element}, {"test_quicklistInsertHeadWhileHeadNodeIsFull", test_quicklistInsertHeadWhileHeadNodeIsFull}, {"test_quicklistInsertTailWhileTailNodeIsFull", test_quicklistInsertTailWhileTailNodeIsFull}, {"test_quicklistInsertOnceInElementsWhileIteratingAtCompress", test_quicklistInsertOnceInElementsWhileIteratingAtCompress}, {"test_quicklistInsertBefore250NewInMiddleOf500ElementsAtCompress", test_quicklistInsertBefore250NewInMiddleOf500ElementsAtCompress}, {"test_quicklistInsertAfter250NewInMiddleOf500ElementsAtCompress", test_quicklistInsertAfter250NewInMiddleOf500ElementsAtCompress}, {"test_quicklistDuplicateEmptyList", test_quicklistDuplicateEmptyList}, {"test_quicklistDuplicateListOf1Element", test_quicklistDuplicateListOf1Element}, {"test_quicklistDuplicateListOf500", test_quicklistDuplicateListOf500}, {"test_quicklistIndex1200From500ListAtFill", test_quicklistIndex1200From500ListAtFill}, {"test_quicklistIndex12From500ListAtFill", test_quicklistIndex12From500ListAtFill}, {"test_quicklistIndex100From500ListAtFill", test_quicklistIndex100From500ListAtFill}, {"test_quicklistIndexTooBig1From50ListAtFill", test_quicklistIndexTooBig1From50ListAtFill}, {"test_quicklistDeleteRangeEmptyList", test_quicklistDeleteRangeEmptyList}, {"test_quicklistDeleteRangeOfEntireNodeInListOfOneNode", test_quicklistDeleteRangeOfEntireNodeInListOfOneNode}, {"test_quicklistDeleteRangeOfEntireNodeWithOverflowCounts", test_quicklistDeleteRangeOfEntireNodeWithOverflowCounts}, {"test_quicklistDeleteMiddle100Of500List", test_quicklistDeleteMiddle100Of500List}, {"test_quicklistDeleteLessThanFillButAcrossNodes", test_quicklistDeleteLessThanFillButAcrossNodes}, {"test_quicklistDeleteNegative1From500List", test_quicklistDeleteNegative1From500List}, {"test_quicklistDeleteNegative1From500ListWithOverflowCounts", test_quicklistDeleteNegative1From500ListWithOverflowCounts}, {"test_quicklistDeleteNegative100From500List", test_quicklistDeleteNegative100From500List}, {"test_quicklistDelete10Count5From50List", test_quicklistDelete10Count5From50List}, {"test_quicklistNumbersOnlyListRead", test_quicklistNumbersOnlyListRead}, {"test_quicklistNumbersLargerListRead", test_quicklistNumbersLargerListRead}, {"test_quicklistNumbersLargerListReadB", test_quicklistNumbersLargerListReadB}, {"test_quicklistLremTestAtCompress", test_quicklistLremTestAtCompress}, {"test_quicklistIterateReverseDeleteAtCompress", test_quicklistIterateReverseDeleteAtCompress}, {"test_quicklistIteratorAtIndexTestAtCompress", test_quicklistIteratorAtIndexTestAtCompress}, {"test_quicklistLtrimTestAAtCompress", test_quicklistLtrimTestAAtCompress}, {"test_quicklistLtrimTestBAtCompress", test_quicklistLtrimTestBAtCompress}, {"test_quicklistLtrimTestCAtCompress", test_quicklistLtrimTestCAtCompress}, {"test_quicklistLtrimTestDAtCompress", test_quicklistLtrimTestDAtCompress}, {"test_quicklistVerifySpecificCompressionOfInteriorNodes", test_quicklistVerifySpecificCompressionOfInteriorNodes}, {"test_quicklistBookmarkGetUpdatedToNextItem", test_quicklistBookmarkGetUpdatedToNextItem}, {"test_quicklistBookmarkLimit", test_quicklistBookmarkLimit}, {"test_quicklistCompressAndDecompressQuicklistListpackNode", test_quicklistCompressAndDecompressQuicklistListpackNode}, {"test_quicklistCompressAndDecomressQuicklistPlainNodeLargeThanUINT32MAX", test_quicklistCompressAndDecomressQuicklistPlainNodeLargeThanUINT32MAX}, {NULL, NULL}}; unitTest __test_rax_c[] = {{"test_raxRandomWalk", test_raxRandomWalk}, {"test_raxIteratorUnitTests", test_raxIteratorUnitTests}, {"test_raxTryInsertUnitTests", test_raxTryInsertUnitTests}, {"test_raxRegressionTest1", test_raxRegressionTest1}, {"test_raxRegressionTest2", test_raxRegressionTest2}, {"test_raxRegressionTest3", test_raxRegressionTest3}, {"test_raxRegressionTest4", test_raxRegressionTest4}, {"test_raxRegressionTest5", test_raxRegressionTest5}, {"test_raxRegressionTest6", test_raxRegressionTest6}, {"test_raxBenchmark", test_raxBenchmark}, {"test_raxHugeKey", test_raxHugeKey}, {"test_raxFuzz", test_raxFuzz}, {NULL, NULL}}; unitTest __test_sds_c[] = {{"test_sds", test_sds}, {"test_typesAndAllocSize", test_typesAndAllocSize}, {"test_sdsHeaderSizes", test_sdsHeaderSizes}, {"test_sdssplitargs", test_sdssplitargs}, {NULL, NULL}}; unitTest __test_sha1_c[] = {{"test_sha1", test_sha1}, {NULL, NULL}}; @@ -176,6 +235,7 @@ struct unitTestSuite { {"test_intset.c", __test_intset_c}, {"test_kvstore.c", __test_kvstore_c}, {"test_listpack.c", __test_listpack_c}, + {"test_quicklist.c", __test_quicklist_c}, {"test_rax.c", __test_rax_c}, {"test_sds.c", __test_sds_c}, {"test_sha1.c", __test_sha1_c}, diff --git a/src/unit/test_kvstore.c b/src/unit/test_kvstore.c index b3eff7d132..062b9f32fc 100644 --- a/src/unit/test_kvstore.c +++ b/src/unit/test_kvstore.c @@ -5,8 +5,7 @@ uint64_t hashTestCallback(const void *key) { return dictGenHashFunction((unsigned char *)key, strlen((char *)key)); } -void freeTestCallback(dict *d, void *val) { - UNUSED(d); +void freeTestCallback(void *val) { zfree(val); } diff --git a/src/unit/test_quicklist.c b/src/unit/test_quicklist.c new file mode 100644 index 0000000000..6addb33f41 --- /dev/null +++ b/src/unit/test_quicklist.c @@ -0,0 +1,2300 @@ +#include +#include +#include +#include "test_help.h" +#include +#include + +#include "../zmalloc.h" +#include "../listpack.h" +#include "../quicklist.c" + +static int options[] = {0, 1, 2, 3, 4, 5, 6, 10}; +static int option_count = 8; + +static int fills[] = {-5, -4, -3, -2, -1, 0, + 1, 2, 32, 66, 128, 999}; +static int fill_count = 12; +static long long runtime[8]; +static unsigned int err = 0; + +/*----------------------------------------------------------------------------- + * Unit Function + *----------------------------------------------------------------------------*/ +/* Return the UNIX time in microseconds */ +static long long ustime(void) { + struct timeval tv; + long long ust; + + gettimeofday(&tv, NULL); + ust = ((long long)tv.tv_sec) * 1000000; + ust += tv.tv_usec; + return ust; +} + +/* Return the UNIX time in milliseconds */ +static long long mstime(void) { + return ustime() / 1000; +} + +/* Generate new string concatenating integer i against string 'prefix' */ +static char *genstr(char *prefix, int i) { + static char result[64] = {0}; + snprintf(result, sizeof(result), "%s%d", prefix, i); + return result; +} + +__attribute__((unused)) static void randstring(unsigned char *target, size_t sz) { + size_t p = 0; + int minval, maxval; + switch (rand() % 3) { + case 0: + minval = 'a'; + maxval = 'z'; + break; + case 1: + minval = '0'; + maxval = '9'; + break; + case 2: + minval = 'A'; + maxval = 'Z'; + break; + default: + abort(); + } + + while (p < sz) + target[p++] = minval + rand() % (maxval - minval + 1); +} + +#define TEST(name) printf("test — %s\n", name); + +#define QL_TEST_VERBOSE 0 +static void ql_info(quicklist *ql) { +#if QL_TEST_VERBOSE + TEST_PRINT_INFO("Container length: %lu\n", ql->len); + TEST_PRINT_INFO("Container size: %lu\n", ql->count); + if (ql->head) + TEST_PRINT_INFO("\t(zsize head: %lu)\n", lpLength(ql->head->entry)); + if (ql->tail) + TEST_PRINT_INFO("\t(zsize tail: %lu)\n", lpLength(ql->tail->entry)); +#else + UNUSED(ql); +#endif +} + +/* Iterate over an entire quicklist. + * Print the list if 'print' == 1. + * + * Returns physical count of elements found by iterating over the list. */ +static int _itrprintr(quicklist *ql, int print, int forward) { + quicklistIter *iter = + quicklistGetIterator(ql, forward ? AL_START_HEAD : AL_START_TAIL); + quicklistEntry entry; + int i = 0; + int p = 0; + quicklistNode *prev = NULL; + while (quicklistNext(iter, &entry)) { + if (entry.node != prev) { + /* Count the number of list nodes too */ + p++; + prev = entry.node; + } + if (print) { + int size = (entry.sz > (1 << 20)) ? 1 << 20 : entry.sz; + TEST_PRINT_INFO("[%3d (%2d)]: [%.*s] (%lld)\n", i, p, size, + (char *)entry.value, entry.longval); + } + i++; + } + quicklistReleaseIterator(iter); + return i; +} + +static int itrprintr(quicklist *ql, int print) { + return _itrprintr(ql, print, 1); +} + +static int itrprintr_rev(quicklist *ql, int print) { + return _itrprintr(ql, print, 0); +} + +#define ql_verify(a, b, c, d, e) \ + do { \ + err += _ql_verify((a), (b), (c), (d), (e)); \ + } while (0) + +static int _ql_verify_compress(quicklist *ql) { + int errors = 0; + if (quicklistAllowsCompression(ql)) { + quicklistNode *node = ql->head; + unsigned int low_raw = ql->compress; + unsigned int high_raw = ql->len - ql->compress; + + for (unsigned int at = 0; at < ql->len; at++, node = node->next) { + if (node && (at < low_raw || at >= high_raw)) { + if (node->encoding != QUICKLIST_NODE_ENCODING_RAW) { + TEST_PRINT_INFO("Incorrect compression: node %d is " + "compressed at depth %d ((%u, %u); total " + "nodes: %lu; size: %zu; recompress: %d)", + at, ql->compress, low_raw, high_raw, ql->len, node->sz, + node->recompress); + errors++; + } + } else { + if (node->encoding != QUICKLIST_NODE_ENCODING_LZF && + !node->attempted_compress) { + TEST_PRINT_INFO("Incorrect non-compression: node %d is NOT " + "compressed at depth %d ((%u, %u); total " + "nodes: %lu; size: %zu; recompress: %d; attempted: %d)", + at, ql->compress, low_raw, high_raw, ql->len, node->sz, + node->recompress, node->attempted_compress); + errors++; + } + } + } + } + return errors; +} + +/* Verify list metadata matches physical list contents. */ +static int _ql_verify(quicklist *ql, uint32_t len, uint32_t count, uint32_t head_count, uint32_t tail_count) { + int errors = 0; + + ql_info(ql); + if (len != ql->len) { + TEST_PRINT_INFO("quicklist length wrong: expected %d, got %lu", len, ql->len); + errors++; + } + + if (count != ql->count) { + TEST_PRINT_INFO("quicklist count wrong: expected %d, got %lu", count, ql->count); + errors++; + } + + int loopr = itrprintr(ql, 0); + if (loopr != (int)ql->count) { + TEST_PRINT_INFO("quicklist cached count not match actual count: expected %lu, got " + "%d", + ql->count, loopr); + errors++; + } + + int rloopr = itrprintr_rev(ql, 0); + if (loopr != rloopr) { + TEST_PRINT_INFO("quicklist has different forward count than reverse count! " + "Forward count is %d, reverse count is %d.", + loopr, rloopr); + errors++; + } + + if (ql->len == 0 && !errors) { + return errors; + } + + if (ql->head && head_count != ql->head->count && + head_count != lpLength(ql->head->entry)) { + TEST_PRINT_INFO("quicklist head count wrong: expected %d, " + "got cached %d vs. actual %lu", + head_count, ql->head->count, lpLength(ql->head->entry)); + errors++; + } + + if (ql->tail && tail_count != ql->tail->count && + tail_count != lpLength(ql->tail->entry)) { + TEST_PRINT_INFO("quicklist tail count wrong: expected %d, " + "got cached %u vs. actual %lu", + tail_count, ql->tail->count, lpLength(ql->tail->entry)); + errors++; + } + + errors += _ql_verify_compress(ql); + return errors; +} + +/* Release iterator and verify compress correctly. */ +static void ql_release_iterator(quicklistIter *iter) { + quicklist *ql = NULL; + if (iter) ql = iter->quicklist; + quicklistReleaseIterator(iter); + if (ql && _ql_verify_compress(ql)) { + abort(); + } +} + +/*----------------------------------------------------------------------------- + * Quicklist Unit Test + *----------------------------------------------------------------------------*/ +int test_quicklistCreateList(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + TEST("create list"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + quicklist *ql = quicklistNew(-2, options[_i]); + ql_verify(ql, 0, 0, 0, 0); + quicklistRelease(ql); + long long stop = mstime(); + runtime[_i] += stop - start; + } + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistAddToTailOfEmptyList(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("add to tail of empty list"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + quicklist *ql = quicklistNew(-2, options[_i]); + quicklistPushTail(ql, "hello", 6); + /* 1 for head and 1 for tail because 1 node = head = tail */ + ql_verify(ql, 1, 1, 1, 1); + quicklistRelease(ql); + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistAddToHeadOfEmptyList(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("add to head of empty list"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + quicklist *ql = quicklistNew(-2, options[_i]); + quicklistPushHead(ql, "hello", 6); + /* 1 for head and 1 for tail because 1 node = head = tail */ + ql_verify(ql, 1, 1, 1, 1); + quicklistRelease(ql); + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistAddToTail5xAtCompress(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("add to tail 5x at compress"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + for (int f = 0; f < fill_count; f++) { + quicklist *ql = quicklistNew(fills[f], options[_i]); + for (int i = 0; i < 5; i++) quicklistPushTail(ql, genstr("hello", i), 32); + if (ql->count != 5) { + err++; + }; + if (fills[f] == 32) ql_verify(ql, 1, 5, 5, 5); + quicklistRelease(ql); + } + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistAddToHead5xAtCompress(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("add to head 5x at compress"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + for (int f = 0; f < fill_count; f++) { + quicklist *ql = quicklistNew(fills[f], options[_i]); + for (int i = 0; i < 5; i++) quicklistPushHead(ql, genstr("hello", i), 32); + if (ql->count != 5) { + err++; + }; + if (fills[f] == 32) ql_verify(ql, 1, 5, 5, 5); + quicklistRelease(ql); + } + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistAddToTail500xAtCompress(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("add to tail 500x at compress"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + for (int f = 0; f < fill_count; f++) { + quicklist *ql = quicklistNew(fills[f], options[_i]); + for (int i = 0; i < 500; i++) quicklistPushTail(ql, genstr("hello", i), 64); + if (ql->count != 500) { + err++; + }; + if (fills[f] == 32) ql_verify(ql, 16, 500, 32, 20); + quicklistRelease(ql); + } + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistAddToHead500xAtCompress(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("add to head 500x at compress"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + for (int f = 0; f < fill_count; f++) { + quicklist *ql = quicklistNew(fills[f], options[_i]); + for (int i = 0; i < 500; i++) quicklistPushHead(ql, genstr("hello", i), 32); + if (ql->count != 500) { + err++; + }; + if (fills[f] == 32) ql_verify(ql, 16, 500, 20, 32); + quicklistRelease(ql); + } + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistRotateEmpty(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("rotate empty"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + quicklist *ql = quicklistNew(-2, options[_i]); + quicklistRotate(ql); + ql_verify(ql, 0, 0, 0, 0); + quicklistRelease(ql); + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistComprassionPlainNode(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("Comprassion Plain node"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + for (int f = 0; f < fill_count; f++) { + size_t large_limit = (fills[f] < 0) ? quicklistNodeNegFillLimit(fills[f]) + 1 : SIZE_SAFETY_LIMIT + 1; + + char buf[large_limit]; + quicklist *ql = quicklistNew(fills[f], 1); + for (int i = 0; i < 500; i++) { + /* Set to 256 to allow the node to be triggered to compress, + * if it is less than 48(nocompress), the test will be successful. */ + snprintf(buf, sizeof(buf), "hello%d", i); + quicklistPushHead(ql, buf, large_limit); + } + + quicklistIter *iter = quicklistGetIterator(ql, AL_START_TAIL); + quicklistEntry entry; + int i = 0; + while (quicklistNext(iter, &entry)) { + TEST_ASSERT(QL_NODE_IS_PLAIN(entry.node)); + snprintf(buf, sizeof(buf), "hello%d", i); + if (strcmp((char *)entry.value, buf)) { + TEST_PRINT_INFO("value [%s] didn't match [%s] at position %d", entry.value, buf, i); + err++; + } + i++; + } + ql_release_iterator(iter); + quicklistRelease(ql); + } + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistNextPlainNode(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("NEXT plain node"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + for (int f = 0; f < fill_count; f++) { + size_t large_limit = (fills[f] < 0) ? quicklistNodeNegFillLimit(fills[f]) + 1 : SIZE_SAFETY_LIMIT + 1; + quicklist *ql = quicklistNew(fills[f], options[_i]); + + char buf[large_limit]; + memcpy(buf, "plain", 5); + quicklistPushHead(ql, buf, large_limit); + quicklistPushHead(ql, buf, large_limit); + quicklistPushHead(ql, "packed3", 7); + quicklistPushHead(ql, "packed4", 7); + quicklistPushHead(ql, buf, large_limit); + + quicklistEntry entry; + quicklistIter *iter = quicklistGetIterator(ql, AL_START_TAIL); + + while (quicklistNext(iter, &entry) != 0) { + if (QL_NODE_IS_PLAIN(entry.node)) + TEST_ASSERT(!memcmp(entry.value, "plain", 5)); + else + TEST_ASSERT(!memcmp(entry.value, "packed", 6)); + } + ql_release_iterator(iter); + quicklistRelease(ql); + } + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistRotatePlainNode(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("rotate plain node"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + for (int f = 0; f < fill_count; f++) { + size_t large_limit = (fills[f] < 0) ? quicklistNodeNegFillLimit(fills[f]) + 1 : SIZE_SAFETY_LIMIT + 1; + + unsigned char *data = NULL; + size_t sz; + long long lv; + int i = 0; + quicklist *ql = quicklistNew(fills[f], options[_i]); + char buf[large_limit]; + memcpy(buf, "hello1", 6); + quicklistPushHead(ql, buf, large_limit); + memcpy(buf, "hello4", 6); + quicklistPushHead(ql, buf, large_limit); + memcpy(buf, "hello3", 6); + quicklistPushHead(ql, buf, large_limit); + memcpy(buf, "hello2", 6); + quicklistPushHead(ql, buf, large_limit); + quicklistRotate(ql); + + for (i = 1; i < 5; i++) { + TEST_ASSERT(QL_NODE_IS_PLAIN(ql->tail)); + quicklistPop(ql, QUICKLIST_HEAD, &data, &sz, &lv); + int temp_char = data[5]; + zfree(data); + TEST_ASSERT(temp_char == ('0' + i)); + } + + ql_verify(ql, 0, 0, 0, 0); + quicklistRelease(ql); + } + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistRotateOneValOnce(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("rotate one val once"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + for (int f = 0; f < fill_count; f++) { + quicklist *ql = quicklistNew(fills[f], options[_i]); + quicklistPushHead(ql, "hello", 6); + quicklistRotate(ql); + /* Ignore compression verify because listpack is + * too small to compress. */ + ql_verify(ql, 1, 1, 1, 1); + quicklistRelease(ql); + } + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistRotate500Val5000TimesAtCompress(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("rotate 500 val 5000 times at compress"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + for (int f = 0; f < fill_count; f++) { + quicklist *ql = quicklistNew(fills[f], options[_i]); + quicklistPushHead(ql, "900", 3); + quicklistPushHead(ql, "7000", 4); + quicklistPushHead(ql, "-1200", 5); + quicklistPushHead(ql, "42", 2); + for (int i = 0; i < 500; i++) quicklistPushHead(ql, genstr("hello", i), 64); + ql_info(ql); + for (int i = 0; i < 5000; i++) { + ql_info(ql); + quicklistRotate(ql); + } + if (fills[f] == 1) + ql_verify(ql, 504, 504, 1, 1); + else if (fills[f] == 2) + ql_verify(ql, 252, 504, 2, 2); + else if (fills[f] == 32) + ql_verify(ql, 16, 504, 32, 24); + quicklistRelease(ql); + } + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistPopEmpty(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("pop empty"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + quicklist *ql = quicklistNew(-2, options[_i]); + quicklistPop(ql, QUICKLIST_HEAD, NULL, NULL, NULL); + ql_verify(ql, 0, 0, 0, 0); + quicklistRelease(ql); + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistPop1StringFrom1(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("pop 1 string from 1"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + quicklist *ql = quicklistNew(-2, options[_i]); + char *populate = genstr("hello", 331); + quicklistPushHead(ql, populate, 32); + unsigned char *data; + size_t sz; + long long lv; + ql_info(ql); + TEST_ASSERT(quicklistPop(ql, QUICKLIST_HEAD, &data, &sz, &lv)); + TEST_ASSERT(data != NULL); + TEST_ASSERT(sz == 32); + if (strcmp(populate, (char *)data)) { + int size = sz; + TEST_PRINT_INFO("Pop'd value (%.*s) didn't equal original value (%s)", size, data, populate); + err++; + } + zfree(data); + ql_verify(ql, 0, 0, 0, 0); + quicklistRelease(ql); + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistPopHead1NumberFrom1(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("pop head 1 number from 1"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + quicklist *ql = quicklistNew(-2, options[_i]); + quicklistPushHead(ql, "55513", 5); + unsigned char *data; + size_t sz; + long long lv; + ql_info(ql); + TEST_ASSERT(quicklistPop(ql, QUICKLIST_HEAD, &data, &sz, &lv)); + TEST_ASSERT(data == NULL); + TEST_ASSERT(lv == 55513); + ql_verify(ql, 0, 0, 0, 0); + quicklistRelease(ql); + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistPopHead500From500(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("pop head 500 from 500"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + quicklist *ql = quicklistNew(-2, options[_i]); + for (int i = 0; i < 500; i++) quicklistPushHead(ql, genstr("hello", i), 32); + ql_info(ql); + for (int i = 0; i < 500; i++) { + unsigned char *data; + size_t sz; + long long lv; + int ret = quicklistPop(ql, QUICKLIST_HEAD, &data, &sz, &lv); + TEST_ASSERT(ret == 1); + TEST_ASSERT(data != NULL); + TEST_ASSERT(sz == 32); + if (strcmp(genstr("hello", 499 - i), (char *)data)) { + int size = sz; + TEST_PRINT_INFO("Pop'd value (%.*s) didn't equal original value (%s)", size, data, genstr("hello", 499 - i)); + err++; + } + zfree(data); + } + ql_verify(ql, 0, 0, 0, 0); + quicklistRelease(ql); + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistPopHead5000From500(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("pop head 5000 from 500"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + quicklist *ql = quicklistNew(-2, options[_i]); + for (int i = 0; i < 500; i++) quicklistPushHead(ql, genstr("hello", i), 32); + for (int i = 0; i < 5000; i++) { + unsigned char *data; + size_t sz; + long long lv; + int ret = quicklistPop(ql, QUICKLIST_HEAD, &data, &sz, &lv); + if (i < 500) { + TEST_ASSERT(ret == 1); + TEST_ASSERT(data != NULL); + TEST_ASSERT(sz == 32); + if (strcmp(genstr("hello", 499 - i), (char *)data)) { + int size = sz; + TEST_PRINT_INFO("Pop'd value (%.*s) didn't equal original value " + "(%s)", + size, data, genstr("hello", 499 - i)); + err++; + } + zfree(data); + } else { + TEST_ASSERT(ret == 0); + } + } + ql_verify(ql, 0, 0, 0, 0); + quicklistRelease(ql); + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistIterateForwardOver500List(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("iterate forward over 500 list"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + quicklist *ql = quicklistNew(-2, options[_i]); + quicklistSetFill(ql, 32); + for (int i = 0; i < 500; i++) quicklistPushHead(ql, genstr("hello", i), 32); + quicklistIter *iter = quicklistGetIterator(ql, AL_START_HEAD); + quicklistEntry entry; + int i = 499, count = 0; + while (quicklistNext(iter, &entry)) { + char *h = genstr("hello", i); + if (strcmp((char *)entry.value, h)) { + TEST_PRINT_INFO("value [%s] didn't match [%s] at position %d", entry.value, h, i); + err++; + } + i--; + count++; + } + if (count != 500) { + TEST_PRINT_INFO("Didn't iterate over exactly 500 elements (%d)", i); + err++; + } + ql_verify(ql, 16, 500, 20, 32); + ql_release_iterator(iter); + quicklistRelease(ql); + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistIterateReverseOver500List(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("iterate reverse over 500 list"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + quicklist *ql = quicklistNew(-2, options[_i]); + quicklistSetFill(ql, 32); + for (int i = 0; i < 500; i++) quicklistPushHead(ql, genstr("hello", i), 32); + quicklistIter *iter = quicklistGetIterator(ql, AL_START_TAIL); + quicklistEntry entry; + int i = 0; + while (quicklistNext(iter, &entry)) { + char *h = genstr("hello", i); + if (strcmp((char *)entry.value, h)) { + TEST_PRINT_INFO("value [%s] didn't match [%s] at position %d", entry.value, h, i); + err++; + } + i++; + } + if (i != 500) { + TEST_PRINT_INFO("Didn't iterate over exactly 500 elements (%d)", i); + err++; + } + ql_verify(ql, 16, 500, 20, 32); + ql_release_iterator(iter); + quicklistRelease(ql); + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistInsertAfter1Element(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("insert after 1 element"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + quicklist *ql = quicklistNew(-2, options[_i]); + quicklistPushHead(ql, "hello", 6); + quicklistEntry entry; + iter = quicklistGetIteratorEntryAtIdx(ql, 0, &entry); + quicklistInsertAfter(iter, &entry, "abc", 4); + ql_release_iterator(iter); + ql_verify(ql, 1, 2, 2, 2); + + /* verify results */ + iter = quicklistGetIteratorEntryAtIdx(ql, 0, &entry); + int sz = entry.sz; + if (strncmp((char *)entry.value, "hello", 5)) { + TEST_PRINT_INFO("Value 0 didn't match, instead got: %.*s", sz, entry.value); + err++; + } + ql_release_iterator(iter); + + iter = quicklistGetIteratorEntryAtIdx(ql, 1, &entry); + sz = entry.sz; + if (strncmp((char *)entry.value, "abc", 3)) { + TEST_PRINT_INFO("Value 1 didn't match, instead got: %.*s", sz, entry.value); + err++; + } + ql_release_iterator(iter); + quicklistRelease(ql); + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistInsertBefore1Element(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("insert before 1 element"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + quicklist *ql = quicklistNew(-2, options[_i]); + quicklistPushHead(ql, "hello", 6); + quicklistEntry entry; + iter = quicklistGetIteratorEntryAtIdx(ql, 0, &entry); + quicklistInsertBefore(iter, &entry, "abc", 4); + ql_release_iterator(iter); + ql_verify(ql, 1, 2, 2, 2); + + /* verify results */ + iter = quicklistGetIteratorEntryAtIdx(ql, 0, &entry); + int sz = entry.sz; + if (strncmp((char *)entry.value, "abc", 3)) { + TEST_PRINT_INFO("Value 0 didn't match, instead got: %.*s", sz, entry.value); + err++; + } + ql_release_iterator(iter); + + iter = quicklistGetIteratorEntryAtIdx(ql, 1, &entry); + sz = entry.sz; + if (strncmp((char *)entry.value, "hello", 5)) { + TEST_PRINT_INFO("Value 1 didn't match, instead got: %.*s", sz, entry.value); + err++; + } + ql_release_iterator(iter); + quicklistRelease(ql); + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistInsertHeadWhileHeadNodeIsFull(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("insert head while head node is full"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + quicklist *ql = quicklistNew(4, options[_i]); + for (int i = 0; i < 10; i++) quicklistPushTail(ql, genstr("hello", i), 6); + quicklistSetFill(ql, -1); + quicklistEntry entry; + iter = quicklistGetIteratorEntryAtIdx(ql, -10, &entry); + char buf[4096] = {0}; + quicklistInsertBefore(iter, &entry, buf, 4096); + ql_release_iterator(iter); + ql_verify(ql, 4, 11, 1, 2); + quicklistRelease(ql); + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistInsertTailWhileTailNodeIsFull(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("insert tail while tail node is full"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + quicklist *ql = quicklistNew(4, options[_i]); + for (int i = 0; i < 10; i++) quicklistPushHead(ql, genstr("hello", i), 6); + quicklistSetFill(ql, -1); + quicklistEntry entry; + iter = quicklistGetIteratorEntryAtIdx(ql, -1, &entry); + char buf[4096] = {0}; + quicklistInsertAfter(iter, &entry, buf, 4096); + ql_release_iterator(iter); + ql_verify(ql, 4, 11, 2, 1); + quicklistRelease(ql); + + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistInsertOnceInElementsWhileIteratingAtCompress(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("insert once in elements while iterating at compress"); + + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + for (int f = 0; f < fill_count; f++) { + quicklist *ql = quicklistNew(fills[f], options[_i]); + quicklistPushTail(ql, "abc", 3); + quicklistSetFill(ql, 1); + quicklistPushTail(ql, "def", 3); /* force to unique node */ + quicklistSetFill(ql, f); + quicklistPushTail(ql, "bob", 3); /* force to reset for +3 */ + quicklistPushTail(ql, "foo", 3); + quicklistPushTail(ql, "zoo", 3); + + itrprintr(ql, 0); + /* insert "bar" before "bob" while iterating over list. */ + quicklistIter *iter = quicklistGetIterator(ql, AL_START_HEAD); + quicklistEntry entry; + while (quicklistNext(iter, &entry)) { + if (!strncmp((char *)entry.value, "bob", 3)) { + /* Insert as fill = 1 so it spills into new node. */ + quicklistInsertBefore(iter, &entry, "bar", 3); + break; /* didn't we fix insert-while-iterating? */ + } + } + ql_release_iterator(iter); + itrprintr(ql, 0); + + /* verify results */ + iter = quicklistGetIteratorEntryAtIdx(ql, 0, &entry); + int sz = entry.sz; + + if (strncmp((char *)entry.value, "abc", 3)) { + TEST_PRINT_INFO("Value 0 didn't match, instead got: %.*s", sz, entry.value); + err++; + } + ql_release_iterator(iter); + + iter = quicklistGetIteratorEntryAtIdx(ql, 1, &entry); + if (strncmp((char *)entry.value, "def", 3)) { + TEST_PRINT_INFO("Value 1 didn't match, instead got: %.*s", sz, entry.value); + err++; + } + ql_release_iterator(iter); + + iter = quicklistGetIteratorEntryAtIdx(ql, 2, &entry); + if (strncmp((char *)entry.value, "bar", 3)) { + TEST_PRINT_INFO("Value 2 didn't match, instead got: %.*s", sz, entry.value); + err++; + } + ql_release_iterator(iter); + + iter = quicklistGetIteratorEntryAtIdx(ql, 3, &entry); + if (strncmp((char *)entry.value, "bob", 3)) { + TEST_PRINT_INFO("Value 3 didn't match, instead got: %.*s", sz, entry.value); + err++; + } + ql_release_iterator(iter); + + iter = quicklistGetIteratorEntryAtIdx(ql, 4, &entry); + if (strncmp((char *)entry.value, "foo", 3)) { + TEST_PRINT_INFO("Value 4 didn't match, instead got: %.*s", sz, entry.value); + err++; + } + ql_release_iterator(iter); + + iter = quicklistGetIteratorEntryAtIdx(ql, 5, &entry); + if (strncmp((char *)entry.value, "zoo", 3)) { + TEST_PRINT_INFO("Value 5 didn't match, instead got: %.*s", sz, entry.value); + err++; + } + ql_release_iterator(iter); + quicklistRelease(ql); + } + long long stop = mstime(); + runtime[_i] += stop - start; + } + + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistInsertBefore250NewInMiddleOf500ElementsAtCompress(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("insert [before] 250 new in middle of 500 elements at compress"); + + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + for (int f = 0; f < fill_count; f++) { + quicklist *ql = quicklistNew(fills[f], options[_i]); + for (int i = 0; i < 500; i++) quicklistPushTail(ql, genstr("hello", i), 32); + for (int i = 0; i < 250; i++) { + quicklistEntry entry; + iter = quicklistGetIteratorEntryAtIdx(ql, 250, &entry); + quicklistInsertBefore(iter, &entry, genstr("abc", i), 32); + ql_release_iterator(iter); + } + if (fills[f] == 32) ql_verify(ql, 25, 750, 32, 20); + quicklistRelease(ql); + } + + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistInsertAfter250NewInMiddleOf500ElementsAtCompress(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("insert [after] 250 new in middle of 500 elements at compress"); + + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + for (int f = 0; f < fill_count; f++) { + quicklist *ql = quicklistNew(fills[f], options[_i]); + for (int i = 0; i < 500; i++) quicklistPushHead(ql, genstr("hello", i), 32); + for (int i = 0; i < 250; i++) { + quicklistEntry entry; + iter = quicklistGetIteratorEntryAtIdx(ql, 250, &entry); + quicklistInsertAfter(iter, &entry, genstr("abc", i), 32); + ql_release_iterator(iter); + } + + if (ql->count != 750) { + TEST_PRINT_INFO("List size not 750, but rather %ld", ql->count); + err++; + } + + if (fills[f] == 32) ql_verify(ql, 26, 750, 20, 32); + quicklistRelease(ql); + } + + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistDuplicateEmptyList(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("duplicate empty list"); + + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + quicklist *ql = quicklistNew(-2, options[_i]); + ql_verify(ql, 0, 0, 0, 0); + quicklist *copy = quicklistDup(ql); + ql_verify(copy, 0, 0, 0, 0); + quicklistRelease(ql); + quicklistRelease(copy); + + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistDuplicateListOf1Element(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("duplicate list of 1 element"); + + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + quicklist *ql = quicklistNew(-2, options[_i]); + quicklistPushHead(ql, genstr("hello", 3), 32); + ql_verify(ql, 1, 1, 1, 1); + quicklist *copy = quicklistDup(ql); + ql_verify(copy, 1, 1, 1, 1); + quicklistRelease(ql); + quicklistRelease(copy); + + long long stop = mstime(); + runtime[_i] += stop - start; + } + + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistDuplicateListOf500(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("duplicate list of 500"); + + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + quicklist *ql = quicklistNew(-2, options[_i]); + quicklistSetFill(ql, 32); + for (int i = 0; i < 500; i++) quicklistPushHead(ql, genstr("hello", i), 32); + ql_verify(ql, 16, 500, 20, 32); + + quicklist *copy = quicklistDup(ql); + ql_verify(copy, 16, 500, 20, 32); + quicklistRelease(ql); + quicklistRelease(copy); + + long long stop = mstime(); + runtime[_i] += stop - start; + } + + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistIndex1200From500ListAtFill(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("index 1,200 from 500 list at fill at compress"); + + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + for (int f = 0; f < fill_count; f++) { + quicklist *ql = quicklistNew(fills[f], options[_i]); + for (int i = 0; i < 500; i++) quicklistPushTail(ql, genstr("hello", i + 1), 32); + quicklistEntry entry; + iter = quicklistGetIteratorEntryAtIdx(ql, 1, &entry); + if (strcmp((char *)entry.value, "hello2") != 0) { + TEST_PRINT_INFO("Value: %s", entry.value); + err++; + } + ql_release_iterator(iter); + + iter = quicklistGetIteratorEntryAtIdx(ql, 200, &entry); + if (strcmp((char *)entry.value, "hello201") != 0) { + TEST_PRINT_INFO("Value: %s", entry.value); + err++; + } + ql_release_iterator(iter); + quicklistRelease(ql); + } + + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistIndex12From500ListAtFill(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("index -1,-2 from 500 list at fill at compress"); + + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + for (int f = 0; f < fill_count; f++) { + quicklist *ql = quicklistNew(fills[f], options[_i]); + for (int i = 0; i < 500; i++) quicklistPushTail(ql, genstr("hello", i + 1), 32); + quicklistEntry entry; + iter = quicklistGetIteratorEntryAtIdx(ql, -1, &entry); + if (strcmp((char *)entry.value, "hello500") != 0) { + TEST_PRINT_INFO("Value: %s", entry.value); + err++; + } + ql_release_iterator(iter); + + iter = quicklistGetIteratorEntryAtIdx(ql, -2, &entry); + if (strcmp((char *)entry.value, "hello499") != 0) { + TEST_PRINT_INFO("Value: %s", entry.value); + err++; + } + ql_release_iterator(iter); + quicklistRelease(ql); + } + + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistIndex100From500ListAtFill(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("index -100 from 500 list at fill at compress"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + for (int f = 0; f < fill_count; f++) { + quicklist *ql = quicklistNew(fills[f], options[_i]); + for (int i = 0; i < 500; i++) quicklistPushTail(ql, genstr("hello", i + 1), 32); + quicklistEntry entry; + iter = quicklistGetIteratorEntryAtIdx(ql, -100, &entry); + if (strcmp((char *)entry.value, "hello401") != 0) { + TEST_PRINT_INFO("Value: %s", entry.value); + err++; + } + ql_release_iterator(iter); + quicklistRelease(ql); + } + + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistIndexTooBig1From50ListAtFill(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("index too big +1 from 50 list at fill at compress"); + + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + for (int f = 0; f < fill_count; f++) { + quicklist *ql = quicklistNew(fills[f], options[_i]); + for (int i = 0; i < 50; i++) quicklistPushTail(ql, genstr("hello", i + 1), 32); + quicklistEntry entry; + int sz = entry.sz; + iter = quicklistGetIteratorEntryAtIdx(ql, 50, &entry); + if (iter) { + TEST_PRINT_INFO("Index found at 50 with 50 list: %.*s", sz, entry.value); + err++; + } + ql_release_iterator(iter); + quicklistRelease(ql); + } + + + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistDeleteRangeEmptyList(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("delete range empty list"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + + quicklist *ql = quicklistNew(-2, options[_i]); + quicklistDelRange(ql, 5, 20); + ql_verify(ql, 0, 0, 0, 0); + quicklistRelease(ql); + + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistDeleteRangeOfEntireNodeInListOfOneNode(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("delete range of entire node in list of one node"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + quicklist *ql = quicklistNew(-2, options[_i]); + for (int i = 0; i < 32; i++) quicklistPushHead(ql, genstr("hello", i), 32); + ql_verify(ql, 1, 32, 32, 32); + quicklistDelRange(ql, 0, 32); + ql_verify(ql, 0, 0, 0, 0); + quicklistRelease(ql); + + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistDeleteRangeOfEntireNodeWithOverflowCounts(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("delete range of entire node with overflow counts"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + quicklist *ql = quicklistNew(-2, options[_i]); + for (int i = 0; i < 32; i++) quicklistPushHead(ql, genstr("hello", i), 32); + ql_verify(ql, 1, 32, 32, 32); + quicklistDelRange(ql, 0, 128); + ql_verify(ql, 0, 0, 0, 0); + quicklistRelease(ql); + + + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistDeleteMiddle100Of500List(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("delete middle 100 of 500 list"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + quicklist *ql = quicklistNew(-2, options[_i]); + quicklistSetFill(ql, 32); + for (int i = 0; i < 500; i++) quicklistPushTail(ql, genstr("hello", i + 1), 32); + ql_verify(ql, 16, 500, 32, 20); + quicklistDelRange(ql, 200, 100); + ql_verify(ql, 14, 400, 32, 20); + quicklistRelease(ql); + + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistDeleteLessThanFillButAcrossNodes(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("delete less than fill but across nodes"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + quicklist *ql = quicklistNew(-2, options[_i]); + quicklistSetFill(ql, 32); + for (int i = 0; i < 500; i++) quicklistPushTail(ql, genstr("hello", i + 1), 32); + ql_verify(ql, 16, 500, 32, 20); + quicklistDelRange(ql, 60, 10); + ql_verify(ql, 16, 490, 32, 20); + quicklistRelease(ql); + + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistDeleteNegative1From500List(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("delete negative 1 from 500 list"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + quicklist *ql = quicklistNew(-2, options[_i]); + quicklistSetFill(ql, 32); + for (int i = 0; i < 500; i++) quicklistPushTail(ql, genstr("hello", i + 1), 32); + ql_verify(ql, 16, 500, 32, 20); + quicklistDelRange(ql, -1, 1); + ql_verify(ql, 16, 499, 32, 19); + quicklistRelease(ql); + + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistDeleteNegative1From500ListWithOverflowCounts(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("delete negative 1 from 500 list with overflow counts"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + quicklist *ql = quicklistNew(-2, options[_i]); + quicklistSetFill(ql, 32); + for (int i = 0; i < 500; i++) quicklistPushTail(ql, genstr("hello", i + 1), 32); + ql_verify(ql, 16, 500, 32, 20); + quicklistDelRange(ql, -1, 128); + ql_verify(ql, 16, 499, 32, 19); + quicklistRelease(ql); + + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistDeleteNegative100From500List(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("delete negative 100 from 500 list"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + quicklist *ql = quicklistNew(-2, options[_i]); + quicklistSetFill(ql, 32); + for (int i = 0; i < 500; i++) quicklistPushTail(ql, genstr("hello", i + 1), 32); + quicklistDelRange(ql, -100, 100); + ql_verify(ql, 13, 400, 32, 16); + quicklistRelease(ql); + + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistDelete10Count5From50List(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("delete -10 count 5 from 50 list"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + quicklist *ql = quicklistNew(-2, options[_i]); + quicklistSetFill(ql, 32); + for (int i = 0; i < 50; i++) quicklistPushTail(ql, genstr("hello", i + 1), 32); + ql_verify(ql, 2, 50, 32, 18); + quicklistDelRange(ql, -10, 5); + ql_verify(ql, 2, 45, 32, 13); + quicklistRelease(ql); + + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistNumbersOnlyListRead(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("numbers only list read"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + quicklist *ql = quicklistNew(-2, options[_i]); + quicklistPushTail(ql, "1111", 4); + quicklistPushTail(ql, "2222", 4); + quicklistPushTail(ql, "3333", 4); + quicklistPushTail(ql, "4444", 4); + ql_verify(ql, 1, 4, 4, 4); + quicklistEntry entry; + iter = quicklistGetIteratorEntryAtIdx(ql, 0, &entry); + if (entry.longval != 1111) { + TEST_PRINT_INFO("Not 1111, %lld", entry.longval); + err++; + } + ql_release_iterator(iter); + + iter = quicklistGetIteratorEntryAtIdx(ql, 1, &entry); + if (entry.longval != 2222) { + TEST_PRINT_INFO("Not 2222, %lld", entry.longval); + err++; + } + ql_release_iterator(iter); + + iter = quicklistGetIteratorEntryAtIdx(ql, 2, &entry); + if (entry.longval != 3333) { + TEST_PRINT_INFO("Not 3333, %lld", entry.longval); + err++; + } + ql_release_iterator(iter); + + iter = quicklistGetIteratorEntryAtIdx(ql, 3, &entry); + if (entry.longval != 4444) { + TEST_PRINT_INFO("Not 4444, %lld", entry.longval); + err++; + } + ql_release_iterator(iter); + + iter = quicklistGetIteratorEntryAtIdx(ql, 4, &entry); + if (iter) { + TEST_PRINT_INFO("Index past elements: %lld", entry.longval); + err++; + } + ql_release_iterator(iter); + + iter = quicklistGetIteratorEntryAtIdx(ql, -1, &entry); + if (entry.longval != 4444) { + TEST_PRINT_INFO("Not 4444 (reverse), %lld", entry.longval); + err++; + } + ql_release_iterator(iter); + + iter = quicklistGetIteratorEntryAtIdx(ql, -2, &entry); + if (entry.longval != 3333) { + TEST_PRINT_INFO("Not 3333 (reverse), %lld", entry.longval); + err++; + } + ql_release_iterator(iter); + + iter = quicklistGetIteratorEntryAtIdx(ql, -3, &entry); + if (entry.longval != 2222) { + TEST_PRINT_INFO("Not 2222 (reverse), %lld", entry.longval); + err++; + } + ql_release_iterator(iter); + + iter = quicklistGetIteratorEntryAtIdx(ql, -4, &entry); + if (entry.longval != 1111) { + TEST_PRINT_INFO("Not 1111 (reverse), %lld", entry.longval); + err++; + } + ql_release_iterator(iter); + + iter = quicklistGetIteratorEntryAtIdx(ql, -5, &entry); + if (iter) { + TEST_PRINT_INFO("Index past elements (reverse), %lld", entry.longval); + err++; + } + ql_release_iterator(iter); + quicklistRelease(ql); + + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistNumbersLargerListRead(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("numbers larger list read"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + quicklist *ql = quicklistNew(-2, options[_i]); + quicklistSetFill(ql, 32); + char num[32]; + long long nums[5000]; + for (int i = 0; i < 5000; i++) { + nums[i] = -5157318210846258176 + i; + int sz = ll2string(num, sizeof(num), nums[i]); + quicklistPushTail(ql, num, sz); + } + quicklistPushTail(ql, "xxxxxxxxxxxxxxxxxxxx", 20); + quicklistEntry entry; + for (int i = 0; i < 5000; i++) { + iter = quicklistGetIteratorEntryAtIdx(ql, i, &entry); + if (entry.longval != nums[i]) { + TEST_PRINT_INFO("[%d] Not longval %lld but rather %lld", i, nums[i], entry.longval); + err++; + } + entry.longval = 0xdeadbeef; + ql_release_iterator(iter); + } + iter = quicklistGetIteratorEntryAtIdx(ql, 5000, &entry); + if (strncmp((char *)entry.value, "xxxxxxxxxxxxxxxxxxxx", 20)) { + TEST_PRINT_INFO("String val not match: %s", entry.value); + err++; + } + ql_verify(ql, 157, 5001, 32, 9); + ql_release_iterator(iter); + quicklistRelease(ql); + + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistNumbersLargerListReadB(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("numbers larger list read B"); + + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + quicklist *ql = quicklistNew(-2, options[_i]); + quicklistPushTail(ql, "99", 2); + quicklistPushTail(ql, "98", 2); + quicklistPushTail(ql, "xxxxxxxxxxxxxxxxxxxx", 20); + quicklistPushTail(ql, "96", 2); + quicklistPushTail(ql, "95", 2); + quicklistReplaceAtIndex(ql, 1, "foo", 3); + quicklistReplaceAtIndex(ql, -1, "bar", 3); + quicklistRelease(ql); + + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistLremTestAtCompress(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("lrem test at compress"); + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + for (int f = 0; f < fill_count; f++) { + quicklist *ql = quicklistNew(fills[f], options[_i]); + char *words[] = {"abc", "foo", "bar", "foobar", "foobared", "zap", "bar", "test", "foo"}; + char *result[] = {"abc", "foo", "foobar", "foobared", "zap", "test", "foo"}; + char *resultB[] = {"abc", "foo", "foobar", "foobared", "zap", "test"}; + for (int i = 0; i < 9; i++) quicklistPushTail(ql, words[i], strlen(words[i])); + + /* lrem 0 bar */ + quicklistIter *iter = quicklistGetIterator(ql, AL_START_HEAD); + quicklistEntry entry; + int i = 0; + while (quicklistNext(iter, &entry)) { + if (quicklistCompare(&entry, (unsigned char *)"bar", 3)) { + quicklistDelEntry(iter, &entry); + } + i++; + } + ql_release_iterator(iter); + + /* check result of lrem 0 bar */ + iter = quicklistGetIterator(ql, AL_START_HEAD); + i = 0; + while (quicklistNext(iter, &entry)) { + /* Result must be: abc, foo, foobar, foobared, zap, test, + * foo */ + int sz = entry.sz; + if (strncmp((char *)entry.value, result[i], entry.sz)) { + TEST_PRINT_INFO("No match at position %d, got %.*s instead of %s", i, sz, entry.value, result[i]); + err++; + } + i++; + } + ql_release_iterator(iter); + + quicklistPushTail(ql, "foo", 3); + + /* lrem -2 foo */ + iter = quicklistGetIterator(ql, AL_START_TAIL); + i = 0; + int del = 2; + while (quicklistNext(iter, &entry)) { + if (quicklistCompare(&entry, (unsigned char *)"foo", 3)) { + quicklistDelEntry(iter, &entry); + del--; + } + if (!del) break; + i++; + } + ql_release_iterator(iter); + + /* check result of lrem -2 foo */ + /* (we're ignoring the '2' part and still deleting all foo + * because + * we only have two foo) */ + iter = quicklistGetIterator(ql, AL_START_TAIL); + i = 0; + size_t resB = sizeof(resultB) / sizeof(*resultB); + while (quicklistNext(iter, &entry)) { + /* Result must be: abc, foo, foobar, foobared, zap, test, + * foo */ + int sz = entry.sz; + if (strncmp((char *)entry.value, resultB[resB - 1 - i], sz)) { + TEST_PRINT_INFO("No match at position %d, got %.*s instead of %s", i, sz, entry.value, + resultB[resB - 1 - i]); + err++; + } + i++; + } + + ql_release_iterator(iter); + quicklistRelease(ql); + } + + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistIterateReverseDeleteAtCompress(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("iterate reverse + delete at compress"); + + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + for (int f = 0; f < fill_count; f++) { + quicklist *ql = quicklistNew(fills[f], options[_i]); + quicklistPushTail(ql, "abc", 3); + quicklistPushTail(ql, "def", 3); + quicklistPushTail(ql, "hij", 3); + quicklistPushTail(ql, "jkl", 3); + quicklistPushTail(ql, "oop", 3); + + quicklistEntry entry; + quicklistIter *iter = quicklistGetIterator(ql, AL_START_TAIL); + int i = 0; + while (quicklistNext(iter, &entry)) { + if (quicklistCompare(&entry, (unsigned char *)"hij", 3)) { + quicklistDelEntry(iter, &entry); + } + i++; + } + ql_release_iterator(iter); + + if (i != 5) { + TEST_PRINT_INFO("Didn't iterate 5 times, iterated %d times.", i); + err++; + } + + /* Check results after deletion of "hij" */ + iter = quicklistGetIterator(ql, AL_START_HEAD); + i = 0; + char *vals[] = {"abc", "def", "jkl", "oop"}; + while (quicklistNext(iter, &entry)) { + if (!quicklistCompare(&entry, (unsigned char *)vals[i], 3)) { + TEST_PRINT_INFO("Value at %d didn't match %s\n", i, vals[i]); + err++; + } + i++; + } + ql_release_iterator(iter); + quicklistRelease(ql); + } + + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistIteratorAtIndexTestAtCompress(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("iterator at index test at compress"); + + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + for (int f = 0; f < fill_count; f++) { + quicklist *ql = quicklistNew(fills[f], options[_i]); + char num[32]; + long long nums[5000]; + for (int i = 0; i < 760; i++) { + nums[i] = -5157318210846258176 + i; + int sz = ll2string(num, sizeof(num), nums[i]); + quicklistPushTail(ql, num, sz); + } + + quicklistEntry entry; + quicklistIter *iter = quicklistGetIteratorAtIdx(ql, AL_START_HEAD, 437); + int i = 437; + while (quicklistNext(iter, &entry)) { + if (entry.longval != nums[i]) { + TEST_PRINT_INFO("Expected %lld, but got %lld", entry.longval, nums[i]); + err++; + } + i++; + } + ql_release_iterator(iter); + quicklistRelease(ql); + } + + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistLtrimTestAAtCompress(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("ltrim test A at compress"); + + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + for (int f = 0; f < fill_count; f++) { + quicklist *ql = quicklistNew(fills[f], options[_i]); + char num[32]; + long long nums[5000]; + for (int i = 0; i < 32; i++) { + nums[i] = -5157318210846258176 + i; + int sz = ll2string(num, sizeof(num), nums[i]); + quicklistPushTail(ql, num, sz); + } + if (fills[f] == 32) ql_verify(ql, 1, 32, 32, 32); + /* ltrim 25 53 (keep [25,32] inclusive = 7 remaining) */ + quicklistDelRange(ql, 0, 25); + quicklistDelRange(ql, 0, 0); + quicklistEntry entry; + for (int i = 0; i < 7; i++) { + iter = quicklistGetIteratorEntryAtIdx(ql, i, &entry); + if (entry.longval != nums[25 + i]) { + TEST_PRINT_INFO("Deleted invalid range! Expected %lld but got " + "%lld", + entry.longval, nums[25 + i]); + err++; + } + ql_release_iterator(iter); + } + if (fills[f] == 32) ql_verify(ql, 1, 7, 7, 7); + quicklistRelease(ql); + } + + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistLtrimTestBAtCompress(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("ltrim test B at compress"); + + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + for (int f = 0; f < fill_count; f++) { + /* Force-disable compression because our 33 sequential + * integers don't compress and the check always fails. */ + quicklist *ql = quicklistNew(fills[f], QUICKLIST_NOCOMPRESS); + char num[32]; + long long nums[5000]; + for (int i = 0; i < 33; i++) { + nums[i] = i; + int sz = ll2string(num, sizeof(num), nums[i]); + quicklistPushTail(ql, num, sz); + } + if (fills[f] == 32) ql_verify(ql, 2, 33, 32, 1); + /* ltrim 5 16 (keep [5,16] inclusive = 12 remaining) */ + quicklistDelRange(ql, 0, 5); + quicklistDelRange(ql, -16, 16); + if (fills[f] == 32) ql_verify(ql, 1, 12, 12, 12); + quicklistEntry entry; + + iter = quicklistGetIteratorEntryAtIdx(ql, 0, &entry); + if (entry.longval != 5) { + TEST_PRINT_INFO("A: longval not 5, but %lld", entry.longval); + err++; + } + ql_release_iterator(iter); + + iter = quicklistGetIteratorEntryAtIdx(ql, -1, &entry); + if (entry.longval != 16) { + TEST_PRINT_INFO("B! got instead: %lld", entry.longval); + err++; + } + quicklistPushTail(ql, "bobobob", 7); + ql_release_iterator(iter); + + iter = quicklistGetIteratorEntryAtIdx(ql, -1, &entry); + int sz = entry.sz; + if (strncmp((char *)entry.value, "bobobob", 7)) { + TEST_PRINT_INFO("Tail doesn't match bobobob, it's %.*s instead", sz, entry.value); + err++; + } + ql_release_iterator(iter); + + for (int i = 0; i < 12; i++) { + iter = quicklistGetIteratorEntryAtIdx(ql, i, &entry); + if (entry.longval != nums[5 + i]) { + TEST_PRINT_INFO("Deleted invalid range! Expected %lld but got " + "%lld", + entry.longval, nums[5 + i]); + err++; + } + + ql_release_iterator(iter); + } + quicklistRelease(ql); + } + + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistLtrimTestCAtCompress(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("ltrim test C at compress"); + + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + for (int f = 0; f < fill_count; f++) { + quicklist *ql = quicklistNew(fills[f], options[_i]); + char num[32]; + long long nums[5000]; + for (int i = 0; i < 33; i++) { + nums[i] = -5157318210846258176 + i; + int sz = ll2string(num, sizeof(num), nums[i]); + quicklistPushTail(ql, num, sz); + } + if (fills[f] == 32) ql_verify(ql, 2, 33, 32, 1); + /* ltrim 3 3 (keep [3,3] inclusive = 1 remaining) */ + quicklistDelRange(ql, 0, 3); + quicklistDelRange(ql, -29, 4000); /* make sure not loop forever */ + if (fills[f] == 32) ql_verify(ql, 1, 1, 1, 1); + quicklistEntry entry; + iter = quicklistGetIteratorEntryAtIdx(ql, 0, &entry); + if (entry.longval != -5157318210846258173) { + err++; + } + ql_release_iterator(iter); + quicklistRelease(ql); + } + + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistLtrimTestDAtCompress(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + quicklistIter *iter; + TEST("ltrim test D at compress"); + + for (int _i = 0; _i < option_count; _i++) { + long long start = mstime(); + for (int f = 0; f < fill_count; f++) { + quicklist *ql = quicklistNew(fills[f], options[_i]); + char num[32]; + long long nums[5000]; + for (int i = 0; i < 33; i++) { + nums[i] = -5157318210846258176 + i; + int sz = ll2string(num, sizeof(num), nums[i]); + quicklistPushTail(ql, num, sz); + } + if (fills[f] == 32) ql_verify(ql, 2, 33, 32, 1); + quicklistDelRange(ql, -12, 3); + if (ql->count != 30) { + TEST_PRINT_INFO("Didn't delete exactly three elements! Count is: %lu", ql->count); + err++; + } + quicklistRelease(ql); + } + + long long stop = mstime(); + runtime[_i] += stop - start; + } + UNUSED(iter); + TEST_ASSERT(err == 0); + return 0; +} + +int test_quicklistVerifySpecificCompressionOfInteriorNodes(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + int accurate = flags & UNIT_TEST_ACCURATE; + + TEST("verify specific compression of interior nodes"); + + /* Run a longer test of compression depth outside of primary test loop. */ + int list_sizes[] = {250, 251, 500, 999, 1000}; + int list_count = accurate ? (int)(sizeof(list_sizes) / sizeof(*list_sizes)) : 1; + for (int list = 0; list < list_count; list++) { + for (int f = 0; f < fill_count; f++) { + for (int depth = 1; depth < 40; depth++) { + /* skip over many redundant test cases */ + quicklist *ql = quicklistNew(fills[f], depth); + for (int i = 0; i < list_sizes[list]; i++) { + quicklistPushTail(ql, genstr("hello TAIL", i + 1), 64); + quicklistPushHead(ql, genstr("hello HEAD", i + 1), 64); + } + + for (int step = 0; step < 2; step++) { + /* test remove node */ + if (step == 1) { + for (int i = 0; i < list_sizes[list] / 2; i++) { + unsigned char *data; + TEST_ASSERT(quicklistPop(ql, QUICKLIST_HEAD, &data, + NULL, NULL)); + zfree(data); + TEST_ASSERT(quicklistPop(ql, QUICKLIST_TAIL, &data, + NULL, NULL)); + zfree(data); + } + } + quicklistNode *node = ql->head; + unsigned int low_raw = ql->compress; + unsigned int high_raw = ql->len - ql->compress; + + for (unsigned int at = 0; at < ql->len; + at++, node = node->next) { + if (at < low_raw || at >= high_raw) { + if (node->encoding != QUICKLIST_NODE_ENCODING_RAW) { + TEST_PRINT_INFO("Incorrect compression: node %d is " + "compressed at depth %d ((%u, %u); total " + "nodes: %lu; size: %zu)", + at, depth, low_raw, high_raw, ql->len, + node->sz); + err++; + } + } else { + if (node->encoding != QUICKLIST_NODE_ENCODING_LZF) { + TEST_PRINT_INFO("Incorrect non-compression: node %d is NOT " + "compressed at depth %d ((%u, %u); total " + "nodes: %lu; size: %zu; attempted: %d)", + at, depth, low_raw, high_raw, ql->len, + node->sz, node->attempted_compress); + err++; + } + } + } + } + + quicklistRelease(ql); + } + } + } + TEST_ASSERT(err == 0); + return 0; +} + +/*----------------------------------------------------------------------------- + * Quicklist Bookmark Unit Test + *----------------------------------------------------------------------------*/ + +int test_quicklistBookmarkGetUpdatedToNextItem(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + TEST("bookmark get updated to next item"); + + quicklist *ql = quicklistNew(1, 0); + quicklistPushTail(ql, "1", 1); + quicklistPushTail(ql, "2", 1); + quicklistPushTail(ql, "3", 1); + quicklistPushTail(ql, "4", 1); + quicklistPushTail(ql, "5", 1); + TEST_ASSERT(ql->len == 5); + /* add two bookmarks, one pointing to the node before the last. */ + TEST_ASSERT(quicklistBookmarkCreate(&ql, "_dummy", ql->head->next)); + TEST_ASSERT(quicklistBookmarkCreate(&ql, "_test", ql->tail->prev)); + /* test that the bookmark returns the right node, delete it and see that the bookmark points to the last node */ + TEST_ASSERT(quicklistBookmarkFind(ql, "_test") == ql->tail->prev); + TEST_ASSERT(quicklistDelRange(ql, -2, 1)); + TEST_ASSERT(quicklistBookmarkFind(ql, "_test") == ql->tail); + /* delete the last node, and see that the bookmark was deleted. */ + TEST_ASSERT(quicklistDelRange(ql, -1, 1)); + TEST_ASSERT(quicklistBookmarkFind(ql, "_test") == NULL); + /* test that other bookmarks aren't affected */ + TEST_ASSERT(quicklistBookmarkFind(ql, "_dummy") == ql->head->next); + TEST_ASSERT(quicklistBookmarkFind(ql, "_missing") == NULL); + TEST_ASSERT(ql->len == 3); + quicklistBookmarksClear(ql); /* for coverage */ + TEST_ASSERT(quicklistBookmarkFind(ql, "_dummy") == NULL); + quicklistRelease(ql); + return 0; +} + +int test_quicklistBookmarkLimit(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + UNUSED(flags); + TEST("bookmark limit"); + + int i; + quicklist *ql = quicklistNew(1, 0); + quicklistPushHead(ql, "1", 1); + for (i = 0; i < QL_MAX_BM; i++) + TEST_ASSERT(quicklistBookmarkCreate(&ql, genstr("", i), ql->head)); + /* when all bookmarks are used, creation fails */ + TEST_ASSERT(!quicklistBookmarkCreate(&ql, "_test", ql->head)); + /* delete one and see that we can now create another */ + TEST_ASSERT(quicklistBookmarkDelete(ql, "0")); + TEST_ASSERT(quicklistBookmarkCreate(&ql, "_test", ql->head)); + /* delete one and see that the rest survive */ + TEST_ASSERT(quicklistBookmarkDelete(ql, "_test")); + for (i = 1; i < QL_MAX_BM; i++) + TEST_ASSERT(quicklistBookmarkFind(ql, genstr("", i)) == ql->head); + /* make sure the deleted ones are indeed gone */ + TEST_ASSERT(!quicklistBookmarkFind(ql, "0")); + TEST_ASSERT(!quicklistBookmarkFind(ql, "_test")); + quicklistRelease(ql); + return 0; +} + +int test_quicklistCompressAndDecompressQuicklistListpackNode(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + TEST("compress and decompress quicklist listpack node"); + + if (!(flags & UNIT_TEST_LARGE_MEMORY)) return 0; + + quicklistNode *node = quicklistCreateNode(); + node->entry = lpNew(0); + + /* Just to avoid triggering the assertion in __quicklistCompressNode(), + * it disables the passing of quicklist head or tail node. */ + node->prev = quicklistCreateNode(); + node->next = quicklistCreateNode(); + + /* Create a rand string */ + size_t sz = (1 << 25); /* 32MB per one entry */ + unsigned char *s = zmalloc(sz); + randstring(s, sz); + + /* Keep filling the node, until it reaches 1GB */ + for (int i = 0; i < 32; i++) { + node->entry = lpAppend(node->entry, s, sz); + node->sz = lpBytes((node)->entry); + + long long start = mstime(); + TEST_ASSERT(__quicklistCompressNode(node)); + TEST_ASSERT(__quicklistDecompressNode(node)); + TEST_PRINT_INFO("Compress and decompress: %zu MB in %.2f seconds.\n", + node->sz / 1024 / 1024, (float)(mstime() - start) / 1000); + } + + zfree(s); + zfree(node->prev); + zfree(node->next); + zfree(node->entry); + zfree(node); + return 0; +} + +int test_quicklistCompressAndDecomressQuicklistPlainNodeLargeThanUINT32MAX(int argc, char **argv, int flags) { + UNUSED(argc); + UNUSED(argv); + TEST("compress and decomress quicklist plain node large than UINT32_MAX"); + + if (!(flags & UNIT_TEST_LARGE_MEMORY)) return 0; + +#if ULONG_MAX >= 0xffffffffffffffff + + size_t sz = (1ull << 32); + unsigned char *s = zmalloc(sz); + randstring(s, sz); + memcpy(s, "helloworld", 10); + memcpy(s + sz - 10, "1234567890", 10); + + quicklistNode *node = __quicklistCreateNode(QUICKLIST_NODE_CONTAINER_PLAIN, s, sz); + + /* Just to avoid triggering the assertion in __quicklistCompressNode(), + * it disables the passing of quicklist head or tail node. */ + node->prev = quicklistCreateNode(); + node->next = quicklistCreateNode(); + + long long start = mstime(); + TEST_ASSERT(__quicklistCompressNode(node)); + TEST_ASSERT(__quicklistDecompressNode(node)); + TEST_PRINT_INFO("Compress and decompress: %zu MB in %.2f seconds.\n", + node->sz / 1024 / 1024, (float)(mstime() - start) / 1000); + + TEST_ASSERT(memcmp(node->entry, "helloworld", 10) == 0); + TEST_ASSERT(memcmp(node->entry + sz - 10, "1234567890", 10) == 0); + zfree(node->prev); + zfree(node->next); + zfree(node->entry); + zfree(node); + +#endif + return 0; +} diff --git a/src/unit/test_sds.c b/src/unit/test_sds.c index b97d0d9d32..30f25e4f6f 100644 --- a/src/unit/test_sds.c +++ b/src/unit/test_sds.c @@ -259,43 +259,44 @@ int test_typesAndAllocSize(int argc, char **argv, int flags) { sds x = sdsnewlen(NULL, 31); TEST_ASSERT_MESSAGE("len 31 type", (x[-1] & SDS_TYPE_MASK) == SDS_TYPE_5); + TEST_ASSERT_MESSAGE("len 31 sdsAllocSize", sdsAllocSize(x) == s_malloc_usable_size(sdsAllocPtr(x))); sdsfree(x); x = sdsnewlen(NULL, 32); TEST_ASSERT_MESSAGE("len 32 type", (x[-1] & SDS_TYPE_MASK) >= SDS_TYPE_8); - TEST_ASSERT_MESSAGE("len 32 sdsAllocSize", sdsAllocSize(x) == s_malloc_size(sdsAllocPtr(x))); + TEST_ASSERT_MESSAGE("len 32 sdsAllocSize", sdsAllocSize(x) == s_malloc_usable_size(sdsAllocPtr(x))); sdsfree(x); x = sdsnewlen(NULL, 252); TEST_ASSERT_MESSAGE("len 252 type", (x[-1] & SDS_TYPE_MASK) >= SDS_TYPE_8); - TEST_ASSERT_MESSAGE("len 252 sdsAllocSize", sdsAllocSize(x) == s_malloc_size(sdsAllocPtr(x))); + TEST_ASSERT_MESSAGE("len 252 sdsAllocSize", sdsAllocSize(x) == s_malloc_usable_size(sdsAllocPtr(x))); sdsfree(x); x = sdsnewlen(NULL, 253); TEST_ASSERT_MESSAGE("len 253 type", (x[-1] & SDS_TYPE_MASK) == SDS_TYPE_16); - TEST_ASSERT_MESSAGE("len 253 sdsAllocSize", sdsAllocSize(x) == s_malloc_size(sdsAllocPtr(x))); + TEST_ASSERT_MESSAGE("len 253 sdsAllocSize", sdsAllocSize(x) == s_malloc_usable_size(sdsAllocPtr(x))); sdsfree(x); x = sdsnewlen(NULL, 65530); TEST_ASSERT_MESSAGE("len 65530 type", (x[-1] & SDS_TYPE_MASK) >= SDS_TYPE_16); - TEST_ASSERT_MESSAGE("len 65530 sdsAllocSize", sdsAllocSize(x) == s_malloc_size(sdsAllocPtr(x))); + TEST_ASSERT_MESSAGE("len 65530 sdsAllocSize", sdsAllocSize(x) == s_malloc_usable_size(sdsAllocPtr(x))); sdsfree(x); x = sdsnewlen(NULL, 65531); TEST_ASSERT_MESSAGE("len 65531 type", (x[-1] & SDS_TYPE_MASK) >= SDS_TYPE_32); - TEST_ASSERT_MESSAGE("len 65531 sdsAllocSize", sdsAllocSize(x) == s_malloc_size(sdsAllocPtr(x))); + TEST_ASSERT_MESSAGE("len 65531 sdsAllocSize", sdsAllocSize(x) == s_malloc_usable_size(sdsAllocPtr(x))); sdsfree(x); #if (LONG_MAX == LLONG_MAX) if (flags & UNIT_TEST_LARGE_MEMORY) { x = sdsnewlen(NULL, 4294967286); TEST_ASSERT_MESSAGE("len 4294967286 type", (x[-1] & SDS_TYPE_MASK) >= SDS_TYPE_32); - TEST_ASSERT_MESSAGE("len 4294967286 sdsAllocSize", sdsAllocSize(x) == s_malloc_size(sdsAllocPtr(x))); + TEST_ASSERT_MESSAGE("len 4294967286 sdsAllocSize", sdsAllocSize(x) == s_malloc_usable_size(sdsAllocPtr(x))); sdsfree(x); x = sdsnewlen(NULL, 4294967287); TEST_ASSERT_MESSAGE("len 4294967287 type", (x[-1] & SDS_TYPE_MASK) == SDS_TYPE_64); - TEST_ASSERT_MESSAGE("len 4294967287 sdsAllocSize", sdsAllocSize(x) == s_malloc_size(sdsAllocPtr(x))); + TEST_ASSERT_MESSAGE("len 4294967287 sdsAllocSize", sdsAllocSize(x) == s_malloc_usable_size(sdsAllocPtr(x))); sdsfree(x); } #endif diff --git a/src/unit/test_zmalloc.c b/src/unit/test_zmalloc.c index 6c1d03e8e1..08444a157e 100644 --- a/src/unit/test_zmalloc.c +++ b/src/unit/test_zmalloc.c @@ -6,6 +6,8 @@ int test_zmallocInitialUsedMemory(int argc, char **argv, int flags) { UNUSED(argv); UNUSED(flags); + /* If this fails, it may be that other tests have failed and the memory has not been released. */ + TEST_PRINT_INFO("test_zmallocInitialUsedMemory; used: %zu\n", zmalloc_used_memory()); TEST_ASSERT(zmalloc_used_memory() == 0); return 0; diff --git a/src/valkey-benchmark.c b/src/valkey-benchmark.c index b22ee8cbed..57cdd6fc16 100644 --- a/src/valkey-benchmark.c +++ b/src/valkey-benchmark.c @@ -199,7 +199,7 @@ static long long showThroughput(struct aeEventLoop *eventLoop, long long id, voi /* Dict callbacks */ static uint64_t dictSdsHash(const void *key); -static int dictSdsKeyCompare(dict *d, const void *key1, const void *key2); +static int dictSdsKeyCompare(const void *key1, const void *key2); /* Implementation */ static long long ustime(void) { @@ -220,10 +220,8 @@ static uint64_t dictSdsHash(const void *key) { return dictGenHashFunction((unsigned char *)key, sdslen((char *)key)); } -static int dictSdsKeyCompare(dict *d, const void *key1, const void *key2) { +static int dictSdsKeyCompare(const void *key1, const void *key2) { int l1, l2; - UNUSED(d); - l1 = sdslen((sds)key1); l2 = sdslen((sds)key2); if (l1 != l2) return 0; diff --git a/src/valkey-cli.c b/src/valkey-cli.c index b4a7fcaf91..0ba03dc6ba 100644 --- a/src/valkey-cli.c +++ b/src/valkey-cli.c @@ -172,9 +172,9 @@ static struct termios orig_termios; /* To restore terminal at exit.*/ /* Dict Helpers */ static uint64_t dictSdsHash(const void *key); -static int dictSdsKeyCompare(dict *d, const void *key1, const void *key2); -static void dictSdsDestructor(dict *d, void *val); -static void dictListDestructor(dict *d, void *val); +static int dictSdsKeyCompare(const void *key1, const void *key2); +static void dictSdsDestructor(void *val); +static void dictListDestructor(void *val); /* Cluster Manager Command Info */ typedef struct clusterManagerCommand { @@ -371,23 +371,19 @@ static uint64_t dictSdsHash(const void *key) { return dictGenHashFunction((unsigned char *)key, sdslen((char *)key)); } -static int dictSdsKeyCompare(dict *d, const void *key1, const void *key2) { +static int dictSdsKeyCompare(const void *key1, const void *key2) { int l1, l2; - UNUSED(d); - l1 = sdslen((sds)key1); l2 = sdslen((sds)key2); if (l1 != l2) return 0; return memcmp(key1, key2, l1) == 0; } -static void dictSdsDestructor(dict *d, void *val) { - UNUSED(d); +static void dictSdsDestructor(void *val) { sdsfree(val); } -void dictListDestructor(dict *d, void *val) { - UNUSED(d); +void dictListDestructor(void *val) { listRelease((list *)val); } @@ -8663,9 +8659,8 @@ static typeinfo *typeinfo_add(dict *types, char *name, typeinfo *type_template) return info; } -void type_free(dict *d, void *val) { +void type_free(void *val) { typeinfo *info = val; - UNUSED(d); if (info->biggest_key) sdsfree(info->biggest_key); sdsfree(info->name); zfree(info); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000000..2a76897bb0 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,5 @@ +add_subdirectory(rdma) + +if (BUILD_TEST_MODULES) + add_subdirectory(modules) +endif () diff --git a/tests/integration/dual-channel-replication.tcl b/tests/integration/dual-channel-replication.tcl index 5302030db9..05bdc130c1 100644 --- a/tests/integration/dual-channel-replication.tcl +++ b/tests/integration/dual-channel-replication.tcl @@ -23,14 +23,20 @@ proc get_client_id_by_last_cmd {r cmd} { return $client_id } -# Wait until the process enters a paused state, then resume the process. -proc wait_and_resume_process idx { +# Wait until the process enters a paused state. +proc wait_process_paused idx { set pid [srv $idx pid] wait_for_condition 50 1000 { [string match "T*" [exec ps -o state= -p $pid]] } else { fail "Process $pid didn't stop, current state is [exec ps -o state= -p $pid]" } +} + +# Wait until the process enters a paused state, then resume the process. +proc wait_and_resume_process idx { + set pid [srv $idx pid] + wait_process_paused $idx resume_process $pid } @@ -790,11 +796,20 @@ start_server {tags {"dual-channel-replication external:skip"}} { } else { fail "Primary did not free repl buf block after sync failure" } + # Full sync will be triggered after the replica is reconnected, pause primary main process after fork. + # In this way, in the subsequent replicaof no one, we won't get the LOADING error if the replica reconnects + # too quickly and enters the loading state. + $primary debug pause-after-fork 1 resume_process $replica_pid set res [wait_for_log_messages -1 {"*Unable to partial resync with replica * for lack of backlog*"} $loglines 2000 10] set loglines [lindex $res 1] } + # Waiting for the primary to enter the paused state, that is, make sure that bgsave is triggered. + wait_process_paused -1 $replica replicaof no one + # Resume the primary and make sure the sync is dropped. + resume_process [srv -1 pid] + $primary debug pause-after-fork 0 wait_for_condition 500 1000 { [s -1 rdb_bgsave_in_progress] eq 0 } else { diff --git a/tests/modules/CMakeLists.txt b/tests/modules/CMakeLists.txt new file mode 100644 index 0000000000..0cac0c4cb6 --- /dev/null +++ b/tests/modules/CMakeLists.txt @@ -0,0 +1,58 @@ +# Build test modules +list(APPEND MODULES_LIST "commandfilter") +list(APPEND MODULES_LIST "basics") +list(APPEND MODULES_LIST "testrdb") +list(APPEND MODULES_LIST "fork") +list(APPEND MODULES_LIST "infotest") +list(APPEND MODULES_LIST "propagate") +list(APPEND MODULES_LIST "misc") +list(APPEND MODULES_LIST "hooks") +list(APPEND MODULES_LIST "blockonkeys") +list(APPEND MODULES_LIST "blockonbackground") +list(APPEND MODULES_LIST "scan") +list(APPEND MODULES_LIST "datatype") +list(APPEND MODULES_LIST "datatype2") +list(APPEND MODULES_LIST "auth") +list(APPEND MODULES_LIST "keyspace_events") +list(APPEND MODULES_LIST "blockedclient") +list(APPEND MODULES_LIST "getkeys") +list(APPEND MODULES_LIST "getchannels") +list(APPEND MODULES_LIST "test_lazyfree") +list(APPEND MODULES_LIST "timer") +list(APPEND MODULES_LIST "defragtest") +list(APPEND MODULES_LIST "keyspecs") +list(APPEND MODULES_LIST "hash") +list(APPEND MODULES_LIST "zset") +list(APPEND MODULES_LIST "stream") +list(APPEND MODULES_LIST "mallocsize") +list(APPEND MODULES_LIST "aclcheck") +list(APPEND MODULES_LIST "list") +list(APPEND MODULES_LIST "subcommands") +list(APPEND MODULES_LIST "reply") +list(APPEND MODULES_LIST "cmdintrospection") +list(APPEND MODULES_LIST "eventloop") +list(APPEND MODULES_LIST "moduleconfigs") +list(APPEND MODULES_LIST "moduleconfigstwo") +list(APPEND MODULES_LIST "publish") +list(APPEND MODULES_LIST "usercall") +list(APPEND MODULES_LIST "postnotifications") +list(APPEND MODULES_LIST "moduleauthtwo") +list(APPEND MODULES_LIST "rdbloadsave") +list(APPEND MODULES_LIST "crash") +list(APPEND MODULES_LIST "cluster") + +foreach (MODULE_NAME ${MODULES_LIST}) + message(STATUS "Building test module: ${MODULE_NAME}") + add_library(${MODULE_NAME} SHARED "${CMAKE_SOURCE_DIR}/tests/modules/${MODULE_NAME}.c") + target_include_directories(${MODULE_NAME} PRIVATE "${CMAKE_SOURCE_DIR}/src") + if (LINUX AND NOT APPLE) + # set the std to gnu11 here, to allow crash.c to get compiled + target_compile_options(${MODULE_NAME} PRIVATE "-std=gnu11") + endif () + set_target_properties(${MODULE_NAME} PROPERTIES PREFIX "") + valkey_install_bin(${MODULE_NAME}) + if (APPLE) + # Some symbols can only be resolved during runtime (they exist in the executable) + target_link_options(${MODULE_NAME} PRIVATE -undefined dynamic_lookup) + endif () +endforeach () diff --git a/tests/rdma/CMakeLists.txt b/tests/rdma/CMakeLists.txt new file mode 100644 index 0000000000..f721e9af52 --- /dev/null +++ b/tests/rdma/CMakeLists.txt @@ -0,0 +1,9 @@ +project(rdma-test) + +# Make sure RDMA build is enabled +if (BUILD_RDMA_MODULE) + add_executable(rdma-test "${CMAKE_SOURCE_DIR}/tests/rdma/rdma-test.c") + target_link_libraries(rdma-test "${RDMA_LIBS}") + target_link_options(rdma-test PRIVATE "-pthread") + valkey_install_bin(rdma-test) +endif () diff --git a/tests/support/cluster_util.tcl b/tests/support/cluster_util.tcl index dd5cd84df2..686f00071b 100644 --- a/tests/support/cluster_util.tcl +++ b/tests/support/cluster_util.tcl @@ -145,6 +145,7 @@ proc wait_for_cluster_size {cluster_size} { # Check that cluster nodes agree about "state", or raise an error. proc wait_for_cluster_state {state} { for {set j 0} {$j < [llength $::servers]} {incr j} { + if {[process_is_paused [srv -$j pid]]} continue wait_for_condition 1000 50 { [CI $j cluster_state] eq $state } else { @@ -277,6 +278,14 @@ proc cluster_get_myself id { return {} } +# Returns the parsed "myself's primary" CLUSTER NODES entry as a dictionary. +proc cluster_get_myself_primary id { + set myself [cluster_get_myself $id] + set replicaof [dict get $myself slaveof] + set node [cluster_get_node_by_id $id $replicaof] + return $node +} + # Get a specific node by ID by parsing the CLUSTER NODES output # of the instance Number 'instance_id' proc cluster_get_node_by_id {instance_id node_id} { diff --git a/tests/unit/cluster/failover2.tcl b/tests/unit/cluster/failover2.tcl index 7bc6a05e95..21c4f4a678 100644 --- a/tests/unit/cluster/failover2.tcl +++ b/tests/unit/cluster/failover2.tcl @@ -64,3 +64,36 @@ start_cluster 3 4 {tags {external:skip cluster} overrides {cluster-ping-interval } } ;# start_cluster + + +start_cluster 7 3 {tags {external:skip cluster} overrides {cluster-ping-interval 1000 cluster-node-timeout 5000}} { + test "Primaries will not time out then they are elected in the same epoch" { + # Since we have the delay time, so these node may not initiate the + # election at the same time (same epoch). But if they do, we make + # sure there is no failover timeout. + + # Killing there primary nodes. + pause_process [srv 0 pid] + pause_process [srv -1 pid] + pause_process [srv -2 pid] + + # Wait for the failover + wait_for_condition 1000 50 { + [s -7 role] == "master" && + [s -8 role] == "master" && + [s -9 role] == "master" + } else { + fail "No failover detected" + } + + # Make sure there is no failover timeout. + verify_no_log_message -7 "*Failover attempt expired*" 0 + verify_no_log_message -8 "*Failover attempt expired*" 0 + verify_no_log_message -9 "*Failover attempt expired*" 0 + + # Resuming these primary nodes, speed up the shutdown. + resume_process [srv 0 pid] + resume_process [srv -1 pid] + resume_process [srv -2 pid] + } +} ;# start_cluster diff --git a/tests/unit/cluster/manual-failover.tcl b/tests/unit/cluster/manual-failover.tcl index 2a9dff934b..78842068fa 100644 --- a/tests/unit/cluster/manual-failover.tcl +++ b/tests/unit/cluster/manual-failover.tcl @@ -183,3 +183,91 @@ test "Wait for instance #0 to return back alive" { } } ;# start_cluster + +start_cluster 3 1 {tags {external:skip cluster} overrides {cluster-ping-interval 1000 cluster-node-timeout 2000}} { + test "Manual failover vote is not limited by two times the node timeout - drop the auth ack" { + set CLUSTER_PACKET_TYPE_FAILOVER_AUTH_ACK 6 + set CLUSTER_PACKET_TYPE_NONE -1 + + # Setting a large timeout to make sure we hit the voted_time limit. + R 0 config set cluster-node-timeout 150000 + R 1 config set cluster-node-timeout 150000 + R 2 config set cluster-node-timeout 150000 + + # Let replica drop FAILOVER_AUTH_ACK so that the election won't + # get the enough votes and the election will time out. + R 3 debug drop-cluster-packet-filter $CLUSTER_PACKET_TYPE_FAILOVER_AUTH_ACK + + # The first manual failover will time out. + R 3 cluster failover + wait_for_log_messages 0 {"*Manual failover timed out*"} 0 1000 50 + wait_for_log_messages -3 {"*Manual failover timed out*"} 0 1000 50 + + # Undo packet drop, so that replica can win the next election. + R 3 debug drop-cluster-packet-filter $CLUSTER_PACKET_TYPE_NONE + + # Make sure the second manual failover will work. + R 3 cluster failover + wait_for_condition 1000 50 { + [s 0 role] eq {slave} && + [s -3 role] eq {master} + } else { + fail "The second failover does not happen" + } + wait_for_cluster_propagation + } +} ;# start_cluster + +start_cluster 3 1 {tags {external:skip cluster} overrides {cluster-ping-interval 1000 cluster-node-timeout 2000}} { + test "Manual failover vote is not limited by two times the node timeout - mixed failover" { + # Make sure the failover is triggered by us. + R 1 config set cluster-replica-validity-factor 0 + R 3 config set cluster-replica-no-failover yes + R 3 config set cluster-replica-validity-factor 0 + + # Pause the primary. + pause_process [srv 0 pid] + wait_for_cluster_state fail + + # Setting a large timeout to make sure we hit the voted_time limit. + R 1 config set cluster-node-timeout 150000 + R 2 config set cluster-node-timeout 150000 + + # R 3 performs an automatic failover and it will work. + R 3 config set cluster-replica-no-failover no + wait_for_condition 1000 50 { + [s -3 role] eq {master} + } else { + fail "The first failover does not happen" + } + + # Resume the primary and wait for it to become a replica. + resume_process [srv 0 pid] + wait_for_condition 1000 50 { + [s 0 role] eq {slave} + } else { + fail "Old primary not converted into replica" + } + wait_for_cluster_propagation + + # The old primary doing a manual failover and wait for it. + R 0 cluster failover + wait_for_condition 1000 50 { + [s 0 role] eq {master} && + [s -3 role] eq {slave} + } else { + fail "The second failover does not happen" + } + wait_for_cluster_propagation + + # R 3 performs a manual failover and it will work. + R 3 cluster failover + wait_for_condition 1000 50 { + [s 0 role] eq {slave} && + [s -3 role] eq {master} + } else { + fail "The third falover does not happen" + } + wait_for_cluster_propagation + } +} ;# start_cluster diff --git a/tests/unit/cluster/replica-migration.tcl b/tests/unit/cluster/replica-migration.tcl index 05d6528684..591d732fce 100644 --- a/tests/unit/cluster/replica-migration.tcl +++ b/tests/unit/cluster/replica-migration.tcl @@ -90,6 +90,8 @@ proc test_migrated_replica {type} { # Wait for the cluster to be ok. wait_for_condition 1000 50 { + [R 3 cluster slots] eq [R 4 cluster slots] && + [R 4 cluster slots] eq [R 7 cluster slots] && [CI 3 cluster_state] eq "ok" && [CI 4 cluster_state] eq "ok" && [CI 7 cluster_state] eq "ok" @@ -187,6 +189,7 @@ proc test_nonempty_replica {type} { # Wait for the cluster to be ok. wait_for_condition 1000 50 { + [R 4 cluster slots] eq [R 7 cluster slots] && [CI 4 cluster_state] eq "ok" && [CI 7 cluster_state] eq "ok" } else { @@ -306,6 +309,8 @@ proc test_sub_replica {type} { # Wait for the cluster to be ok. wait_for_condition 1000 50 { + [R 3 cluster slots] eq [R 4 cluster slots] && + [R 4 cluster slots] eq [R 7 cluster slots] && [CI 3 cluster_state] eq "ok" && [CI 4 cluster_state] eq "ok" && [CI 7 cluster_state] eq "ok" @@ -400,3 +405,23 @@ start_cluster 4 4 {tags {external:skip cluster} overrides {cluster-node-timeout start_cluster 4 4 {tags {external:skip cluster} overrides {cluster-node-timeout 1000 cluster-migration-barrier 999}} { test_cluster_setslot "setslot" } my_slot_allocation cluster_allocate_replicas ;# start_cluster + +start_cluster 3 0 {tags {external:skip cluster} overrides {cluster-node-timeout 1000 cluster-migration-barrier 999}} { + test "Empty primary will check and delete the dirty slots" { + R 2 config set cluster-allow-replica-migration no + + # Write a key to slot 0. + R 2 incr key_977613 + + # Move slot 0 from primary 2 to primary 0. + R 0 cluster bumpepoch + R 0 cluster setslot 0 node [R 0 cluster myid] + + # Wait for R 2 to report that it is an empty primary (cluster-allow-replica-migration no) + wait_for_log_messages -2 {"*I am now an empty primary*"} 0 1000 50 + + # Make sure primary 0 will delete the dirty slots. + verify_log_message -2 "*Deleting keys in dirty slot 0*" 0 + assert_equal [R 2 dbsize] 0 + } +} my_slot_allocation cluster_allocate_replicas ;# start_cluster diff --git a/tests/unit/cluster/slot-migration.tcl b/tests/unit/cluster/slot-migration.tcl index d798971968..289c20578d 100644 --- a/tests/unit/cluster/slot-migration.tcl +++ b/tests/unit/cluster/slot-migration.tcl @@ -14,17 +14,61 @@ proc get_cluster_role {srv_idx} { return $role } +proc get_myself_primary_flags {srv_idx} { + set flags [dict get [cluster_get_myself_primary $srv_idx] flags] + return $flags +} + +proc get_myself_primary_linkstate {srv_idx} { + set linkstate [dict get [cluster_get_myself_primary $srv_idx] linkstate] + return $linkstate +} + proc wait_for_role {srv_idx role} { + # Wait for the role, make sure the replication role matches. wait_for_condition 100 100 { [lindex [split [R $srv_idx ROLE] " "] 0] eq $role } else { + puts "R $srv_idx ROLE: [R $srv_idx ROLE]" fail "R $srv_idx didn't assume the replication $role in time" } + + if {$role eq "slave"} { + # Wait for the replication link, make sure the replication link is normal. + wait_for_condition 100 100 { + [s -$srv_idx master_link_status] eq "up" + } else { + puts "R $srv_idx INFO REPLICATION: [R $srv_idx INFO REPLICATION]" + fail "R $srv_idx didn't assume the replication link in time" + } + } + + # Wait for the cluster role, make sure the cluster role matches. wait_for_condition 100 100 { [get_cluster_role $srv_idx] eq $role } else { + puts "R $srv_idx CLUSTER NODES: [R $srv_idx CLUSTER NODES]" fail "R $srv_idx didn't assume the cluster $role in time" } + + if {$role eq "slave"} { + # Wait for the flags, make sure the primary node is not failed. + wait_for_condition 100 100 { + [get_myself_primary_flags $srv_idx] eq "master" + } else { + puts "R $srv_idx CLUSTER NODES: [R $srv_idx CLUSTER NODES]" + fail "R $srv_idx didn't assume the primary state in time" + } + + # Wait for the cluster link, make sure that the cluster connection is normal. + wait_for_condition 100 100 { + [get_myself_primary_linkstate $srv_idx] eq "connected" + } else { + puts "R $srv_idx CLUSTER NODES: [R $srv_idx CLUSTER NODES]" + fail "R $srv_idx didn't assume the cluster primary link in time" + } + } + wait_for_cluster_propagation } diff --git a/tests/unit/cluster/slot-ownership.tcl b/tests/unit/cluster/slot-ownership.tcl index 0f3e3cc4f7..0073c2904f 100644 --- a/tests/unit/cluster/slot-ownership.tcl +++ b/tests/unit/cluster/slot-ownership.tcl @@ -59,3 +59,88 @@ start_cluster 2 2 {tags {external:skip cluster}} { } } } + +start_cluster 3 1 {tags {external:skip cluster} overrides {shutdown-timeout 100}} { + test "Primary lost a slot during the shutdown waiting" { + R 0 set FOO 0 + + # Pause the replica. + pause_process [srv -3 pid] + + # Incr the key and immediately shutdown the primary. + # The primary waits for the replica to replicate before exiting. + R 0 incr FOO + exec kill -SIGTERM [srv 0 pid] + wait_for_condition 50 100 { + [s 0 shutdown_in_milliseconds] > 0 + } else { + fail "Primary not indicating ongoing shutdown." + } + + # Move the slot to other primary + R 1 cluster bumpepoch + R 1 cluster setslot [R 1 cluster keyslot FOO] node [R 1 cluster myid] + + # Waiting for dirty slot update. + wait_for_log_messages 0 {"*Deleting keys in dirty slot*"} 0 1000 10 + + # Resume the replica and make sure primary exits normally instead of crashing. + resume_process [srv -3 pid] + wait_for_log_messages 0 {"*Valkey is now ready to exit, bye bye*"} 0 1000 10 + + # Make sure that the replica will become the new primary and does not own the key. + wait_for_condition 1000 50 { + [s -3 role] eq {master} + } else { + fail "The replica was not converted into primary" + } + assert_error {ERR no such key} {R 3 debug object foo} + } +} + +start_cluster 3 1 {tags {external:skip cluster}} { + test "Primary lost a slot during the manual failover pausing" { + R 0 set FOO 0 + + # Set primaries to drop the FAILOVER_AUTH_REQUEST packets, so that + # primary 0 will pause until the failover times out. + R 1 debug drop-cluster-packet-filter 5 + R 2 debug drop-cluster-packet-filter 5 + + # Replica doing the manual failover. + R 3 cluster failover + + # Move the slot to other primary + R 1 cluster bumpepoch + R 1 cluster setslot [R 1 cluster keyslot FOO] node [R 1 cluster myid] + + # Waiting for dirty slot update. + wait_for_log_messages 0 {"*Deleting keys in dirty slot*"} 0 1000 10 + + # Make sure primary doesn't crash when deleting the keys. + R 0 ping + + R 1 debug drop-cluster-packet-filter -1 + R 2 debug drop-cluster-packet-filter -1 + } +} + +start_cluster 3 1 {tags {external:skip cluster}} { + test "Primary lost a slot during the client pause command" { + R 0 set FOO 0 + + R 0 client pause 1000000000 write + + # Move the slot to other primary + R 1 cluster bumpepoch + R 1 cluster setslot [R 1 cluster keyslot FOO] node [R 1 cluster myid] + + # Waiting for dirty slot update. + wait_for_log_messages 0 {"*Deleting keys in dirty slot*"} 0 1000 10 + + # Make sure primary doesn't crash when deleting the keys. + R 0 ping + + R 0 client unpause + } +} diff --git a/tests/unit/expire.tcl b/tests/unit/expire.tcl index d85ce7ee68..fba425f62d 100644 --- a/tests/unit/expire.tcl +++ b/tests/unit/expire.tcl @@ -832,6 +832,80 @@ start_server {tags {"expire"}} { close_replication_stream $repl assert_equal [r debug set-active-expire 1] {OK} } {} {needs:debug} + + test {Import mode should forbid active expiration} { + r flushall + + r config set import-mode yes + assert_equal [r client import-source on] {OK} + + r set foo1 bar PX 1 + r set foo2 bar PX 1 + after 100 + + assert_equal [r dbsize] {2} + + assert_equal [r client import-source off] {OK} + r config set import-mode no + + # Verify all keys have expired + wait_for_condition 40 100 { + [r dbsize] eq 0 + } else { + fail "Keys did not actively expire." + } + } + + test {Import mode should forbid lazy expiration} { + r flushall + r debug set-active-expire 0 + + r config set import-mode yes + assert_equal [r client import-source on] {OK} + + r set foo1 1 PX 1 + after 10 + + r get foo1 + assert_equal [r dbsize] {1} + + assert_equal [r client import-source off] {OK} + r config set import-mode no + + r get foo1 + + assert_equal [r dbsize] {0} + + assert_equal [r debug set-active-expire 1] {OK} + } {} {needs:debug} + + test {RANDOMKEY can return expired key in import mode} { + r flushall + + r config set import-mode yes + assert_equal [r client import-source on] {OK} + + r set foo1 bar PX 1 + after 10 + + set client [valkey [srv "host"] [srv "port"] 0 $::tls] + if {!$::singledb} { + $client select 9 + } + assert_equal [$client ttl foo1] {-2} + + assert_equal [r randomkey] {foo1} + + assert_equal [r client import-source off] {OK} + r config set import-mode no + + # Verify all keys have expired + wait_for_condition 40 100 { + [r dbsize] eq 0 + } else { + fail "Keys did not actively expire." + } + } } start_cluster 1 0 {tags {"expire external:skip cluster"}} { diff --git a/tests/unit/info.tcl b/tests/unit/info.tcl index 61d1acd1f8..278a1d8e33 100644 --- a/tests/unit/info.tcl +++ b/tests/unit/info.tcl @@ -424,8 +424,7 @@ start_server {tags {"info" "external:skip"}} { set info [r info clients] assert_equal [getInfoProperty $info pubsub_clients] {1} # non-pubsub clients should not be involved - catch {unsubscribe $rd2 {non-exist-chan}} e - assert_match {*NOSUB*} $e + assert_equal {0} [unsubscribe $rd2 {non-exist-chan}] set info [r info clients] assert_equal [getInfoProperty $info pubsub_clients] {1} # close all clients diff --git a/tests/unit/keyspace.tcl b/tests/unit/keyspace.tcl index ba55c1b8ea..1936f5e217 100644 --- a/tests/unit/keyspace.tcl +++ b/tests/unit/keyspace.tcl @@ -47,6 +47,10 @@ start_server {tags {"keyspace"}} { r dbsize } {0} + test {KEYS with empty DB} { + assert_equal {} [r keys *] + } + test "DEL against expired key" { r debug set-active-expire 0 r setex keyExpire 1 valExpire @@ -554,3 +558,14 @@ foreach {type large} [array get largevalue] { r KEYS [string repeat "*?" 50000] } {} } + +start_cluster 1 0 {tags {"keyspace external:skip cluster"}} { + test {KEYS with empty DB in cluster mode} { + assert_equal {} [r keys *] + assert_equal {} [r keys foo*] + } + + test {KEYS with empty slot in cluster mode} { + assert_equal {} [r keys foo] + } +} diff --git a/tests/unit/maxmemory.tcl b/tests/unit/maxmemory.tcl index d4e62246f1..89e9699a3e 100644 --- a/tests/unit/maxmemory.tcl +++ b/tests/unit/maxmemory.tcl @@ -611,3 +611,21 @@ start_server {tags {"maxmemory" "external:skip"}} { assert {[r object freq foo] == 5} } } + +start_server {tags {"maxmemory" "external:skip"}} { + test {Import mode should forbid eviction} { + r set key val + r config set import-mode yes + assert_equal [r client import-source on] {OK} + r config set maxmemory-policy allkeys-lru + r config set maxmemory 1 + + assert_equal [r dbsize] {1} + assert_error {OOM command not allowed*} {r set key1 val1} + + assert_equal [r client import-source off] {OK} + r config set import-mode no + + assert_equal [r dbsize] {0} + } +} \ No newline at end of file diff --git a/tests/unit/moduleapi/fork.tcl b/tests/unit/moduleapi/fork.tcl index 9d1f9c184c..bf53bd2db8 100644 --- a/tests/unit/moduleapi/fork.tcl +++ b/tests/unit/moduleapi/fork.tcl @@ -26,7 +26,7 @@ start_server {tags {"modules"}} { # module fork twice assert_error {Fork failed} {r fork.create 0 1} - assert {[count_log_message 0 "Can't fork for module: File exists"] eq "1"} + assert {[count_log_message 0 "Can't fork for module: Operation already in progress"] eq "1"} r fork.kill diff --git a/tests/unit/pause.tcl b/tests/unit/pause.tcl index 38c13afc46..b18a32d48f 100644 --- a/tests/unit/pause.tcl +++ b/tests/unit/pause.tcl @@ -260,6 +260,33 @@ start_server {tags {"pause network"}} { r client unpause } + test "Test eviction is skipped during client pause" { + r flushall + set evicted_keys [s 0 evicted_keys] + + r multi + r set foo{t} bar + r config set maxmemory-policy allkeys-random + r config set maxmemory 1 + r client PAUSE 50000 WRITE + r exec + + # No keys should actually have been evicted. + assert_match $evicted_keys [s 0 evicted_keys] + + # The previous config set triggers a time event, but due to the pause, + # no eviction has been made. After the unpause, a eviction will happen. + r client unpause + wait_for_condition 1000 10 { + [expr $evicted_keys + 1] eq [s 0 evicted_keys] + } else { + fail "Key is not evicted" + } + + r config set maxmemory 0 + r config set maxmemory-policy noeviction + } + test "Test both active and passive expires are skipped during client pause" { set expired_keys [s 0 expired_keys] r multi diff --git a/tests/unit/pubsub.tcl b/tests/unit/pubsub.tcl index 68dc79a4a4..24b78b6e5a 100644 --- a/tests/unit/pubsub.tcl +++ b/tests/unit/pubsub.tcl @@ -109,12 +109,9 @@ start_server {tags {"pubsub network"}} { $rd1 close } - test "UNSUBSCRIBE and PUNSUBSCRIBE from non-subscribed channels" { + test "UNSUBSCRIBE from non-subscribed channels" { set rd1 [valkey_deferring_client] - foreach command {unsubscribe punsubscribe} { - catch {$command $rd1 {foo bar quux}} e - assert_match {*NOSUB*} $e - } + assert_equal {0 0 0} [unsubscribe $rd1 {foo bar quux}] # clean up clients $rd1 close } @@ -204,6 +201,14 @@ start_server {tags {"pubsub network"}} { $rd close } {0} {resp3} + test "PUNSUBSCRIBE from non-subscribed channels" { + set rd1 [valkey_deferring_client] + assert_equal {0 0 0} [punsubscribe $rd1 {foo.* bar.* quux.*}] + + # clean up clients + $rd1 close + } + test "NUMSUB returns numbers, not strings (#1561)" { r pubsub numsub abc def } {abc 0 def 0} @@ -241,6 +246,16 @@ start_server {tags {"pubsub network"}} { $rd1 close } + test "PUNSUBSCRIBE and UNSUBSCRIBE should always reply" { + # Make sure we are not subscribed to any channel at all. + r punsubscribe + r unsubscribe + # Now check if the commands still reply correctly. + set reply1 [r punsubscribe] + set reply2 [r unsubscribe] + concat $reply1 $reply2 + } {punsubscribe {} 0 unsubscribe {} 0} + ### Keyspace events notification tests test "Keyspace notifications: we receive keyspace notifications" { diff --git a/tests/unit/pubsubshard.tcl b/tests/unit/pubsubshard.tcl index d62a415705..e0e1e2972b 100644 --- a/tests/unit/pubsubshard.tcl +++ b/tests/unit/pubsubshard.tcl @@ -74,8 +74,9 @@ start_server {tags {"pubsubshard external:skip"}} { test "SUNSUBSCRIBE from non-subscribed channels" { set rd1 [valkey_deferring_client] - catch {sunsubscribe $rd1 {foo}} e - assert_match {*NOSUB*} $e + assert_equal {0} [sunsubscribe $rd1 {foo}] + assert_equal {0} [sunsubscribe $rd1 {bar}] + assert_equal {0} [sunsubscribe $rd1 {quux}] # clean up clients $rd1 close diff --git a/valkey.conf b/valkey.conf index 7c7b9da43e..bf82b01874 100644 --- a/valkey.conf +++ b/valkey.conf @@ -818,6 +818,13 @@ replica-priority 100 # # replica-ignore-disk-write-errors no +# Make the primary forbid expiration and eviction. +# This is useful for sync tools, because expiration and eviction may cause the data corruption. +# Sync tools can mark their connections as importing source by CLIENT IMPORT-SOURCE. +# NOTICE: Clients should avoid writing the same key on the source server and the destination server. +# +# import-mode no + # ----------------------------------------------------------------------------- # By default, Sentinel includes all replicas in its reports. A replica # can be excluded from Sentinel's announcements. An unannounced replica