From 842a28f4a190b815376daa8260e89cd8cd436b9b Mon Sep 17 00:00:00 2001 From: Luca Palmieri <20745048+LukeMathWalker@users.noreply.github.com> Date: Tue, 19 May 2026 09:04:29 +0200 Subject: [PATCH 01/13] Configure RediSearch to avoid regenerating C headers for Rust modules (#15220) Fixes the nightly build breakage. It requires https://github.com/RediSearch/RediSearch/pull/9669 to be merged first. --- modules/redisearch/Makefile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/redisearch/Makefile b/modules/redisearch/Makefile index a56e9fc70..3f84bb98e 100644 --- a/modules/redisearch/Makefile +++ b/modules/redisearch/Makefile @@ -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 - From 31896140d1e940cad43d725e830cff0e49d060e5 Mon Sep 17 00:00:00 2001 From: Vitah Lin Date: Tue, 19 May 2026 18:27:33 +0800 Subject: [PATCH 02/13] Fix diskless replicas drop during rdb pipe test (#15131) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR is based on: valkey-io/valkey#3511 Close https://github.com/redis/redis/issues/14983 ## Summary During diskless replication, if **any single replica** cannot accept a write (TCP send buffer full / `EAGAIN`), the master stops reading the RDB pipe entirely, stalling data delivery to **all** replicas — including fast ones that are ready to receive data. The failure reason is similar to https://github.com/redis/redis/pull/14946, the socket buffer is more easy to fill. ## Root Cause In `rdbPipeReadHandler`, the master reads from the child's RDB pipe and writes to all replica sockets in a loop. When `connWrite` to any replica returns a partial write (socket send buffer full), the handler: 1. Installs a per-replica `rdbPipeWriteHandler` and increments `rdb_pipe_numconns_writing` 2. **Removes the pipe read event** via `aeDeleteFileEvent(server.el, server.rdb_pipe_read, AE_READABLE)`, stopping all pipe reads The pipe read event is only re-enabled when **all** pending write handlers complete (`rdb_pipe_numconns_writing == 0`), meaning the **slowest replica dictates the throughput for all replicas**. ## Observed Behavior With one slow replica (consuming at ~290 KB/s due to `key-load-delay`): - Master bursts ~1.3 MB of RDB data until the slow replica's socket send buffer fills - `rdbPipeReadHandler` disables the pipe read event - **All replicas starve for 4–5 seconds** while the slow replica drains its buffer - Cycle repeats: burst → stall → burst → stall Ultimately, it leads to a very slow synchronization process of the entire master and replica. ### Changes 1. Skip the entire `diskless replicas drop during rdb pipe` test under Valgrind to avoid timing flakiness on slow env. 2. Move `start_server` inside the `foreach all_drop` loop so each subcase gets a fresh master instead of sharing state across subcases. 3. For `no / slow / fast / all` subcases, replica 0 runs with `key-load-delay 500`, which combined with the blocked-writer TCP back-pressure can stall the RDB-saving child indefinitely; shrink the dataset to ~40 MB so the transfer still exercises the blocked-writer path but completes in reasonable time instead of hanging on the TCP deadlock. For the timeout subcase, replica 0 does not run with `key-load-delay 500`, so to avoid the TCP deadlock we still reduce the dataset somewhat, but keep it larger than the other subcases. Otherwise the kernel TCP send buffer can absorb the whole RDB, and we'd miss the repl_last_partial_write != 0 "(full sync)" timeout path and only hit the "(streaming sync)" path instead. 5. For the `all` subcase, set `rdb-key-save-delay 1000` on the master so the RDB child keeps generating data while both replicas are killed, ensuring the last-replica-drop path is exercised rather than racing with normal completion. 6. Move the slow-replica `pause_process()` so it happens only in the timeout subcase, not after killing replicas, so Redis observes the disconnect promptly in non-timeout flows. 7. In the timeout subcase, set `repl-timeout` 2, wait inline for `*Disconnecting timedout replica (full sync)*`, then restore `repl-timeout` 60 so the remaining replica can finish the streamed RDB. --------- Co-authored-by: Sarthak Aggarwal Co-authored-by: debing.sun --- tests/integration/replication.tcl | 111 +++++++++++++++++++++--------- 1 file changed, 80 insertions(+), 31 deletions(-) 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 From 95040d61d59ecd8e85b7c6042aef7031a48579ca Mon Sep 17 00:00:00 2001 From: "debing.sun" Date: Wed, 20 May 2026 18:07:14 +0800 Subject: [PATCH 03/13] Replace INCREX out-of-bounds policy to a single SATURATE option (#15237) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow https://github.com/redis/redis/issues/15045 ## Summary Simplify INCREX's out-of-bounds policy: The original INCREX shipped with three out-of-bounds policies — OVERFLOW FAIL, OVERFLOW SAT, OVERFLOW REJECT — but FAIL and REJECT are functionally redundant: both leave the key untouched when the result is out of bounds. They differ only in how the caller is notified (error reply vs. [current_value, 0] array reply), which forces the user to make a stylistic choice with no real semantic difference. This PR collapses the three policies into one clear behavior: * Default: the operation is rejected; the key value and TTL are left unchanged, and the reply is [current_value, 0]. Callers detect non-application by checking the applied-increment field; no error-handling branch is required. * SATURATE: the result is saturated to UBOUND / LBOUND, or to the type limits (LLONG_MAX/MIN for BYINT, ±LDBL_MAX for BYFLOAT) when no explicit bound is given. New syntax: INCREX [BYFLOAT increment | BYINT increment] [LBOUND lowerbound] [UBOUND upperbound] [SATURATE] [EX seconds | PX milliseconds | EXAT seconds-timestamp | PXAT milliseconds-timestamp | PERSIST] [ENX] --------- Co-authored-by: Ozan Tezcan --- src/commands.def | 9 +- src/commands/increx.json | 25 +---- src/t_string.c | 91 ++++++----------- tests/unit/type/increx.tcl | 193 ++++++++++++++++--------------------- 4 files changed, 119 insertions(+), 199 deletions(-) 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/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/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*} From 457a1a542d1aab9903d1ba91d4ea68ba07aee02b Mon Sep 17 00:00:00 2001 From: Vitah Lin Date: Wed, 20 May 2026 19:01:45 +0800 Subject: [PATCH 04/13] Test fix "too many open files" on macOS (#14853) On macOS, running `make test` often fails with "too many open files" due to the low default limit (usually 256). This PR increases the limit by adding `ulimit -n 4096` so that the tests have enough file descriptors for concurrent connections. --- runtest | 5 +++++ runtest-cluster | 5 +++++ runtest-moduleapi | 5 +++++ runtest-sentinel | 5 +++++ 4 files changed, 20 insertions(+) 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="" From ba1a4b2c8f37beec5a9be040c37c755207f0cc8d Mon Sep 17 00:00:00 2001 From: Omer Shadmi <76992134+oshadmi@users.noreply.github.com> Date: Wed, 20 May 2026 15:45:06 +0300 Subject: [PATCH 05/13] Update RediSearch module to v8.8.0 (#15228) Updates the bundled RediSearch module version used by the Redis 8.8 branch from `v8.7.91` to `v8.8.0`. --- modules/redisearch/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/redisearch/Makefile b/modules/redisearch/Makefile index 3f84bb98e..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 From f9b7fa0a9691fca310075482d5046fbd5f9f0cfb Mon Sep 17 00:00:00 2001 From: Tom Gabsow Date: Mon, 25 May 2026 11:52:41 +0300 Subject: [PATCH 06/13] Update DataType Modules to v8.8.0 (#15238) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Bumps `MODULE_VERSION` for RedisBloom, RedisJSON, and RedisTimeSeries from `v8.7.91` to `v8.8.0`. | Module | From | To | |-----------------|---------|--------| | RedisBloom | v8.7.91 | v8.8.0 | | RedisJSON | v8.7.91 | v8.8.0 | | RedisTimeSeries | v8.7.91 | v8.8.0 | ## Module changes (v8.7.91 → v8.8.0) ### RedisBloom ([v8.7.91...v8.8.0](https://github.com/RedisBloom/RedisBloom/compare/v8.7.91...v8.8.0)) - MOD-15418 — fix load rdb mem leak ([#1007](https://github.com/RedisBloom/RedisBloom/pull/1007)) - Revert "fix redis version" ([#1010](https://github.com/RedisBloom/RedisBloom/pull/1010)) - bump version to v8.8.0 ### RedisJSON ([v8.7.91...v8.8.0](https://github.com/RedisJSON/RedisJSON/compare/v8.7.91...v8.8.0)) - Revert "fix redis version" ([#1597](https://github.com/RedisJSON/RedisJSON/pull/1597)) - bump version v8.8.0 ### RedisTimeSeries ([v8.7.91...v8.8.0](https://github.com/RedisTimeSeries/RedisTimeSeries/compare/v8.7.91...v8.8.0)) - MOD-14439 — Detect cluster topology changes during a multi-shard command and return an appropriate error ([#1930](https://github.com/RedisTimeSeries/RedisTimeSeries/pull/1930)) - Revert Docker and CI redis-ref from 8.8 back to unstable ([#2033](https://github.com/RedisTimeSeries/RedisTimeSeries/pull/2033)) - bump version v8.8.0 ## Test plan - [ ] CI passes for all three modules at the new pinned version - [ ] \`make all\` builds RedisBloom, RedisJSON, RedisTimeSeries cleanly against \`unstable\` - [ ] Module tests run under \`make test\` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) --- modules/redisbloom/Makefile | 2 +- modules/redisjson/Makefile | 2 +- modules/redistimeseries/Makefile | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/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 From 368786ead55b0fc73df6cba57522aa560f81be4e Mon Sep 17 00:00:00 2001 From: Vitah Lin Date: Mon, 25 May 2026 19:20:09 +0800 Subject: [PATCH 07/13] Update GH Actions dependency (Node.js 20 deprecation) (#15216) This is a follow-up to [redis/redis#14938](https://github.com/redis/redis/pull/14938), which upgraded GitHub Actions to newer stable versions for the upcoming Node.js 20 deprecation on GitHub Actions runners. That PR missed two remaining action updates in `daily.yml`: - `cross-platform-actions/action` - `py-actions/py-dependency-install` ### Why replace `py-actions/py-dependency-install` `py-actions/py-dependency-install` is no longer an actively maintained dependency installation action, so keeping it in CI increases maintenance and supply-chain risk over time. The replacement uses GitHub's official `actions/setup-python` action, which is actively maintained and supports built-in `pip` dependency caching. Installing dependencies with `python -m pip install -r ./utils/req-res-validator/requirements.txt` also makes the workflow behavior explicit and easier to debug. --- .github/workflows/daily.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) 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' || '' }} From 1587755a223ad9c5d0cfc8c506617de2593bbe8a Mon Sep 17 00:00:00 2001 From: anotherJJz <470623352@qq.com> Date: Mon, 25 May 2026 22:56:44 +0800 Subject: [PATCH 08/13] Add RM_GetClusterNodeSlotRanges module API (#14953) Add new module API `RM_GetClusterNodeSlotRanges` that allows modules to query slot ranges for any cluster node by its node ID, not just the local node: ```c RedisModuleSlotRangeArray *RM_GetClusterNodeSlotRanges(RedisModuleCtx *ctx, const char *nodeid) ``` --------- Co-authored-by: Ozan Tezcan Co-authored-by: Yuan Wang --- src/cluster.c | 19 ++++++++--- src/cluster.h | 1 + src/module.c | 23 +++++++++++++ src/redismodule.h | 2 ++ tests/modules/atomicslotmigration.c | 33 ++++++++++++++++++ tests/unit/cluster/atomic-slot-migration.tcl | 35 ++++++++++++++++++++ 6 files changed, 108 insertions(+), 5 deletions(-) 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/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/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/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 {} + } +} } From 8757b125b21769332b40c98dbaf900d9a545e6fb Mon Sep 17 00:00:00 2001 From: dariaguy <61630209+dariaguy@users.noreply.github.com> Date: Mon, 25 May 2026 23:18:16 +0300 Subject: [PATCH 09/13] Remove handle-werrors target from modules/Makefile (#15257) --- README.md | 13 ++++++------- modules/Makefile | 19 +++---------------- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 21de64642..7442029ba 100644 --- a/README.md +++ b/README.md @@ -273,7 +273,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 ``` @@ -339,7 +339,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 ``` @@ -393,7 +393,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 ``` @@ -450,7 +450,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 ``` @@ -569,7 +569,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 ``` @@ -686,7 +686,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 ``` @@ -758,7 +758,6 @@ Tested with the following Docker images: export HOMEBREW_PREFIX="$(brew --prefix)" export BUILD_WITH_MODULES=yes export BUILD_TLS=yes - export DISABLE_WERRORS=yes 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" 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 From 13d9553673ca68bbaa50b3c39dc37449149e322d Mon Sep 17 00:00:00 2001 From: Cong Chen <10412450+gentcys@users.noreply.github.com> Date: Tue, 26 May 2026 08:54:39 +0800 Subject: [PATCH 10/13] Add concurrency groups to cancel stale GH workflow runs (#15224) This is based on [valkey-io/valkey#849](https://github.com/valkey-io/valkey/pull/849) Introduce `concurrency` and `group` keywords into workflows execpt from `redis_docs_sync` (which is triggered on release event only) and `daily`. https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency With this, only one workflow can run in a group at any time. --------- Co-authored-by: Yury-Fridlyand --- .github/workflows/ci.yml | 4 ++++ .github/workflows/codecov.yml | 4 ++++ .github/workflows/codeql-analysis.yml | 4 ++++ .github/workflows/coverity.yml | 5 +++++ .github/workflows/external.yml | 4 ++++ .github/workflows/reply-schemas-linter.yml | 4 ++++ .github/workflows/spell-check.yml | 4 ++++ 7 files changed, 29 insertions(+) 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/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 From fa08257fcbbcd289955b88086ad4a724620a5e3a Mon Sep 17 00:00:00 2001 From: Vitah Lin Date: Tue, 26 May 2026 08:57:13 +0800 Subject: [PATCH 11/13] Fix flaky redis-cli reverse search no-result test (#15119) ## Problem The `redis-cli` reverse-search test for the no-result case can be flaky in slower CI environments. `read_cli` may return too early when CLI output is fragmented or delayed. It currently gives up after only 5 consecutive empty reads, with a 10ms delay between reads, which can make the test assert before the expected `(empty array)` output is printed. ## Changes Increase the `read_cli` consecutive empty-read threshold from `5` to `100`. This keeps the existing read behavior unchanged when data is available, but allows the helper to wait longer for delayed/fragmented CLI output before giving up. --------- Co-authored-by: debing.sun --- tests/integration/redis-cli.tcl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From f023cf026bee64c44e2a59545fb55b55d5a0950c Mon Sep 17 00:00:00 2001 From: Vitah Lin Date: Tue, 26 May 2026 09:35:01 +0800 Subject: [PATCH 12/13] Adjust fastfloat tests for libc-dependent nan payload parsing (#15110) ## Problem `strtod()` handles some `nan(n-char-sequence)` inputs differently across libc implementations. For example, `nan(ab!c)` and `nan(ab c)` may be accepted on some platforms but rejected on others. The existing test treated these inputs as fixed invalid cases, which can fail on platforms whose libc accepts them. ## Changes - Move libc-dependent `nan(...)` cases out of the fixed invalid test list. - Add a helper to verify `fast_float_strtod()` matches the platform `strtod()` behavior for these cases, including value, `endptr`, and success/failure status. - Keep the existing parser behavior unchanged. --------- Co-authored-by: debing.sun --- src/fast_float_strtod.c | 40 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) 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 From 138263a1b480fcd2e756be27f369203a46481d06 Mon Sep 17 00:00:00 2001 From: "h.o.t. neglected" Date: Mon, 25 May 2026 23:55:38 -0400 Subject: [PATCH 13/13] Fix cache line false sharing on per-IO-thread stat counters (#14907) `stat_io_reads_processed[]` and `stat_io_writes_processed[]` were per-IO-thread arrays inside `struct redisServer` that suffer from false sharing. This PR moves the two stat counters into the IOThread struct, which is already `__attribute__((aligned(CACHE_LINE_SIZE)))`. Each IO thread's counters now sit on a separate cache line, eliminating the cross-thread contention. - Added io_reads_processed and io_writes_processed fields to IOThread struct - Removed stat_io_reads_processed[] and stat_io_writes_processed[] from struct redisServer - Made IOThreads[] non-static with extern declaration in server.h --- src/iothread.c | 2 +- src/networking.c | 4 ++-- src/replication.c | 2 +- src/server.c | 12 ++++++------ src/server.h | 6 ++++-- 5 files changed, 14 insertions(+), 12 deletions(-) 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/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/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 */