diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 53b9b43be..92eb4e296 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,10 @@ name: CI on: [push, pull_request] +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.head_ref || github.ref }} + cancel-in-progress: true + jobs: test-ubuntu-latest: diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index a5e3ebd01..cec367b90 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -4,6 +4,10 @@ name: "Codecov" # where each PR needs to be compared against the coverage of the head commit on: [push, pull_request] +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.head_ref || github.ref }} + cancel-in-progress: true + permissions: contents: read diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 117161a9c..f0a12a0a6 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -6,6 +6,10 @@ on: # run weekly new vulnerability was added to the database - cron: '0 0 * * 0' +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.head_ref || github.ref }} + cancel-in-progress: true + jobs: analyze: name: Analyze diff --git a/.github/workflows/coverity.yml b/.github/workflows/coverity.yml index 4c99adb92..3f125ae4d 100644 --- a/.github/workflows/coverity.yml +++ b/.github/workflows/coverity.yml @@ -6,6 +6,11 @@ on: - cron: '0 0 * * *' # Support manual execution workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.head_ref || github.ref }} + cancel-in-progress: true + jobs: coverity: if: github.repository == 'redis/redis' diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index 029ec4530..fd067686c 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -1105,7 +1105,7 @@ jobs: repository: ${{ env.GITHUB_REPOSITORY }} ref: ${{ env.GITHUB_HEAD_REF }} - name: test - uses: cross-platform-actions/action@v0.30.0 + uses: cross-platform-actions/action@v1.0.0 with: operating_system: freebsd environment_variables: MAKE @@ -1221,10 +1221,14 @@ jobs: - name: cluster tests if: true && !contains(github.event.inputs.skiptests, 'cluster') run: ./runtest-cluster --log-req-res --dont-clean --force-resp3 ${{github.event.inputs.cluster_test_args}} - - name: Install Python dependencies - uses: py-actions/py-dependency-install@30aa0023464ed4b5b116bd9fbdab87acf01a484e # v4.1.0 + - name: Set up Python + uses: actions/setup-python@v6 with: - path: "./utils/req-res-validator/requirements.txt" + python-version: "3.x" + cache: "pip" + cache-dependency-path: "./utils/req-res-validator/requirements.txt" + - name: Install Python dependencies + run: python -m pip install -r ./utils/req-res-validator/requirements.txt - name: validator run: ./utils/req-res-log-validator.py --verbose --fail-missing-reply-schemas ${{ (!contains(github.event.inputs.skiptests, 'redis') && !contains(github.event.inputs.skiptests, 'module') && !contains(github.event.inputs.sentinel, 'redis') && !contains(github.event.inputs.skiptests, 'cluster')) && github.event.inputs.test_args == '' && github.event.inputs.cluster_test_args == '' && '--fail-commands-not-all-hit' || '' }} diff --git a/.github/workflows/external.yml b/.github/workflows/external.yml index 9dd3340aa..75501d248 100644 --- a/.github/workflows/external.yml +++ b/.github/workflows/external.yml @@ -6,6 +6,10 @@ on: schedule: - cron: '0 0 * * *' +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.head_ref || github.ref }} + cancel-in-progress: true + jobs: test-external-standalone: runs-on: ubuntu-latest diff --git a/.github/workflows/reply-schemas-linter.yml b/.github/workflows/reply-schemas-linter.yml index 539e739f3..9e292927d 100644 --- a/.github/workflows/reply-schemas-linter.yml +++ b/.github/workflows/reply-schemas-linter.yml @@ -8,6 +8,10 @@ on: paths: - 'src/commands/*.json' +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.head_ref || github.ref }} + cancel-in-progress: true + jobs: reply-schemas-linter: runs-on: ubuntu-latest diff --git a/.github/workflows/spell-check.yml b/.github/workflows/spell-check.yml index 48b949b05..a0efc05d1 100644 --- a/.github/workflows/spell-check.yml +++ b/.github/workflows/spell-check.yml @@ -9,6 +9,10 @@ on: push: pull_request: +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.head_ref || github.ref }} + cancel-in-progress: true + jobs: build: name: Spellcheck diff --git a/README.md b/README.md index 3b1e04bba..d7ca1a962 100644 --- a/README.md +++ b/README.md @@ -266,7 +266,7 @@ Tested with the following Docker image: ```sh cd /usr/src/redis- - export BUILD_TLS=yes BUILD_WITH_MODULES=yes INSTALL_RUST_TOOLCHAIN=yes DISABLE_WERRORS=yes + export BUILD_TLS=yes BUILD_WITH_MODULES=yes INSTALL_RUST_TOOLCHAIN=yes make -j "$(nproc)" all ``` @@ -320,7 +320,7 @@ Tested with the following Docker image: ```sh cd /usr/src/redis- - export BUILD_TLS=yes BUILD_WITH_MODULES=yes INSTALL_RUST_TOOLCHAIN=yes DISABLE_WERRORS=yes + export BUILD_TLS=yes BUILD_WITH_MODULES=yes INSTALL_RUST_TOOLCHAIN=yes make -j "$(nproc)" all ``` @@ -389,7 +389,7 @@ Tested with the following Docker image: ```sh cd /usr/src/redis- - export BUILD_TLS=yes BUILD_WITH_MODULES=yes INSTALL_RUST_TOOLCHAIN=yes DISABLE_WERRORS=yes + export BUILD_TLS=yes BUILD_WITH_MODULES=yes INSTALL_RUST_TOOLCHAIN=yes make -j "$(nproc)" all ``` @@ -446,7 +446,7 @@ Tested with the following Docker images: ```sh cd /usr/src/redis- - export BUILD_TLS=yes BUILD_WITH_MODULES=yes INSTALL_RUST_TOOLCHAIN=yes DISABLE_WERRORS=yes + export BUILD_TLS=yes BUILD_WITH_MODULES=yes INSTALL_RUST_TOOLCHAIN=yes make -j "$(nproc)" all ``` @@ -565,7 +565,7 @@ Tested with the following Docker images: ```sh source /etc/profile.d/gcc-toolset-13.sh cd /usr/src/redis- - export BUILD_TLS=yes BUILD_WITH_MODULES=yes INSTALL_RUST_TOOLCHAIN=yes DISABLE_WERRORS=yes + export BUILD_TLS=yes BUILD_WITH_MODULES=yes INSTALL_RUST_TOOLCHAIN=yes make -j "$(nproc)" all ``` @@ -682,7 +682,7 @@ Tested with the following Docker images: ```sh source /etc/profile.d/gcc-toolset-13.sh cd /usr/src/redis- - export BUILD_TLS=yes BUILD_WITH_MODULES=yes INSTALL_RUST_TOOLCHAIN=yes DISABLE_WERRORS=yes + export BUILD_TLS=yes BUILD_WITH_MODULES=yes INSTALL_RUST_TOOLCHAIN=yes make -j "$(nproc)" all ``` @@ -915,7 +915,7 @@ The following instructions apply to both Intel and Apple Silicon (ARM) Macs. export BUILD_TLS=yes export DISABLE_WERRORS=yes export LTO=0 - PATH="$HOMEBREW_PREFIX/opt/llvm@18/bin:$HOMEBREW_PREFIX/opt/make/libexec/gnubin:$HOMEBREW_PREFIX/opt/gnu-sed/libexec/gnubin:$HOMEBREW_PREFIX/opt/coreutils/libexec/gnubin:$PATH" + PATH="$HOMEBREW_PREFIX/opt/libtool/libexec/gnubin:$HOMEBREW_PREFIX/opt/llvm@18/bin:$HOMEBREW_PREFIX/opt/make/libexec/gnubin:$HOMEBREW_PREFIX/opt/gnu-sed/libexec/gnubin:$HOMEBREW_PREFIX/opt/coreutils/libexec/gnubin:$PATH" export LDFLAGS="-L$HOMEBREW_PREFIX/opt/llvm@18/lib" export CPPFLAGS="-I$HOMEBREW_PREFIX/opt/llvm@18/include" mkdir -p build_dir/etc diff --git a/modules/Makefile b/modules/Makefile index cba978269..667794160 100644 --- a/modules/Makefile +++ b/modules/Makefile @@ -11,7 +11,7 @@ all: prepare_source get_source: $(call submake,$@) -prepare_source: get_source handle-werrors setup_environment +prepare_source: get_source setup_environment clean: $(call submake,$@) @@ -25,7 +25,7 @@ pristine: install: $(call submake,$@) -setup_environment: install-rust handle-werrors +setup_environment: install-rust clean_environment: uninstall-rust @@ -74,17 +74,4 @@ ifeq ($(INSTALL_RUST_TOOLCHAIN),yes) fi endif -handle-werrors: get_source -ifeq ($(DISABLE_WERRORS),yes) - @echo "Disabling -Werror for all modules" - @for dir in $(SUBDIRS); do \ - echo "Processing $$dir"; \ - find $$dir/src -type f \ - \( -name "Makefile" \ - -o -name "*.mk" \ - -o -name "CMakeLists.txt" \) \ - -exec sed -i 's/-Werror//g' {} +; \ - done -endif - -.PHONY: all clean distclean install $(SUBDIRS) setup_environment clean_environment install-rust uninstall-rust handle-werrors +.PHONY: all clean distclean install $(SUBDIRS) setup_environment clean_environment install-rust uninstall-rust diff --git a/modules/redisbloom/Makefile b/modules/redisbloom/Makefile index 2fa608a0e..5da5dd605 100644 --- a/modules/redisbloom/Makefile +++ b/modules/redisbloom/Makefile @@ -1,5 +1,5 @@ SRC_DIR = src -MODULE_VERSION = v8.7.91 +MODULE_VERSION = v8.8.0 MODULE_REPO = https://github.com/redisbloom/redisbloom TARGET_MODULE = $(SRC_DIR)/bin/$(FULL_VARIANT)/redisbloom.so diff --git a/modules/redisearch/Makefile b/modules/redisearch/Makefile index a56e9fc70..14eb9a79b 100644 --- a/modules/redisearch/Makefile +++ b/modules/redisearch/Makefile @@ -1,5 +1,5 @@ SRC_DIR = src -MODULE_VERSION = v8.7.91 +MODULE_VERSION = v8.8.0 MODULE_REPO = https://github.com/redisearch/redisearch TARGET_MODULE = $(SRC_DIR)/bin/$(FULL_VARIANT)/search-community/redisearch.so @@ -7,10 +7,14 @@ TARGET_MODULE = $(SRC_DIR)/bin/$(FULL_VARIANT)/search-community/redisearch.so LTO ?= 1 export LTO +# Use the committed C headers for Rust modules, rather than regenerating them +# from Rust source. Override with REDISEARCH_GENERATE_HEADERS=1. +REDISEARCH_GENERATE_HEADERS ?= 0 +export REDISEARCH_GENERATE_HEADERS + # Set INLINE_LSE_ATOMICS=1 for perf improvement on common ARM CPUs (i.e. Graviton2/3/4); no effect on x86 or macOS. # Default 0 keeps the binary runnable on pre-Armv8.1-a cores (Cortex-A72, Graviton1, RPi4) that would otherwise SIGILL at module load. INLINE_LSE_ATOMICS ?= 0 export INLINE_LSE_ATOMICS include ../common.mk - diff --git a/modules/redisjson/Makefile b/modules/redisjson/Makefile index e85e5297d..3108f8f41 100644 --- a/modules/redisjson/Makefile +++ b/modules/redisjson/Makefile @@ -1,5 +1,5 @@ SRC_DIR = src -MODULE_VERSION = v8.7.91 +MODULE_VERSION = v8.8.0 MODULE_REPO = https://github.com/redisjson/redisjson TARGET_MODULE = $(SRC_DIR)/bin/$(FULL_VARIANT)/rejson.so diff --git a/modules/redistimeseries/Makefile b/modules/redistimeseries/Makefile index b5da541dd..1b14f1fea 100644 --- a/modules/redistimeseries/Makefile +++ b/modules/redistimeseries/Makefile @@ -1,5 +1,5 @@ SRC_DIR = src -MODULE_VERSION = v8.7.91 +MODULE_VERSION = v8.8.0 MODULE_REPO = https://github.com/redistimeseries/redistimeseries TARGET_MODULE = $(SRC_DIR)/bin/$(FULL_VARIANT)/redistimeseries.so diff --git a/runtest b/runtest index 09b9d491b..24bc6236b 100755 --- a/runtest +++ b/runtest @@ -1,4 +1,9 @@ #!/bin/sh +# Raise open files limit (macOS default 256 is too low for tests). +OPEN_FILE_LIMIT=$(ulimit -n 2>/dev/null) +if [ -n "$OPEN_FILE_LIMIT" ] && [ "$OPEN_FILE_LIMIT" != "unlimited" ] && [ "$OPEN_FILE_LIMIT" -lt 1024 ]; then + ulimit -n 1024 2>/dev/null || true +fi TCL_VERSIONS="8.5 8.6 8.7 9.0" TCLSH="" diff --git a/runtest-cluster b/runtest-cluster index b7e68fb65..d98e55926 100755 --- a/runtest-cluster +++ b/runtest-cluster @@ -1,4 +1,9 @@ #!/bin/sh +# Raise open files limit (macOS default 256 is too low for tests). +OPEN_FILE_LIMIT=$(ulimit -n 2>/dev/null) +if [ -n "$OPEN_FILE_LIMIT" ] && [ "$OPEN_FILE_LIMIT" != "unlimited" ] && [ "$OPEN_FILE_LIMIT" -lt 1024 ]; then + ulimit -n 1024 2>/dev/null || true +fi TCL_VERSIONS="8.5 8.6 8.7 9.0" TCLSH="" diff --git a/runtest-moduleapi b/runtest-moduleapi index 368d6eca5..2e4109390 100755 --- a/runtest-moduleapi +++ b/runtest-moduleapi @@ -1,4 +1,9 @@ #!/bin/sh +# Raise open files limit (macOS default 256 is too low for tests). +OPEN_FILE_LIMIT=$(ulimit -n 2>/dev/null) +if [ -n "$OPEN_FILE_LIMIT" ] && [ "$OPEN_FILE_LIMIT" != "unlimited" ] && [ "$OPEN_FILE_LIMIT" -lt 1024 ]; then + ulimit -n 1024 2>/dev/null || true +fi TCL_VERSIONS="8.5 8.6 8.7 9.0" TCLSH="" [ -z "$MAKE" ] && MAKE=make diff --git a/runtest-sentinel b/runtest-sentinel index 82ffce24e..ca8c5d2d7 100755 --- a/runtest-sentinel +++ b/runtest-sentinel @@ -1,4 +1,9 @@ #!/bin/sh +# Raise open files limit (macOS default 256 is too low for tests). +OPEN_FILE_LIMIT=$(ulimit -n 2>/dev/null) +if [ -n "$OPEN_FILE_LIMIT" ] && [ "$OPEN_FILE_LIMIT" != "unlimited" ] && [ "$OPEN_FILE_LIMIT" -lt 1024 ]; then + ulimit -n 1024 2>/dev/null || true +fi TCL_VERSIONS="8.5 8.6 8.7 9.0" TCLSH="" diff --git a/src/cluster.c b/src/cluster.c index 637b5dd9a..0ba4b8bb9 100644 --- a/src/cluster.c +++ b/src/cluster.c @@ -2107,17 +2107,26 @@ int clusterCanAccessKeysInSlot(int slot) { return 0; } -/* Return the slot ranges that belong to the current node or its master. */ +/* Return the slot ranges that belong to the current node or its master. + * In non-cluster mode, returns the full slot range (0-16383). */ slotRangeArray *clusterGetLocalSlotRanges(void) { - slotRangeArray *slots = NULL; - if (!server.cluster_enabled) { - slots = slotRangeArrayCreate(1); + slotRangeArray *slots = slotRangeArrayCreate(1); slotRangeArraySet(slots, 0, 0, CLUSTER_SLOTS - 1); return slots; } - clusterNode *master = clusterNodeGetMaster(getMyClusterNode()); + return clusterGetNodeSlotRanges(getMyClusterNode()); +} + +/* Returns the slot ranges owned by the given node. + * If the node is a replica, the master's slot ranges are returned. + * Returns an empty array if the node has no slots. */ +slotRangeArray *clusterGetNodeSlotRanges(clusterNode *node) { + slotRangeArray *slots = NULL; + + serverAssert(server.cluster_enabled && node != NULL); + clusterNode *master = clusterNodeGetMaster(node); if (master) { for (int i = 0; i < CLUSTER_SLOTS; i++) { if (clusterNodeCoversSlot(master, i)) diff --git a/src/cluster.h b/src/cluster.h index b594a7dbd..a124f18cc 100644 --- a/src/cluster.h +++ b/src/cluster.h @@ -154,6 +154,7 @@ int getSlotOrReply(client *c, robj *o); int clusterIsMySlot(int slot); int clusterCanAccessKeysInSlot(int slot); struct slotRangeArray *clusterGetLocalSlotRanges(void); +struct slotRangeArray *clusterGetNodeSlotRanges(clusterNode *node); /* functions with shared implementations */ clusterNode *getNodeByQuery(client *c, struct redisCommand *cmd, robj **argv, int argc, int *hashslot, diff --git a/src/commands.def b/src/commands.def index 9b5692aa3..3bafaee3d 100644 --- a/src/commands.def +++ b/src/commands.def @@ -11859,13 +11859,6 @@ struct COMMAND_ARG INCREX_increment_Subargs[] = { {MAKE_ARG("integer",ARG_TYPE_INTEGER,-1,"BYINT",NULL,NULL,CMD_ARG_NONE,0,NULL)}, }; -/* INCREX overflow_block argument table */ -struct COMMAND_ARG INCREX_overflow_block_Subargs[] = { -{MAKE_ARG("fail",ARG_TYPE_PURE_TOKEN,-1,"FAIL",NULL,NULL,CMD_ARG_NONE,0,NULL)}, -{MAKE_ARG("sat",ARG_TYPE_PURE_TOKEN,-1,"SAT",NULL,NULL,CMD_ARG_NONE,0,NULL)}, -{MAKE_ARG("reject",ARG_TYPE_PURE_TOKEN,-1,"REJECT",NULL,NULL,CMD_ARG_NONE,0,NULL)}, -}; - /* INCREX expiration argument table */ struct COMMAND_ARG INCREX_expiration_Subargs[] = { {MAKE_ARG("seconds",ARG_TYPE_INTEGER,-1,"EX",NULL,NULL,CMD_ARG_NONE,0,NULL)}, @@ -11879,7 +11872,7 @@ struct COMMAND_ARG INCREX_expiration_Subargs[] = { struct COMMAND_ARG INCREX_Args[] = { {MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, {MAKE_ARG("increment",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,2,NULL),.subargs=INCREX_increment_Subargs}, -{MAKE_ARG("overflow-block",ARG_TYPE_ONEOF,-1,"OVERFLOW","Out-of-bounds policy; defaults to FAIL. Missing LBOUND/UBOUND default to the type limits (LLONG_MIN/LLONG_MAX for BYINT, -LDBL_MAX/LDBL_MAX for BYFLOAT).",NULL,CMD_ARG_OPTIONAL,3,NULL),.subargs=INCREX_overflow_block_Subargs}, +{MAKE_ARG("saturate",ARG_TYPE_PURE_TOKEN,-1,"SATURATE","Saturate the result to LBOUND/UBOUND (or the type limits when no explicit bound is given) when out of bounds. Without this option, out-of-bounds operations are rejected and reply [current_value, 0].",NULL,CMD_ARG_OPTIONAL,0,NULL)}, {MAKE_ARG("lowerbound",ARG_TYPE_STRING,-1,"LBOUND","Integer when used with BYINT, floating-point when used with BYFLOAT.",NULL,CMD_ARG_OPTIONAL,0,NULL)}, {MAKE_ARG("upperbound",ARG_TYPE_STRING,-1,"UBOUND","Integer when used with BYINT, floating-point when used with BYFLOAT.",NULL,CMD_ARG_OPTIONAL,0,NULL)}, {MAKE_ARG("expiration",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,5,NULL),.subargs=INCREX_expiration_Subargs}, diff --git a/src/commands/increx.json b/src/commands/increx.json index 964822b49..5bf2232b9 100644 --- a/src/commands/increx.json +++ b/src/commands/increx.json @@ -74,28 +74,11 @@ ] }, { - "name": "overflow-block", - "token": "OVERFLOW", - "type": "oneof", + "name": "saturate", + "token": "SATURATE", + "type": "pure-token", "optional": true, - "summary": "Out-of-bounds policy; defaults to FAIL. Missing LBOUND/UBOUND default to the type limits (LLONG_MIN/LLONG_MAX for BYINT, -LDBL_MAX/LDBL_MAX for BYFLOAT).", - "arguments": [ - { - "name": "fail", - "type": "pure-token", - "token": "FAIL" - }, - { - "name": "sat", - "type": "pure-token", - "token": "SAT" - }, - { - "name": "reject", - "type": "pure-token", - "token": "REJECT" - } - ] + "summary": "Saturate the result to LBOUND/UBOUND (or the type limits when no explicit bound is given) when out of bounds. Without this option, out-of-bounds operations are rejected and reply [current_value, 0]." }, { "name": "lowerbound", diff --git a/src/fast_float_strtod.c b/src/fast_float_strtod.c index 8039c5a9b..f1e3fba47 100644 --- a/src/fast_float_strtod.c +++ b/src/fast_float_strtod.c @@ -780,6 +780,11 @@ static int ff_eq(double a, double b) { return a == b; } +static int is_parse_failed(const char *s, size_t len, const char *eptr, int err, double d) { + return ((size_t)(eptr - s) != len) || err == EINVAL || + (err == ERANGE && (d == HUGE_VAL || d == -HUGE_VAL || fpclassify(d) == FP_ZERO)); +} + static void run_ff_tests(ff_testcase *cases, int n, int expect_failed) { for (int i = 0; i < n; i++) { const char *s = cases[i].input; @@ -788,8 +793,7 @@ static void run_ff_tests(ff_testcase *cases, int n, int expect_failed) { errno = 0; double d = fast_float_strtod(s, len, &eptr); - int failed = ((size_t)(eptr - s) != len) || errno == EINVAL || - (errno == ERANGE && (d == HUGE_VAL || d == -HUGE_VAL || fpclassify(d) == FP_ZERO)); + int failed = is_parse_failed(s, len, eptr, errno, d); int ok = (expect_failed == failed) && ff_eq(d, cases[i].expected); char descr[128]; if (ok) @@ -802,6 +806,28 @@ static void run_ff_tests(ff_testcase *cases, int n, int expect_failed) { } } +static void run_ff_libc_compat_tests(const char **cases, int n) { + for (int i = 0; i < n; i++) { + const char *s = cases[i]; + size_t len = strlen(s); + char *eptr, *libc_eptr; + + errno = 0; + double d = fast_float_strtod(s, len, &eptr); + int err = errno; + + errno = 0; + double libc_d = strtod(s, &libc_eptr); + int libc_err = errno; + + int failed = is_parse_failed(s, len, eptr, err, d); + int libc_failed = is_parse_failed(s, len, libc_eptr, libc_err, libc_d); + char descr[128]; + snprintf(descr, sizeof(descr), "ff matches libc strtod: \"%s\"", s); + test_cond(descr, failed == libc_failed && (eptr - s) == (libc_eptr - s) && ff_eq(d, libc_d)); + } +} + int fastFloatTest(int argc, char **argv, int flags) { UNUSED(argc); UNUSED(argv); @@ -1015,8 +1041,6 @@ int fastFloatTest(int argc, char **argv, int flags) { {"na", 0}, {"nan(", NAN}, /* unclosed paren */ {"nan(abc", NAN}, /* missing closing paren */ - {"nan(ab!c)", NAN}, /* invalid char in paren */ - {"nan(ab c)", NAN}, /* space in paren */ {"nanx", NAN}, /* trailing garbage */ }; run_ff_tests(nan_invalid, COUNTOF(nan_invalid), 1); @@ -1043,6 +1067,14 @@ int fastFloatTest(int argc, char **argv, int flags) { eptr == big && ff_eq(d, 0.0)); } + /* The accepted character set for nan(n-char-sequence) is libc-dependent. + * Preserve strtod-compatible behavior instead of asserting a fixed result. */ + const char *nan_libc_compat[] = { + "nan(ab!c)", + "nan(ab c)", + }; + run_ff_libc_compat_tests(nan_libc_compat, COUNTOF(nan_libc_compat)); + return 0; } #endif diff --git a/src/iothread.c b/src/iothread.c index 73919cce1..3ee3e674b 100644 --- a/src/iothread.c +++ b/src/iothread.c @@ -11,7 +11,7 @@ #include "server.h" /* IO threads. */ -static IOThread IOThreads[IO_THREADS_MAX_NUM]; +IOThread IOThreads[IO_THREADS_MAX_NUM]; /* For main thread */ static list *mainThreadPendingClientsToIOThreads[IO_THREADS_MAX_NUM]; /* Clients to IO threads */ diff --git a/src/module.c b/src/module.c index 9843e6ccc..50a594987 100644 --- a/src/module.c +++ b/src/module.c @@ -9808,6 +9808,28 @@ int RM_GetClusterNodeInfo(RedisModuleCtx *ctx, const char *id, char *ip, char *m return REDISMODULE_OK; } +/* Returns the slot ranges owned by the cluster node identified by `nodeid`. + * + * An optional `ctx` can be provided to enable auto-memory management. + * An empty array is returned if cluster mode is disabled (no cluster nodes + * exist) or if no node matches `nodeid`. + * If the node is a replica, the slot ranges of its master are returned. + * + * The returned array must be freed with RM_ClusterFreeSlotRanges(). */ +RedisModuleSlotRangeArray *RM_GetClusterNodeSlotRanges(RedisModuleCtx *ctx, const char *nodeid) { + slotRangeArray *slots; + + if (!server.cluster_enabled) { + slots = slotRangeArrayCreate(0); + } else { + clusterNode *node = clusterLookupNode(nodeid, CLUSTER_NAMELEN); + slots = node ? clusterGetNodeSlotRanges(node) : slotRangeArrayCreate(0); + } + + if (ctx) autoMemoryAdd(ctx, REDISMODULE_AM_SLOTRANGEARRAY, slots); + return (RedisModuleSlotRangeArray *)slots; +} + /* Set Redis Cluster flags in order to change the normal behavior of * Redis Cluster, especially with the goal of disabling certain functions. * This is useful for modules that use the Cluster API in order to create @@ -15576,6 +15598,7 @@ void moduleRegisterCoreAPI(void) { REGISTER_API(RegisterClusterMessageReceiver); REGISTER_API(SendClusterMessage); REGISTER_API(GetClusterNodeInfo); + REGISTER_API(GetClusterNodeSlotRanges); REGISTER_API(GetClusterNodesList); REGISTER_API(FreeClusterNodesList); REGISTER_API(CreateTimer); diff --git a/src/networking.c b/src/networking.c index 3bcd74e82..2f5384c3b 100644 --- a/src/networking.c +++ b/src/networking.c @@ -2777,7 +2777,7 @@ static inline int _writeToClientSlave(client *c, ssize_t *nwritten) { int writeToClient(client *c, int handler_installed) { if (!(c->io_flags & CLIENT_IO_WRITE_ENABLED)) return C_OK; /* Update the number of writes of io threads on server */ - atomicIncr(server.stat_io_writes_processed[c->running_tid], 1); + atomicIncr(IOThreads[c->running_tid].io_writes_processed, 1); ssize_t nwritten = 0, totwritten = 0; const int is_slave = clientTypeIsSlave(c); @@ -3833,7 +3833,7 @@ void readQueryFromClient(connection *conn) { c->stat_total_read_events++; /* Update the number of reads of io threads on server */ - atomicIncr(server.stat_io_reads_processed[c->running_tid], 1); + atomicIncr(IOThreads[c->running_tid].io_reads_processed, 1); readlen = PROTO_IOBUF_LEN; /* If this is a multi bulk request, and we are processing a bulk reply diff --git a/src/redismodule.h b/src/redismodule.h index f0d9e8aa6..59a592c4a 100644 --- a/src/redismodule.h +++ b/src/redismodule.h @@ -1390,6 +1390,7 @@ REDISMODULE_API int (*RedisModule_BlockedClientDisconnected)(RedisModuleCtx *ctx REDISMODULE_API void (*RedisModule_RegisterClusterMessageReceiver)(RedisModuleCtx *ctx, uint8_t type, RedisModuleClusterMessageReceiver callback) REDISMODULE_ATTR; REDISMODULE_API int (*RedisModule_SendClusterMessage)(RedisModuleCtx *ctx, const char *target_id, uint8_t type, const char *msg, uint32_t len) REDISMODULE_ATTR; REDISMODULE_API int (*RedisModule_GetClusterNodeInfo)(RedisModuleCtx *ctx, const char *id, char *ip, char *master_id, int *port, int *flags) REDISMODULE_ATTR; +REDISMODULE_API RedisModuleSlotRangeArray *(*RedisModule_GetClusterNodeSlotRanges)(RedisModuleCtx *ctx, const char *nodeid) REDISMODULE_ATTR; REDISMODULE_API char ** (*RedisModule_GetClusterNodesList)(RedisModuleCtx *ctx, size_t *numnodes) REDISMODULE_ATTR; REDISMODULE_API void (*RedisModule_FreeClusterNodesList)(char **ids) REDISMODULE_ATTR; REDISMODULE_API RedisModuleTimerID (*RedisModule_CreateTimer)(RedisModuleCtx *ctx, mstime_t period, RedisModuleTimerProc callback, void *data) REDISMODULE_ATTR; @@ -1795,6 +1796,7 @@ static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int REDISMODULE_GET_API(RegisterClusterMessageReceiver); REDISMODULE_GET_API(SendClusterMessage); REDISMODULE_GET_API(GetClusterNodeInfo); + REDISMODULE_GET_API(GetClusterNodeSlotRanges); REDISMODULE_GET_API(GetClusterNodesList); REDISMODULE_GET_API(FreeClusterNodesList); REDISMODULE_GET_API(CreateTimer); diff --git a/src/replication.c b/src/replication.c index 44d81ba51..aaedabd12 100644 --- a/src/replication.c +++ b/src/replication.c @@ -4010,7 +4010,7 @@ static void rdbChannelReplDataBufClear(void) { static int replDataBufReadIntoLastBlock(connection *conn, replDataBuf *buf, void (*error_handler)(connection *conn)) { - atomicIncr(server.stat_io_reads_processed[IOTHREAD_MAIN_THREAD_ID], 1); + atomicIncr(IOThreads[IOTHREAD_MAIN_THREAD_ID].io_reads_processed, 1); replDataBufBlock *block = listNodeValue(listLast(buf->blocks)); serverAssert(block && block->size > block->used); diff --git a/src/server.c b/src/server.c index b1bafa003..df660175e 100644 --- a/src/server.c +++ b/src/server.c @@ -2895,8 +2895,8 @@ void resetServerStats(void) { server.stat_sync_partial_ok = 0; server.stat_sync_partial_err = 0; for (j = 0; j < IO_THREADS_MAX_NUM; j++) { - atomicSet(server.stat_io_reads_processed[j], 0); - atomicSet(server.stat_io_writes_processed[j], 0); + atomicSet(IOThreads[j].io_reads_processed, 0); + atomicSet(IOThreads[j].io_writes_processed, 0); } atomicSet(server.stat_client_qbuf_limit_disconnections, 0); server.stat_client_outbuf_limit_disconnections = 0; @@ -6623,8 +6623,8 @@ sds genRedisInfoString(dict *section_dict, int all_sections, int everything) { info = sdscatprintf(info, "# Threads\r\n"); long long reads, writes; for (j = 0; j < server.io_threads_num; j++) { - atomicGet(server.stat_io_reads_processed[j], reads); - atomicGet(server.stat_io_writes_processed[j], writes); + atomicGet(IOThreads[j].io_reads_processed, reads); + atomicGet(IOThreads[j].io_writes_processed, writes); info = sdscatprintf(info, "io_thread_%d:clients=%d,reads=%lld,writes=%lld\r\n", j, server.io_threads_clients_num[j], reads, writes); stat_total_reads_processed += reads; @@ -6661,10 +6661,10 @@ sds genRedisInfoString(dict *section_dict, int all_sections, int everything) { if (!stat_io_ops_processed_calculated) { long long reads, writes; for (j = 0; j < server.io_threads_num; j++) { - atomicGet(server.stat_io_reads_processed[j], reads); + atomicGet(IOThreads[j].io_reads_processed, reads); stat_total_reads_processed += reads; if (j != 0) stat_io_reads_processed += reads; /* Skip the main thread */ - atomicGet(server.stat_io_writes_processed[j], writes); + atomicGet(IOThreads[j].io_writes_processed, writes); stat_total_writes_processed += writes; if (j != 0) stat_io_writes_processed += writes; /* Skip the main thread */ } diff --git a/src/server.h b/src/server.h index 2a6fa5fcb..9318eec68 100644 --- a/src/server.h +++ b/src/server.h @@ -1677,8 +1677,12 @@ typedef struct __attribute__((aligned(CACHE_LINE_SIZE))) { pthread_mutex_t pending_clients_mutex; /* Mutex for pending write list */ list *pending_clients_to_main_thread; /* Clients that are waiting to be executed by the main thread. */ list *clients; /* IO thread managed clients. */ + redisAtomic long long io_reads_processed; /* Number of read events processed */ + redisAtomic long long io_writes_processed; /* Number of write events processed */ } IOThread; +extern IOThread IOThreads[IO_THREADS_MAX_NUM]; + /* Context for streaming replDataBuf to database */ typedef struct replDataBufToDbCtx { void *privdata; /* Private data of context */ @@ -2157,8 +2161,6 @@ struct redisServer { long long stat_unexpected_error_replies; /* Number of unexpected (aof-loading, replica to master, etc.) error replies */ long long stat_total_error_replies; /* Total number of issued error replies ( command + rejected errors ) */ long long stat_dump_payload_sanitizations; /* Number deep dump payloads integrity validations. */ - redisAtomic long long stat_io_reads_processed[IO_THREADS_MAX_NUM]; /* Number of read events processed by IO / Main threads */ - redisAtomic long long stat_io_writes_processed[IO_THREADS_MAX_NUM]; /* Number of write events processed by IO / Main threads */ redisAtomic long long stat_client_qbuf_limit_disconnections; /* Total number of clients reached query buf length limit */ long long stat_client_outbuf_limit_disconnections; /* Total number of clients reached output buf length limit */ long long stat_cluster_incompatible_ops; /* Number of operations that are incompatible with cluster mode */ diff --git a/src/t_string.c b/src/t_string.c index 4f5019e4e..d09b8ab24 100644 --- a/src/t_string.c +++ b/src/t_string.c @@ -1003,15 +1003,13 @@ void incrbyfloatCommand(client *c) { #define OBJ_INCREX_BYINT (1<<1) /* Set if integer increment is given */ #define OBJ_INCREX_LBOUND (1<<2) /* Set if lower bound of increx result is given */ #define OBJ_INCREX_UBOUND (1<<3) /* Set if upper bound of increx result is given */ -#define OBJ_INCREX_OVERFLOW_FAIL (1<<4) /* Return an error when the result is out of bounds (default) */ -#define OBJ_INCREX_OVERFLOW_SAT (1<<5) /* Saturate the result to LBOUND/UBOUND/type limits instead of failing */ -#define OBJ_INCREX_OVERFLOW_REJECT (1<<6) /* Leave the key unchanged and reply [current_value, 0] when the result is out of bounds */ -#define OBJ_INCREX_ENX (1<<7) /* Set expiration only when the key has no expiry */ -#define OBJ_INCREX_PERSIST (1<<8) /* Set if we need to remove the ttl */ -#define OBJ_INCREX_EX (1<<9) /* Set if time in seconds is given */ -#define OBJ_INCREX_PX (1<<10) /* Set if time in ms is given */ -#define OBJ_INCREX_EXAT (1<<11) /* Set if timestamp in second is given */ -#define OBJ_INCREX_PXAT (1<<12) /* Set if timestamp in ms is given */ +#define OBJ_INCREX_SATURATE (1<<4) /* Saturate the result to LBOUND/UBOUND/type limits when out of bounds. */ +#define OBJ_INCREX_ENX (1<<5) /* Set expiration only when the key has no expiry */ +#define OBJ_INCREX_PERSIST (1<<6) /* Set if we need to remove the ttl */ +#define OBJ_INCREX_EX (1<<7) /* Set if time in seconds is given */ +#define OBJ_INCREX_PX (1<<8) /* Set if time in ms is given */ +#define OBJ_INCREX_EXAT (1<<9) /* Set if timestamp in second is given */ +#define OBJ_INCREX_PXAT (1<<10) /* Set if timestamp in ms is given */ /* INCREX argument structure */ typedef struct { @@ -1076,20 +1074,8 @@ static int parseIncrExArgumentsOrReply(client *c, int start_pos, incrExArgs *arg args->flags |= OBJ_INCREX_UBOUND; upper_bound = next; j++; - } else if (!strcasecmp(opt, "OVERFLOW") && next && - !(args->flags & (OBJ_INCREX_OVERFLOW_FAIL|OBJ_INCREX_OVERFLOW_SAT|OBJ_INCREX_OVERFLOW_REJECT))) - { - if (!strcasecmp(next->ptr, "FAIL")) { - args->flags |= OBJ_INCREX_OVERFLOW_FAIL; - } else if (!strcasecmp(next->ptr, "SAT")) { - args->flags |= OBJ_INCREX_OVERFLOW_SAT; - } else if (!strcasecmp(next->ptr, "REJECT")) { - args->flags |= OBJ_INCREX_OVERFLOW_REJECT; - } else { - addReplyError(c, "OVERFLOW policy must be FAIL, SAT or REJECT"); - return C_ERR; - } - j++; + } else if (!strcasecmp(opt, "SATURATE") && !(args->flags & OBJ_INCREX_SATURATE)) { + args->flags |= OBJ_INCREX_SATURATE; } else if (!strcasecmp(opt, "ENX") && !(args->flags & (OBJ_INCREX_ENX|OBJ_INCREX_PERSIST))) { args->flags |= OBJ_INCREX_ENX; } else if (!strcasecmp(opt, "PERSIST") && !(args->flags & (expire_flags|OBJ_INCREX_ENX))) { @@ -1167,7 +1153,7 @@ static int parseIncrExArgumentsOrReply(client *c, int start_pos, incrExArgs *arg /* * INCREX [BYFLOAT increment | BYINT increment] [LBOUND lowerbound] - * [UBOUND upperbound] [OVERFLOW ] + * [UBOUND upperbound] [SATURATE] * [EX seconds | PX milliseconds | EXAT seconds-timestamp | PXAT milliseconds-timestamp | PERSIST] [ENX] * * Increments the numeric value of a key and optionally updates its expiration time. @@ -1181,15 +1167,13 @@ static int parseIncrExArgumentsOrReply(client *c, int start_pos, incrExArgs *arg * Range options: * LBOUND and UBOUND optionally restrict the result to a range. The behavior * when the result would land outside that range (or, with no explicit bound, - * would overflow the type limits) is controlled by OVERFLOW: - * - OVERFLOW FAIL (default): the operation is rejected with an error, - * matching the semantics of INCRBY/INCRBYFLOAT. - * - OVERFLOW SAT: the result is silently capped at UBOUND / floored at LBOUND - * (or saturated to the type limits when no explicit bound is - * given) instead of producing an error. - * - OVERFLOW REJECT: the operation is silently skipped (the key value and TTL - * are left unchanged) and the reply is the current value with - * an applied increment of 0, instead of producing an error. + * would overflow the type limits) is controlled by SATURATE: + * - Default: the operation is rejected (the key value and TTL are left + * unchanged) and the reply is the current value with an applied + * increment of 0. + * - SATURATE: the result is capped at UBOUND / floored at LBOUND (or + * saturated to the type limits when no explicit bound is given) + * instead of being rejected. * * Expiration options: * At most one of the following may be specified: @@ -1203,7 +1187,6 @@ static int parseIncrExArgumentsOrReply(client *c, int start_pos, incrExArgs *arg * ENX restricts expiration updates to keys that currently have no TTL. * * Reply: - * - (Simple Error) if any parameter is invalid, or if BYFLOAT produces NaN or Infinity. * - (Array) of two Bulk Strings on success: * 1. The new value of the key after the increment. * 2. The actual increment applied. @@ -1225,9 +1208,9 @@ void increxCommand(client *c) { if (checkType(c, o, OBJ_STRING)) return; int byfloat = args.flags & OBJ_INCREX_BYFLOAT; - /* FAIL is the default when no OVERFLOW policy is specified. */ - int fail_mode = !(args.flags & (OBJ_INCREX_OVERFLOW_SAT | OBJ_INCREX_OVERFLOW_REJECT)); - int reject_mode = args.flags & OBJ_INCREX_OVERFLOW_REJECT; + /* By default the operation is rejected on out-of-bounds: + * leave the key unchanged and reply [current_value, 0]. */ + int sat_mode = args.flags & OBJ_INCREX_SATURATE; if (byfloat) { long double lb = args.lb_ld, ub = args.ub_ld; if (getLongDoubleFromObjectOrReply(c, o, &value_ld, NULL) != C_OK) @@ -1244,24 +1227,17 @@ void increxCommand(client *c) { value_ld += args.incr_ld; int overflow = isinf(value_ld); if (overflow || value_ld > ub || value_ld < lb) { - /* FAIL: return an error. */ - if (fail_mode) { - addReplyError(c, overflow ? "increment would produce Infinity" : - "value is out of bounds"); - return; - } - /* Result is infinite or out of [LBOUND, UBOUND]: - * FAIL: error; SAT: clamp to +/-LDBL_MAX or the breached bound; - * REJECT: leave key untouched, reply [current_value, 0]. */ - if (reject_mode) { + * default: reject (leave key untouched, reply [current_value, 0]); + * SATURATE: clamp to +/-LDBL_MAX or the breached bound. */ + if (!sat_mode) { addReplyArrayLen(c, 2); addReplyHumanLongDouble(c, oldvalue_ld); addReplyHumanLongDouble(c, 0); return; } - /* SAT: clamp the result. */ + /* SATURATE: clamp the result. */ if (overflow) value_ld = (args.incr_ld >= 0) ? ub : lb; else @@ -1271,7 +1247,7 @@ void increxCommand(client *c) { long double delta = value_ld - oldvalue_ld; if (isinf(delta)) { /* The applied delta cannot be represented as a valid long double. This can - * only happen under OVERFLOW SAT when the saturated result and the + * only happen under SATURATE when the saturated result and the * prior value sit at opposite ends of the type range. */ addReplyError(c, "applied increment would be Infinity"); return; @@ -1288,24 +1264,17 @@ void increxCommand(client *c) { oldvalue_ll = value_ll; int overflow = add_overflow_ll(oldvalue_ll, args.incr_ll, &value_ll); if (overflow || value_ll > ub || value_ll < lb) { - /* FAIL: return an error. */ - if (fail_mode) { - addReplyError(c, overflow ? "increment or decrement would overflow" : - "value is out of bounds"); - return; - } - /* Result overflows long long or is out of [LBOUND, UBOUND]: - * FAIL: error; SAT: clamp to LLONG_MAX/LLONG_MIN or the breached bound; - * REJECT: leave key untouched, reply [current_value, 0]. */ - if (reject_mode) { + * default: reject (leave key untouched, reply [current_value, 0]); + * SATURATE: clamp to LLONG_MAX/LLONG_MIN or the breached bound. */ + if (!sat_mode) { addReplyArrayLen(c, 2); addReplyLongLong(c, oldvalue_ll); addReplyLongLong(c, 0); return; } - /* SAT: clamp the result. */ + /* SATURATE: clamp the result. */ if (overflow) value_ll = (args.incr_ll >= 0) ? ub : lb; else @@ -1315,7 +1284,7 @@ void increxCommand(client *c) { long long delta = 0; if (sub_overflow_ll(value_ll, oldvalue_ll, &delta)) { /* The applied delta cannot be represented as a long long. This can - * only happen under OVERFLOW SAT when the saturated result and the + * only happen under SATURATE when the saturated result and the * prior value sit at opposite ends of the type range. */ addReplyError(c, "applied increment would overflow"); return; diff --git a/tests/integration/redis-cli.tcl b/tests/integration/redis-cli.tcl index 98477468e..2ab7d764f 100644 --- a/tests/integration/redis-cli.tcl +++ b/tests/integration/redis-cli.tcl @@ -42,7 +42,7 @@ start_server {tags {"cli"}} { # We may have a short read, try to read some more. set empty_reads 0 - while {$empty_reads < 5} { + while {$empty_reads < 100} { set buf [read $fd] if {[string length $buf] == 0} { after 10 diff --git a/tests/integration/replication.tcl b/tests/integration/replication.tcl index 05ed71ee0..0611a970e 100644 --- a/tests/integration/replication.tcl +++ b/tests/integration/replication.tcl @@ -886,27 +886,44 @@ proc compute_cpu_usage {start end} { return [ list $pucpu $pscpu ] } - +if {!$::valgrind} { # test diskless rdb pipe with multiple replicas, which may drop half way -start_server {tags {"repl external:skip tsan:skip"} overrides {save ""}} { - set master [srv 0 client] - $master config set repl-diskless-sync yes - $master config set repl-diskless-sync-delay 5 - $master config set repl-diskless-sync-max-replicas 2 - set master_host [srv 0 host] - set master_port [srv 0 port] - set master_pid [srv 0 pid] - # put enough data in the db that the rdb file will be bigger than the socket buffers - # and since we'll have key-load-delay of 100, 20000 keys will take at least 2 seconds - # we also need the replica to process requests during transfer (which it does only once in 2mb) - $master debug populate 20000 test 10000 - $master config set rdbcompression no - $master config set repl-rdb-channel no - # If running on Linux, we also measure utime/stime to detect possible I/O handling issues - set os [catch {exec uname}] - set measure_time [expr {$os == "Linux"} ? 1 : 0] - foreach all_drop {no slow fast all timeout} { +foreach all_drop {no slow fast all timeout} { + start_server {tags {"repl external:skip tsan:skip"} overrides {save ""}} { + set master [srv 0 client] + $master config set repl-diskless-sync yes + $master config set repl-diskless-sync-delay 5 + $master config set repl-diskless-sync-max-replicas 2 + set master_host [srv 0 host] + set master_port [srv 0 port] + set master_pid [srv 0 pid] + if {$all_drop == "timeout"} { + # Use a larger RDB (~100 MB) so it cannot fit into the kernel TCP + # send buffer (autotuning can absorb tens of MB on some hosts). We + # need the primary to hit the blocked writer path + # (repl_last_partial_write != 0) while the slow replica is paused, + # so the cron triggers the "(full sync)" timeout path instead of + # the replica being moved to ONLINE prematurely and timing out via + # the "(streaming sync)" path. + $master debug populate 10000 test 10000 + } else { + # Put enough data in the db that the RDB is comfortably larger than the + # pipe and socket buffers so the primary can hit the blocked writer path, + # but keep it small enough that slow TLS CI runners don't spend minutes + # draining an oversized transfer (~40 MB uncompressed). + $master debug populate 4000 test 10000 + } + $master config set rdbcompression no + $master config set repl-rdb-channel no + # If running on Linux, we also measure utime/stime to detect possible I/O handling issues + set os [catch {exec uname}] + set measure_time [expr {$os == "Linux"} ? 1 : 0] + test "diskless $all_drop replicas drop during rdb pipe" { + # Reset config that the timeout subcase may change, so a failing + # subcase does not leave the next one with an aggressive timeout. + $master config set repl-timeout 60 + $master config set rdb-key-save-delay 0 set replicas {} set replicas_alive {} # start one replica that will read the rdb fast, and one that will be slow @@ -923,7 +940,24 @@ start_server {tags {"repl external:skip tsan:skip"} overrides {save ""}} { set loglines [count_log_lines -2] [lindex $replicas 0] config set repl-diskless-load swapdb [lindex $replicas 1] config set repl-diskless-load swapdb - [lindex $replicas 0] config set key-load-delay 100 ;# 20k keys and 100 microseconds sleep means at least 2 seconds + if {$all_drop == "all"} { + # Keep the RDB child generating data long enough for + # both replicas to be killed before the pipe reaches + # EOF, so this subcase still covers the last-replica + # drop path instead of racing with normal completion. + $master config set rdb-key-save-delay 1000 + } + # For non-timeout subcases, use key-load-delay to keep + # replica 0 as a steady slow reader for the entire RDB + # transfer. This keeps the expected diskless pipe code + # paths covered without accepting alternate log outcomes. + if {$all_drop != "timeout"} { + # 4k keys with 500 microseconds each keeps replica 0 + # slow for about 2 seconds, which is long enough to + # fill the pipe without turning the transfer into a + # multi-minute TLS run. + [lindex $replicas 0] config set key-load-delay 500 + } [lindex $replicas 0] replicaof $master_host $master_port [lindex $replicas 1] replicaof $master_host $master_port @@ -937,9 +971,16 @@ start_server {tags {"repl external:skip tsan:skip"} overrides {save ""}} { set start_time [clock seconds] } - # wait a while so that the pipe socket writer will be - # blocked on write (since replica 0 is slow to read from the socket) - after 500 + if {$all_drop != "timeout"} { + # key-load-delay is already throttling the slow + # replica; just wait for the pipe to fill. + after 500 + } else { + # For the timeout subcase, stop the slow reader so it + # reaches repl-timeout during full sync. + pause_process [srv -1 pid] + after 500 + } # add some command to be present in the command stream after the rdb. $master incr $all_drop @@ -954,14 +995,17 @@ start_server {tags {"repl external:skip tsan:skip"} overrides {save ""}} { set replicas_alive [lreplace $replicas_alive 0 0] } if {$all_drop == "timeout"} { + # Let one replica hit repl-timeout while the slow reader + # is paused, then restore a generous timeout so the + # remaining replica can finish the streamed RDB. $master config set repl-timeout 2 - # we want the slow replica to hang on a key for very long so it'll reach repl-timeout - pause_process [srv -1 pid] - after 2000 + wait_for_log_messages -2 {"*Disconnecting timedout replica (full sync)*"} $loglines 200 100 + $master config set repl-timeout 60 } - # wait for rdb child to exit - wait_for_condition 500 100 { + # Use a single generous budget for all subcases; successful + # runs still exit early once the child is done. + wait_for_condition 5000 100 { [s -2 rdb_bgsave_in_progress] == 0 } else { fail "rdb child didn't terminate" @@ -978,7 +1022,6 @@ start_server {tags {"repl external:skip tsan:skip"} overrides {save ""}} { wait_for_log_messages -2 {"*Diskless rdb transfer, done reading from pipe, 1 replicas still up*"} $loglines 1 1 } if {$all_drop == "timeout"} { - wait_for_log_messages -2 {"*Disconnecting timedout replica (full sync)*"} $loglines 1 1 wait_for_log_messages -2 {"*Diskless rdb transfer, done reading from pipe, 1 replicas still up*"} $loglines 1 1 # master disconnected the slow replica, remove from array set replicas_alive [lreplace $replicas_alive 0 0] @@ -1002,18 +1045,23 @@ start_server {tags {"repl external:skip tsan:skip"} overrides {save ""}} { assert {$master_utime < 70} assert {$master_stime < 70} } - if {!$::no_latency && ($all_drop == "none" || $all_drop == "fast")} { + if {!$::no_latency && ($all_drop == "no" || $all_drop == "fast")} { assert {$master_utime < 15} assert {$master_stime < 15} } } + # In the "no" case both replicas stay alive through the + # full streamed RDB, so on slow TLS runners the final + # ONLINE transition can lag behind child exit. + set replica_online_wait_tries [expr {$all_drop == "no" ? 600 : 150}] + # verify the data integrity foreach replica $replicas_alive { # Wait that replicas acknowledge they are online so # we are sure that DBSIZE and DEBUG DIGEST will not # fail because of timing issues. - wait_for_condition 150 100 { + wait_for_condition $replica_online_wait_tries 100 { [lindex [$replica role] 3] eq {connected} } else { fail "replicas still not connected after some time" @@ -1038,6 +1086,7 @@ start_server {tags {"repl external:skip tsan:skip"} overrides {save ""}} { } } } +} ;# end of valgrind test "diskless replication child being killed is collected" { # when diskless master is waiting for the replica to become writable diff --git a/tests/modules/atomicslotmigration.c b/tests/modules/atomicslotmigration.c index 83393cd9c..26860bc90 100644 --- a/tests/modules/atomicslotmigration.c +++ b/tests/modules/atomicslotmigration.c @@ -90,6 +90,36 @@ int testClusterGetLocalSlotRanges(RedisModuleCtx *ctx, RedisModuleString **argv, return REDISMODULE_OK; } +/* Test command for RedisModule_GetClusterNodeSlotRanges */ +int testGetClusterNodeSlotRanges(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + if (argc != 2) { + return RedisModule_WrongArity(ctx); + } + + const char *nodeid = RedisModule_StringPtrLen(argv[1], NULL); + + static int use_auto_memory = 0; + use_auto_memory = !use_auto_memory; + + RedisModuleSlotRangeArray *slots; + if (use_auto_memory) { + RedisModule_AutoMemory(ctx); + slots = RedisModule_GetClusterNodeSlotRanges(ctx, nodeid); + } else { + slots = RedisModule_GetClusterNodeSlotRanges(NULL, nodeid); + } + + RedisModule_ReplyWithArray(ctx, slots->num_ranges); + for (int i = 0; i < slots->num_ranges; i++) { + RedisModule_ReplyWithArray(ctx, 2); + RedisModule_ReplyWithLongLong(ctx, slots->ranges[i].start); + RedisModule_ReplyWithLongLong(ctx, slots->ranges[i].end); + } + if (!use_auto_memory) + RedisModule_ClusterFreeSlotRanges(NULL, slots); + return REDISMODULE_OK; +} + /* Helper function to check if a slot range array contains a given slot. */ int slotRangeArrayContains(RedisModuleSlotRangeArray *sra, unsigned int slot) { for (int i = 0; i < sra->num_ranges; i++) @@ -562,6 +592,9 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) if (RedisModule_CreateCommand(ctx, "asm.cluster_get_local_slot_ranges", testClusterGetLocalSlotRanges, "", 0, 0, 0) == REDISMODULE_ERR) return REDISMODULE_ERR; + if (RedisModule_CreateCommand(ctx, "asm.get_cluster_node_slot_ranges", testGetClusterNodeSlotRanges, "", 0, 0, 0) == REDISMODULE_ERR) + return REDISMODULE_ERR; + if (RedisModule_CreateCommand(ctx, "asm.get_last_deleted_key", getLastDeletedKey, "", 0, 0, 0) == REDISMODULE_ERR) return REDISMODULE_ERR; diff --git a/tests/unit/cluster/atomic-slot-migration.tcl b/tests/unit/cluster/atomic-slot-migration.tcl index 74eee55f0..b5d2de08a 100644 --- a/tests/unit/cluster/atomic-slot-migration.tcl +++ b/tests/unit/cluster/atomic-slot-migration.tcl @@ -2943,6 +2943,32 @@ start_cluster 3 6 [list tags {external:skip cluster modules} config_lines [list assert_equal [R 1 asm.cluster_get_local_slot_ranges] {} assert_equal [R 4 asm.cluster_get_local_slot_ranges] {} } + + test "Test RM_GetClusterNodeSlotRanges for local node" { + set local_id [R 0 cluster myid] + set ranges [R 0 asm.get_cluster_node_slot_ranges $local_id] + set local_ranges [R 0 asm.cluster_get_local_slot_ranges] + assert_equal $ranges $local_ranges + } + + test "Test RM_GetClusterNodeSlotRanges for remote node" { + set node2_id [R 2 cluster myid] + set ranges [R 0 asm.get_cluster_node_slot_ranges $node2_id] + set remote_ranges [R 2 asm.cluster_get_local_slot_ranges] + assert_equal $ranges $remote_ranges + } + + test "Test RM_GetClusterNodeSlotRanges for non-existent node" { + set ranges [R 0 asm.get_cluster_node_slot_ranges "0000000000000000000000000000000000000000"] + assert_equal $ranges {} + } + + test "Test RM_GetClusterNodeSlotRanges for replica returns master slots" { + set replica3_id [R 3 cluster myid] + set ranges [R 0 asm.get_cluster_node_slot_ranges $replica3_id] + set master_ranges [R 0 asm.cluster_get_local_slot_ranges] + assert_equal $ranges $master_ranges + } } set testmodule [file normalize tests/modules/atomicslotmigration.so] @@ -3056,4 +3082,13 @@ start_server {tags "cluster external:skip"} { assert_equal [r asm.cluster_get_local_slot_ranges] {{0 16383}} } } + +start_server {tags "cluster external:skip"} { + test "Test RM_GetClusterNodeSlotRanges without cluster" { + r module load $testmodule + set local_id "nonexistent-node-id" + set ranges [r asm.get_cluster_node_slot_ranges $local_id] + assert_equal $ranges {} + } +} } diff --git a/tests/unit/type/increx.tcl b/tests/unit/type/increx.tcl index 1797cfbb5..26d2e641c 100644 --- a/tests/unit/type/increx.tcl +++ b/tests/unit/type/increx.tcl @@ -26,13 +26,13 @@ start_server {tags {"increx"}} { test {INCREX - BYINT saturates to UBOUND} { r set mykey 50 - assert_equal [r increx mykey BYINT 100 UBOUND 80 OVERFLOW SAT] {80 30} + assert_equal [r increx mykey BYINT 100 UBOUND 80 SATURATE] {80 30} assert_equal [r get mykey] 80 } test {INCREX - BYINT saturates to LBOUND} { r set mykey 10 - assert_equal [r increx mykey BYINT -100 LBOUND 0 OVERFLOW SAT] {0 -10} + assert_equal [r increx mykey BYINT -100 LBOUND 0 SATURATE] {0 -10} assert_equal [r get mykey] 0 } @@ -41,40 +41,40 @@ start_server {tags {"increx"}} { assert_equal [r increx mykey BYINT 1 LBOUND 0 UBOUND 10] {6 1} } - test {INCREX - BYINT positive overflow with OVERFLOW SAT saturates to LLONG_MAX} { + test {INCREX - BYINT positive overflow with SATURATE saturates to LLONG_MAX} { # LLONG_MAX = 9223372036854775807 r set mykey 9223372036854775800 - assert_equal [r increx mykey BYINT 9223372036854775800 OVERFLOW SAT] {9223372036854775807 7} + assert_equal [r increx mykey BYINT 9223372036854775800 SATURATE] {9223372036854775807 7} assert_equal [r get mykey] 9223372036854775807 } - test {INCREX - BYINT positive overflow with OVERFLOW SAT and UBOUND saturates to UBOUND} { + test {INCREX - BYINT positive overflow with SATURATE and UBOUND saturates to UBOUND} { # LLONG_MAX = 9223372036854775807 r set mykey 9223372036854775800 - assert_equal [r increx mykey BYINT 9223372036854775800 UBOUND 9223372036854775807 OVERFLOW SAT] {9223372036854775807 7} + assert_equal [r increx mykey BYINT 9223372036854775800 UBOUND 9223372036854775807 SATURATE] {9223372036854775807 7} assert_equal [r get mykey] 9223372036854775807 } - test {INCREX - BYINT negative overflow with OVERFLOW SAT saturates to LLONG_MIN} { + test {INCREX - BYINT negative overflow with SATURATE saturates to LLONG_MIN} { # LLONG_MIN = -9223372036854775808 r set mykey -9223372036854775800 - assert_equal [r increx mykey BYINT -9223372036854775800 OVERFLOW SAT] {-9223372036854775808 -8} + assert_equal [r increx mykey BYINT -9223372036854775800 SATURATE] {-9223372036854775808 -8} assert_equal [r get mykey] -9223372036854775808 } - test {INCREX - BYINT negative overflow with OVERFLOW SAT and LBOUND saturates to LBOUND} { + test {INCREX - BYINT negative overflow with SATURATE and LBOUND saturates to LBOUND} { # LLONG_MIN = -9223372036854775808 r set mykey -9223372036854775800 - assert_equal [r increx mykey BYINT -9223372036854775800 LBOUND -9223372036854775808 OVERFLOW SAT] {-9223372036854775808 -8} + assert_equal [r increx mykey BYINT -9223372036854775800 LBOUND -9223372036854775808 SATURATE] {-9223372036854775808 -8} assert_equal [r get mykey] -9223372036854775808 } - test {INCREX - BYINT SAT rejects when applied delta would overflow long long} { + test {INCREX - BYINT SATURATE rejects when applied delta would overflow long long} { # The saturated result lands at LLONG_MIN while the prior value is positive, # so the reported delta would not fit in a long long. r set mykey 9223372036854775800 assert_error "*applied increment would overflow*" { - r increx mykey BYINT 1 OVERFLOW SAT UBOUND -9223372036854775808 + r increx mykey BYINT 1 SATURATE UBOUND -9223372036854775808 } } @@ -102,9 +102,9 @@ start_server {tags {"increx"}} { test {INCREX - BYFLOAT saturates to UBOUND/LBOUND} { r set mykey 10 - assert_equal [lmap v [r increx mykey BYFLOAT 100 UBOUND 42.5 OVERFLOW SAT] {roundFloat $v}] {42.5 32.5} + assert_equal [lmap v [r increx mykey BYFLOAT 100 UBOUND 42.5 SATURATE] {roundFloat $v}] {42.5 32.5} r set mykey 0 - assert_equal [lmap v [r increx mykey BYFLOAT -100 LBOUND -5.5 OVERFLOW SAT] {roundFloat $v}] {-5.5 -5.5} + assert_equal [lmap v [r increx mykey BYFLOAT -100 LBOUND -5.5 SATURATE] {roundFloat $v}] {-5.5 -5.5} } # On some platforms strtold("+inf") with valgrind returns a non-inf result @@ -127,34 +127,35 @@ start_server {tags {"increx"}} { # --------------------------------------------------------------------- # Non-existent key whose default 0 is already outside [LBOUND, UBOUND] - # and the increment cannot bring it back into range -> refuse to create. + # and the increment cannot bring it back into range -> default policy + # leaves the key absent and replies [0, 0]. # --------------------------------------------------------------------- test {INCREX - BYINT/BYFLOAT on non-existent key refuses to create when result stays below LBOUND} { r del mykey - assert_error "*value is out of bounds*" {r increx mykey BYINT 5 LBOUND 10} + assert_equal [r increx mykey BYINT 5 LBOUND 10] {0 0} assert_equal [r exists mykey] 0 - assert_error "*value is out of bounds*" {r increx mykey BYFLOAT -0.5 UBOUND -1.5} + assert_equal [lmap v [r increx mykey BYFLOAT -0.5 UBOUND -1.5] {roundFloat $v}] {0 0} assert_equal [r exists mykey] 0 } # --------------------------------------------------------------------- # Existing key whose value is already outside [LBOUND, UBOUND] is treated - # the same as an in-range value pushed outside by the increment: OVERFLOW - # FAIL errors out and OVERFLOW SAT saturates the result. + # the same as an in-range value pushed outside by the increment: the + # default policy leaves the key alone and SATURATE saturates. # --------------------------------------------------------------------- test {INCREX - BYFLOAT existing value already outside bounds} { - # Above UBOUND, same-side increment: FAIL errors, SAT saturates to UBOUND. + # Above UBOUND, same-side increment: default leaves value unchanged, SATURATE saturates to UBOUND. r set mykey 50.5 - assert_error "*out of bounds*" {r increx mykey BYFLOAT 5.5 UBOUND 30} + assert_equal [lmap v [r increx mykey BYFLOAT 5.5 UBOUND 30] {roundFloat $v}] {50.5 0} assert_equal [roundFloat [r get mykey]] 50.5 - assert_equal [lmap v [r increx mykey BYFLOAT 5.5 UBOUND 30 OVERFLOW SAT] {roundFloat $v}] {30 -20.5} + assert_equal [lmap v [r increx mykey BYFLOAT 5.5 UBOUND 30 SATURATE] {roundFloat $v}] {30 -20.5} - # Below LBOUND, same-side decrement: SAT saturates to LBOUND. + # Below LBOUND, same-side decrement: SATURATE saturates to LBOUND. r set mykey -50.5 - assert_equal [lmap v [r increx mykey BYFLOAT -5.5 LBOUND -30 OVERFLOW SAT] {roundFloat $v}] {-30 20.5} + assert_equal [lmap v [r increx mykey BYFLOAT -5.5 LBOUND -30 SATURATE] {roundFloat $v}] {-30 20.5} # Increment that brings the out-of-range value back inside is applied normally. r set mykey 50 @@ -162,15 +163,15 @@ start_server {tags {"increx"}} { } test {INCREX - BYINT existing value already outside bounds} { - # Above UBOUND, same-side increment: FAIL errors, SAT saturates to UBOUND. + # Above UBOUND, same-side increment: default leaves value unchanged, SATURATE saturates to UBOUND. r set mykey 50 - assert_error "*out of bounds*" {r increx mykey BYINT 5 UBOUND 30} + assert_equal [r increx mykey BYINT 5 UBOUND 30] {50 0} assert_equal [r get mykey] 50 - assert_equal [r increx mykey BYINT 5 UBOUND 30 OVERFLOW SAT] {30 -20} + assert_equal [r increx mykey BYINT 5 UBOUND 30 SATURATE] {30 -20} - # Below LBOUND, same-side decrement: SAT saturates to LBOUND. + # Below LBOUND, same-side decrement: SATURATE saturates to LBOUND. r set mykey -50 - assert_equal [r increx mykey BYINT -5 LBOUND -30 OVERFLOW SAT] {-30 20} + assert_equal [r increx mykey BYINT -5 LBOUND -30 SATURATE] {-30 20} # Increment that brings the out-of-range value back inside is applied normally. r set mykey 50 @@ -178,37 +179,34 @@ start_server {tags {"increx"}} { } # --------------------------------------------------------------------- - # Out-of-range behavior: OVERFLOW FAIL (the default) errors out (like - # INCRBY); OVERFLOW SAT saturates the result silently. + # Out-of-range behavior: by default the operation is rejected + # (reply is [current_value, 0]); SATURATE saturates the result. # --------------------------------------------------------------------- - test {INCREX - BYINT OVERFLOW FAIL rejects increment exceeding UBOUND; OVERFLOW SAT saturates it} { + test {INCREX - BYINT default rejects increment exceeding UBOUND; SATURATE saturates it} { r set mykey 10 - assert_error "*out of bounds*" {r increx mykey BYINT 10 UBOUND 15} - # Value is unchanged after the error + assert_equal [r increx mykey BYINT 10 UBOUND 15] {10 0} + # Value is unchanged assert_equal [r get mykey] 10 - # OVERFLOW FAIL is the explicit form of the default - assert_error "*out of bounds*" {r increx mykey BYINT 10 UBOUND 15 OVERFLOW FAIL} - assert_equal [r get mykey] 10 - # OVERFLOW SAT saturates the result at UBOUND - assert_equal [r increx mykey BYINT 10 UBOUND 15 OVERFLOW SAT] {15 5} + # SATURATE saturates the result at UBOUND + assert_equal [r increx mykey BYINT 10 UBOUND 15 SATURATE] {15 5} assert_equal [r get mykey] 15 } - test {INCREX - BYINT OVERFLOW FAIL rejects decrement falling below LBOUND; OVERFLOW SAT floors it} { + test {INCREX - BYINT default rejects decrement falling below LBOUND; SATURATE floors it} { r set mykey 10 - assert_error "*out of bounds*" {r increx mykey BYINT -10 LBOUND 5} + assert_equal [r increx mykey BYINT -10 LBOUND 5] {10 0} assert_equal [r get mykey] 10 - # OVERFLOW SAT floors the result at LBOUND - assert_equal [r increx mykey BYINT -10 LBOUND 5 OVERFLOW SAT] {5 -5} + # SATURATE floors the result at LBOUND + assert_equal [r increx mykey BYINT -10 LBOUND 5 SATURATE] {5 -5} assert_equal [r get mykey] 5 } - test {INCREX - BYINT within bounds is unaffected by OVERFLOW policy} { + test {INCREX - BYINT within bounds is unaffected by SATURATE} { r set mykey 10 assert_equal [r increx mykey BYINT 3 UBOUND 20] {13 3} - assert_equal [r increx mykey BYINT -3 LBOUND 0 OVERFLOW SAT] {10 -3} - assert_equal [r increx mykey BYINT 1 UBOUND 20 OVERFLOW FAIL] {11 1} + assert_equal [r increx mykey BYINT -3 LBOUND 0 SATURATE] {10 -3} + assert_equal [r increx mykey BYINT 1 UBOUND 20] {11 1} } test {INCREX - BYINT with both LBOUND and UBOUND} { @@ -216,13 +214,13 @@ start_server {tags {"increx"}} { # Within range -> allowed assert_equal [r increx mykey BYINT 2 LBOUND 0 UBOUND 10] {7 2} # Exceeds UBOUND -> rejected, value unchanged - assert_error "*out of bounds*" {r increx mykey BYINT 10 LBOUND 0 UBOUND 10} + assert_equal [r increx mykey BYINT 10 LBOUND 0 UBOUND 10] {7 0} # Falls below LBOUND -> rejected, value unchanged - assert_error "*out of bounds*" {r increx mykey BYINT -20 LBOUND 0 UBOUND 10} + assert_equal [r increx mykey BYINT -20 LBOUND 0 UBOUND 10] {7 0} assert_equal [r get mykey] 7 - # OVERFLOW SAT saturates at the bounds - assert_equal [r increx mykey BYINT 10 LBOUND 0 UBOUND 10 OVERFLOW SAT] {10 3} - assert_equal [r increx mykey BYINT -20 LBOUND 0 UBOUND 10 OVERFLOW SAT] {0 -10} + # SATURATE saturates at the bounds + assert_equal [r increx mykey BYINT 10 LBOUND 0 UBOUND 10 SATURATE] {10 3} + assert_equal [r increx mykey BYINT -20 LBOUND 0 UBOUND 10 SATURATE] {0 -10} } test {INCREX - BYINT at exact bound value is accepted} { @@ -233,26 +231,26 @@ start_server {tags {"increx"}} { assert_equal [r increx mykey BYINT -10 LBOUND 0] {0 -10} } - test {INCREX - BYFLOAT OVERFLOW FAIL rejects increment exceeding UBOUND; OVERFLOW SAT saturates it} { + test {INCREX - BYFLOAT default rejects increment exceeding UBOUND; SATURATE saturates it} { r set mykey 10.0 - assert_error "ERR value is out of bounds*" {r increx mykey BYFLOAT 10.0 UBOUND 15.5} + assert_equal [lmap v [r increx mykey BYFLOAT 10.0 UBOUND 15.5] {roundFloat $v}] {10 0} assert_equal [roundFloat [r get mykey]] 10 - # OVERFLOW SAT saturates the result at UBOUND - assert_equal [lmap v [r increx mykey BYFLOAT 10.0 UBOUND 15.5 OVERFLOW SAT] {roundFloat $v}] {15.5 5.5} + # SATURATE saturates the result at UBOUND + assert_equal [lmap v [r increx mykey BYFLOAT 10.0 UBOUND 15.5 SATURATE] {roundFloat $v}] {15.5 5.5} } - test {INCREX - BYFLOAT OVERFLOW FAIL rejects decrement falling below LBOUND; OVERFLOW SAT floors it} { + test {INCREX - BYFLOAT default rejects decrement falling below LBOUND; SATURATE floors it} { r set mykey 10.0 - assert_error "ERR value is out of bounds*" {r increx mykey BYFLOAT -10.0 LBOUND 5.5} + assert_equal [lmap v [r increx mykey BYFLOAT -10.0 LBOUND 5.5] {roundFloat $v}] {10 0} assert_equal [roundFloat [r get mykey]] 10 - # OVERFLOW SAT floors the result at LBOUND - assert_equal [lmap v [r increx mykey BYFLOAT -10.0 LBOUND 5.5 OVERFLOW SAT] {roundFloat $v}] {5.5 -4.5} + # SATURATE floors the result at LBOUND + assert_equal [lmap v [r increx mykey BYFLOAT -10.0 LBOUND 5.5 SATURATE] {roundFloat $v}] {5.5 -4.5} } - test {INCREX - BYFLOAT within bounds is unaffected by OVERFLOW policy} { + test {INCREX - BYFLOAT within bounds is unaffected by SATURATE policy} { r set mykey 1.5 assert_equal [lmap v [r increx mykey BYFLOAT 0.25 UBOUND 10.0] {roundFloat $v}] {1.75 0.25} - assert_equal [lmap v [r increx mykey BYFLOAT 0.25 UBOUND 10.0 OVERFLOW SAT] {roundFloat $v}] {2 0.25} + assert_equal [lmap v [r increx mykey BYFLOAT 0.25 UBOUND 10.0 SATURATE] {roundFloat $v}] {2 0.25} } test {INCREX - BYFLOAT with both LBOUND and UBOUND} { @@ -260,9 +258,9 @@ start_server {tags {"increx"}} { # Within range -> allowed assert_equal [lmap v [r increx mykey BYFLOAT 1.5 LBOUND 0 UBOUND 10] {roundFloat $v}] {6.5 1.5} # Exceeds UBOUND -> rejected - assert_error "ERR value is out of bounds*" {r increx mykey BYFLOAT 10 LBOUND 0 UBOUND 10} + assert_equal [lmap v [r increx mykey BYFLOAT 10 LBOUND 0 UBOUND 10] {roundFloat $v}] {6.5 0} # Falls below LBOUND -> rejected - assert_error "ERR value is out of bounds*" {r increx mykey BYFLOAT -20 LBOUND 0 UBOUND 10} + assert_equal [lmap v [r increx mykey BYFLOAT -20 LBOUND 0 UBOUND 10] {roundFloat $v}] {6.5 0} assert_equal [lmap v [r get mykey] {roundFloat $v}] {6.5} } @@ -272,22 +270,22 @@ start_server {tags {"increx"}} { assert_equal [lmap v [r increx mykey BYFLOAT -10.0 LBOUND 0] {roundFloat $v}] {0 -10} } - test {INCREX - BYINT positive overflow: default errors, OVERFLOW SAT saturates} { + test {INCREX - BYINT positive overflow: default rejects, SATURATE saturates} { # LLONG_MAX = 9223372036854775807 r set mykey 9223372036854775800 - assert_error "*increment or decrement would overflow*" {r increx mykey BYINT 9223372036854775800 UBOUND 9223372036854775807} + assert_equal [r increx mykey BYINT 9223372036854775800 UBOUND 9223372036854775807] {9223372036854775800 0} assert_equal [r get mykey] 9223372036854775800 - # OVERFLOW SAT: overflow saturates to LLONG_MAX, then saturates to UBOUND - assert_equal [r increx mykey BYINT 9223372036854775800 UBOUND 9223372036854775807 OVERFLOW SAT] {9223372036854775807 7} + # SATURATE: overflow saturates to LLONG_MAX, then saturates to UBOUND + assert_equal [r increx mykey BYINT 9223372036854775800 UBOUND 9223372036854775807 SATURATE] {9223372036854775807 7} } - test {INCREX - BYINT negative overflow: default errors, OVERFLOW SAT saturates} { + test {INCREX - BYINT negative overflow: default rejects, SATURATE saturates} { # LLONG_MIN = -9223372036854775808 r set mykey -9223372036854775800 - assert_error "*increment or decrement would overflow*" {r increx mykey BYINT -9223372036854775800 LBOUND -9223372036854775808} + assert_equal [r increx mykey BYINT -9223372036854775800 LBOUND -9223372036854775808] {-9223372036854775800 0} assert_equal [r get mykey] -9223372036854775800 - # OVERFLOW SAT: overflow saturates to LLONG_MIN, then saturates to LBOUND - assert_equal [r increx mykey BYINT -9223372036854775800 LBOUND -9223372036854775808 OVERFLOW SAT] {-9223372036854775808 -8} + # SATURATE: overflow saturates to LLONG_MIN, then saturates to LBOUND + assert_equal [r increx mykey BYINT -9223372036854775800 LBOUND -9223372036854775808 SATURATE] {-9223372036854775808 -8} } test {INCREX - BYINT on new key (created from zero) with bound} { @@ -296,7 +294,7 @@ start_server {tags {"increx"}} { assert_equal [r increx mykey BYINT 5 UBOUND 10] {5 5} r del mykey # Increment from 0 exceeds UBOUND -> rejected, key not created - assert_error "*out of bounds*" {r increx mykey BYINT 15 UBOUND 10} + assert_equal [r increx mykey BYINT 15 UBOUND 10] {0 0} assert_equal [r exists mykey] 0 } @@ -306,28 +304,28 @@ start_server {tags {"increx"}} { assert_equal [lmap v [r increx mykey BYFLOAT 5.5 UBOUND 10] {roundFloat $v}] {5.5 5.5} r del mykey # Increment from 0 exceeds UBOUND -> rejected, key not created - assert_error "ERR value is out of bounds*" {r increx mykey BYFLOAT 15.5 UBOUND 10} + assert_equal [lmap v [r increx mykey BYFLOAT 15.5 UBOUND 10] {roundFloat $v}] {0 0} assert_equal [r exists mykey] 0 } - test {INCREX - default with no bound behaves like INCRBY/INCRBYFLOAT} { + test {INCREX - default with no bound saturates to type limits with SATURATE, rejects otherwise} { # In-range increments behave like INCRBY/INCRBYFLOAT. r set mykey 10 assert_equal [r increx mykey BYINT 1] {11 1} assert_equal [lmap v [r increx mykey BYFLOAT 1.0] {roundFloat $v}] {12 1} assert_equal [r increx mykey] {13 1} - # BYINT overflow without an explicit bound -> error (like INCRBY). + # BYINT overflow without an explicit bound -> default rejects (reply [current, 0]). r set mykey 9223372036854775800 - assert_error "*increment or decrement would overflow*" {r increx mykey BYINT 9223372036854775800} + assert_equal [r increx mykey BYINT 9223372036854775800] {9223372036854775800 0} assert_equal [r get mykey] 9223372036854775800 } - test {INCREX - error aborts before side effects: neither value nor TTL is modified} { + test {INCREX - reject aborts before side effects: neither value nor TTL is modified} { r del mykey r set mykey 10 # An out-of-range result aborts the command before any side effect. - assert_error "*out of bounds*" {r increx mykey BYINT 100 UBOUND 15 EX 100} + assert_equal [r increx mykey BYINT 100 UBOUND 15 EX 100] {10 0} assert_equal [r get mykey] 10 assert_equal [r ttl mykey] -1 @@ -339,32 +337,11 @@ start_server {tags {"increx"}} { r del mykey r set mykey 10 - # OVERFLOW SAT also updates the TTL when saturation kicks in. - assert_equal [r increx mykey BYINT 100 UBOUND 15 OVERFLOW SAT EX 200] {15 5} + # SATURATE also updates the TTL when saturation kicks in. + assert_equal [r increx mykey BYINT 100 UBOUND 15 SATURATE EX 200] {15 5} assert_morethan [r ttl mykey] 0 } - # --------------------------------------------------------------------- - # OVERFLOW REJECT: leave the key (and TTL) unchanged and reply - # [current_value, 0] when the result would be out of bounds, instead of - # producing an error. - # --------------------------------------------------------------------- - - test {INCREX - BYINT REJECT on overflow leaves value unchanged, in-range applies normally} { - # llong overflow path - r set mykey 9223372036854775800 - assert_equal [r increx mykey BYINT 9223372036854775800 OVERFLOW REJECT] {9223372036854775800 0} - assert_equal [r get mykey] 9223372036854775800 - # UBOUND / LBOUND paths - r set mykey 10 - assert_equal [r increx mykey BYINT 100 UBOUND 15 OVERFLOW REJECT] {10 0} - assert_equal [r increx mykey BYINT -100 LBOUND 5 OVERFLOW REJECT] {10 0} - assert_equal [r get mykey] 10 - # In-range increment is applied normally - assert_equal [r increx mykey BYINT 3 UBOUND 20 OVERFLOW REJECT] {13 3} - assert_equal [r get mykey] 13 - } - # --------------------------------------------------------------------- # Argument parsing / syntax validation # --------------------------------------------------------------------- @@ -401,10 +378,8 @@ start_server {tags {"increx"}} { assert_error "*syntax error*" {r increx mykey BYFLOAT 1.0 BYFLOAT 2.0} assert_error "*syntax error*" {r increx mykey LBOUND 0 LBOUND 1} assert_error "*syntax error*" {r increx mykey UBOUND 9 UBOUND 8} - assert_error "*syntax error*" {r increx mykey OVERFLOW FAIL OVERFLOW SAT LBOUND 0} - assert_error "*syntax error*" {r increx mykey OVERFLOW SAT OVERFLOW SAT LBOUND 0} - assert_error "*syntax error*" {r increx mykey OVERFLOW REJECT OVERFLOW SAT LBOUND 0} - assert_error "*syntax error*" {r increx mykey OVERFLOW REJECT OVERFLOW REJECT LBOUND 0} + assert_error "*syntax error*" {r increx mykey SATURATE SATURATE LBOUND 0} + assert_error "*syntax error*" {r increx mykey SAT LBOUND 0} assert_error "*syntax error*" {r increx mykey ENX ENX EX 10} assert_error "*syntax error*" {r increx mykey PERSIST PERSIST} assert_error "*syntax error*" {r increx mykey EX 10 EX 20} @@ -585,7 +560,7 @@ start_server {tags {"increx"}} { # LBOUND/UBOUND interleaved with increment r set mykey 5 - assert_equal [r increx mykey LBOUND 0 BYINT 100 UBOUND 10 OVERFLOW SAT] {10 5} + assert_equal [r increx mykey LBOUND 0 BYINT 100 UBOUND 10 SATURATE] {10 5} } # --------------------------------------------------------------------- @@ -713,11 +688,11 @@ start_server {tags {"increx"}} { r flushall set repl [attach_to_replication_stream] r set mykey 50 - # With UBOUND + OVERFLOW SAT the final value is saturated; the SET + # With UBOUND + SATURATE the final value is saturated; the SET # rewrite must carry the saturated value (80), not the unbounded 150. - r increx mykey BYINT 100 UBOUND 80 OVERFLOW SAT + r increx mykey BYINT 100 UBOUND 80 SATURATE r set myfloat 10 - r increx myfloat BYFLOAT 100 UBOUND 42.5 OVERFLOW SAT + r increx myfloat BYFLOAT 100 UBOUND 42.5 SATURATE assert_replication_stream $repl { {select *} {set mykey 50*}