Compare commits

...
27 Commits
Author SHA1 Message Date
YaacovHazan 7e0f533932 Redis 7.4.5
CI / test-ubuntu-latest (push) Has been cancelled
CI / test-sanitizer-address (push) Has been cancelled
CI / build-debian-old (push) Has been cancelled
CI / build-macos-latest (push) Has been cancelled
CI / build-32bit (push) Has been cancelled
CI / build-libc-malloc (push) Has been cancelled
CI / build-centos-jemalloc (push) Has been cancelled
CI / build-old-chain-jemalloc (push) Has been cancelled
External Server Tests / test-external-standalone (push) Has been cancelled
External Server Tests / test-external-cluster (push) Has been cancelled
External Server Tests / test-external-nodebug (push) Has been cancelled
Spellcheck / Spellcheck (push) Has been cancelled
2025-07-06 14:59:57 +03:00
Mincho Paskalev dfa5b12627 Remove string cat usage in tcl tests in order to support tcl8.5 2025-07-06 14:59:57 +03:00
Ozan Tezcan 39ea429d03 Retry accept() even if accepted connection reports an error (CVE-2025-48367)
In case of accept4() returns an error, we should check errno value and decide if we should retry accept4() without waiting next event loop iteration.
2025-07-06 14:59:57 +03:00
debing.sunandoranagra 8658e2aeb9 Fix out of bounds write in hyperloglog commands (CVE-2025-32023)
Co-authored-by: oranagra <oran@redislabs.com>
2025-07-06 14:59:57 +03:00
yzc-yzc 4c6ffd5125 Fix negative offset issue for ZRANGEBY[SCORE|LEX] command (#14043)
Fix #13952

This PR ensures that ZRANGE_SCORE/LEX command with a negative offset
will return empty.
2025-07-06 14:59:57 +03:00
YaacovHazan 08961e16db Redis 7.4.4
CI / test-ubuntu-latest (push) Has been cancelled
CI / test-sanitizer-address (push) Has been cancelled
CI / build-debian-old (push) Has been cancelled
CI / build-macos-latest (push) Has been cancelled
CI / build-32bit (push) Has been cancelled
CI / build-libc-malloc (push) Has been cancelled
CI / build-centos-jemalloc (push) Has been cancelled
CI / build-old-chain-jemalloc (push) Has been cancelled
External Server Tests / test-external-standalone (push) Has been cancelled
External Server Tests / test-external-cluster (push) Has been cancelled
External Server Tests / test-external-nodebug (push) Has been cancelled
Spellcheck / Spellcheck (push) Has been cancelled
2025-05-27 15:38:26 +03:00
YaacovHazan e93df78b8b Check length of AOF file name in redis-check-aof (CVE-2025-27151)
Ensure that the length of the input file name does not exceed PATH_MAX
2025-05-27 15:38:26 +03:00
debing.sunandYuan Wang f9530e77d1 Correctly update kvstore overhead after emptying or releasing dict (#13984)
Close #13973

This PR fixed two bugs.
1)  `overhead_hashtable_lut` isn't updated correctly
    This bug was introduced by https://github.com/redis/redis/pull/12913
We only update `overhead_hashtable_lut` at the beginning and end of
rehashing, but we forgot to update it when a dict is emptied or
released.

This PR introduces a new `bucketChanged` callback to track the change
changes in the bucket size.
Now, `rehashingStarted` and `rehashingCompleted` callbacks are no longer
responsible for bucket changes, but are entirely handled by
`bucketChanged`, this can also avoid that we need to register three
callbacks to track the change of bucket size, now only one is needed.

In most cases, it will be triggered together with `rehashingStarted` or
`rehashingCompleted`,
except when a dict is being emptied or released, in these cases, even if
the dict is not rehashing, we still need to subtract its current size.

On the other hand, `overhead_hashtable_lut` was duplicated with
`bucket_count`, so we remove `overhead_hashtable_lut` and use
`bucket_count` instead.

Note that this bug only happens with cluster mode, because we don't use
KVSTORE_FREE_EMPTY_DICTS without cluster.

2) The size of `dict_size_index` repeatedly counted in terms of memory
usage.
`dict_size_index` is created at startup, so its memory usage has been
counted into `used_memory_startup`.
However, when we want to count the overhead, we repeat the calculation,
which may cause the overhead to exceed the total memory usage.

---------

Co-authored-by: Yuan Wang <yuan.wang@redis.com>
2025-05-27 15:38:26 +03:00
Vitah Lin 9d11a4f862 Fix tls port update not reflected in CLUSTER SLOTS (#13966)
### Problem 

A previous PR (https://github.com/redis/redis/pull/13932) fixed the TCP
port issue in CLUSTER SLOTS, but it seems the handling of the TLS port
was overlooked.

There is this comment in the `addNodeToNodeReply` function in the
`cluster.c` file:
```c
    /* Report TLS ports to TLS client, and report non-TLS port to non-TLS client. */
    addReplyLongLong(c, clusterNodeClientPort(node, shouldReturnTlsInfo()));
    addReplyBulkCBuffer(c, clusterNodeGetName(node), CLUSTER_NAMELEN);
```

### Fixed 

This PR fixes the TLS port issue and adds relevant tests.
2025-05-27 15:38:26 +03:00
nesty92 5f317a632c Fix incorrect lag due to trimming stream via XTRIM or XADD command (#13958)
This PR fix the lag calculation by ensuring that when consumer group's last_id
is behind the first entry, the consumer group's entries read is considered
invalid and recalculated from the start of the stream

Supplement to PR #13473 

Close #13957

Signed-off-by: Ernesto Alejandro Santana Hidalgo <ernesto.alejandrosantana@gmail.com>
2025-05-27 15:38:26 +03:00
Stav-Levi 38e9e8ed38 Fix port update not reflected in CLUSTER SLOTS (#13932)
Close https://github.com/redis/redis/issues/13892 
config set port cmd updates server.port. cluster slot retrieves
information about cluster slots and their associated nodes. the fix
updates this info when config set port cmd is done, so cluster slots cmd
returns the right value.
2025-05-27 15:38:26 +03:00
YaacovHazan 24080114cc Redis 7.4.3 (#13963)
CI / test-ubuntu-latest (push) Has been cancelled
CI / test-sanitizer-address (push) Has been cancelled
CI / build-debian-old (push) Has been cancelled
CI / build-macos-latest (push) Has been cancelled
CI / build-32bit (push) Has been cancelled
CI / build-libc-malloc (push) Has been cancelled
CI / build-centos-jemalloc (push) Has been cancelled
CI / build-old-chain-jemalloc (push) Has been cancelled
External Server Tests / test-external-standalone (push) Has been cancelled
External Server Tests / test-external-cluster (push) Has been cancelled
External Server Tests / test-external-nodebug (push) Has been cancelled
Spellcheck / Spellcheck (push) Has been cancelled
2025-04-23 14:54:51 +03:00
YaacovHazan b9405a7d9d Redis 7.4.3 2025-04-23 10:26:24 +03:00
YaacovHazan a93a319ba2 Limiting output buffer for unauthenticated client (CVE-2025-21605)
For unauthenticated clients the output buffer is limited to prevent
them from abusing it by not reading the replies
2025-04-23 10:18:06 +03:00
YaacovHazan c214e0f1eb Avoid sanitizer warning for stable CI 2025-04-22 14:53:32 +03:00
Vitah Lin c2289ea402 Fix oldTC CI dk.archive.ubuntu.com could not connect (#13961) 2025-04-22 12:16:02 +03:00
Jason 3138dbac1b Ignore shardId updates from replica nodes (#13877)
Close https://github.com/redis/redis/issues/13868

This bug was introduced by https://github.com/redis/redis/pull/13468

## Issue
To maintain compatibility with older versions that do not support
shardid, when a replica passes a shardid, we also update the master’s
shardid accordingly.

However, when both the master and replica support shardid, an issue
arises: in one moment, the master may pass a shardid, causing us to
update both the master and all its replicas to match the master’s
shardid. But if the replica later passes a different shardid, we would
then update the master’s shardid again, leading to continuous changes in
shardid.

## Solution
Regardless of the situation, we always ensure that the replica’s shardid
remains consistent with the master’s shardid.
2025-04-22 10:50:48 +03:00
debing.sunandRain Valentine 3415d7ae63 Fix defrag when type/encoding changes during scan (#13883)
This PR is based on: https://github.com/valkey-io/valkey/pull/1801

[SoftlyRaining](https://github.com/SoftlyRaining) was hunting for defrag
bugs with Jim and found a couple of improvements to make. Jim pointed
out that in several of the callbacks, if the encoding were to change it
simply returns without doing anything to `cursor` to make it reach 0,
meaning that it would continue no-op working on that item without making
any progress. Type and encoding can change while the defrag scan is in
progress if the value is mutated or replaced by something else with the
same key.

---------

Signed-off-by: Rain Valentine <rsg000@gmail.com>
Co-authored-by: Rain Valentine <rsg000@gmail.com>
2025-04-22 10:50:37 +03:00
Benson-liandyoungmore1024 6717df1388 Fix potential infinite loop of RANDOMKEY during client pause (#13863)
The bug mentioned in this
[#13862](https://github.com/redis/redis/issues/13862) has been fixed.

---------

Signed-off-by: li-benson <1260437731@qq.com>
Signed-off-by: youngmore1024 <youngmore1024@outlook.com>
Co-authored-by: youngmore1024 <youngmore1024@outlook.com>
2025-04-22 10:50:21 +03:00
debing.sunandoranagra 949d4efa36 Fix crash during SLAVEOF when clients are blocked on lazyfree (#13853)
After https://github.com/redis/redis/pull/13167, when a client calls
`FLUSHDB` command, we still async empty database, and the client was
blocked until the lazyfree completes.

1) If another client calls `SLAVEOF` command during this time, the
server will unblock all blocked clients, including those blocked by the
lazyfree. However, when unblocking a lazyfree blocked client, we forgot
to call `updateStatsOnUnblock()`, which ultimately triggered the
following assertion.

2) If a client blocked by Lazyfree is unblocked midway, and at this
point the `bio_comp_list` has already received the completion
notification for the bio, we might end up processing a client that has
already been unblocked in `flushallSyncBgDone()`. Therefore, we need to
filter it out.

---------

Co-authored-by: oranagra <oran@redislabs.com>
2025-04-22 10:29:35 +03:00
9b45a148f2 Fix wrong behavior of XREAD + after last entry of stream have been removed (#13632)
Close #13628

This PR changes behavior of special `+` id of XREAD command. Now it uses
`streamLastValidID` to find last entry instead of `last_id` field of
stream object.
This PR adds test for the issue.

**Notes**

Initial idea to update `last_id` while executing XDEL seems to be wrong.
`last_id` is used to strore last generated id and not id of last entry.

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
Co-authored-by: guybe7 <guy.benoish@redislabs.com>
2025-04-22 10:25:22 +03:00
Yuan Wang 5d7b60e117 Fix wrongly updating fsynced_reploff_pending when appendfsync=everysecond (#13793)
```
if (server.aof_fsync == AOF_FSYNC_EVERYSEC &&
    server.aof_last_incr_fsync_offset != server.aof_last_incr_size &&
    server.mstime - server.aof_last_fsync >= 1000 &&
    !(sync_in_progress = aofFsyncInProgress())) {
    goto try_fsync;
```
In https://github.com/redis/redis/pull/12622, when when
appendfsync=everysecond, if redis has written some data to AOF but not
`fsync`, and less than 1 second has passed since the last `fsync `,
redis will won't fsync AOF, but we will update `
fsynced_reploff_pending`, so it cause the `WAITAOF` to return
prematurely.

this bug is introduced in https://github.com/redis/redis/pull/12622,
from 7.4

The bug fix
https://github.com/redis/redis/pull/13793/commits/1bd6688bcae4add51dc829d96776359dfa39b100
is just as follows:
```diff
diff --git a/src/aof.c b/src/aof.c
index 8ccd8d8f8..521b30449 100644
--- a/src/aof.c
+++ b/src/aof.c
@@ -1096,8 +1096,11 @@ void flushAppendOnlyFile(int force) {
              * in which case master_repl_offset will increase but fsynced_reploff_pending won't be updated
              * (because there's no reason, from the AOF POV, to call fsync) and then WAITAOF may wait on
              * the higher offset (which contains data that was only propagated to replicas, and not to AOF) */
-            if (!sync_in_progress && server.aof_fsync != AOF_FSYNC_NO)
+            if (server.aof_last_incr_fsync_offset == server.aof_last_incr_size &&
+                !(sync_in_progress = aofFsyncInProgress()))
+            {
                 atomicSet(server.fsynced_reploff_pending, server.master_repl_offset);
+            }
             return;
```
Additionally, we slightly refactored fsync AOF to make it simpler, as
https://github.com/redis/redis/pull/13793/commits/584f008d1c39b90ae1d4862a313e04e3b426b136
2025-04-22 10:25:15 +03:00
Mingyi Kang 7a9584b15d Bump actions/upload-artifact from 3 to 4 (#13780)
Update `upload-artifact` from v3 to v4 to avoid the failure of `External
Server Tests` (I encountered this error when opening
[#13779](https://github.com/redis/redis/pull/13779)):

> Error: This request has been automatically failed because it uses a
deprecated version of `actions/upload-artifact: v3`. Learn more:
https://github.blog/changelog/2024-04-16-deprecation-notice-v3-of-the-artifact-actions/
2025-04-22 10:25:07 +03:00
debing.sun 98077651bf Update info.tcl test to revert client output limits sooner (#13738)
This PR is based on: https://github.com/valkey-io/valkey/pull/1462

We set the client output buffer limits to 10 bytes, and then execute
info stats which produces more than 10 bytes of output, which can cause
that command to throw an error.

I'm not sure why it wasn't consistently erroring before, might have been
some change related to the ubuntu upgrade though.

failed CI:
https://github.com/redis/redis/actions/runs/12738281720/job/35500381299

------
Co-authored-by: Madelyn Olson
[madelyneolson@gmail.com](mailto:madelyneolson@gmail.com)
2025-04-22 10:25:00 +03:00
Ozan Tezcananddebing.sun b2a8c675a7 Fix memory leak of jemalloc tcache on function flush command (#13661)
Starting from https://github.com/redis/redis/pull/13133, we allocate a
jemalloc thread cache and use it for lua vm.
On certain cases, like `script flush` or `function flush` command, we
free the existing thread cache and create a new one.

Though, for `function flush`, we were not actually destroying the
existing thread cache itself. Each call creates a new thread cache on
jemalloc and we leak the previous thread cache instances. Jemalloc
allows maximum 4096 thread cache instances. If we reach this limit,
Redis prints "Failed creating the lua jemalloc tcache" log and abort.

There are other cases that can cause this memory leak, including
replication scenarios when emptyData() is called.

The implication is that it looks like redis `used_memory` is low, but
`allocator_allocated` and RSS remain high.

Co-authored-by: debing.sun <debing.sun@redis.com>
2025-04-22 10:24:30 +03:00
nafrafanddebing.sun a96216bb9b Fix module loadex command crash due to invalid config (#13653)
Fix to https://github.com/redis/redis/issues/13650

providing an invalid config to a module with datatype crashes when redis
tries to unload the module due to the invalid config

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
2025-04-22 10:16:24 +03:00
Steveanddebing.sun 2619411a4c Avoid cluster.nodes load corruption due to shard-id generation (#13468)
PR #13428 doesn't fully resolve an issue where corruption errors can
still occur on loading of cluster.nodes file - seen on upgrade where
there were no shard_ids (from old Redis), 7.2.5 loading generated new
random ones, and persisted them to the file before gossip/handshake
could propagate the correct ones (or some other nodes unreachable).
This results in a primary/replica having differing shard_id in the
cluster.nodes and then the server cannot startup - reports corruption.

This PR builds on #13428 by simply ignoring the replica's shard_id in
cluster.nodes (if it exists), and uses the replica's primary's shard_id.
Additional handling was necessary to cover the case where the replica
appears before the primary in cluster.nodes, where it will first use a
generated shard_id for the primary, and then correct after it loads the
primary cluster.nodes entry.

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
2025-04-22 10:16:14 +03:00
42 changed files with 562 additions and 127 deletions
+2 -2
View File
@@ -91,8 +91,8 @@ jobs:
run: |
apt-get update
apt-get install -y gnupg2
echo "deb http://dk.archive.ubuntu.com/ubuntu/ xenial main" >> /etc/apt/sources.list
echo "deb http://dk.archive.ubuntu.com/ubuntu/ xenial universe" >> /etc/apt/sources.list
echo "deb http://archive.ubuntu.com/ubuntu/ xenial main" >> /etc/apt/sources.list
echo "deb http://archive.ubuntu.com/ubuntu/ xenial universe" >> /etc/apt/sources.list
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 40976EAF437D05B5
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32
apt-get update
+6 -6
View File
@@ -1073,8 +1073,8 @@ jobs:
run: |
apt-get update
apt-get install -y gnupg2
echo "deb http://dk.archive.ubuntu.com/ubuntu/ xenial main" >> /etc/apt/sources.list
echo "deb http://dk.archive.ubuntu.com/ubuntu/ xenial universe" >> /etc/apt/sources.list
echo "deb http://archive.ubuntu.com/ubuntu/ xenial main" >> /etc/apt/sources.list
echo "deb http://archive.ubuntu.com/ubuntu/ xenial universe" >> /etc/apt/sources.list
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 40976EAF437D05B5
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32
apt-get update
@@ -1121,8 +1121,8 @@ jobs:
run: |
apt-get update
apt-get install -y gnupg2
echo "deb http://dk.archive.ubuntu.com/ubuntu/ xenial main" >> /etc/apt/sources.list
echo "deb http://dk.archive.ubuntu.com/ubuntu/ xenial universe" >> /etc/apt/sources.list
echo "deb http://archive.ubuntu.com/ubuntu/ xenial main" >> /etc/apt/sources.list
echo "deb http://archive.ubuntu.com/ubuntu/ xenial universe" >> /etc/apt/sources.list
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 40976EAF437D05B5
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32
apt-get update
@@ -1175,8 +1175,8 @@ jobs:
run: |
apt-get update
apt-get install -y gnupg2
echo "deb http://dk.archive.ubuntu.com/ubuntu/ xenial main" >> /etc/apt/sources.list
echo "deb http://dk.archive.ubuntu.com/ubuntu/ xenial universe" >> /etc/apt/sources.list
echo "deb http://archive.ubuntu.com/ubuntu/ xenial main" >> /etc/apt/sources.list
echo "deb http://archive.ubuntu.com/ubuntu/ xenial universe" >> /etc/apt/sources.list
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 40976EAF437D05B5
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32
apt-get update
+3 -3
View File
@@ -27,7 +27,7 @@ jobs:
--tags -slow
- name: Archive redis log
if: ${{ failure() }}
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: test-external-redis-log
path: external-redis.log
@@ -55,7 +55,7 @@ jobs:
--tags -slow
- name: Archive redis log
if: ${{ failure() }}
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: test-external-cluster-log
path: external-redis-cluster.log
@@ -79,7 +79,7 @@ jobs:
--tags "-slow -needs:debug"
- name: Archive redis log
if: ${{ failure() }}
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: test-external-redis-nodebug-log
path: external-redis-nodebug.log
+46
View File
@@ -11,6 +11,52 @@ CRITICAL: There is a critical bug affecting MOST USERS. Upgrade ASAP.
SECURITY: There are security fixes in the release.
--------------------------------------------------------------------------------
================================================================================
Redis 7.4.5 Released Sun 6 Jul 2025 12:00:00 IST
================================================================================
Update urgency: `SECURITY`: There are security fixes in the release.
### Security fixes
* (CVE-2025-32023) Fix out-of-bounds write in `HyperLogLog` commands
* (CVE-2025-48367) Retry accepting other connections even if the accepted connection reports an error
================================================================================
Redis 7.4.4 Released Tue 27 May 2025 12:00:00 IST
================================================================================
Update urgency: `SECURITY`: There are security fixes in the release.
### Security fixes
* (CVE-2025-27151) redis-check-aof may lead to stack overflow and potential RCE
### Bug fixes
- #13966, #13932 `CLUSTER SLOTS` - TLS port update not reflected in CLUSTER SLOTS
- #13958 `XTRIM`, `XADD` - incorrect lag due to trimming stream
================================================================================
Redis 7.4.3 Released Wed 23 Apr 2025 12:00:00 IST
================================================================================
Update urgency: `SECURITY`: There are security fixes in the release.
### Security fixes
- (CVE-2025-21605) An unauthenticated client can cause an unlimited growth of output buffers
### Bug fixes
- #13661 `FUNCTION FLUSH` - memory leak when using jemalloc
- #13793 `WAITAOF` returns prematurely
- #13853 `SLAVEOF` - crash when clients are blocked on lazy free
- #13863 `RANDOMKEY` - infinite loop during client pause
- #13877 ShardID inconsistency when both primary and replica support it
================================================================================
Redis Community Edition 7.4.2 Released Mon 6 Jan 2025 12:30:00 IDT
+3
View File
@@ -110,6 +110,9 @@ else
ifeq ($(SANITIZER),undefined)
MALLOC=libc
CFLAGS+=-fsanitize=undefined -fno-sanitize-recover=all -fno-omit-frame-pointer
ifeq (clang,$(CLANG))
CFLAGS+=-fno-sanitize=function
endif
LDFLAGS+=-fsanitize=undefined
else
ifeq ($(SANITIZER),thread)
+24
View File
@@ -786,3 +786,27 @@ int anetIsFifo(char *filepath) {
if (stat(filepath, &sb) == -1) return 0;
return S_ISFIFO(sb.st_mode);
}
/* This function must be called after accept4() fails. It returns 1 if 'err'
* indicates accepted connection faced an error, and it's okay to continue
* accepting next connection by calling accept4() again. Other errors either
* indicate programming errors, e.g. calling accept() on a closed fd or indicate
* a resource limit has been reached, e.g. -EMFILE, open fd limit has been
* reached. In the latter case, caller might wait until resources are available.
* See accept4() documentation for details. */
int anetAcceptFailureNeedsRetry(int err) {
if (err == ECONNABORTED)
return 1;
#if defined(__linux__)
/* For details, see 'Error Handling' section on
* https://man7.org/linux/man-pages/man2/accept.2.html */
if (err == ENETDOWN || err == EPROTO || err == ENOPROTOOPT ||
err == EHOSTDOWN || err == ENONET || err == EHOSTUNREACH ||
err == EOPNOTSUPP || err == ENETUNREACH)
{
return 1;
}
#endif
return 0;
}
+1
View File
@@ -52,5 +52,6 @@ int anetPipe(int fds[2], int read_flags, int write_flags);
int anetSetSockMarkId(char *err, int fd, uint32_t id);
int anetGetError(int fd);
int anetIsFifo(char *filepath);
int anetAcceptFailureNeedsRetry(int err);
#endif
+25 -26
View File
@@ -1048,35 +1048,34 @@ void flushAppendOnlyFile(int force) {
mstime_t latency;
if (sdslen(server.aof_buf) == 0) {
/* Check if we need to do fsync even the aof buffer is empty,
* because previously in AOF_FSYNC_EVERYSEC mode, fsync is
* called only when aof buffer is not empty, so if users
* stop write commands before fsync called in one second,
* the data in page cache cannot be flushed in time. */
if (server.aof_fsync == AOF_FSYNC_EVERYSEC &&
server.aof_last_incr_fsync_offset != server.aof_last_incr_size &&
server.mstime - server.aof_last_fsync >= 1000 &&
!(sync_in_progress = aofFsyncInProgress())) {
goto try_fsync;
/* Check if we need to do fsync even the aof buffer is empty,
* the reason is described in the previous AOF_FSYNC_EVERYSEC block,
* and AOF_FSYNC_ALWAYS is also checked here to handle a case where
* aof_fsync is changed from everysec to always. */
} else if (server.aof_fsync == AOF_FSYNC_ALWAYS &&
server.aof_last_incr_fsync_offset != server.aof_last_incr_size)
{
goto try_fsync;
} else {
if (server.aof_last_incr_fsync_offset == server.aof_last_incr_size) {
/* All data is fsync'd already: Update fsynced_reploff_pending just in case.
* This is needed to avoid a WAITAOF hang in case a module used RM_Call with the NO_AOF flag,
* in which case master_repl_offset will increase but fsynced_reploff_pending won't be updated
* (because there's no reason, from the AOF POV, to call fsync) and then WAITAOF may wait on
* the higher offset (which contains data that was only propagated to replicas, and not to AOF) */
if (!sync_in_progress && server.aof_fsync != AOF_FSYNC_NO)
* This is needed to avoid a WAITAOF hang in case a module used RM_Call
* with the NO_AOF flag, in which case master_repl_offset will increase but
* fsynced_reploff_pending won't be updated (because there's no reason, from
* the AOF POV, to call fsync) and then WAITAOF may wait on the higher offset
* (which contains data that was only propagated to replicas, and not to AOF) */
if (!aofFsyncInProgress())
atomicSet(server.fsynced_reploff_pending, server.master_repl_offset);
return;
} else {
/* Check if we need to do fsync even the aof buffer is empty,
* because previously in AOF_FSYNC_EVERYSEC mode, fsync is
* called only when aof buffer is not empty, so if users
* stop write commands before fsync called in one second,
* the data in page cache cannot be flushed in time. */
if (server.aof_fsync == AOF_FSYNC_EVERYSEC &&
server.mstime - server.aof_last_fsync >= 1000 &&
!(sync_in_progress = aofFsyncInProgress()))
goto try_fsync;
/* Check if we need to do fsync even the aof buffer is empty,
* the reason is described in the previous AOF_FSYNC_EVERYSEC block,
* and AOF_FSYNC_ALWAYS is also checked here to handle a case where
* aof_fsync is changed from everysec to always. */
if (server.aof_fsync == AOF_FSYNC_ALWAYS)
goto try_fsync;
}
return;
}
if (server.aof_fsync == AOF_FSYNC_EVERYSEC)
+1
View File
@@ -270,6 +270,7 @@ void disconnectAllBlockedClients(void) {
if (c->bstate.btype == BLOCKED_LAZYFREE) {
addReply(c, shared.ok); /* No reason lazy-free to fail */
updateStatsOnUnblock(c, 0, 0, 0);
c->flags &= ~CLIENT_PENDING_COMMAND;
unblockClient(c, 1);
} else {
+37 -30
View File
@@ -93,6 +93,7 @@ int auxTlsPortPresent(clusterNode *n);
static void clusterBuildMessageHdr(clusterMsg *hdr, int type, size_t msglen);
void freeClusterLink(clusterLink *link);
int verifyClusterNodeId(const char *name, int length);
static void updateShardId(clusterNode *node, const char *shard_id);
int getNodeDefaultClientPort(clusterNode *n) {
return server.tls_cluster ? n->tls_port : n->tcp_port;
@@ -203,12 +204,11 @@ int auxShardIdSetter(clusterNode *n, void *value, int length) {
return C_ERR;
}
memcpy(n->shard_id, value, CLUSTER_NAMELEN);
/* if n already has replicas, make sure they all agree
* on the shard id */
/* if n already has replicas, make sure they all use
* the primary shard id */
for (int i = 0; i < n->numslaves; i++) {
if (memcmp(n->slaves[i]->shard_id, n->shard_id, CLUSTER_NAMELEN) != 0) {
return C_ERR;
}
if (memcmp(n->slaves[i]->shard_id, n->shard_id, CLUSTER_NAMELEN) != 0)
updateShardId(n->slaves[i], n->shard_id);
}
clusterAddNodeToShard(value, n);
return C_OK;
@@ -550,18 +550,12 @@ int clusterLoadConfig(char *filename) {
clusterAddNode(master);
}
/* shard_id can be absent if we are loading a nodes.conf generated
* by an older version of Redis; we should follow the primary's
* shard_id in this case */
if (auxFieldHandlers[af_shard_id].isPresent(n) == 0) {
memcpy(n->shard_id, master->shard_id, CLUSTER_NAMELEN);
clusterAddNodeToShard(master->shard_id, n);
} else if (clusterGetNodesInMyShard(master) != NULL &&
memcmp(master->shard_id, n->shard_id, CLUSTER_NAMELEN) != 0)
{
/* If the primary has been added to a shard, make sure this
* node has the same persisted shard id as the primary. */
goto fmterr;
}
* by an older version of Redis;
* ignore replica's shard_id in the file, only use the primary's.
* If replica precedes primary in file, it will be corrected
* later by the auxShardIdSetter */
memcpy(n->shard_id, master->shard_id, CLUSTER_NAMELEN);
clusterAddNodeToShard(master->shard_id, n);
n->slaveof = master;
clusterNodeAddSlave(master,n);
} else if (auxFieldHandlers[af_shard_id].isPresent(n) == 0) {
@@ -908,22 +902,33 @@ static void updateAnnouncedHumanNodename(clusterNode *node, char *new) {
clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG);
}
static void assignShardIdToNode(clusterNode *node, const char *shard_id, int flag) {
clusterRemoveNodeFromShard(node);
memcpy(node->shard_id, shard_id, CLUSTER_NAMELEN);
clusterAddNodeToShard(shard_id, node);
clusterDoBeforeSleep(flag);
}
static void updateShardId(clusterNode *node, const char *shard_id) {
if (shard_id && memcmp(node->shard_id, shard_id, CLUSTER_NAMELEN) != 0) {
clusterRemoveNodeFromShard(node);
memcpy(node->shard_id, shard_id, CLUSTER_NAMELEN);
clusterAddNodeToShard(shard_id, node);
clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG);
}
if (shard_id && myself != node && myself->slaveof == node) {
if (memcmp(myself->shard_id, shard_id, CLUSTER_NAMELEN) != 0) {
/* shard-id can diverge right after a rolling upgrade
* from pre-7.2 releases */
clusterRemoveNodeFromShard(myself);
memcpy(myself->shard_id, shard_id, CLUSTER_NAMELEN);
clusterAddNodeToShard(shard_id, myself);
clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG|CLUSTER_TODO_FSYNC_CONFIG);
/* We always make our best effort to keep the shard-id consistent
* between the master and its replicas:
*
* 1. When updating the master's shard-id, we simultaneously update the
* shard-id of all its replicas to ensure consistency.
* 2. When updating replica's shard-id, if it differs from its master's shard-id,
* we discard this replica's shard-id and continue using master's shard-id.
* This applies even if the master does not support shard-id, in which
* case we rely on the master's randomly generated shard-id. */
if (node->slaveof == NULL) {
assignShardIdToNode(node, shard_id, CLUSTER_TODO_SAVE_CONFIG);
for (int i = 0; i < clusterNodeNumSlaves(node); i++) {
clusterNode *slavenode = clusterNodeGetSlave(node, i);
if (memcmp(slavenode->shard_id, shard_id, CLUSTER_NAMELEN) != 0)
assignShardIdToNode(slavenode, shard_id, CLUSTER_TODO_SAVE_CONFIG|CLUSTER_TODO_FSYNC_CONFIG);
}
} else if (memcmp(node->slaveof->shard_id, shard_id, CLUSTER_NAMELEN) == 0) {
assignShardIdToNode(node, shard_id, CLUSTER_TODO_SAVE_CONFIG);
}
}
}
@@ -1245,6 +1250,8 @@ void clusterAcceptHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
while(max--) {
cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
if (cfd == ANET_ERR) {
if (anetAcceptFailureNeedsRetry(errno))
continue;
if (errno != EWOULDBLOCK)
serverLog(LL_VERBOSE,
"Error accepting cluster node: %s", server.neterr);
+2
View File
@@ -2450,6 +2450,7 @@ static int updatePort(const char **err) {
listener->bindaddr = server.bindaddr;
listener->bindaddr_count = server.bindaddr_count;
listener->port = server.port;
clusterUpdateMyselfAnnouncedPorts();
listener->ct = connectionByType(CONN_TYPE_SOCKET);
if (changeListener(listener) == C_ERR) {
*err = "Unable to listen on this port. Check server logs.";
@@ -2671,6 +2672,7 @@ static int applyTLSPort(const char **err) {
listener->bindaddr_count = server.bindaddr_count;
listener->port = server.tls_port;
listener->ct = connectionByType(CONN_TYPE_TLS);
clusterUpdateMyselfAnnouncedPorts();
if (changeListener(listener) == C_ERR) {
*err = "Unable to listen on this port. Check server logs.";
return 0;
+2 -2
View File
@@ -348,7 +348,7 @@ robj *dbRandomKey(redisDb *db) {
key = dictGetKey(de);
keyobj = createStringObject(key,sdslen(key));
if (dbFindExpires(db, key)) {
if (allvolatile && server.masterhost && --maxtries == 0) {
if (allvolatile && (server.masterhost || isPausedActions(PAUSE_ACTION_EXPIRE)) && --maxtries == 0) {
/* If the DB is composed only of keys with an expire set,
* it could happen that all the keys are already logically
* expired in the slave, so the function cannot stop because
@@ -702,7 +702,7 @@ void flushallSyncBgDone(uint64_t client_id) {
client *c = lookupClientByID(client_id);
/* Verify that client still exists */
if (!c) return;
if (!(c && c->flags & CLIENT_BLOCKED)) return;
/* Update current_client (Called functions might rely on it) */
client *old_client = server.current_client;
+11 -18
View File
@@ -394,8 +394,7 @@ long scanLaterList(robj *ob, unsigned long *cursor, long long endtime) {
quicklistNode *node;
long iterations = 0;
int bookmark_failed = 0;
if (ob->type != OBJ_LIST || ob->encoding != OBJ_ENCODING_QUICKLIST)
return 0;
serverAssert(ob->type == OBJ_LIST && ob->encoding == OBJ_ENCODING_QUICKLIST);
if (*cursor == 0) {
/* if cursor is 0, we start new iteration */
@@ -444,8 +443,7 @@ void scanLaterZsetCallback(void *privdata, const dictEntry *_de) {
}
void scanLaterZset(robj *ob, unsigned long *cursor) {
if (ob->type != OBJ_ZSET || ob->encoding != OBJ_ENCODING_SKIPLIST)
return;
serverAssert(ob->type == OBJ_ZSET && ob->encoding == OBJ_ENCODING_SKIPLIST);
zset *zs = (zset*)ob->ptr;
dict *d = zs->dict;
scanLaterZsetData data = {zs};
@@ -461,8 +459,7 @@ void scanCallbackCountScanned(void *privdata, const dictEntry *de) {
}
void scanLaterSet(robj *ob, unsigned long *cursor) {
if (ob->type != OBJ_SET || ob->encoding != OBJ_ENCODING_HT)
return;
serverAssert(ob->type == OBJ_SET && ob->encoding == OBJ_ENCODING_HT);
dict *d = ob->ptr;
dictDefragFunctions defragfns = {
.defragAlloc = activeDefragAlloc,
@@ -472,8 +469,7 @@ void scanLaterSet(robj *ob, unsigned long *cursor) {
}
void scanLaterHash(robj *ob, unsigned long *cursor) {
if (ob->type != OBJ_HASH || ob->encoding != OBJ_ENCODING_HT)
return;
serverAssert(ob->type == OBJ_HASH && ob->encoding == OBJ_ENCODING_HT);
dict *d = ob->ptr;
dictDefragFunctions defragfns = {
.defragAlloc = activeDefragAlloc,
@@ -568,10 +564,7 @@ int scanLaterStreamListpacks(robj *ob, unsigned long *cursor, long long endtime)
static unsigned char last[sizeof(streamID)];
raxIterator ri;
long iterations = 0;
if (ob->type != OBJ_STREAM || ob->encoding != OBJ_ENCODING_STREAM) {
*cursor = 0;
return 0;
}
serverAssert(ob->type == OBJ_STREAM && ob->encoding == OBJ_ENCODING_STREAM);
stream *s = ob->ptr;
raxStart(&ri,s->rax);
@@ -919,22 +912,22 @@ void defragOtherGlobals(void) {
int defragLaterItem(dictEntry *de, unsigned long *cursor, long long endtime, int dbid) {
if (de) {
robj *ob = dictGetVal(de);
if (ob->type == OBJ_LIST) {
if (ob->type == OBJ_LIST && ob->encoding == OBJ_ENCODING_QUICKLIST) {
return scanLaterList(ob, cursor, endtime);
} else if (ob->type == OBJ_SET) {
} else if (ob->type == OBJ_SET && ob->encoding == OBJ_ENCODING_HT) {
scanLaterSet(ob, cursor);
} else if (ob->type == OBJ_ZSET) {
} else if (ob->type == OBJ_ZSET && ob->encoding == OBJ_ENCODING_SKIPLIST) {
scanLaterZset(ob, cursor);
} else if (ob->type == OBJ_HASH) {
} else if (ob->type == OBJ_HASH && ob->encoding == OBJ_ENCODING_HT) {
scanLaterHash(ob, cursor);
} else if (ob->type == OBJ_STREAM) {
} else if (ob->type == OBJ_STREAM && ob->encoding == OBJ_ENCODING_STREAM) {
return scanLaterStreamListpacks(ob, cursor, endtime);
} else if (ob->type == OBJ_MODULE) {
robj keyobj;
initStaticStringObject(keyobj, dictGetKey(de));
return moduleLateDefrag(&keyobj, ob, cursor, endtime, dbid);
} else {
*cursor = 0; /* object type may have changed since we schedule it for later */
*cursor = 0; /* object type/encoding may have changed since we schedule it for later */
}
} else {
*cursor = 0; /* object may have been deleted already */
+15
View File
@@ -260,12 +260,16 @@ int _dictResize(dict *d, unsigned long size, int* malloc_failed)
d->ht_table[1] = new_ht_table;
d->rehashidx = 0;
if (d->type->rehashingStarted) d->type->rehashingStarted(d);
if (d->type->bucketChanged)
d->type->bucketChanged(d, DICTHT_SIZE(d->ht_size_exp[1]));
/* Is this the first initialization or is the first hash table empty? If so
* it's not really a rehashing, we can just set the first hash table so that
* it can accept keys. */
if (d->ht_table[0] == NULL || d->ht_used[0] == 0) {
if (d->type->rehashingCompleted) d->type->rehashingCompleted(d);
if (d->type->bucketChanged)
d->type->bucketChanged(d, -(long long)DICTHT_SIZE(d->ht_size_exp[0]));
if (d->ht_table[0]) zfree(d->ht_table[0]);
d->ht_size_exp[0] = new_ht_size_exp;
d->ht_used[0] = new_ht_used;
@@ -363,6 +367,8 @@ static int dictCheckRehashingCompleted(dict *d) {
if (d->ht_used[0] != 0) return 0;
if (d->type->rehashingCompleted) d->type->rehashingCompleted(d);
if (d->type->bucketChanged)
d->type->bucketChanged(d, -(long long)DICTHT_SIZE(d->ht_size_exp[0]));
zfree(d->ht_table[0]);
/* Copy the new ht onto the old one */
d->ht_table[0] = d->ht_table[1];
@@ -725,6 +731,10 @@ void dictRelease(dict *d)
if (dictIsRehashing(d) && d->type->rehashingCompleted)
d->type->rehashingCompleted(d);
/* Subtract the size of all buckets. */
if (d->type->bucketChanged)
d->type->bucketChanged(d, -(long long)dictBuckets(d));
if (d->type->onDictRelease)
d->type->onDictRelease(d);
@@ -1619,6 +1629,11 @@ void dictEmpty(dict *d, void(callback)(dict*)) {
* destroying the dict fake completion. */
if (dictIsRehashing(d) && d->type->rehashingCompleted)
d->type->rehashingCompleted(d);
/* Subtract the size of all buckets. */
if (d->type->bucketChanged)
d->type->bucketChanged(d, -(long long)dictBuckets(d));
_dictClear(d,0,callback);
_dictClear(d,1,callback);
d->rehashidx = -1;
+3
View File
@@ -43,6 +43,9 @@ typedef struct dictType {
/* Invoked at the end of dict initialization/rehashing of all the entries from old to new ht. Both ht still exists
* and are cleaned up after this callback. */
void (*rehashingCompleted)(dict *d);
/* Invoked when the size of the dictionary changes.
* The `delta` parameter can be positive (size increase) or negative (size decrease). */
void (*bucketChanged)(dict *d, long long delta);
/* Allow a dict to carry extra caller-defined metadata. The
* extra memory is initialized to 0 when a dict is allocated. */
size_t (*dictMetadataBytes)(dict *d);
+6 -2
View File
@@ -259,13 +259,17 @@ void scriptingInit(int setup) {
void freeLuaScriptsSync(dict *lua_scripts, list *lua_scripts_lru_list, lua_State *lua) {
dictRelease(lua_scripts);
listRelease(lua_scripts_lru_list);
lua_gc(lua, LUA_GCCOLLECT, 0);
lua_close(lua);
#if defined(USE_JEMALLOC)
/* When lua is closed, destroy the previously used private tcache. */
void *ud = (global_State*)G(lua)->ud;
unsigned int lua_tcache = (unsigned int)(uintptr_t)ud;
#endif
lua_gc(lua, LUA_GCCOLLECT, 0);
lua_close(lua);
#if defined(USE_JEMALLOC)
je_mallctl("tcache.destroy", NULL, NULL, (void *)&lua_tcache, sizeof(unsigned int));
#endif
}
+13
View File
@@ -23,6 +23,9 @@
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#if defined(USE_JEMALLOC)
#include <lstate.h>
#endif
#define LUA_ENGINE_NAME "LUA"
#define REGISTRY_ENGINE_CTX_NAME "__ENGINE_CTX__"
@@ -189,9 +192,19 @@ static void luaEngineFreeFunction(void *engine_ctx, void *compiled_function) {
static void luaEngineFreeCtx(void *engine_ctx) {
luaEngineCtx *lua_engine_ctx = engine_ctx;
#if defined(USE_JEMALLOC)
/* When lua is closed, destroy the previously used private tcache. */
void *ud = (global_State*)G(lua_engine_ctx->lua)->ud;
unsigned int lua_tcache = (unsigned int)(uintptr_t)ud;
#endif
lua_gc(lua_engine_ctx->lua, LUA_GCCOLLECT, 0);
lua_close(lua_engine_ctx->lua);
zfree(lua_engine_ctx);
#if defined(USE_JEMALLOC)
je_mallctl("tcache.destroy", NULL, NULL, (void *)&lua_tcache, sizeof(unsigned int));
#endif
}
static void luaRegisterFunctionArgsInitialize(registerFunctionArgs *register_f_args,
+42 -5
View File
@@ -566,6 +566,7 @@ int hllSparseToDense(robj *o) {
struct hllhdr *hdr, *oldhdr = (struct hllhdr*)sparse;
int idx = 0, runlen, regval;
uint8_t *p = (uint8_t*)sparse, *end = p+sdslen(sparse);
int valid = 1;
/* If the representation is already the right one return ASAP. */
hdr = (struct hllhdr*) sparse;
@@ -585,16 +586,27 @@ int hllSparseToDense(robj *o) {
while(p < end) {
if (HLL_SPARSE_IS_ZERO(p)) {
runlen = HLL_SPARSE_ZERO_LEN(p);
if ((runlen + idx) > HLL_REGISTERS) { /* Overflow. */
valid = 0;
break;
}
idx += runlen;
p++;
} else if (HLL_SPARSE_IS_XZERO(p)) {
runlen = HLL_SPARSE_XZERO_LEN(p);
if ((runlen + idx) > HLL_REGISTERS) { /* Overflow. */
valid = 0;
break;
}
idx += runlen;
p += 2;
} else {
runlen = HLL_SPARSE_VAL_LEN(p);
regval = HLL_SPARSE_VAL_VALUE(p);
if ((runlen + idx) > HLL_REGISTERS) break; /* Overflow. */
if ((runlen + idx) > HLL_REGISTERS) { /* Overflow. */
valid = 0;
break;
}
while(runlen--) {
HLL_DENSE_SET_REGISTER(hdr->registers,idx,regval);
idx++;
@@ -605,7 +617,7 @@ int hllSparseToDense(robj *o) {
/* If the sparse representation was valid, we expect to find idx
* set to HLL_REGISTERS. */
if (idx != HLL_REGISTERS) {
if (!valid || idx != HLL_REGISTERS) {
sdsfree(dense);
return C_ERR;
}
@@ -902,27 +914,40 @@ int hllSparseAdd(robj *o, unsigned char *ele, size_t elesize) {
void hllSparseRegHisto(uint8_t *sparse, int sparselen, int *invalid, int* reghisto) {
int idx = 0, runlen, regval;
uint8_t *end = sparse+sparselen, *p = sparse;
int valid = 1;
while(p < end) {
if (HLL_SPARSE_IS_ZERO(p)) {
runlen = HLL_SPARSE_ZERO_LEN(p);
if ((runlen + idx) > HLL_REGISTERS) { /* Overflow. */
valid = 0;
break;
}
idx += runlen;
reghisto[0] += runlen;
p++;
} else if (HLL_SPARSE_IS_XZERO(p)) {
runlen = HLL_SPARSE_XZERO_LEN(p);
if ((runlen + idx) > HLL_REGISTERS) { /* Overflow. */
valid = 0;
break;
}
idx += runlen;
reghisto[0] += runlen;
p += 2;
} else {
runlen = HLL_SPARSE_VAL_LEN(p);
regval = HLL_SPARSE_VAL_VALUE(p);
if ((runlen + idx) > HLL_REGISTERS) { /* Overflow. */
valid = 0;
break;
}
idx += runlen;
reghisto[regval] += runlen;
p++;
}
}
if (idx != HLL_REGISTERS && invalid) *invalid = 1;
if ((!valid || idx != HLL_REGISTERS) && invalid) *invalid = 1;
}
/* ========================= HyperLogLog Count ==============================
@@ -1070,22 +1095,34 @@ int hllMerge(uint8_t *max, robj *hll) {
} else {
uint8_t *p = hll->ptr, *end = p + sdslen(hll->ptr);
long runlen, regval;
int valid = 1;
p += HLL_HDR_SIZE;
i = 0;
while(p < end) {
if (HLL_SPARSE_IS_ZERO(p)) {
runlen = HLL_SPARSE_ZERO_LEN(p);
if ((runlen + i) > HLL_REGISTERS) { /* Overflow. */
valid = 0;
break;
}
i += runlen;
p++;
} else if (HLL_SPARSE_IS_XZERO(p)) {
runlen = HLL_SPARSE_XZERO_LEN(p);
if ((runlen + i) > HLL_REGISTERS) { /* Overflow. */
valid = 0;
break;
}
i += runlen;
p += 2;
} else {
runlen = HLL_SPARSE_VAL_LEN(p);
regval = HLL_SPARSE_VAL_VALUE(p);
if ((runlen + i) > HLL_REGISTERS) break; /* Overflow. */
if ((runlen + i) > HLL_REGISTERS) { /* Overflow. */
valid = 0;
break;
}
while(runlen--) {
if (regval > max[i]) max[i] = regval;
i++;
@@ -1093,7 +1130,7 @@ int hllMerge(uint8_t *max, robj *hll) {
p++;
}
}
if (i != HLL_REGISTERS) return C_ERR;
if (!valid || i != HLL_REGISTERS) return C_ERR;
}
return C_OK;
}
+10 -11
View File
@@ -40,7 +40,6 @@ struct _kvstore {
unsigned long long key_count; /* Total number of keys in this kvstore. */
unsigned long long bucket_count; /* Total number of buckets in this kvstore across dictionaries. */
unsigned long long *dict_size_index; /* Binary indexed tree (BIT) that describes cumulative key frequencies up until given dict-index. */
size_t overhead_hashtable_lut; /* The overhead of all dictionaries. */
size_t overhead_hashtable_rehashing; /* The overhead of dictionaries rehashing. */
};
@@ -190,8 +189,6 @@ static void kvstoreDictRehashingStarted(dict *d) {
unsigned long long from, to;
dictRehashingInfo(d, &from, &to);
kvs->bucket_count += to; /* Started rehashing (Add the new ht size) */
kvs->overhead_hashtable_lut += to;
kvs->overhead_hashtable_rehashing += from;
}
@@ -209,11 +206,17 @@ static void kvstoreDictRehashingCompleted(dict *d) {
unsigned long long from, to;
dictRehashingInfo(d, &from, &to);
kvs->bucket_count -= from; /* Finished rehashing (Remove the old ht size) */
kvs->overhead_hashtable_lut -= from;
kvs->overhead_hashtable_rehashing -= from;
}
/* Updates the bucket count for the given dictionary in a DB. It adds the new ht size
* of the dictionary or removes the old ht size of the dictionary from the total
* sum of buckets for a DB. */
static void kvstoreDictBucketChanged(dict *d, long long delta) {
kvstore *kvs = d->type->userdata;
kvs->bucket_count += delta;
}
/* Returns the size of the DB dict metadata in bytes. */
static size_t kvstoreDictMetadataSize(dict *d) {
UNUSED(d);
@@ -246,6 +249,7 @@ kvstore *kvstoreCreate(dictType *type, int num_dicts_bits, int flags) {
kvs->dtype.dictMetadataBytes = kvstoreDictMetadataSize;
kvs->dtype.rehashingStarted = kvstoreDictRehashingStarted;
kvs->dtype.rehashingCompleted = kvstoreDictRehashingCompleted;
kvs->dtype.bucketChanged = kvstoreDictBucketChanged;
kvs->num_dicts_bits = num_dicts_bits;
kvs->num_dicts = 1 << kvs->num_dicts_bits;
@@ -261,7 +265,6 @@ kvstore *kvstoreCreate(dictType *type, int num_dicts_bits, int flags) {
kvs->resize_cursor = 0;
kvs->dict_size_index = kvs->num_dicts > 1? zcalloc(sizeof(unsigned long long) * (kvs->num_dicts + 1)) : NULL;
kvs->bucket_count = 0;
kvs->overhead_hashtable_lut = 0;
kvs->overhead_hashtable_rehashing = 0;
return kvs;
@@ -287,7 +290,6 @@ void kvstoreEmpty(kvstore *kvs, void(callback)(dict*)) {
kvs->bucket_count = 0;
if (kvs->dict_size_index)
memset(kvs->dict_size_index, 0, sizeof(unsigned long long) * (kvs->num_dicts + 1));
kvs->overhead_hashtable_lut = 0;
kvs->overhead_hashtable_rehashing = 0;
}
@@ -339,9 +341,6 @@ size_t kvstoreMemUsage(kvstore *kvs) {
/* Values are dict* shared with kvs->dicts */
mem += listLength(kvs->rehashing) * sizeof(listNode);
if (kvs->dict_size_index)
mem += sizeof(unsigned long long) * (kvs->num_dicts + 1);
return mem;
}
@@ -661,7 +660,7 @@ uint64_t kvstoreIncrementallyRehash(kvstore *kvs, uint64_t threshold_us) {
}
size_t kvstoreOverheadHashtableLut(kvstore *kvs) {
return kvs->overhead_hashtable_lut * sizeof(dictEntry *);
return kvs->bucket_count * sizeof(dictEntry *);
}
size_t kvstoreOverheadHashtableRehashing(kvstore *kvs) {
+8 -5
View File
@@ -12364,7 +12364,7 @@ int moduleLoad(const char *path, void **module_argv, int module_argc, int is_loa
}
if (post_load_err) {
moduleUnload(ctx.module->name, NULL);
serverAssert(moduleUnload(ctx.module->name, NULL, 1) == C_OK);
moduleFreeContext(&ctx);
return C_ERR;
}
@@ -12380,14 +12380,17 @@ int moduleLoad(const char *path, void **module_argv, int module_argc, int is_loa
/* Unload the module registered with the specified name. On success
* C_OK is returned, otherwise C_ERR is returned and errmsg is set
* with an appropriate message. */
int moduleUnload(sds name, const char **errmsg) {
* with an appropriate message.
* Only forcefully unload this module, passing forced_unload != 0,
* if it is certain that it has not yet been in use (e.g., immediate
* unload on failed load). */
int moduleUnload(sds name, const char **errmsg, int forced_unload) {
struct RedisModule *module = dictFetchValue(modules,name);
if (module == NULL) {
*errmsg = "no such module with that name";
return C_ERR;
} else if (listLength(module->types)) {
} else if (listLength(module->types) && !forced_unload) {
*errmsg = "the module exports one or more module-side data "
"types, can't unload";
return C_ERR;
@@ -13170,7 +13173,7 @@ NULL
} else if (!strcasecmp(subcmd,"unload") && c->argc == 3) {
const char *errmsg = NULL;
if (moduleUnload(c->argv[2]->ptr, &errmsg) == C_OK)
if (moduleUnload(c->argv[2]->ptr, &errmsg, 0) == C_OK)
addReply(c,shared.ok);
else {
if (errmsg == NULL) errmsg = "operation not possible.";
+5
View File
@@ -3926,6 +3926,11 @@ int checkClientOutputBufferLimits(client *c) {
int soft = 0, hard = 0, class;
unsigned long used_mem = getClientOutputBufferMemoryUsage(c);
/* For unauthenticated clients the output buffer is limited to prevent
* them from abusing it by not reading the replies */
if (used_mem > 1024 && authRequired(c))
return 1;
class = getClientType(c);
/* For the purpose of output buffer limiting, masters are handled
* like normal clients. */
+6
View File
@@ -556,6 +556,12 @@ int redis_check_aof_main(int argc, char **argv) {
goto invalid_args;
}
/* Check if filepath is longer than PATH_MAX */
if (strlen(filepath) > PATH_MAX) {
printf("Error: filepath is too long (exceeds PATH_MAX)\n");
goto invalid_args;
}
/* In the glibc implementation dirname may modify their argument. */
memcpy(temp_filepath, filepath, strlen(filepath) + 1);
dirpath = dirname(temp_filepath);
+2 -2
View File
@@ -64,7 +64,7 @@ lua_State *createLuaState(void) {
size_t sz = sizeof(unsigned int);
int err = je_mallctl("tcache.create", (void *)&tcache, &sz, NULL, 0);
if (err) {
serverLog(LL_WARNING, "Failed creating the lua jemalloc tcache.");
serverLog(LL_WARNING, "Failed creating the lua jemalloc tcache (err=%d).", err);
exit(1);
}
@@ -79,7 +79,7 @@ void luaEnvInit(void) {
size_t sz = sizeof(unsigned int);
int err = je_mallctl("arenas.create", (void *)&arena, &sz, NULL, 0);
if (err) {
serverLog(LL_WARNING, "Failed creating the lua jemalloc arena.");
serverLog(LL_WARNING, "Failed creating the lua jemalloc arena (err=%d).", err);
exit(1);
}
server.lua_arena = arena;
+1 -1
View File
@@ -2510,7 +2510,7 @@ void moduleInitModulesSystem(void);
void moduleInitModulesSystemLast(void);
void modulesCron(void);
int moduleLoad(const char *path, void **argv, int argc, int is_loadex);
int moduleUnload(sds name, const char **errmsg);
int moduleUnload(sds name, const char **errmsg, int forced_unload);
void moduleLoadFromQueue(void);
int moduleGetCommandKeysViaAPI(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result);
int moduleGetCommandChannelsViaAPI(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result);
+2
View File
@@ -298,6 +298,8 @@ static void connSocketAcceptHandler(aeEventLoop *el, int fd, void *privdata, int
while(max--) {
cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
if (cfd == ANET_ERR) {
if (anetAcceptFailureNeedsRetry(errno))
continue;
if (errno != EWOULDBLOCK)
serverLog(LL_WARNING,
"Accepting client connection: %s", server.neterr);
+10 -8
View File
@@ -1694,7 +1694,10 @@ size_t streamReplyWithRange(client *c, stream *s, streamID *start, streamID *end
while(streamIteratorGetID(&si,&id,&numfields)) {
/* Update the group last_id if needed. */
if (group && streamCompareID(&id,&group->last_id) > 0) {
if (group->entries_read != SCG_INVALID_ENTRIES_READ && !streamRangeHasTombstones(s,&group->last_id,NULL)) {
if (group->entries_read != SCG_INVALID_ENTRIES_READ &&
streamCompareID(&group->last_id, &s->first_id) >= 0 &&
!streamRangeHasTombstones(s,&group->last_id,NULL))
{
/* A valid counter and no tombstones between the group's last-delivered-id
* and the stream's last-generated-id mean we can increment the read counter
* to keep tracking the group's progress. */
@@ -2299,14 +2302,13 @@ void xreadCommand(client *c) {
"just return an empty result set.");
goto cleanup;
}
if (o) {
if (o && ((stream *)o->ptr)->length) {
stream *s = o->ptr;
ids[id_idx] = s->last_id;
if (streamDecrID(&ids[id_idx]) != C_OK) {
/* shouldn't happen */
addReplyError(c,"the stream last element ID is 0-0");
goto cleanup;
}
/* We need to get the last valid ID.
* It is impossible to use s->last_id because
* entry with s->last_id may have been removed. */
streamLastValidID(s, &ids[id_idx]);
streamDecrID(&ids[id_idx]);
} else {
ids[id_idx].ms = 0;
ids[id_idx].seq = 0;
+7 -1
View File
@@ -3277,7 +3277,7 @@ void genericZrangebyscoreCommand(zrange_result_handler *handler,
handler->beginResultEmission(handler, -1);
/* For invalid offset, return directly. */
if (offset > 0 && offset >= (long)zsetLength(zobj)) {
if (offset < 0 || (offset > 0 && offset >= (long)zsetLength(zobj))) {
handler->finalizeResultEmission(handler, 0);
return;
}
@@ -3551,6 +3551,12 @@ void genericZrangebylexCommand(zrange_result_handler *handler,
handler->beginResultEmission(handler, -1);
/* For invalid offset, return directly. */
if (offset < 0 || (offset > 0 && offset >= (long)zsetLength(zobj))) {
handler->finalizeResultEmission(handler, 0);
return;
}
if (zobj->encoding == OBJ_ENCODING_LISTPACK) {
unsigned char *zl = zobj->ptr;
unsigned char *eptr, *sptr;
+2
View File
@@ -755,6 +755,8 @@ static void tlsAcceptHandler(aeEventLoop *el, int fd, void *privdata, int mask)
while(max--) {
cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
if (cfd == ANET_ERR) {
if (anetAcceptFailureNeedsRetry(errno))
continue;
if (errno != EWOULDBLOCK)
serverLog(LL_WARNING,
"Accepting client connection: %s", server.neterr);
+2
View File
@@ -101,6 +101,8 @@ static void connUnixAcceptHandler(aeEventLoop *el, int fd, void *privdata, int m
while(max--) {
cfd = anetUnixAccept(server.neterr, fd);
if (cfd == ANET_ERR) {
if (anetAcceptFailureNeedsRetry(errno))
continue;
if (errno != EWOULDBLOCK)
serverLog(LL_WARNING,
"Accepting client connection: %s", server.neterr);
+2 -2
View File
@@ -1,2 +1,2 @@
#define REDIS_VERSION "7.4.2"
#define REDIS_VERSION_NUM 0x00070402
#define REDIS_VERSION "7.4.5"
#define REDIS_VERSION_NUM 0x00070405
+9
View File
@@ -312,3 +312,12 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
return REDISMODULE_OK;
}
int RedisModule_OnUnload(RedisModuleCtx *ctx) {
REDISMODULE_NOT_USED(ctx);
if (datatype) {
RedisModule_Free(datatype);
datatype = NULL;
}
return REDISMODULE_OK;
}
+18
View File
@@ -45,6 +45,24 @@ start_server {tags {"auth external:skip"} overrides {requirepass foobar}} {
assert_match {*unauthenticated bulk length*} $e
$rr close
}
test {For unauthenticated clients output buffer is limited} {
set rr [redis [srv "host"] [srv "port"] 1 $::tls]
$rr SET x 5
catch {[$rr read]} e
assert_match {*NOAUTH Authentication required*} $e
# Fill the output buffer in a loop without reading it and make
# sure the client disconnected.
# Considering the socket eat some of the replies, we are testing
# that such client can't consume more than few MB's.
catch {
for {set j 0} {$j < 1000000} {incr j} {
$rr SET x 5
}
} e
assert_match {I/O error reading reply} $e
}
}
start_server {tags {"auth_binary_password external:skip"}} {
@@ -47,4 +47,29 @@ start_cluster 2 2 {tags {external:skip cluster}} {
R 0 config set cluster-announce-bus-port 0
assert_match "*@$base_bus_port *" [R 0 CLUSTER NODES]
}
test "CONFIG SET port updates cluster-announced port" {
set count [expr [llength $::servers] + 1]
# Get the original port and change to new_port
if {$::tls} {
set orig_port [lindex [R 0 config get tls-port] 1]
} else {
set orig_port [lindex [R 0 config get port] 1]
}
assert {$orig_port != ""}
set new_port [find_available_port $orig_port $count]
if {$::tls} {
R 0 config set tls-port $new_port
} else {
R 0 config set port $new_port
}
# Verify that the new port appears in the output of cluster slots
wait_for_condition 50 100 {
[string match "*$new_port*" [R 0 cluster slots]]
} else {
fail "Cluster announced port was not updated in cluster slots"
}
}
}
+55
View File
@@ -137,6 +137,61 @@ start_server {tags {"hll"}} {
set e
} {*WRONGTYPE*}
test {Corrupted sparse HyperLogLogs doesn't cause overflow and out-of-bounds with XZERO opcode} {
r del hll
# Create a sparse-encoded HyperLogLog header
set header "HYLL"
set payload [binary format c12 {1 0 0 0 0 0 0 0 0 0 0 0}]
set pl [binary format a4a12 $header $payload]
# Create an XZERO opcode with the maximum run length of 16384(2^14)
set runlen [expr 16384 - 1]
set chunk [binary format cc [expr {0b01000000 | ($runlen >> 8)}] [expr {$runlen & 0xff}]]
# Fill the HLL with more than 131072(2^17) XZERO opcodes to make the total
# run length exceed 4GB, will cause an integer overflow.
set repeat [expr 131072 + 1000]
for {set i 0} {$i < $repeat} {incr i} {
append pl $chunk
}
# Create a VAL opcode with a value that will cause out-of-bounds.
append pl [binary format c 0b11111111]
r set hll $pl
# This should not overflow and out-of-bounds.
assert_error {*INVALIDOBJ*} {r pfcount hll hll}
assert_error {*INVALIDOBJ*} {r pfdebug getreg hll}
r ping
}
test {Corrupted sparse HyperLogLogs doesn't cause overflow and out-of-bounds with ZERO opcode} {
r del hll
# Create a sparse-encoded HyperLogLog header
set header "HYLL"
set payload [binary format c12 {1 0 0 0 0 0 0 0 0 0 0 0}]
set pl [binary format a4a12 $header $payload]
# # Create an ZERO opcode with the maximum run length of 64(2^6)
set chunk [binary format c [expr {0b00000000 | 0x3f}]]
# Fill the HLL with more than 33554432(2^17) ZERO opcodes to make the total
# run length exceed 4GB, will cause an integer overflow.
set repeat [expr 33554432 + 1000]
for {set i 0} {$i < $repeat} {incr i} {
append pl $chunk
}
# Create a VAL opcode with a value that will cause out-of-bounds.
append pl [binary format c 0b11111111]
r set hll $pl
# This should not overflow and out-of-bounds.
assert_error {*INVALIDOBJ*} {r pfcount hll hll}
assert_error {*INVALIDOBJ*} {r pfdebug getreg hll}
r ping
}
test {Corrupted dense HyperLogLogs are detected: Wrong length} {
r del hll
r pfadd hll a b c
+39 -2
View File
@@ -1,3 +1,16 @@
#
# Copyright (c) 2009-Present, Redis Ltd.
# All rights reserved.
#
# Copyright (c) 2024-present, Valkey contributors.
# All rights reserved.
#
# Licensed under your choice of the Redis Source Available License 2.0
# (RSALv2) or the Server Side Public License v1 (SSPLv1).
#
# Portions of this file are available under BSD3 terms; see REDISCONTRIBUTIONS for more information.
#
proc cmdstat {cmd} {
return [cmdrstat $cmd r]
}
@@ -386,10 +399,10 @@ start_server {tags {"info" "external:skip"}} {
r config set client-output-buffer-limit "normal 10 0 0"
r set key [string repeat a 100000] ;# to trigger output buffer limit check this needs to be big
catch {r get key}
r config set client-output-buffer-limit $org_outbuf_limit
set info [r info stats]
assert_equal [getInfoProperty $info client_output_buffer_limit_disconnections] {1}
r config set client-output-buffer-limit $org_outbuf_limit
} {OK} {logreqres:skip} ;# same as obuf-limits.tcl, skip logreqres
} {} {logreqres:skip} ;# same as obuf-limits.tcl, skip logreqres
test {clients: pubsub clients} {
set info [r info clients]
@@ -522,3 +535,27 @@ start_server {tags {"info" "external:skip"}} {
assert_equal [dict get $mem_stats db.dict.rehashing.count] {1}
}
}
start_cluster 1 0 {tags {external:skip cluster}} {
test "Verify that LUT overhead is properly updated when dicts are emptied or reused (issue #13973)" {
R 0 set k v ;# Make dbs overhead displayed
set info_mem [r memory stats]
set overhead_main [dict get $info_mem db.0 overhead.hashtable.main]
set overhead_expires [dict get $info_mem db.0 overhead.hashtable.expires]
assert_range $overhead_main 1 5000
assert_range $overhead_expires 1 1000
# In cluster mode, we use KVSTORE_FREE_EMPTY_DICTS to ensure that dicts
# are freed when they are emptied. This test verifies that after a dict
# is cleared, the lut overhead is properly updated, preventing it from
# growing indefinitely.
for {set j 1} {$j <= 500} {incr j} {
R 0 set k v
R 0 del k
}
R 0 set k v ;# Make dbs overhead displayed
set info_mem [r memory stats]
assert_equal [dict get $info_mem db.0 overhead.hashtable.main] $overhead_main
assert_equal [dict get $info_mem db.0 overhead.hashtable.expires] $overhead_expires
}
}
+14
View File
@@ -174,4 +174,18 @@ start_server {tags {"lazyfree"}} {
assert_equal [s lazyfreed_objects] 2
$rd close
}
test "Unblocks client blocked on lazyfree via REPLICAOF command" {
set rd [redis_deferring_client]
populate 50000 ;# Just to make flushdb async slower
$rd flushdb
wait_for_blocked_client
# Test that slaveof command unblocks clients without assertion failure
r slaveof 127.0.0.1 0
assert_equal [$rd read] {OK}
$rd close
r ping
r slaveof no one
} {OK} {external:skip}
}
+5
View File
@@ -1,6 +1,11 @@
set testmodule [file normalize tests/modules/datatype.so]
start_server {tags {"modules"}} {
test {DataType: test loadex with invalid config} {
catch { r module loadex $testmodule CONFIG invalid_config 1 } e
assert_match {*ERR Error loading the extension*} $e
}
r module load $testmodule
test {DataType: Test module is sane, GET/SET work.} {
+20
View File
@@ -359,6 +359,26 @@ start_server {tags {"pause network"}} {
} {bar2}
}
test "Test the randomkey command will not cause the server to get into an infinite loop during the client pause write" {
r flushall
r multi
r set key value px 3
r client pause 10000 write
r exec
after 5
wait_for_condition 50 100 {
[r randomkey] == "key"
} else {
fail "execute randomkey failed, caused by the infinite loop"
}
r client unpause
assert_equal [r randomkey] {}
}
# Make sure we unpause at the end
r client unpause
}
+29
View File
@@ -1884,6 +1884,35 @@ start_server {tags {"scripting needs:debug"}} {
}
start_server {tags {"scripting"}} {
test "Test script flush will not leak memory - script:$is_eval" {
r flushall
r script flush
r function flush
# This is a best-effort test to check we don't leak some resources on
# script flush and function flush commands. For lua vm, we create a
# jemalloc thread cache. On each script flush command, thread cache is
# destroyed and we create a new one. In this test, running script flush
# many times to verify there is no increase in the memory usage while
# re-creating some of the resources for lua vm.
set used_memory [s used_memory]
set allocator_allocated [s allocator_allocated]
r multi
for {set j 1} {$j <= 500} {incr j} {
if {$is_eval} {
r SCRIPT FLUSH
} else {
r FUNCTION FLUSH
}
}
r exec
# Verify used memory is not (much) higher.
assert_lessthan [s used_memory] [expr $used_memory*1.5]
assert_lessthan [s allocator_allocated] [expr $allocator_allocated*1.5]
}
test "Verify Lua performs GC correctly after script loading" {
set dummy_script "--[string repeat x 10]\nreturn "
set n 50000
+13
View File
@@ -1300,6 +1300,19 @@ start_server {
assert_equal [dict get $group entries-read] 1
assert_equal [dict get $group lag] 1
# When all the entries are read, the lag is always 0.
r XREADGROUP GROUP mygroup alice STREAMS x >
set reply [r XINFO STREAM x FULL]
set group [lindex [dict get $reply groups] 0]
assert_equal [dict get $group entries-read] 5
assert_equal [dict get $group lag] 0
r XADD x 6-0 data f
set reply [r XINFO STREAM x FULL]
set group [lindex [dict get $reply groups] 0]
assert_equal [dict get $group entries-read] 5
assert_equal [dict get $group lag] 1
# When all the entries were deleted, the lag is always 0.
r XTRIM x MAXLEN 0
set reply [r XINFO STREAM x FULL]
+27
View File
@@ -435,6 +435,17 @@ start_server {
# verify nil is still received when reading last entry
assert_equal [r XREAD STREAMS lestream +] {}
# case when stream created empty
# make sure the stream is not initialized
r DEL lestream
# create empty stream with XGROUP CREATE
r XGROUP CREATE lestream legroup $ MKSTREAM
# verify nil is received when reading last entry
assert_equal [r XREAD STREAMS lestream +] {}
}
test {XREAD last element blocking from empty stream} {
@@ -510,6 +521,22 @@ start_server {
assert_equal $res {{lestream {{3-0 {k3 v3}}}}}
}
test "XREAD: read last element after XDEL (issue #13628)" {
# Should return actual last element after XDEL of current last element
# Add 2 entries to a stream and delete last one
r DEL stream
r XADD stream 1-0 f 1
r XADD stream 2-0 f 2
r XDEL stream 2-0
# Read last entry
set res [r XREAD STREAMS stream +]
# Verify the last entry was read
assert_equal $res {{stream {{1-0 {f 1}}}}}
}
test "XREAD: XADD + DEL should not awake client" {
set rd [redis_deferring_client]
r del s1
+9 -1
View File
@@ -583,20 +583,24 @@ start_server {tags {"zset"}} {
assert_equal {d e f} [r zrangebyscore zset 0 10 LIMIT 2 3]
assert_equal {d e f} [r zrangebyscore zset 0 10 LIMIT 2 10]
assert_equal {} [r zrangebyscore zset 0 10 LIMIT 20 10]
assert_equal {} [r zrangebyscore zset 0 10 LIMIT -1 2]
assert_equal {f e} [r zrevrangebyscore zset 10 0 LIMIT 0 2]
assert_equal {d c b} [r zrevrangebyscore zset 10 0 LIMIT 2 3]
assert_equal {d c b} [r zrevrangebyscore zset 10 0 LIMIT 2 10]
assert_equal {} [r zrevrangebyscore zset 10 0 LIMIT 20 10]
assert_equal {} [r zrevrangebyscore zset 10 0 LIMIT -1 2]
# zrangebyscore uses different logic when offset > ZSKIPLIST_MAX_SEARCH
create_long_zset zset 30
assert_equal {i12 i13 i14} [r zrangebyscore zset 0 20 LIMIT 12 3]
assert_equal {i14 i15} [r zrangebyscore zset 0 20 LIMIT 14 2]
assert_equal {i19 i20 i21} [r zrangebyscore zset 0 30 LIMIT 19 3]
assert_equal {i29} [r zrangebyscore zset 10 30 LIMIT 19 2]
assert_equal {i29} [r zrangebyscore zset 10 30 LIMIT 19 2]
assert_equal {} [r zrangebyscore zset 0 20 LIMIT -1 3]
assert_equal {i17 i16 i15} [r zrevrangebyscore zset 30 10 LIMIT 12 3]
assert_equal {i6 i5} [r zrevrangebyscore zset 20 0 LIMIT 14 2]
assert_equal {i2 i1 i0} [r zrevrangebyscore zset 20 0 LIMIT 18 5]
assert_equal {i0} [r zrevrangebyscore zset 20 0 LIMIT 20 5]
assert_equal {} [r zrevrangebyscore zset 30 10 LIMIT -1 3]
}
test "ZRANGEBYSCORE with LIMIT and WITHSCORES - $encoding" {
@@ -680,9 +684,11 @@ start_server {tags {"zset"}} {
assert_equal {bar} [r zrangebylex zset \[bar \[down LIMIT 0 1]
assert_equal {cool} [r zrangebylex zset \[bar \[down LIMIT 1 1]
assert_equal {bar cool down} [r zrangebylex zset \[bar \[down LIMIT 0 100]
assert_equal {} [r zrangebylex zset - \[cool LIMIT -1 2]
assert_equal {omega hill great foo elephant} [r zrevrangebylex zset + \[d LIMIT 0 5]
assert_equal {omega hill great foo} [r zrevrangebylex zset + \[d LIMIT 0 4]
assert_equal {great foo elephant} [r zrevrangebylex zset + \[d LIMIT 2 3]
assert_equal {} [r zrevrangebylex zset + \[d LIMIT -1 5]
# zrangebylex uses different logic when offset > ZSKIPLIST_MAX_SEARCH
create_long_lex_zset
assert_equal {max null} [r zrangebylex zset - \[tree LIMIT 12 2]
@@ -692,12 +698,14 @@ start_server {tags {"zset"}} {
assert_equal {max} [r zrangebylex zset \[max \[null LIMIT 0 1]
assert_equal {null} [r zrangebylex zset \[max \[null LIMIT 1 1]
assert_equal {max null omega point} [r zrangebylex zset \[max \[point LIMIT 0 100]
assert_equal {} [r zrangebylex zset - \[tree LIMIT -1 2]
assert_equal {tree sea result query point} [r zrevrangebylex zset + \[o LIMIT 0 5]
assert_equal {tree sea result query} [r zrevrangebylex zset + \[o LIMIT 0 4]
assert_equal {omega null max lip} [r zrevrangebylex zset + \[l LIMIT 5 4]
assert_equal {elephant down} [r zrevrangebylex zset + \[a LIMIT 15 2]
assert_equal {bar alpha} [r zrevrangebylex zset + - LIMIT 18 6]
assert_equal {hill great foo} [r zrevrangebylex zset + \[c LIMIT 12 3]
assert_equal {} [r zrevrangebylex zset + \[o LIMIT -1 5]
}
test "ZRANGEBYLEX with invalid lex range specifiers - $encoding" {