Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fe3e74a38 | ||
|
|
4955375ec7 | ||
|
|
406a365f44 | ||
|
|
2dd4cca363 | ||
|
|
bcae770819 | ||
|
|
bf802b0764 | ||
|
|
ac03e3721d | ||
|
|
31227f4faf | ||
|
|
f6f11f3ef1 | ||
|
|
569584d463 | ||
|
|
ea3e8b79a1 | ||
|
|
74609d44cd | ||
|
|
ea05c6ac47 | ||
|
|
05aed4cab9 | ||
|
|
de7f2f87f7 | ||
|
|
fb8755a636 | ||
|
|
d3d94ccf2e | ||
|
|
00a8e72cfc | ||
|
|
a31b516e25 | ||
|
|
c77b8f45e9 | ||
|
|
a7afd1d2b2 | ||
|
|
3fcddfb61f | ||
|
|
3c9f5954b5 | ||
|
|
3b1b1d1486 | ||
|
|
60f22ca830 | ||
|
|
6ceadfb580 | ||
|
|
7f0a7f0a69 | ||
|
|
3264deb24e | ||
|
|
6c6489280c | ||
|
|
2b88db90aa | ||
|
|
b94b714f81 | ||
|
|
806459f481 | ||
|
|
3a08819f51 | ||
|
|
6f0ddc9d92 | ||
|
|
731f2dc5c7 | ||
|
|
bf643a63c8 | ||
|
|
e4ddc34463 | ||
|
|
6d5d754119 | ||
|
|
e3e631f394 | ||
|
|
6263823e54 | ||
|
|
4dd8b1faa9 | ||
|
|
8038eb3147 | ||
|
|
89742a95db | ||
|
|
60e9e630bd | ||
|
|
e750c619b2 | ||
|
|
93fb83b4cb |
@@ -0,0 +1,24 @@
|
||||
name: redis_docs_sync
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
redis_docs_sync:
|
||||
if: github.repository == 'redis/redis'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.DOCS_APP_ID }}
|
||||
private-key: ${{ secrets.DOCS_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Invoke workflow on redis/docs
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||
RELEASE_NAME: ${{ github.event.release.tag_name }}
|
||||
run: |
|
||||
gh workflow run -R redis/docs redis_docs_sync.yaml -f release="${RELEASE_NAME}"
|
||||
+75
-11
@@ -1,16 +1,80 @@
|
||||
Hello! This file is just a placeholder, since this is the "unstable" branch
|
||||
of Redis, the place where all the development happens.
|
||||
Redis Community Edition 8.0 release notes
|
||||
=========================================
|
||||
|
||||
There is no release notes for this branch, it gets forked into another branch
|
||||
every time there is a partial feature freeze in order to eventually create
|
||||
a new stable release.
|
||||
===========================================================
|
||||
8.0-M01 (v7.9.224) Released Thu 12 Sep 2024 10:00:00 IST
|
||||
===========================================================
|
||||
|
||||
Usually "unstable" is stable enough for you to use it in development environments
|
||||
however you should never use it in production environments. It is possible
|
||||
to download the latest stable release here:
|
||||
This is the first Milestone of Redis Community Edition 8.0.
|
||||
|
||||
https://download.redis.io/redis-stable.tar.gz
|
||||
Milestones are non-feature-complete pre-releases. Pre-releases are not suitable for production use.
|
||||
Once we reach feature-completeness we will release RC1.
|
||||
|
||||
More information is available at https://redis.io
|
||||
### Headlines:
|
||||
|
||||
Redis 8.0 introduces new data structures: JSON, time series, and 5 probabilistic data structures (previously available as separate Redis modules) and incorporates Redis scalable query engine (including vector search).
|
||||
|
||||
8.0-M01 is available as a Docker image and can be downloaded from [Docker Hub](https://hub.docker.com/_/redis). Additional distributions will be introduced in upcoming pre-releases.
|
||||
|
||||
### Supported upgrade paths (by replication or persistence) to 8.0-M01
|
||||
|
||||
|
||||
- From previous Redis versions, without modules
|
||||
|
||||
The following upgrade paths (by replication or persistence) to 8.0-M01 are not yet tested and will be introduced in upcoming pre-releases:
|
||||
- From previous Redis versions with modules (RediSearch, RedisJSON, RedisTimeSeries, RedisBloom)
|
||||
- From Redis Stack 7.2 or 7.4
|
||||
|
||||
### New Features in binary distributions
|
||||
|
||||
- 7 new data structures: JSON, Time series, Bloom filter, Cuckoo filter, Count-min sketch, Top-k, t-digest
|
||||
- Redis scalable query engine (including vector search)
|
||||
|
||||
### Potentially breaking changes
|
||||
|
||||
- #12272 `GETRANGE` returns an empty bulk when the negative end index is out of range
|
||||
- #12395 Optimize `SCAN` command when matching data type
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- #13510 Fix `RM_RdbLoad` to enable AOF after RDB loading is completed
|
||||
- #13489 `ACL CAT` - return module commands
|
||||
- #13476 Fix a race condition in the `cache_memory` of `functionsLibCtx`
|
||||
- #13473 Fix incorrect lag due to trimming stream via `XTRIM` command
|
||||
- #13338 Fix incorrect lag field in `XINFO` when tombstone is after the `last_id` of the consume group
|
||||
- #13470 On `HDEL` of last field - update the global hash field expiration data structure
|
||||
- #13465 Cluster: Pass extensions to node if extension processing is handled by it
|
||||
- #13443 Cluster: Ensure validity of myself when loading cluster config
|
||||
- #13422 Cluster: Fix `CLUSTER SHARDS` command returns empty array
|
||||
|
||||
### Modules API
|
||||
|
||||
- #13509 New API calls: `RM_DefragAllocRaw`, `RM_DefragFreeRaw`, and `RM_RegisterDefragCallbacks` - defrag API to allocate and free raw memory
|
||||
|
||||
### Performance and resource utilization improvements
|
||||
|
||||
- #13503 Avoid overhead of comparison function pointer calls in listpack `lpFind`
|
||||
- #13505 Optimize `STRING` datatype write commands
|
||||
- #13499 Optimize `SMEMBERS` command
|
||||
- #13494 Optimize `GEO*` commands reply
|
||||
- #13490 Optimize `HELLO` command
|
||||
- #13488 Optimize client query buffer
|
||||
- #12395 Optimize `SCAN` command when matching data type
|
||||
- #13529 Optimize `LREM`, `LPOS`, `LINSERT`, and `LINDEX` commands
|
||||
- #13516 Optimize `LRANGE` and other commands that perform several writes to client buffers per call
|
||||
- #13431 Avoid `used_memory` contention when updating from multiple threads
|
||||
|
||||
### Other general improvements
|
||||
|
||||
- #13495 Reply `-LOADING` on replica while flushing the db
|
||||
|
||||
### CLI tools
|
||||
|
||||
- #13411 redis-cli: Fix wrong `dbnum` showed after the client reconnected
|
||||
|
||||
### Notes
|
||||
|
||||
- No backward compatibility for replication or persistence.
|
||||
- Additional distributions, upgrade paths, features, and improvements will be introduced in upcoming pre-releases.
|
||||
- With the GA release of 8.0 we will deprecate Redis Stack.
|
||||
|
||||
Happy hacking!
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
# Top level makefile, the real shit is at src/Makefile
|
||||
# Top level makefile, the real stuff is at ./src/Makefile and in ./modules/Makefile
|
||||
|
||||
SUBDIRS = src
|
||||
ifeq ($(BUILD_WITH_MODULES), yes)
|
||||
SUBDIRS += modules
|
||||
endif
|
||||
|
||||
default: all
|
||||
|
||||
.DEFAULT:
|
||||
cd src && $(MAKE) $@
|
||||
for dir in $(SUBDIRS); do $(MAKE) -C $$dir $@; done
|
||||
|
||||
install:
|
||||
cd src && $(MAKE) $@
|
||||
for dir in $(SUBDIRS); do $(MAKE) -C $$dir $@; done
|
||||
|
||||
.PHONY: install
|
||||
|
||||
@@ -16,7 +16,6 @@ Another good example is to think of Redis as a more complex version of memcached
|
||||
If you want to know more, this is a list of selected starting points:
|
||||
|
||||
* Introduction to Redis data types. https://redis.io/topics/data-types-intro
|
||||
* Try Redis directly inside your browser. https://try.redis.io
|
||||
* The full list of Redis commands. https://redis.io/commands
|
||||
* There is much more inside the official Redis documentation. https://redis.io/documentation
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -296,7 +296,7 @@ static int isUnsupportedTerm(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Raw mode: 1960 magic shit. */
|
||||
/* Raw mode: 1960's magic. */
|
||||
static int enableRawMode(int fd) {
|
||||
if (getenv("FAKETTY_WITH_PROMPT") != NULL) {
|
||||
return 0;
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
|
||||
SUBDIRS = redisjson redistimeseries redisbloom redisearch
|
||||
|
||||
define submake
|
||||
for dir in $(SUBDIRS); do $(MAKE) -C $$dir $(1); done
|
||||
endef
|
||||
|
||||
all: prepare_source
|
||||
$(call submake,$@)
|
||||
|
||||
get_source:
|
||||
$(call submake,$@)
|
||||
|
||||
prepare_source: get_source handle-werrors setup_environment
|
||||
|
||||
clean:
|
||||
$(call submake,$@)
|
||||
|
||||
distclean: clean_environment
|
||||
$(call submake,$@)
|
||||
|
||||
pristine:
|
||||
$(call submake,$@)
|
||||
|
||||
install:
|
||||
$(call submake,$@)
|
||||
|
||||
setup_environment: install-rust handle-werrors
|
||||
|
||||
clean_environment: uninstall-rust
|
||||
|
||||
# Keep all of the Rust stuff in one place
|
||||
install-rust:
|
||||
ifeq ($(INSTALL_RUST_TOOLCHAIN),yes)
|
||||
@RUST_VERSION=1.80.1; \
|
||||
case "$$(uname -m)" in \
|
||||
'x86_64') RUST_INSTALLER="rust-$${RUST_VERSION}-x86_64-unknown-linux-gnu"; RUST_SHA256="85e936d5d36970afb80756fa122edcc99bd72a88155f6bdd514f5d27e778e00a" ;; \
|
||||
'aarch64') RUST_INSTALLER="rust-$${RUST_VERSION}-aarch64-unknown-linux-gnu"; RUST_SHA256="2e89bad7857711a1c11d017ea28fbfeec54076317763901194f8f5decbac1850" ;; \
|
||||
*) echo >&2 "Unsupported architecture: '$$(uname -m)'"; exit 1 ;; \
|
||||
esac; \
|
||||
wget --quiet -O $${RUST_INSTALLER}.tar.xz https://static.rust-lang.org/dist/$${RUST_INSTALLER}.tar.xz; \
|
||||
echo "$${RUST_SHA256} $${RUST_INSTALLER}.tar.xz" | sha256sum -c --quiet || { echo "Rust standalone installer checksum failed!"; exit 1; }; \
|
||||
tar -xf $${RUST_INSTALLER}.tar.xz; \
|
||||
(cd $${RUST_INSTALLER} && ./install.sh); \
|
||||
rm -rf $${RUST_INSTALLER}
|
||||
endif
|
||||
|
||||
uninstall-rust:
|
||||
ifeq ($(INSTALL_RUST_TOOLCHAIN),yes)
|
||||
@if [ -x "/usr/local/lib/rustlib/uninstall.sh" ]; then \
|
||||
echo "Uninstalling Rust using uninstall.sh script"; \
|
||||
rm -rf ~/.cargo; \
|
||||
/usr/local/lib/rustlib/uninstall.sh; \
|
||||
else \
|
||||
echo "WARNING: Rust toolchain not found or uninstall script is missing."; \
|
||||
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
|
||||
@@ -0,0 +1,49 @@
|
||||
PREFIX ?= /usr/local
|
||||
INSTALL_DIR ?= $(DESTDIR)$(PREFIX)/lib/redis/modules
|
||||
INSTALL ?= install
|
||||
|
||||
# This logic *partially* follows the current module build system. It is a bit awkward and
|
||||
# should be changed if/when the modules' build process is refactored.
|
||||
|
||||
ARCH_MAP_x86_64 := x64
|
||||
ARCH_MAP_i386 := x86
|
||||
ARCH_MAP_i686 := x86
|
||||
ARCH_MAP_aarch64 := arm64v8
|
||||
ARCH_MAP_arm64 := arm64v8
|
||||
|
||||
OS := $(shell uname -s | tr '[:upper:]' '[:lower:]')
|
||||
ARCH := $(ARCH_MAP_$(shell uname -m))
|
||||
ifeq ($(ARCH),)
|
||||
$(error Unrecognized CPU architecture $(shell uname -m))
|
||||
endif
|
||||
|
||||
FULL_VARIANT := $(OS)-$(ARCH)-release
|
||||
|
||||
# Common rules for all modules, based on per-module configuration
|
||||
|
||||
all: $(TARGET_MODULE)
|
||||
|
||||
$(TARGET_MODULE): get_source
|
||||
$(MAKE) -C $(SRC_DIR)
|
||||
|
||||
get_source: $(SRC_DIR)/.prepared
|
||||
|
||||
$(SRC_DIR)/.prepared:
|
||||
mkdir -p $(SRC_DIR)
|
||||
git clone --recursive --depth 1 --branch $(MODULE_VERSION) $(MODULE_REPO) $(SRC_DIR)
|
||||
touch $@
|
||||
|
||||
clean:
|
||||
-$(MAKE) -C $(SRC_DIR) clean
|
||||
|
||||
distclean:
|
||||
-$(MAKE) -C $(SRC_DIR) distclean
|
||||
|
||||
pristine:
|
||||
-rm -rf $(SRC_DIR)
|
||||
|
||||
install: $(TARGET_MODULE)
|
||||
mkdir -p $(INSTALL_DIR)
|
||||
$(INSTALL) -m 0755 -D $(TARGET_MODULE) $(INSTALL_DIR)
|
||||
|
||||
.PHONY: all clean distclean pristine install
|
||||
@@ -0,0 +1,6 @@
|
||||
SRC_DIR = src
|
||||
MODULE_VERSION = v2.8.2
|
||||
MODULE_REPO = https://github.com/redisbloom/redisbloom
|
||||
TARGET_MODULE = $(SRC_DIR)/bin/$(FULL_VARIANT)/redisbloom.so
|
||||
|
||||
include ../common.mk
|
||||
@@ -0,0 +1,7 @@
|
||||
SRC_DIR = src
|
||||
MODULE_VERSION = v7.99.0
|
||||
MODULE_REPO = https://github.com/redisearch/redisearch
|
||||
TARGET_MODULE = $(SRC_DIR)/bin/$(FULL_VARIANT)/coord-oss/redisearch.so
|
||||
|
||||
include ../common.mk
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
SRC_DIR = src
|
||||
MODULE_VERSION = v2.8.4
|
||||
MODULE_REPO = https://github.com/redisjson/redisjson
|
||||
TARGET_MODULE = $(SRC_DIR)/bin/$(FULL_VARIANT)/rejson.so
|
||||
|
||||
include ../common.mk
|
||||
|
||||
$(SRC_DIR)/.cargo_fetched:
|
||||
cd $(SRC_DIR) && cargo fetch
|
||||
|
||||
get_source: $(SRC_DIR)/.cargo_fetched
|
||||
@@ -0,0 +1,6 @@
|
||||
SRC_DIR = src
|
||||
MODULE_VERSION = v1.12.2
|
||||
MODULE_REPO = https://github.com/redistimeseries/redistimeseries
|
||||
TARGET_MODULE = $(SRC_DIR)/bin/$(FULL_VARIANT)/redistimeseries.so
|
||||
|
||||
include ../common.mk
|
||||
@@ -2762,7 +2762,6 @@ void aclCatWithFlags(client *c, dict *commands, uint64_t cflag, int *arraylen) {
|
||||
|
||||
while ((de = dictNext(di)) != NULL) {
|
||||
struct redisCommand *cmd = dictGetVal(de);
|
||||
if (cmd->flags & CMD_MODULE) continue;
|
||||
if (cmd->acl_categories & cflag) {
|
||||
addReplyBulkCBuffer(c, cmd->fullname, sdslen(cmd->fullname));
|
||||
(*arraylen)++;
|
||||
|
||||
@@ -997,6 +997,29 @@ int startAppendOnly(void) {
|
||||
return C_OK;
|
||||
}
|
||||
|
||||
void startAppendOnlyWithRetry(void) {
|
||||
unsigned int tries, max_tries = 10;
|
||||
for (tries = 0; tries < max_tries; ++tries) {
|
||||
if (startAppendOnly() == C_OK)
|
||||
break;
|
||||
serverLog(LL_WARNING, "Failed to enable AOF! Trying it again in one second.");
|
||||
sleep(1);
|
||||
}
|
||||
if (tries == max_tries) {
|
||||
serverLog(LL_WARNING, "FATAL: AOF can't be turned on. Exiting now.");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Called after "appendonly" config is changed. */
|
||||
void applyAppendOnlyConfig(void) {
|
||||
if (!server.aof_enabled && server.aof_state != AOF_OFF) {
|
||||
stopAppendOnly();
|
||||
} else if (server.aof_enabled && server.aof_state == AOF_OFF) {
|
||||
startAppendOnlyWithRetry();
|
||||
}
|
||||
}
|
||||
|
||||
/* This is a wrapper to the write syscall in order to retry on short writes
|
||||
* or if the syscall gets interrupted. It could look strange that we retry
|
||||
* on short writes given that we are writing to a block device: normally if
|
||||
|
||||
+124
@@ -783,6 +783,130 @@ unsigned int countKeysInSlot(unsigned int slot) {
|
||||
return kvstoreDictSize(server.db->keys, slot);
|
||||
}
|
||||
|
||||
/* Add detailed information of a node to the output buffer of the given client. */
|
||||
void addNodeDetailsToShardReply(client *c, clusterNode *node) {
|
||||
|
||||
int reply_count = 0;
|
||||
char *hostname;
|
||||
void *node_replylen = addReplyDeferredLen(c);
|
||||
|
||||
addReplyBulkCString(c, "id");
|
||||
addReplyBulkCBuffer(c, clusterNodeGetName(node), CLUSTER_NAMELEN);
|
||||
reply_count++;
|
||||
|
||||
if (clusterNodeTcpPort(node)) {
|
||||
addReplyBulkCString(c, "port");
|
||||
addReplyLongLong(c, clusterNodeTcpPort(node));
|
||||
reply_count++;
|
||||
}
|
||||
|
||||
if (clusterNodeTlsPort(node)) {
|
||||
addReplyBulkCString(c, "tls-port");
|
||||
addReplyLongLong(c, clusterNodeTlsPort(node));
|
||||
reply_count++;
|
||||
}
|
||||
|
||||
addReplyBulkCString(c, "ip");
|
||||
addReplyBulkCString(c, clusterNodeIp(node));
|
||||
reply_count++;
|
||||
|
||||
addReplyBulkCString(c, "endpoint");
|
||||
addReplyBulkCString(c, clusterNodePreferredEndpoint(node));
|
||||
reply_count++;
|
||||
|
||||
hostname = clusterNodeHostname(node);
|
||||
if (hostname != NULL && *hostname != '\0') {
|
||||
addReplyBulkCString(c, "hostname");
|
||||
addReplyBulkCString(c, hostname);
|
||||
reply_count++;
|
||||
}
|
||||
|
||||
long long node_offset;
|
||||
if (clusterNodeIsMyself(node)) {
|
||||
node_offset = clusterNodeIsSlave(node) ? replicationGetSlaveOffset() : server.master_repl_offset;
|
||||
} else {
|
||||
node_offset = clusterNodeReplOffset(node);
|
||||
}
|
||||
|
||||
addReplyBulkCString(c, "role");
|
||||
addReplyBulkCString(c, clusterNodeIsSlave(node) ? "replica" : "master");
|
||||
reply_count++;
|
||||
|
||||
addReplyBulkCString(c, "replication-offset");
|
||||
addReplyLongLong(c, node_offset);
|
||||
reply_count++;
|
||||
|
||||
addReplyBulkCString(c, "health");
|
||||
const char *health_msg = NULL;
|
||||
if (clusterNodeIsFailing(node)) {
|
||||
health_msg = "fail";
|
||||
} else if (clusterNodeIsSlave(node) && node_offset == 0) {
|
||||
health_msg = "loading";
|
||||
} else {
|
||||
health_msg = "online";
|
||||
}
|
||||
addReplyBulkCString(c, health_msg);
|
||||
reply_count++;
|
||||
|
||||
setDeferredMapLen(c, node_replylen, reply_count);
|
||||
}
|
||||
|
||||
static clusterNode *clusterGetMasterFromShard(void *shard_handle) {
|
||||
clusterNode *n = NULL;
|
||||
void *node_it = clusterShardHandleGetNodeIterator(shard_handle);
|
||||
while((n = clusterShardNodeIteratorNext(node_it)) != NULL) {
|
||||
if (!clusterNodeIsFailing(n)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
clusterShardNodeIteratorFree(node_it);
|
||||
if (!n) return NULL;
|
||||
return clusterNodeGetMaster(n);
|
||||
}
|
||||
|
||||
/* Add the shard reply of a single shard based off the given primary node. */
|
||||
void addShardReplyForClusterShards(client *c, void *shard_handle) {
|
||||
serverAssert(clusterGetShardNodeCount(shard_handle) > 0);
|
||||
addReplyMapLen(c, 2);
|
||||
addReplyBulkCString(c, "slots");
|
||||
|
||||
/* Use slot_info_pairs from the primary only */
|
||||
clusterNode *master_node = clusterGetMasterFromShard(shard_handle);
|
||||
|
||||
if (master_node && clusterNodeHasSlotInfo(master_node)) {
|
||||
serverAssert((clusterNodeSlotInfoCount(master_node) % 2) == 0);
|
||||
addReplyArrayLen(c, clusterNodeSlotInfoCount(master_node));
|
||||
for (int i = 0; i < clusterNodeSlotInfoCount(master_node); i++)
|
||||
addReplyLongLong(c, (unsigned long)clusterNodeSlotInfoEntry(master_node, i));
|
||||
} else {
|
||||
/* If no slot info pair is provided, the node owns no slots */
|
||||
addReplyArrayLen(c, 0);
|
||||
}
|
||||
|
||||
addReplyBulkCString(c, "nodes");
|
||||
addReplyArrayLen(c, clusterGetShardNodeCount(shard_handle));
|
||||
void *node_it = clusterShardHandleGetNodeIterator(shard_handle);
|
||||
for (clusterNode *n = clusterShardNodeIteratorNext(node_it); n != NULL; n = clusterShardNodeIteratorNext(node_it)) {
|
||||
addNodeDetailsToShardReply(c, n);
|
||||
clusterFreeNodesSlotsInfo(n);
|
||||
}
|
||||
clusterShardNodeIteratorFree(node_it);
|
||||
}
|
||||
|
||||
/* Add to the output buffer of the given client, an array of slot (start, end)
|
||||
* pair owned by the shard, also the primary and set of replica(s) along with
|
||||
* information about each node. */
|
||||
void clusterCommandShards(client *c) {
|
||||
addReplyArrayLen(c, clusterGetShardCount());
|
||||
/* This call will add slot_info_pairs to all nodes */
|
||||
clusterGenNodesSlotsInfo(0);
|
||||
dictIterator *shard_it = clusterGetShardIterator();
|
||||
for(void *shard_handle = clusterNextShardHandle(shard_it); shard_handle != NULL; shard_handle = clusterNextShardHandle(shard_it)) {
|
||||
addShardReplyForClusterShards(c, shard_handle);
|
||||
}
|
||||
clusterFreeShardIterator(shard_it);
|
||||
}
|
||||
|
||||
void clusterCommandHelp(client *c) {
|
||||
const char *help[] = {
|
||||
"COUNTKEYSINSLOT <slot>",
|
||||
|
||||
+20
-1
@@ -97,7 +97,7 @@ int clusterManualFailoverTimeLimit(void);
|
||||
void clusterCommandSlots(client * c);
|
||||
void clusterCommandMyId(client *c);
|
||||
void clusterCommandMyShardId(client *c);
|
||||
void clusterCommandShards(client *c);
|
||||
|
||||
sds clusterGenNodeDescription(client *c, clusterNode *node, int tls_primary);
|
||||
|
||||
int clusterNodeCoversSlot(clusterNode *n, int slot);
|
||||
@@ -142,4 +142,23 @@ int isValidAuxString(char *s, unsigned int length);
|
||||
void migrateCommand(client *c);
|
||||
void clusterCommand(client *c);
|
||||
ConnectionType *connTypeOfCluster(void);
|
||||
|
||||
void clusterGenNodesSlotsInfo(int filter);
|
||||
void clusterFreeNodesSlotsInfo(clusterNode *n);
|
||||
int clusterNodeSlotInfoCount(clusterNode *n);
|
||||
uint16_t clusterNodeSlotInfoEntry(clusterNode *n, int idx);
|
||||
int clusterNodeHasSlotInfo(clusterNode *n);
|
||||
|
||||
int clusterGetShardCount(void);
|
||||
void *clusterGetShardIterator(void);
|
||||
void *clusterNextShardHandle(void *shard_iterator);
|
||||
void clusterFreeShardIterator(void *shard_iterator);
|
||||
int clusterGetShardNodeCount(void *shard);
|
||||
void *clusterShardHandleGetNodeIterator(void *shard);
|
||||
clusterNode *clusterShardNodeIteratorNext(void *node_iterator);
|
||||
void clusterShardNodeIteratorFree(void *node_iterator);
|
||||
clusterNode *clusterShardNodeFirst(void *shard);
|
||||
|
||||
int clusterNodeTcpPort(clusterNode *node);
|
||||
int clusterNodeTlsPort(clusterNode *node);
|
||||
#endif /* __CLUSTER_H */
|
||||
|
||||
+76
-108
@@ -2,8 +2,13 @@
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -634,6 +639,8 @@ int clusterLoadConfig(char *filename) {
|
||||
}
|
||||
/* Config sanity check */
|
||||
if (server.cluster->myself == NULL) goto fmterr;
|
||||
if (!(myself->flags & (CLUSTER_NODE_MASTER | CLUSTER_NODE_SLAVE))) goto fmterr;
|
||||
if (nodeIsSlave(myself) && myself->slaveof == NULL) goto fmterr;
|
||||
|
||||
zfree(line);
|
||||
fclose(fp);
|
||||
@@ -2577,9 +2584,6 @@ uint32_t writePingExt(clusterMsg *hdr, int gossipcount) {
|
||||
extensions++;
|
||||
|
||||
if (hdr != NULL) {
|
||||
if (extensions != 0) {
|
||||
hdr->mflags[0] |= CLUSTERMSG_FLAG0_EXT_DATA;
|
||||
}
|
||||
hdr->extensions = htons(extensions);
|
||||
}
|
||||
|
||||
@@ -2769,6 +2773,9 @@ int clusterProcessPacket(clusterLink *link) {
|
||||
}
|
||||
|
||||
sender = getNodeFromLinkAndMsg(link, hdr);
|
||||
if (sender && (hdr->mflags[0] & CLUSTERMSG_FLAG0_EXT_DATA)) {
|
||||
sender->flags |= CLUSTER_NODE_EXTENSIONS_SUPPORTED;
|
||||
}
|
||||
|
||||
/* Update the last time we saw any data from this node. We
|
||||
* use this in order to avoid detecting a timeout from a node that
|
||||
@@ -3534,6 +3541,8 @@ static void clusterBuildMessageHdr(clusterMsg *hdr, int type, size_t msglen) {
|
||||
/* Set the message flags. */
|
||||
if (clusterNodeIsMaster(myself) && server.cluster->mf_end)
|
||||
hdr->mflags[0] |= CLUSTERMSG_FLAG0_PAUSED;
|
||||
hdr->mflags[0] |= CLUSTERMSG_FLAG0_EXT_DATA; /* Always make other nodes know that
|
||||
* this node supports extension data. */
|
||||
|
||||
hdr->totlen = htonl(msglen);
|
||||
}
|
||||
@@ -3612,7 +3621,9 @@ void clusterSendPing(clusterLink *link, int type) {
|
||||
* to put inside the packet. */
|
||||
estlen = sizeof(clusterMsg) - sizeof(union clusterMsgData);
|
||||
estlen += (sizeof(clusterMsgDataGossip)*(wanted + pfail_wanted));
|
||||
estlen += writePingExt(NULL, 0);
|
||||
if (link->node && nodeSupportsExtensions(link->node)) {
|
||||
estlen += writePingExt(NULL, 0);
|
||||
}
|
||||
/* Note: clusterBuildMessageHdr() expects the buffer to be always at least
|
||||
* sizeof(clusterMsg) or more. */
|
||||
if (estlen < (int)sizeof(clusterMsg)) estlen = sizeof(clusterMsg);
|
||||
@@ -3682,7 +3693,9 @@ void clusterSendPing(clusterLink *link, int type) {
|
||||
|
||||
/* Compute the actual total length and send! */
|
||||
uint32_t totlen = 0;
|
||||
totlen += writePingExt(hdr, gossipcount);
|
||||
if (link->node && nodeSupportsExtensions(link->node)) {
|
||||
totlen += writePingExt(hdr, gossipcount);
|
||||
}
|
||||
totlen += sizeof(clusterMsg)-sizeof(union clusterMsgData);
|
||||
totlen += (sizeof(clusterMsgDataGossip)*gossipcount);
|
||||
serverAssert(gossipcount < USHRT_MAX);
|
||||
@@ -5582,113 +5595,68 @@ void clusterUpdateSlots(client *c, unsigned char *slots, int del) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Add detailed information of a node to the output buffer of the given client. */
|
||||
void addNodeDetailsToShardReply(client *c, clusterNode *node) {
|
||||
int reply_count = 0;
|
||||
void *node_replylen = addReplyDeferredLen(c);
|
||||
addReplyBulkCString(c, "id");
|
||||
addReplyBulkCBuffer(c, node->name, CLUSTER_NAMELEN);
|
||||
reply_count++;
|
||||
|
||||
if (node->tcp_port) {
|
||||
addReplyBulkCString(c, "port");
|
||||
addReplyLongLong(c, node->tcp_port);
|
||||
reply_count++;
|
||||
}
|
||||
|
||||
if (node->tls_port) {
|
||||
addReplyBulkCString(c, "tls-port");
|
||||
addReplyLongLong(c, node->tls_port);
|
||||
reply_count++;
|
||||
}
|
||||
|
||||
addReplyBulkCString(c, "ip");
|
||||
addReplyBulkCString(c, node->ip);
|
||||
reply_count++;
|
||||
|
||||
addReplyBulkCString(c, "endpoint");
|
||||
addReplyBulkCString(c, clusterNodePreferredEndpoint(node));
|
||||
reply_count++;
|
||||
|
||||
if (sdslen(node->hostname) != 0) {
|
||||
addReplyBulkCString(c, "hostname");
|
||||
addReplyBulkCBuffer(c, node->hostname, sdslen(node->hostname));
|
||||
reply_count++;
|
||||
}
|
||||
|
||||
long long node_offset;
|
||||
if (node->flags & CLUSTER_NODE_MYSELF) {
|
||||
node_offset = nodeIsSlave(node) ? replicationGetSlaveOffset() : server.master_repl_offset;
|
||||
} else {
|
||||
node_offset = node->repl_offset;
|
||||
}
|
||||
|
||||
addReplyBulkCString(c, "role");
|
||||
addReplyBulkCString(c, nodeIsSlave(node) ? "replica" : "master");
|
||||
reply_count++;
|
||||
|
||||
addReplyBulkCString(c, "replication-offset");
|
||||
addReplyLongLong(c, node_offset);
|
||||
reply_count++;
|
||||
|
||||
addReplyBulkCString(c, "health");
|
||||
const char *health_msg = NULL;
|
||||
if (nodeFailed(node)) {
|
||||
health_msg = "fail";
|
||||
} else if (nodeIsSlave(node) && node_offset == 0) {
|
||||
health_msg = "loading";
|
||||
} else {
|
||||
health_msg = "online";
|
||||
}
|
||||
addReplyBulkCString(c, health_msg);
|
||||
reply_count++;
|
||||
|
||||
setDeferredMapLen(c, node_replylen, reply_count);
|
||||
int clusterGetShardCount(void) {
|
||||
return dictSize(server.cluster->shards);
|
||||
}
|
||||
|
||||
/* Add the shard reply of a single shard based off the given primary node. */
|
||||
void addShardReplyForClusterShards(client *c, list *nodes) {
|
||||
serverAssert(listLength(nodes) > 0);
|
||||
clusterNode *n = listNodeValue(listFirst(nodes));
|
||||
addReplyMapLen(c, 2);
|
||||
addReplyBulkCString(c, "slots");
|
||||
|
||||
/* Use slot_info_pairs from the primary only */
|
||||
n = clusterNodeGetMaster(n);
|
||||
|
||||
if (n->slot_info_pairs != NULL) {
|
||||
serverAssert((n->slot_info_pairs_count % 2) == 0);
|
||||
addReplyArrayLen(c, n->slot_info_pairs_count);
|
||||
for (int i = 0; i < n->slot_info_pairs_count; i++)
|
||||
addReplyLongLong(c, (unsigned long)n->slot_info_pairs[i]);
|
||||
} else {
|
||||
/* If no slot info pair is provided, the node owns no slots */
|
||||
addReplyArrayLen(c, 0);
|
||||
}
|
||||
|
||||
addReplyBulkCString(c, "nodes");
|
||||
addReplyArrayLen(c, listLength(nodes));
|
||||
listIter li;
|
||||
listRewind(nodes, &li);
|
||||
for (listNode *ln = listNext(&li); ln != NULL; ln = listNext(&li)) {
|
||||
clusterNode *n = listNodeValue(ln);
|
||||
addNodeDetailsToShardReply(c, n);
|
||||
clusterFreeNodesSlotsInfo(n);
|
||||
}
|
||||
void *clusterGetShardIterator(void) {
|
||||
return dictGetSafeIterator(server.cluster->shards);
|
||||
}
|
||||
|
||||
/* Add to the output buffer of the given client, an array of slot (start, end)
|
||||
* pair owned by the shard, also the primary and set of replica(s) along with
|
||||
* information about each node. */
|
||||
void clusterCommandShards(client *c) {
|
||||
addReplyArrayLen(c, dictSize(server.cluster->shards));
|
||||
/* This call will add slot_info_pairs to all nodes */
|
||||
clusterGenNodesSlotsInfo(0);
|
||||
dictIterator *di = dictGetSafeIterator(server.cluster->shards);
|
||||
for(dictEntry *de = dictNext(di); de != NULL; de = dictNext(di)) {
|
||||
addShardReplyForClusterShards(c, dictGetVal(de));
|
||||
}
|
||||
dictReleaseIterator(di);
|
||||
void *clusterNextShardHandle(void *shard_iterator) {
|
||||
dictEntry *de = dictNext(shard_iterator);
|
||||
if(de == NULL) return NULL;
|
||||
return dictGetVal(de);
|
||||
}
|
||||
|
||||
void clusterFreeShardIterator(void *shard_iterator) {
|
||||
dictReleaseIterator(shard_iterator);
|
||||
}
|
||||
|
||||
int clusterNodeHasSlotInfo(clusterNode *n) {
|
||||
return n->slot_info_pairs != NULL;
|
||||
}
|
||||
|
||||
int clusterNodeSlotInfoCount(clusterNode *n) {
|
||||
return n->slot_info_pairs_count;
|
||||
}
|
||||
|
||||
uint16_t clusterNodeSlotInfoEntry(clusterNode *n, int idx) {
|
||||
return n->slot_info_pairs[idx];
|
||||
}
|
||||
|
||||
int clusterGetShardNodeCount(void *shard) {
|
||||
return listLength((list*)shard);
|
||||
}
|
||||
|
||||
void *clusterShardHandleGetNodeIterator(void *shard) {
|
||||
listIter *li = zmalloc(sizeof(listIter));
|
||||
listRewind((list*)shard, li);
|
||||
return li;
|
||||
}
|
||||
|
||||
void clusterShardNodeIteratorFree(void *node_iterator) {
|
||||
zfree(node_iterator);
|
||||
}
|
||||
|
||||
clusterNode *clusterShardNodeIteratorNext(void *node_iterator) {
|
||||
listNode *item = listNext((listIter*)node_iterator);
|
||||
if (item == NULL) return NULL;
|
||||
return listNodeValue(item);
|
||||
}
|
||||
|
||||
clusterNode *clusterShardNodeFirst(void *shard) {
|
||||
listNode *item = listFirst((list*)shard);
|
||||
if (item == NULL) return NULL;
|
||||
return listNodeValue(item);
|
||||
}
|
||||
|
||||
int clusterNodeTcpPort(clusterNode *node) {
|
||||
return node->tcp_port;
|
||||
}
|
||||
|
||||
int clusterNodeTlsPort(clusterNode *node) {
|
||||
return node->tls_port;
|
||||
}
|
||||
|
||||
sds genClusterInfoString(void) {
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
#ifndef CLUSTER_LEGACY_H
|
||||
#define CLUSTER_LEGACY_H
|
||||
|
||||
@@ -51,6 +64,7 @@ typedef struct clusterLink {
|
||||
#define CLUSTER_NODE_MEET 128 /* Send a MEET message to this node */
|
||||
#define CLUSTER_NODE_MIGRATE_TO 256 /* Master eligible for replica migration. */
|
||||
#define CLUSTER_NODE_NOFAILOVER 512 /* Slave will not try to failover. */
|
||||
#define CLUSTER_NODE_EXTENSIONS_SUPPORTED 1024 /* This node supports extensions. */
|
||||
#define CLUSTER_NODE_NULL_NAME "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
|
||||
|
||||
#define nodeIsSlave(n) ((n)->flags & CLUSTER_NODE_SLAVE)
|
||||
@@ -59,6 +73,7 @@ typedef struct clusterLink {
|
||||
#define nodeTimedOut(n) ((n)->flags & CLUSTER_NODE_PFAIL)
|
||||
#define nodeFailed(n) ((n)->flags & CLUSTER_NODE_FAIL)
|
||||
#define nodeCantFailover(n) ((n)->flags & CLUSTER_NODE_NOFAILOVER)
|
||||
#define nodeSupportsExtensions(n) ((n)->flags & CLUSTER_NODE_EXTENSIONS_SUPPORTED)
|
||||
|
||||
/* This structure represent elements of node->fail_reports. */
|
||||
typedef struct clusterNodeFailReport {
|
||||
|
||||
+9
-5
@@ -3778,7 +3778,9 @@ struct COMMAND_ARG HPEXPIRETIME_Args[] = {
|
||||
|
||||
#ifndef SKIP_CMD_TIPS_TABLE
|
||||
/* HPTTL tips */
|
||||
#define HPTTL_Tips NULL
|
||||
const char *HPTTL_Tips[] = {
|
||||
"nondeterministic_output",
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_KEY_SPECS_TABLE
|
||||
@@ -3956,7 +3958,9 @@ struct COMMAND_ARG HSTRLEN_Args[] = {
|
||||
|
||||
#ifndef SKIP_CMD_TIPS_TABLE
|
||||
/* HTTL tips */
|
||||
#define HTTL_Tips NULL
|
||||
const char *HTTL_Tips[] = {
|
||||
"nondeterministic_output",
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_KEY_SPECS_TABLE
|
||||
@@ -11044,13 +11048,13 @@ struct COMMAND_STRUCT redisCommandTable[] = {
|
||||
{MAKE_CMD("hpexpire","Set expiry for hash field using relative time to expire (milliseconds)","O(N) where N is the number of specified fields","7.4.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HPEXPIRE_History,0,HPEXPIRE_Tips,0,hpexpireCommand,-6,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HPEXPIRE_Keyspecs,1,NULL,4),.args=HPEXPIRE_Args},
|
||||
{MAKE_CMD("hpexpireat","Set expiry for hash field using an absolute Unix timestamp (milliseconds)","O(N) where N is the number of specified fields","7.4.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HPEXPIREAT_History,0,HPEXPIREAT_Tips,0,hpexpireatCommand,-6,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HPEXPIREAT_Keyspecs,1,NULL,4),.args=HPEXPIREAT_Args},
|
||||
{MAKE_CMD("hpexpiretime","Returns the expiration time of a hash field as a Unix timestamp, in msec.","O(N) where N is the number of specified fields","7.4.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HPEXPIRETIME_History,0,HPEXPIRETIME_Tips,0,hpexpiretimeCommand,-5,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HPEXPIRETIME_Keyspecs,1,NULL,2),.args=HPEXPIRETIME_Args},
|
||||
{MAKE_CMD("hpttl","Returns the TTL in milliseconds of a hash field.","O(N) where N is the number of specified fields","7.4.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HPTTL_History,0,HPTTL_Tips,0,hpttlCommand,-5,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HPTTL_Keyspecs,1,NULL,2),.args=HPTTL_Args},
|
||||
{MAKE_CMD("hpttl","Returns the TTL in milliseconds of a hash field.","O(N) where N is the number of specified fields","7.4.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HPTTL_History,0,HPTTL_Tips,1,hpttlCommand,-5,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HPTTL_Keyspecs,1,NULL,2),.args=HPTTL_Args},
|
||||
{MAKE_CMD("hrandfield","Returns one or more random fields from a hash.","O(N) where N is the number of fields returned","6.2.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HRANDFIELD_History,0,HRANDFIELD_Tips,1,hrandfieldCommand,-2,CMD_READONLY,ACL_CATEGORY_HASH,HRANDFIELD_Keyspecs,1,NULL,2),.args=HRANDFIELD_Args},
|
||||
{MAKE_CMD("hscan","Iterates over fields and values of a hash.","O(1) for every call. O(N) for a complete iteration, including enough command calls for the cursor to return back to 0. N is the number of elements inside the collection.","2.8.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSCAN_History,0,HSCAN_Tips,1,hscanCommand,-3,CMD_READONLY,ACL_CATEGORY_HASH,HSCAN_Keyspecs,1,NULL,5),.args=HSCAN_Args},
|
||||
{MAKE_CMD("hset","Creates or modifies the value of a field in a hash.","O(1) for each field/value pair added, so O(N) to add N field/value pairs when the command is called with multiple field/value pairs.","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSET_History,1,HSET_Tips,0,hsetCommand,-4,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HSET_Keyspecs,1,NULL,2),.args=HSET_Args},
|
||||
{MAKE_CMD("hsetnx","Sets the value of a field in a hash only when the field doesn't exist.","O(1)","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSETNX_History,0,HSETNX_Tips,0,hsetnxCommand,4,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HSETNX_Keyspecs,1,NULL,3),.args=HSETNX_Args},
|
||||
{MAKE_CMD("hstrlen","Returns the length of the value of a field.","O(1)","3.2.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HSTRLEN_History,0,HSTRLEN_Tips,0,hstrlenCommand,3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HSTRLEN_Keyspecs,1,NULL,2),.args=HSTRLEN_Args},
|
||||
{MAKE_CMD("httl","Returns the TTL in seconds of a hash field.","O(N) where N is the number of specified fields","7.4.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HTTL_History,0,HTTL_Tips,0,httlCommand,-5,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HTTL_Keyspecs,1,NULL,2),.args=HTTL_Args},
|
||||
{MAKE_CMD("httl","Returns the TTL in seconds of a hash field.","O(N) where N is the number of specified fields","7.4.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HTTL_History,0,HTTL_Tips,1,httlCommand,-5,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HTTL_Keyspecs,1,NULL,2),.args=HTTL_Args},
|
||||
{MAKE_CMD("hvals","Returns all values in a hash.","O(N) where N is the size of the hash.","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HVALS_History,0,HVALS_Tips,1,hvalsCommand,2,CMD_READONLY,ACL_CATEGORY_HASH,HVALS_Keyspecs,1,NULL,1),.args=HVALS_Args},
|
||||
/* hyperloglog */
|
||||
{MAKE_CMD("pfadd","Adds elements to a HyperLogLog key. Creates the key if it doesn't exist.","O(1) to add every element.","2.8.9",CMD_DOC_NONE,NULL,NULL,"hyperloglog",COMMAND_GROUP_HYPERLOGLOG,PFADD_History,0,PFADD_Tips,0,pfaddCommand,-2,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HYPERLOGLOG,PFADD_Keyspecs,1,NULL,2),.args=PFADD_Args},
|
||||
@@ -11141,7 +11145,7 @@ struct COMMAND_STRUCT redisCommandTable[] = {
|
||||
{MAKE_CMD("sintercard","Returns the number of members of the intersect of multiple sets.","O(N*M) worst case where N is the cardinality of the smallest set and M is the number of sets.","7.0.0",CMD_DOC_NONE,NULL,NULL,"set",COMMAND_GROUP_SET,SINTERCARD_History,0,SINTERCARD_Tips,0,sinterCardCommand,-3,CMD_READONLY,ACL_CATEGORY_SET,SINTERCARD_Keyspecs,1,sintercardGetKeys,3),.args=SINTERCARD_Args},
|
||||
{MAKE_CMD("sinterstore","Stores the intersect of multiple sets in a key.","O(N*M) worst case where N is the cardinality of the smallest set and M is the number of sets.","1.0.0",CMD_DOC_NONE,NULL,NULL,"set",COMMAND_GROUP_SET,SINTERSTORE_History,0,SINTERSTORE_Tips,0,sinterstoreCommand,-3,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_SET,SINTERSTORE_Keyspecs,2,NULL,2),.args=SINTERSTORE_Args},
|
||||
{MAKE_CMD("sismember","Determines whether a member belongs to a set.","O(1)","1.0.0",CMD_DOC_NONE,NULL,NULL,"set",COMMAND_GROUP_SET,SISMEMBER_History,0,SISMEMBER_Tips,0,sismemberCommand,3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_SET,SISMEMBER_Keyspecs,1,NULL,2),.args=SISMEMBER_Args},
|
||||
{MAKE_CMD("smembers","Returns all members of a set.","O(N) where N is the set cardinality.","1.0.0",CMD_DOC_NONE,NULL,NULL,"set",COMMAND_GROUP_SET,SMEMBERS_History,0,SMEMBERS_Tips,1,sinterCommand,2,CMD_READONLY,ACL_CATEGORY_SET,SMEMBERS_Keyspecs,1,NULL,1),.args=SMEMBERS_Args},
|
||||
{MAKE_CMD("smembers","Returns all members of a set.","O(N) where N is the set cardinality.","1.0.0",CMD_DOC_NONE,NULL,NULL,"set",COMMAND_GROUP_SET,SMEMBERS_History,0,SMEMBERS_Tips,1,smembersCommand,2,CMD_READONLY,ACL_CATEGORY_SET,SMEMBERS_Keyspecs,1,NULL,1),.args=SMEMBERS_Args},
|
||||
{MAKE_CMD("smismember","Determines whether multiple members belong to a set.","O(N) where N is the number of elements being checked for membership","6.2.0",CMD_DOC_NONE,NULL,NULL,"set",COMMAND_GROUP_SET,SMISMEMBER_History,0,SMISMEMBER_Tips,0,smismemberCommand,-3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_SET,SMISMEMBER_Keyspecs,1,NULL,2),.args=SMISMEMBER_Args},
|
||||
{MAKE_CMD("smove","Moves a member from one set to another.","O(1)","1.0.0",CMD_DOC_NONE,NULL,NULL,"set",COMMAND_GROUP_SET,SMOVE_History,0,SMOVE_Tips,0,smoveCommand,4,CMD_WRITE|CMD_FAST,ACL_CATEGORY_SET,SMOVE_Keyspecs,2,NULL,3),.args=SMOVE_Args},
|
||||
{MAKE_CMD("spop","Returns one or more random members from a set after removing them. Deletes the set if the last member was popped.","Without the count argument O(1), otherwise O(N) where N is the value of the passed count.","1.0.0",CMD_DOC_NONE,NULL,NULL,"set",COMMAND_GROUP_SET,SPOP_History,1,SPOP_Tips,1,spopCommand,-2,CMD_WRITE|CMD_FAST,ACL_CATEGORY_SET,SPOP_Keyspecs,1,NULL,2),.args=SPOP_Args},
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
"acl_categories": [
|
||||
"HASH"
|
||||
],
|
||||
"command_tips": [
|
||||
"NONDETERMINISTIC_OUTPUT"
|
||||
],
|
||||
"key_specs": [
|
||||
{
|
||||
"flags": [
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
"acl_categories": [
|
||||
"HASH"
|
||||
],
|
||||
"command_tips": [
|
||||
"NONDETERMINISTIC_OUTPUT"
|
||||
],
|
||||
"key_specs": [
|
||||
{
|
||||
"flags": [
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"group": "set",
|
||||
"since": "1.0.0",
|
||||
"arity": 2,
|
||||
"function": "sinterCommand",
|
||||
"function": "smembersCommand",
|
||||
"command_flags": [
|
||||
"READONLY"
|
||||
],
|
||||
|
||||
+8
-1
@@ -2502,6 +2502,13 @@ static int updateWatchdogPeriod(const char **err) {
|
||||
}
|
||||
|
||||
static int updateAppendonly(const char **err) {
|
||||
/* If loading flag is set, AOF might have been stopped temporarily, and it
|
||||
* will be restarted depending on server.aof_enabled flag after loading is
|
||||
* completed. So, we just need to update 'server.aof_enabled' which has been
|
||||
* updated already before calling this function. */
|
||||
if (server.loading)
|
||||
return 1;
|
||||
|
||||
if (!server.aof_enabled && server.aof_state != AOF_OFF) {
|
||||
stopAppendOnly();
|
||||
} else if (server.aof_enabled && server.aof_state == AOF_OFF) {
|
||||
@@ -3080,7 +3087,7 @@ standardConfig static_configs[] = {
|
||||
createBoolConfig("activedefrag", NULL, DEBUG_CONFIG | MODIFIABLE_CONFIG, server.active_defrag_enabled, 0, isValidActiveDefrag, NULL),
|
||||
createBoolConfig("syslog-enabled", NULL, IMMUTABLE_CONFIG, server.syslog_enabled, 0, NULL, NULL),
|
||||
createBoolConfig("cluster-enabled", NULL, IMMUTABLE_CONFIG, server.cluster_enabled, 0, NULL, NULL),
|
||||
createBoolConfig("appendonly", NULL, MODIFIABLE_CONFIG | DENY_LOADING_CONFIG, server.aof_enabled, 0, NULL, updateAppendonly),
|
||||
createBoolConfig("appendonly", NULL, MODIFIABLE_CONFIG, server.aof_enabled, 0, NULL, updateAppendonly),
|
||||
createBoolConfig("cluster-allow-reads-when-down", NULL, MODIFIABLE_CONFIG, server.cluster_allow_reads_when_down, 0, NULL, NULL),
|
||||
createBoolConfig("cluster-allow-pubsubshard-when-down", NULL, MODIFIABLE_CONFIG, server.cluster_allow_pubsubshard_when_down, 1, NULL, NULL),
|
||||
createBoolConfig("crash-log-enabled", NULL, MODIFIABLE_CONFIG, server.crashlog_enabled, 1, NULL, updateSighandlerEnabled),
|
||||
|
||||
@@ -32,6 +32,14 @@
|
||||
#define redis_stat stat
|
||||
#endif
|
||||
|
||||
#ifndef CACHE_LINE_SIZE
|
||||
#if defined(__aarch64__) && defined(__APPLE__)
|
||||
#define CACHE_LINE_SIZE 128
|
||||
#else
|
||||
#define CACHE_LINE_SIZE 64
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/* Test for proc filesystem */
|
||||
#ifdef __linux__
|
||||
#define HAVE_PROC_STAT 1
|
||||
|
||||
@@ -49,6 +49,9 @@ void updateLFU(robj *val) {
|
||||
* found in the specified DB. This function implements the functionality of
|
||||
* lookupKeyRead(), lookupKeyWrite() and their ...WithFlags() variants.
|
||||
*
|
||||
* 'deref' is an optional output dictEntry reference argument, to get the
|
||||
* associated dictEntry* of the key in case the key is found.
|
||||
*
|
||||
* Side-effects of calling this function:
|
||||
*
|
||||
* 1. A key gets expired if it reached it's TTL.
|
||||
@@ -72,7 +75,7 @@ void updateLFU(robj *val) {
|
||||
* Even if the key expiry is master-driven, we can correctly report a key is
|
||||
* expired on replicas even if the master is lagging expiring our key via DELs
|
||||
* in the replication link. */
|
||||
robj *lookupKey(redisDb *db, robj *key, int flags) {
|
||||
robj *lookupKey(redisDb *db, robj *key, int flags, dictEntry **deref) {
|
||||
dictEntry *de = dbFind(db, key->ptr);
|
||||
robj *val = NULL;
|
||||
if (de) {
|
||||
@@ -123,6 +126,7 @@ robj *lookupKey(redisDb *db, robj *key, int flags) {
|
||||
/* TODO: Use separate misses stats and notify event for WRITE */
|
||||
}
|
||||
|
||||
if (val && deref) *deref = de;
|
||||
return val;
|
||||
}
|
||||
|
||||
@@ -137,7 +141,7 @@ robj *lookupKey(redisDb *db, robj *key, int flags) {
|
||||
* the key. */
|
||||
robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) {
|
||||
serverAssert(!(flags & LOOKUP_WRITE));
|
||||
return lookupKey(db, key, flags);
|
||||
return lookupKey(db, key, flags, NULL);
|
||||
}
|
||||
|
||||
/* Like lookupKeyReadWithFlags(), but does not use any flag, which is the
|
||||
@@ -153,13 +157,20 @@ robj *lookupKeyRead(redisDb *db, robj *key) {
|
||||
* Returns the linked value object if the key exists or NULL if the key
|
||||
* does not exist in the specified DB. */
|
||||
robj *lookupKeyWriteWithFlags(redisDb *db, robj *key, int flags) {
|
||||
return lookupKey(db, key, flags | LOOKUP_WRITE);
|
||||
return lookupKey(db, key, flags | LOOKUP_WRITE, NULL);
|
||||
}
|
||||
|
||||
robj *lookupKeyWrite(redisDb *db, robj *key) {
|
||||
return lookupKeyWriteWithFlags(db, key, LOOKUP_NONE);
|
||||
}
|
||||
|
||||
/* Like lookupKeyWrite(), but accepts an optional dictEntry input,
|
||||
* which can be used if we already have one, thus saving the dbFind call.
|
||||
*/
|
||||
robj *lookupKeyWriteWithDictEntry(redisDb *db, robj *key, dictEntry **deref) {
|
||||
return lookupKey(db, key, LOOKUP_NONE | LOOKUP_WRITE, deref);
|
||||
}
|
||||
|
||||
robj *lookupKeyReadOrReply(client *c, robj *key, robj *reply) {
|
||||
robj *o = lookupKeyRead(c->db, key);
|
||||
if (!o) addReplyOrErrorObject(c, reply);
|
||||
@@ -294,6 +305,14 @@ void dbReplaceValue(redisDb *db, robj *key, robj *val) {
|
||||
dbSetValue(db, key, val, 0, NULL);
|
||||
}
|
||||
|
||||
/* Replace an existing key with a new value, we just replace value and don't
|
||||
* emit any events.
|
||||
* The dictEntry input is optional, can be used if we already have one.
|
||||
*/
|
||||
void dbReplaceValueWithDictEntry(redisDb *db, robj *key, robj *val, dictEntry *de) {
|
||||
dbSetValue(db, key, val, 0, de);
|
||||
}
|
||||
|
||||
/* High level Set operation. This function can be used in order to set
|
||||
* a key, whatever it was existing or not, to a new object.
|
||||
*
|
||||
@@ -308,6 +327,12 @@ void dbReplaceValue(redisDb *db, robj *key, robj *val) {
|
||||
* The client 'c' argument may be set to NULL if the operation is performed
|
||||
* in a context where there is no clear client performing the operation. */
|
||||
void setKey(client *c, redisDb *db, robj *key, robj *val, int flags) {
|
||||
setKeyWithDictEntry(c,db,key,val,flags,NULL);
|
||||
}
|
||||
|
||||
/* Like setKey(), but accepts an optional dictEntry input,
|
||||
* which can be used if we already have one, thus saving the dictFind call. */
|
||||
void setKeyWithDictEntry(client *c, redisDb *db, robj *key, robj *val, int flags, dictEntry *de) {
|
||||
int keyfound = 0;
|
||||
|
||||
if (flags & SETKEY_ALREADY_EXIST)
|
||||
@@ -322,7 +347,7 @@ void setKey(client *c, redisDb *db, robj *key, robj *val, int flags) {
|
||||
} else if (keyfound<0) {
|
||||
dbAddInternal(db,key,val,1);
|
||||
} else {
|
||||
dbSetValue(db,key,val,1,NULL);
|
||||
dbSetValue(db,key,val,1,de);
|
||||
}
|
||||
incrRefCount(val);
|
||||
if (!(flags & SETKEY_KEEPTTL)) removeExpire(db,key);
|
||||
@@ -452,12 +477,18 @@ int dbDelete(redisDb *db, robj *key) {
|
||||
* using an sdscat() call to append some data, or anything else.
|
||||
*/
|
||||
robj *dbUnshareStringValue(redisDb *db, robj *key, robj *o) {
|
||||
return dbUnshareStringValueWithDictEntry(db,key,o,NULL);
|
||||
}
|
||||
|
||||
/* Like dbUnshareStringValue(), but accepts a optional dictEntry,
|
||||
* which can be used if we already have one, thus saving the dbFind call. */
|
||||
robj *dbUnshareStringValueWithDictEntry(redisDb *db, robj *key, robj *o, dictEntry *de) {
|
||||
serverAssert(o->type == OBJ_STRING);
|
||||
if (o->refcount != 1 || o->encoding != OBJ_ENCODING_RAW) {
|
||||
robj *decoded = getDecodedObject(o);
|
||||
o = createRawStringObject(decoded->ptr, sdslen(decoded->ptr));
|
||||
decrRefCount(decoded);
|
||||
dbReplaceValue(db,key,o);
|
||||
dbReplaceValueWithDictEntry(db,key,o,de);
|
||||
}
|
||||
return o;
|
||||
}
|
||||
@@ -575,11 +606,11 @@ redisDb *initTempDb(void) {
|
||||
}
|
||||
|
||||
/* Discard tempDb, this can be slow (similar to FLUSHALL), but it's always async. */
|
||||
void discardTempDb(redisDb *tempDb, void(callback)(dict*)) {
|
||||
void discardTempDb(redisDb *tempDb) {
|
||||
int async = 1;
|
||||
|
||||
/* Release temp DBs. */
|
||||
emptyDbStructure(tempDb, -1, async, callback);
|
||||
emptyDbStructure(tempDb, -1, async, NULL);
|
||||
for (int i=0; i<server.dbnum; i++) {
|
||||
/* Destroy global HFE DS before deleting the hashes since ebuckets DS is
|
||||
* embedded in the stored objects. */
|
||||
@@ -944,11 +975,10 @@ void scanCallback(void *privdata, const dictEntry *de) {
|
||||
serverAssert(!((data->type != LLONG_MAX) && o));
|
||||
|
||||
/* Filter an element if it isn't the type we want. */
|
||||
/* TODO: uncomment in redis 8.0
|
||||
if (!o && data->type != LLONG_MAX) {
|
||||
robj *rval = dictGetVal(de);
|
||||
if (!objectTypeCompare(rval, data->type)) return;
|
||||
}*/
|
||||
}
|
||||
|
||||
/* Filter element if it does not match the pattern. */
|
||||
void *keyStr = dictGetKey(de);
|
||||
@@ -1095,9 +1125,8 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) {
|
||||
typename = c->argv[i+1]->ptr;
|
||||
type = getObjectTypeByName(typename);
|
||||
if (type == LLONG_MAX) {
|
||||
/* TODO: uncomment in redis 8.0
|
||||
addReplyErrorFormat(c, "unknown type name '%s'", typename);
|
||||
return; */
|
||||
return;
|
||||
}
|
||||
i+= 2;
|
||||
} else if (!strcasecmp(c->argv[i]->ptr, "novalues")) {
|
||||
@@ -1285,16 +1314,7 @@ void scanGenericCommand(client *c, robj *o, unsigned long long cursor) {
|
||||
while ((ln = listNext(&li))) {
|
||||
sds key = listNodeValue(ln);
|
||||
initStaticStringObject(kobj, key);
|
||||
/* Filter an element if it isn't the type we want. */
|
||||
/* TODO: remove this in redis 8.0 */
|
||||
if (typename) {
|
||||
robj* typecheck = lookupKeyReadWithFlags(c->db, &kobj, LOOKUP_NOTOUCH|LOOKUP_NONOTIFY);
|
||||
if (!typecheck || !objectTypeCompare(typecheck, type)) {
|
||||
listDelNode(keys, ln);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (expireIfNeeded(c->db, &kobj, 0) != KEY_VALID) {
|
||||
if (expireIfNeeded(c->db, &kobj, 0)) {
|
||||
listDelNode(keys, ln);
|
||||
}
|
||||
}
|
||||
@@ -1839,16 +1859,23 @@ int removeExpire(redisDb *db, robj *key) {
|
||||
return kvstoreDictDelete(db->expires, getKeySlot(key->ptr), key->ptr) == DICT_OK;
|
||||
}
|
||||
|
||||
|
||||
/* Set an expire to the specified key. If the expire is set in the context
|
||||
* of an user calling a command 'c' is the client, otherwise 'c' is set
|
||||
* to NULL. The 'when' parameter is the absolute unix time in milliseconds
|
||||
* after which the key will no longer be considered valid. */
|
||||
void setExpire(client *c, redisDb *db, robj *key, long long when) {
|
||||
dictEntry *kde, *de, *existing;
|
||||
setExpireWithDictEntry(c,db,key,when,NULL);
|
||||
}
|
||||
|
||||
/* Like setExpire(), but accepts an optional dictEntry input,
|
||||
* which can be used if we already have one, thus saving the kvstoreDictFind call. */
|
||||
void setExpireWithDictEntry(client *c, redisDb *db, robj *key, long long when, dictEntry *kde) {
|
||||
dictEntry *de, *existing;
|
||||
|
||||
/* Reuse the sds from the main dict in the expire dict */
|
||||
int slot = getKeySlot(key->ptr);
|
||||
kde = kvstoreDictFind(db->keys, slot, key->ptr);
|
||||
if (!kde) kde = kvstoreDictFind(db->keys, slot, key->ptr);
|
||||
serverAssertWithInfo(NULL,key,kde != NULL);
|
||||
de = kvstoreDictAddRaw(db->expires, slot, dictGetKey(kde), &existing);
|
||||
if (existing) {
|
||||
|
||||
@@ -567,6 +567,7 @@ NULL
|
||||
addReplyError(c,"Error trying to load the RDB dump, check server logs.");
|
||||
return;
|
||||
}
|
||||
applyAppendOnlyConfig(); /* Check if AOF config was changed while loading */
|
||||
serverLog(LL_NOTICE,"DB reloaded by DEBUG RELOAD");
|
||||
addReply(c,shared.ok);
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"loadaof")) {
|
||||
@@ -582,6 +583,7 @@ NULL
|
||||
addReplyError(c, "Error trying to load the AOF files, check server logs.");
|
||||
return;
|
||||
}
|
||||
applyAppendOnlyConfig(); /* Check if AOF config was changed while loading */
|
||||
server.dirty = 0; /* Prevent AOF / replication */
|
||||
serverLog(LL_NOTICE,"Append Only File loaded by DEBUG LOADAOF");
|
||||
addReply(c,shared.ok);
|
||||
|
||||
@@ -54,6 +54,17 @@ void* activeDefragAlloc(void *ptr) {
|
||||
return newptr;
|
||||
}
|
||||
|
||||
/* Raw memory allocation for defrag, avoid using tcache. */
|
||||
void *activeDefragAllocRaw(size_t size) {
|
||||
return zmalloc_no_tcache(size);
|
||||
}
|
||||
|
||||
/* Raw memory free for defrag, avoid using tcache. */
|
||||
void activeDefragFreeRaw(void *ptr) {
|
||||
zfree_no_tcache(ptr);
|
||||
server.stat_active_defrag_hits++;
|
||||
}
|
||||
|
||||
/*Defrag helper for sds strings
|
||||
*
|
||||
* returns NULL in case the allocation wasn't moved.
|
||||
@@ -1078,6 +1089,7 @@ void activeDefragCycle(void) {
|
||||
slot = -1;
|
||||
defrag_later_item_in_progress = 0;
|
||||
db = NULL;
|
||||
moduleDefragEnd();
|
||||
goto update_metrics;
|
||||
}
|
||||
return;
|
||||
@@ -1119,6 +1131,10 @@ void activeDefragCycle(void) {
|
||||
break; /* this will exit the function and we'll continue on the next cycle */
|
||||
}
|
||||
|
||||
if (current_db == -1) {
|
||||
moduleDefragStart();
|
||||
}
|
||||
|
||||
/* Move on to next database, and stop if we reached the last one. */
|
||||
if (++current_db >= server.dbnum) {
|
||||
/* defrag other items not part of the db / keys */
|
||||
@@ -1140,6 +1156,8 @@ void activeDefragCycle(void) {
|
||||
db = NULL;
|
||||
server.active_defrag_running = 0;
|
||||
|
||||
moduleDefragEnd();
|
||||
|
||||
computeDefragCycles(); /* if another scan is needed, start it right away */
|
||||
if (server.active_defrag_running != 0 && ustime() < endtime)
|
||||
continue;
|
||||
@@ -1259,6 +1277,16 @@ void *activeDefragAlloc(void *ptr) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void *activeDefragAllocRaw(size_t size) {
|
||||
/* fallback to regular allocation */
|
||||
return zmalloc(size);
|
||||
}
|
||||
|
||||
void activeDefragFreeRaw(void *ptr) {
|
||||
/* fallback to regular free */
|
||||
zfree(ptr);
|
||||
}
|
||||
|
||||
robj *activeDefragStringOb(robj *ob) {
|
||||
UNUSED(ob);
|
||||
return NULL;
|
||||
|
||||
+12
@@ -257,6 +257,18 @@ dictStats* dictGetStatsHt(dict *d, int htidx, int full);
|
||||
void dictCombineStats(dictStats *from, dictStats *into);
|
||||
void dictFreeStats(dictStats *stats);
|
||||
|
||||
#define dictForEach(d, ty, m, ...) do { \
|
||||
dictIterator *di = dictGetIterator(d); \
|
||||
dictEntry *de; \
|
||||
while ((de = dictNext(di)) != NULL) { \
|
||||
ty *m = dictGetVal(de); \
|
||||
do { \
|
||||
__VA_ARGS__ \
|
||||
} while(0); \
|
||||
} \
|
||||
dictReleaseIterator(di); \
|
||||
} while(0);
|
||||
|
||||
#ifdef REDIS_TEST
|
||||
int dictTest(int argc, char *argv[], int flags);
|
||||
#endif
|
||||
|
||||
+1
-3
@@ -465,12 +465,10 @@ void expireSlaveKeys(void) {
|
||||
if ((dbids & 1) != 0) {
|
||||
redisDb *db = server.db+dbid;
|
||||
dictEntry *expire = dbFindExpires(db, keyname);
|
||||
int expired = 0;
|
||||
|
||||
if (expire &&
|
||||
activeExpireCycleTryExpire(server.db+dbid,expire,start))
|
||||
{
|
||||
expired = 1;
|
||||
/* Propagate the DEL (writable replicas do not propagate anything to other replicas,
|
||||
* but they might propagate to AOF) and trigger module hooks. */
|
||||
postExecutionUnitOperations();
|
||||
@@ -480,7 +478,7 @@ void expireSlaveKeys(void) {
|
||||
* corresponding bit in the new bitmap we set as value.
|
||||
* At the end of the loop if the bitmap is zero, it means we
|
||||
* no longer need to keep track of this key. */
|
||||
if (expire && !expired) {
|
||||
else if (expire) {
|
||||
noexpire++;
|
||||
new_dbids |= (uint64_t)1 << dbid;
|
||||
}
|
||||
|
||||
+1
-1
@@ -171,7 +171,7 @@ void functionsLibCtxClear(functionsLibCtx *lib_ctx) {
|
||||
stats->n_lib = 0;
|
||||
}
|
||||
dictReleaseIterator(iter);
|
||||
curr_functions_lib_ctx->cache_memory = 0;
|
||||
lib_ctx->cache_memory = 0;
|
||||
}
|
||||
|
||||
void functionsLibCtxClearCurrent(int async) {
|
||||
|
||||
@@ -796,8 +796,8 @@ void georadiusGeneric(client *c, int srcKeyIndex, int flags) {
|
||||
|
||||
if (withcoords) {
|
||||
addReplyArrayLen(c, 2);
|
||||
addReplyHumanLongDouble(c, gp->longitude);
|
||||
addReplyHumanLongDouble(c, gp->latitude);
|
||||
addReplyDouble(c,gp->longitude);
|
||||
addReplyDouble(c,gp->latitude);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -959,8 +959,8 @@ void geoposCommand(client *c) {
|
||||
continue;
|
||||
}
|
||||
addReplyArrayLen(c,2);
|
||||
addReplyHumanLongDouble(c,xy[0]);
|
||||
addReplyHumanLongDouble(c,xy[1]);
|
||||
addReplyDouble(c,xy[0]);
|
||||
addReplyDouble(c,xy[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-12
@@ -429,7 +429,7 @@ uint64_t MurmurHash64A (const void * key, int len, unsigned int seed) {
|
||||
* of the pattern 000..1 of the element hash. As a side effect 'regp' is
|
||||
* set to the register index this element hashes to. */
|
||||
int hllPatLen(unsigned char *ele, size_t elesize, long *regp) {
|
||||
uint64_t hash, bit, index;
|
||||
uint64_t hash, index;
|
||||
int count;
|
||||
|
||||
/* Count the number of zeroes starting from bit HLL_REGISTERS
|
||||
@@ -439,21 +439,14 @@ int hllPatLen(unsigned char *ele, size_t elesize, long *regp) {
|
||||
* Note that the final "1" ending the sequence of zeroes must be
|
||||
* included in the count, so if we find "001" the count is 3, and
|
||||
* the smallest count possible is no zeroes at all, just a 1 bit
|
||||
* at the first position, that is a count of 1.
|
||||
*
|
||||
* This may sound like inefficient, but actually in the average case
|
||||
* there are high probabilities to find a 1 after a few iterations. */
|
||||
* at the first position, that is a count of 1. */
|
||||
hash = MurmurHash64A(ele,elesize,0xadc83b19ULL);
|
||||
index = hash & HLL_P_MASK; /* Register index. */
|
||||
hash >>= HLL_P; /* Remove bits used to address the register. */
|
||||
hash |= ((uint64_t)1<<HLL_Q); /* Make sure the loop terminates
|
||||
and count will be <= Q+1. */
|
||||
bit = 1;
|
||||
count = 1; /* Initialized to 1 since we count the "00000...1" pattern. */
|
||||
while((hash & bit) == 0) {
|
||||
count++;
|
||||
bit <<= 1;
|
||||
}
|
||||
|
||||
count = __builtin_ctzll(hash) + 1;
|
||||
*regp = (int) index;
|
||||
return count;
|
||||
}
|
||||
@@ -648,7 +641,7 @@ int hllSparseSet(robj *o, long index, uint8_t count) {
|
||||
* for future reallocates on incremental growth. But we do not allocate more than
|
||||
* 'server.hll_sparse_max_bytes' bytes for the sparse representation.
|
||||
* If the available size of hyperloglog sds string is not enough for the increment
|
||||
* we need, we promote the hypreloglog to dense representation in 'step 3'.
|
||||
* we need, we promote the hyperloglog to dense representation in 'step 3'.
|
||||
*/
|
||||
if (sdsalloc(o->ptr) < server.hll_sparse_max_bytes && sdsavail(o->ptr) < 3) {
|
||||
size_t newlen = sdslen(o->ptr) + 3;
|
||||
|
||||
+28
-1
@@ -124,7 +124,9 @@ static void cumulativeKeyCountAdd(kvstore *kvs, int didx, long delta) {
|
||||
|
||||
dict *d = kvstoreGetDict(kvs, didx);
|
||||
size_t dsize = dictSize(d);
|
||||
int non_empty_dicts_delta = dsize == 1? 1 : dsize == 0? -1 : 0;
|
||||
/* Increment if dsize is 1 and delta is positive (first element inserted, dict becomes non-empty).
|
||||
* Decrement if dsize is 0 (dict becomes empty). */
|
||||
int non_empty_dicts_delta = (dsize == 1 && delta > 0) ? 1 : (dsize == 0) ? -1 : 0;
|
||||
kvs->non_empty_dicts += non_empty_dicts_delta;
|
||||
|
||||
/* BIT does not need to be calculated when there's only one dict. */
|
||||
@@ -1026,6 +1028,31 @@ int kvstoreTest(int argc, char **argv, int flags) {
|
||||
kvstoreRelease(kvs);
|
||||
}
|
||||
|
||||
TEST("Verify non-empty dict count is correctly updated") {
|
||||
kvstore *kvs = kvstoreCreate(&KvstoreDictTestType, 2, KVSTORE_ALLOCATE_DICTS_ON_DEMAND);
|
||||
for (int idx = 0; idx < 4; idx++) {
|
||||
for (i = 0; i < 16; i++) {
|
||||
de = kvstoreDictAddRaw(kvs, idx, stringFromInt(i), NULL);
|
||||
assert(de != NULL);
|
||||
/* When the first element is inserted, the number of non-empty dictionaries is increased by 1. */
|
||||
if (i == 0) assert(kvstoreNumNonEmptyDicts(kvs) == idx + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Step by step, clear all dictionaries and ensure non-empty dict count is updated */
|
||||
for (int idx = 0; idx < 4; idx++) {
|
||||
kvs_di = kvstoreGetDictSafeIterator(kvs, idx);
|
||||
while((de = kvstoreDictIteratorNext(kvs_di)) != NULL) {
|
||||
key = dictGetKey(de);
|
||||
assert(kvstoreDictDelete(kvs, idx, key) == DICT_OK);
|
||||
/* When the dictionary is emptied, the number of non-empty dictionaries is reduced by 1. */
|
||||
if (kvstoreDictSize(kvs, idx) == 0) assert(kvstoreNumNonEmptyDicts(kvs) == 3 - idx);
|
||||
}
|
||||
kvstoreReleaseDictIterator(kvs_di);
|
||||
}
|
||||
kvstoreRelease(kvs);
|
||||
}
|
||||
|
||||
kvstoreRelease(kvs1);
|
||||
kvstoreRelease(kvs2);
|
||||
return 0;
|
||||
|
||||
+10
-4
@@ -687,8 +687,8 @@ int lpGetIntegerValue(unsigned char *p, long long *lval) {
|
||||
* will be returned. 'user' is passed to this callback.
|
||||
* Skip 'skip' entries between every comparison.
|
||||
* Returns NULL when the field could not be found. */
|
||||
unsigned char *lpFindCb(unsigned char *lp, unsigned char *p,
|
||||
void *user, lpCmp cmp, unsigned int skip)
|
||||
static inline unsigned char *lpFindCbInternal(unsigned char *lp, unsigned char *p,
|
||||
void *user, lpCmp cmp, unsigned int skip)
|
||||
{
|
||||
int skipcnt = 0;
|
||||
unsigned char *value;
|
||||
@@ -707,7 +707,7 @@ unsigned char *lpFindCb(unsigned char *lp, unsigned char *p,
|
||||
assert(p >= lp + LP_HDR_SIZE && p + entry_size < lp + lp_bytes);
|
||||
}
|
||||
|
||||
if (cmp(lp, p, user, value, ll) == 0)
|
||||
if (unlikely(cmp(lp, p, user, value, ll) == 0))
|
||||
return p;
|
||||
|
||||
/* Reset skip count */
|
||||
@@ -734,6 +734,12 @@ unsigned char *lpFindCb(unsigned char *lp, unsigned char *p,
|
||||
return NULL;
|
||||
}
|
||||
|
||||
unsigned char *lpFindCb(unsigned char *lp, unsigned char *p,
|
||||
void *user, lpCmp cmp, unsigned int skip)
|
||||
{
|
||||
return lpFindCbInternal(lp, p, user, cmp, skip);
|
||||
}
|
||||
|
||||
struct lpFindArg {
|
||||
unsigned char *s; /* Item to search */
|
||||
uint32_t slen; /* Item len */
|
||||
@@ -787,7 +793,7 @@ unsigned char *lpFind(unsigned char *lp, unsigned char *p, unsigned char *s,
|
||||
.s = s,
|
||||
.slen = slen
|
||||
};
|
||||
return lpFindCb(lp, p, &arg, lpFindCmp, skip);
|
||||
return lpFindCbInternal(lp, p, &arg, lpFindCmp, skip);
|
||||
}
|
||||
|
||||
/* Insert, delete or replace the specified string element 'elestr' of length
|
||||
|
||||
+72
-13
@@ -2287,6 +2287,8 @@ void RM_SetModuleAttribs(RedisModuleCtx *ctx, const char *name, int ver, int api
|
||||
module->options = 0;
|
||||
module->info_cb = 0;
|
||||
module->defrag_cb = 0;
|
||||
module->defrag_start_cb = 0;
|
||||
module->defrag_end_cb = 0;
|
||||
module->loadmod = NULL;
|
||||
module->num_commands_with_acl_categories = 0;
|
||||
module->onload = 1;
|
||||
@@ -4274,7 +4276,7 @@ int RM_SetAbsExpire(RedisModuleKey *key, mstime_t expire) {
|
||||
void RM_ResetDataset(int restart_aof, int async) {
|
||||
if (restart_aof && server.aof_state != AOF_OFF) stopAppendOnly();
|
||||
flushAllDataAndResetRDB((async? EMPTYDB_ASYNC: EMPTYDB_NO_FLAGS) | EMPTYDB_NOFUNCTIONS);
|
||||
if (server.aof_enabled && restart_aof) restartAOFAfterSYNC();
|
||||
if (server.aof_enabled && restart_aof) startAppendOnlyWithRetry();
|
||||
}
|
||||
|
||||
/* Returns the number of keys in the current db. */
|
||||
@@ -12463,10 +12465,15 @@ void modulePipeReadable(aeEventLoop *el, int fd, void *privdata, int mask) {
|
||||
/* Helper function for the MODULE and HELLO command: send the list of the
|
||||
* loaded modules to the client. */
|
||||
void addReplyLoadedModules(client *c) {
|
||||
const long ln = dictSize(modules);
|
||||
/* In case no module is load we avoid iterator creation */
|
||||
addReplyArrayLen(c,ln);
|
||||
if (ln == 0) {
|
||||
return;
|
||||
}
|
||||
dictIterator *di = dictGetIterator(modules);
|
||||
dictEntry *de;
|
||||
|
||||
addReplyArrayLen(c,dictSize(modules));
|
||||
while ((de = dictNext(di)) != NULL) {
|
||||
sds name = dictGetKey(de);
|
||||
struct RedisModule *module = dictGetVal(de);
|
||||
@@ -13070,7 +13077,7 @@ int RM_RdbLoad(RedisModuleCtx *ctx, RedisModuleRdbStream *stream, int flags) {
|
||||
int ret = rdbLoad(stream->data.filename,NULL,RDBFLAGS_NONE);
|
||||
|
||||
if (server.current_client) unprotectClient(server.current_client);
|
||||
if (server.aof_state != AOF_OFF) startAppendOnly();
|
||||
if (server.aof_enabled) startAppendOnlyWithRetry();
|
||||
|
||||
if (ret != RDB_OK) {
|
||||
errno = (ret == RDB_NOT_EXIST) ? ENOENT : EIO;
|
||||
@@ -13451,6 +13458,16 @@ int RM_RegisterDefragFunc(RedisModuleCtx *ctx, RedisModuleDefragFunc cb) {
|
||||
return REDISMODULE_OK;
|
||||
}
|
||||
|
||||
/* Register a defrag callbacks that will be called when defrag operation starts and ends.
|
||||
*
|
||||
* The callbacks are the same as `RM_RegisterDefragFunc` but the user
|
||||
* can also assume the callbacks are called when the defrag operation starts and ends. */
|
||||
int RM_RegisterDefragCallbacks(RedisModuleCtx *ctx, RedisModuleDefragFunc start, RedisModuleDefragFunc end) {
|
||||
ctx->module->defrag_start_cb = start;
|
||||
ctx->module->defrag_end_cb = end;
|
||||
return REDISMODULE_OK;
|
||||
}
|
||||
|
||||
/* When the data type defrag callback iterates complex structures, this
|
||||
* function should be called periodically. A zero (false) return
|
||||
* indicates the callback may continue its work. A non-zero value (true)
|
||||
@@ -13529,6 +13546,30 @@ void *RM_DefragAlloc(RedisModuleDefragCtx *ctx, void *ptr) {
|
||||
return activeDefragAlloc(ptr);
|
||||
}
|
||||
|
||||
/* Allocate memory for defrag purposes
|
||||
*
|
||||
* On the common cases user simply want to reallocate a pointer with a single
|
||||
* owner. For such usecase RM_DefragAlloc is enough. But on some usecases the user
|
||||
* might want to replace a pointer with multiple owners in different keys.
|
||||
* In such case, an in place replacement can not work because the other key still
|
||||
* keep a pointer to the old value.
|
||||
*
|
||||
* RM_DefragAllocRaw and RM_DefragFreeRaw allows to control when the memory
|
||||
* for defrag purposes will be allocated and when it will be freed,
|
||||
* allow to support more complex defrag usecases. */
|
||||
void *RM_DefragAllocRaw(RedisModuleDefragCtx *ctx, size_t size) {
|
||||
UNUSED(ctx);
|
||||
return activeDefragAllocRaw(size);
|
||||
}
|
||||
|
||||
/* Free memory for defrag purposes
|
||||
*
|
||||
* See RM_DefragAllocRaw for more information. */
|
||||
void RM_DefragFreeRaw(RedisModuleDefragCtx *ctx, void *ptr) {
|
||||
UNUSED(ctx);
|
||||
activeDefragFreeRaw(ptr);
|
||||
}
|
||||
|
||||
/* Defrag a RedisModuleString previously allocated by RM_Alloc, RM_Calloc, etc.
|
||||
* See RM_DefragAlloc() for more information on how the defragmentation process
|
||||
* works.
|
||||
@@ -13610,17 +13651,32 @@ int moduleDefragValue(robj *key, robj *value, int dbid) {
|
||||
|
||||
/* Call registered module API defrag functions */
|
||||
void moduleDefragGlobals(void) {
|
||||
dictIterator *di = dictGetIterator(modules);
|
||||
dictEntry *de;
|
||||
dictForEach(modules, struct RedisModule, module,
|
||||
if (module->defrag_cb) {
|
||||
RedisModuleDefragCtx defrag_ctx = { 0, NULL, NULL, -1};
|
||||
module->defrag_cb(&defrag_ctx);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
while ((de = dictNext(di)) != NULL) {
|
||||
struct RedisModule *module = dictGetVal(de);
|
||||
if (!module->defrag_cb)
|
||||
continue;
|
||||
RedisModuleDefragCtx defrag_ctx = { 0, NULL, NULL, -1};
|
||||
module->defrag_cb(&defrag_ctx);
|
||||
}
|
||||
dictReleaseIterator(di);
|
||||
/* Call registered module API defrag start functions */
|
||||
void moduleDefragStart(void) {
|
||||
dictForEach(modules, struct RedisModule, module,
|
||||
if (module->defrag_start_cb) {
|
||||
RedisModuleDefragCtx defrag_ctx = { 0, NULL, NULL, -1};
|
||||
module->defrag_start_cb(&defrag_ctx);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/* Call registered module API defrag end functions */
|
||||
void moduleDefragEnd(void) {
|
||||
dictForEach(modules, struct RedisModule, module,
|
||||
if (module->defrag_end_cb) {
|
||||
RedisModuleDefragCtx defrag_ctx = { 0, NULL, NULL, -1};
|
||||
module->defrag_end_cb(&defrag_ctx);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/* Returns the name of the key currently being processed.
|
||||
@@ -13980,7 +14036,10 @@ void moduleRegisterCoreAPI(void) {
|
||||
REGISTER_API(GetCurrentCommandName);
|
||||
REGISTER_API(GetTypeMethodVersion);
|
||||
REGISTER_API(RegisterDefragFunc);
|
||||
REGISTER_API(RegisterDefragCallbacks);
|
||||
REGISTER_API(DefragAlloc);
|
||||
REGISTER_API(DefragAllocRaw);
|
||||
REGISTER_API(DefragFreeRaw);
|
||||
REGISTER_API(DefragRedisModuleString);
|
||||
REGISTER_API(DefragShouldStop);
|
||||
REGISTER_API(DefragCursorSet);
|
||||
|
||||
+96
-32
@@ -26,7 +26,11 @@ static void setProtocolError(const char *errstr, client *c);
|
||||
static void pauseClientsByClient(mstime_t end, int isPauseClientAll);
|
||||
int postponeClientRead(client *c);
|
||||
char *getClientSockname(client *c);
|
||||
static inline int clientTypeIsSlave(client *c);
|
||||
int ProcessingEventsWhileBlocked = 0; /* See processEventsWhileBlocked(). */
|
||||
__thread sds thread_reusable_qb = NULL;
|
||||
__thread int thread_reusable_qb_used = 0; /* Avoid multiple clients using reusable query
|
||||
* buffer due to nested command execution. */
|
||||
|
||||
/* Return the size consumed from the allocator, for the specified SDS string,
|
||||
* including internal fragmentation. This function is used in order to compute
|
||||
@@ -144,7 +148,7 @@ client *createClient(connection *conn) {
|
||||
c->ref_repl_buf_node = NULL;
|
||||
c->ref_block_pos = 0;
|
||||
c->qb_pos = 0;
|
||||
c->querybuf = sdsempty();
|
||||
c->querybuf = NULL;
|
||||
c->querybuf_peak = 0;
|
||||
c->reqtype = 0;
|
||||
c->argc = 0;
|
||||
@@ -391,7 +395,7 @@ void _addReplyToBufferOrList(client *c, const char *s, size_t len) {
|
||||
* replication link that caused a reply to be generated we'll simply disconnect it.
|
||||
* Note this is the simplest way to check a command added a response. Replication links are used to write data but
|
||||
* not for responses, so we should normally never get here on a replica client. */
|
||||
if (getClientType(c) == CLIENT_TYPE_SLAVE) {
|
||||
if (unlikely(clientTypeIsSlave(c))) {
|
||||
sds cmdname = c->lastcmd ? c->lastcmd->fullname : NULL;
|
||||
logInvalidUseAndFreeClientAsync(c, "Replica generated a reply to command '%s'",
|
||||
cmdname ? cmdname : "<unknown>");
|
||||
@@ -733,7 +737,7 @@ void *addReplyDeferredLen(client *c) {
|
||||
* replication link that caused a reply to be generated we'll simply disconnect it.
|
||||
* Note this is the simplest way to check a command added a response. Replication links are used to write data but
|
||||
* not for responses, so we should normally never get here on a replica client. */
|
||||
if (getClientType(c) == CLIENT_TYPE_SLAVE) {
|
||||
if (unlikely(clientTypeIsSlave(c))) {
|
||||
sds cmdname = c->lastcmd ? c->lastcmd->fullname : NULL;
|
||||
logInvalidUseAndFreeClientAsync(c, "Replica generated a reply to command '%s'",
|
||||
cmdname ? cmdname : "<unknown>");
|
||||
@@ -970,6 +974,12 @@ void addReplyLongLong(client *c, long long ll) {
|
||||
}
|
||||
}
|
||||
|
||||
void addReplyLongLongFromStr(client *c, robj *str) {
|
||||
addReplyProto(c,":",1);
|
||||
addReply(c,str);
|
||||
addReplyProto(c,"\r\n",2);
|
||||
}
|
||||
|
||||
void addReplyAggregateLen(client *c, long length, int prefix) {
|
||||
serverAssert(length >= 0);
|
||||
if (prepareClientToWrite(c) != C_OK) return;
|
||||
@@ -1242,7 +1252,7 @@ void copyReplicaOutputBuffer(client *dst, client *src) {
|
||||
/* Return true if the specified client has pending reply buffers to write to
|
||||
* the socket. */
|
||||
int clientHasPendingReplies(client *c) {
|
||||
if (getClientType(c) == CLIENT_TYPE_SLAVE) {
|
||||
if (unlikely(clientTypeIsSlave(c))) {
|
||||
/* Replicas use global shared replication buffer instead of
|
||||
* private output buffer. */
|
||||
serverAssert(c->bufpos == 0 && listLength(c->reply) == 0);
|
||||
@@ -1575,6 +1585,28 @@ void deauthenticateAndCloseClient(client *c) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Resets the reusable query buffer used by the given client.
|
||||
* If any data remained in the buffer, the client will take ownership of the buffer
|
||||
* and a new empty buffer will be allocated for the reusable buffer. */
|
||||
static void resetReusableQueryBuf(client *c) {
|
||||
serverAssert(c->flags & CLIENT_REUSABLE_QUERYBUFFER);
|
||||
if (c->querybuf != thread_reusable_qb || sdslen(c->querybuf) > c->qb_pos) {
|
||||
/* If querybuf has been reallocated or there is still data left,
|
||||
* let the client take ownership of the reusable buffer. */
|
||||
thread_reusable_qb = NULL;
|
||||
} else {
|
||||
/* It is safe to dereference and reuse the reusable query buffer. */
|
||||
c->querybuf = NULL;
|
||||
c->qb_pos = 0;
|
||||
sdsclear(thread_reusable_qb);
|
||||
}
|
||||
|
||||
/* Mark that the client is no longer using the reusable query buffer
|
||||
* and indicate that it is no longer used by any client. */
|
||||
c->flags &= ~CLIENT_REUSABLE_QUERYBUFFER;
|
||||
thread_reusable_qb_used = 0;
|
||||
}
|
||||
|
||||
void freeClient(client *c) {
|
||||
listNode *ln;
|
||||
|
||||
@@ -1623,12 +1655,14 @@ void freeClient(client *c) {
|
||||
}
|
||||
|
||||
/* Log link disconnection with slave */
|
||||
if (getClientType(c) == CLIENT_TYPE_SLAVE) {
|
||||
if (clientTypeIsSlave(c)) {
|
||||
serverLog(LL_NOTICE,"Connection with replica %s lost.",
|
||||
replicationGetSlaveName(c));
|
||||
}
|
||||
|
||||
/* Free the query buffer */
|
||||
if (c->flags & CLIENT_REUSABLE_QUERYBUFFER)
|
||||
resetReusableQueryBuf(c);
|
||||
sdsfree(c->querybuf);
|
||||
c->querybuf = NULL;
|
||||
|
||||
@@ -1703,7 +1737,7 @@ void freeClient(client *c) {
|
||||
/* We need to remember the time when we started to have zero
|
||||
* attached slaves, as after some time we'll free the replication
|
||||
* backlog. */
|
||||
if (getClientType(c) == CLIENT_TYPE_SLAVE && listLength(server.slaves) == 0)
|
||||
if (clientTypeIsSlave(c) && listLength(server.slaves) == 0)
|
||||
server.repl_no_slaves_since = server.unixtime;
|
||||
refreshGoodSlavesCount();
|
||||
/* Fire the replica change modules event. */
|
||||
@@ -1916,7 +1950,7 @@ static int _writevToClient(client *c, ssize_t *nwritten) {
|
||||
* to client. */
|
||||
int _writeToClient(client *c, ssize_t *nwritten) {
|
||||
*nwritten = 0;
|
||||
if (getClientType(c) == CLIENT_TYPE_SLAVE) {
|
||||
if (unlikely(clientTypeIsSlave(c))) {
|
||||
serverAssert(c->bufpos == 0 && listLength(c->reply) == 0);
|
||||
|
||||
replBufBlock *o = listNodeValue(c->ref_repl_buf_node);
|
||||
@@ -2003,7 +2037,7 @@ int writeToClient(client *c, int handler_installed) {
|
||||
!(c->flags & CLIENT_SLAVE)) break;
|
||||
}
|
||||
|
||||
if (getClientType(c) == CLIENT_TYPE_SLAVE) {
|
||||
if (unlikely(clientTypeIsSlave(c))) {
|
||||
atomicIncr(server.stat_net_repl_output_bytes, totwritten);
|
||||
} else {
|
||||
atomicIncr(server.stat_net_output_bytes, totwritten);
|
||||
@@ -2207,7 +2241,7 @@ int processInlineBuffer(client *c) {
|
||||
/* Newline from slaves can be used to refresh the last ACK time.
|
||||
* This is useful for a slave to ping back while loading a big
|
||||
* RDB file. */
|
||||
if (querylen == 0 && getClientType(c) == CLIENT_TYPE_SLAVE)
|
||||
if (querylen == 0 && clientTypeIsSlave(c))
|
||||
c->repl_ack_time = server.unixtime;
|
||||
|
||||
/* Masters should never send us inline protocol to run actual
|
||||
@@ -2674,6 +2708,11 @@ void readQueryFromClient(connection *conn) {
|
||||
if (c->reqtype == PROTO_REQ_MULTIBULK && c->multibulklen && c->bulklen != -1
|
||||
&& c->bulklen >= PROTO_MBULK_BIG_ARG)
|
||||
{
|
||||
/* For big argv, the client always uses its private query buffer.
|
||||
* Using the reusable query buffer would eventually expand it beyond 32k,
|
||||
* causing the client to take ownership of the reusable query buffer. */
|
||||
if (!c->querybuf) c->querybuf = sdsempty();
|
||||
|
||||
ssize_t remaining = (size_t)(c->bulklen+2)-(sdslen(c->querybuf)-c->qb_pos);
|
||||
big_arg = 1;
|
||||
|
||||
@@ -2685,6 +2724,26 @@ void readQueryFromClient(connection *conn) {
|
||||
* but doesn't need align to the next arg, we can read more data. */
|
||||
if (c->flags & CLIENT_MASTER && readlen < PROTO_IOBUF_LEN)
|
||||
readlen = PROTO_IOBUF_LEN;
|
||||
} else if (c->querybuf == NULL) {
|
||||
if (unlikely(thread_reusable_qb_used)) {
|
||||
/* The reusable query buffer is already used by another client,
|
||||
* switch to using the client's private query buffer. This only
|
||||
* occurs when commands are executed nested via processEventsWhileBlocked(). */
|
||||
c->querybuf = sdsnewlen(NULL, PROTO_IOBUF_LEN);
|
||||
sdsclear(c->querybuf);
|
||||
} else {
|
||||
/* Create the reusable query buffer if it doesn't exist. */
|
||||
if (!thread_reusable_qb) {
|
||||
thread_reusable_qb = sdsnewlen(NULL, PROTO_IOBUF_LEN);
|
||||
sdsclear(thread_reusable_qb);
|
||||
}
|
||||
|
||||
/* Assign the reusable query buffer to the client and mark it as in use. */
|
||||
serverAssert(sdslen(thread_reusable_qb) == 0);
|
||||
c->querybuf = thread_reusable_qb;
|
||||
c->flags |= CLIENT_REUSABLE_QUERYBUFFER;
|
||||
thread_reusable_qb_used = 1;
|
||||
}
|
||||
}
|
||||
|
||||
qblen = sdslen(c->querybuf);
|
||||
@@ -2708,7 +2767,7 @@ void readQueryFromClient(connection *conn) {
|
||||
nread = connRead(c->conn, c->querybuf+qblen, readlen);
|
||||
if (nread == -1) {
|
||||
if (connGetState(conn) == CONN_STATE_CONNECTED) {
|
||||
return;
|
||||
goto done;
|
||||
} else {
|
||||
serverLog(LL_VERBOSE, "Reading from client: %s",connGetLastError(c->conn));
|
||||
freeClientAsync(c);
|
||||
@@ -2760,6 +2819,10 @@ void readQueryFromClient(connection *conn) {
|
||||
c = NULL;
|
||||
|
||||
done:
|
||||
if (c && (c->flags & CLIENT_REUSABLE_QUERYBUFFER)) {
|
||||
serverAssert(c->qb_pos == 0); /* Ensure the client's query buffer is trimmed in processInputBuffer */
|
||||
resetReusableQueryBuf(c);
|
||||
}
|
||||
beforeNextClient(c);
|
||||
}
|
||||
|
||||
@@ -2875,8 +2938,8 @@ sds catClientInfoString(sds s, client *client) {
|
||||
" ssub=%i", (int) dictSize(client->pubsubshard_channels),
|
||||
" multi=%i", (client->flags & CLIENT_MULTI) ? client->mstate.count : -1,
|
||||
" watch=%i", (int) listLength(client->watched_keys),
|
||||
" qbuf=%U", (unsigned long long) sdslen(client->querybuf),
|
||||
" qbuf-free=%U", (unsigned long long) sdsavail(client->querybuf),
|
||||
" qbuf=%U", client->querybuf ? (unsigned long long) sdslen(client->querybuf) : 0,
|
||||
" qbuf-free=%U", client->querybuf ? (unsigned long long) sdsavail(client->querybuf) : 0,
|
||||
" argv-mem=%U", (unsigned long long) client->argv_len_sum,
|
||||
" multi-mem=%U", (unsigned long long) client->mstate.argv_len_sums,
|
||||
" rbs=%U", (unsigned long long) client->buf_usable_size,
|
||||
@@ -3662,29 +3725,30 @@ void helloCommand(client *c) {
|
||||
if (ver) c->resp = ver;
|
||||
addReplyMapLen(c,6 + !server.sentinel_mode);
|
||||
|
||||
addReplyBulkCString(c,"server");
|
||||
addReplyBulkCString(c,"redis");
|
||||
ADD_REPLY_BULK_CBUFFER_STRING_CONSTANT(c,"server");
|
||||
ADD_REPLY_BULK_CBUFFER_STRING_CONSTANT(c,"redis");
|
||||
|
||||
addReplyBulkCString(c,"version");
|
||||
addReplyBulkCString(c,REDIS_VERSION);
|
||||
ADD_REPLY_BULK_CBUFFER_STRING_CONSTANT(c,"version");
|
||||
ADD_REPLY_BULK_CBUFFER_STRING_CONSTANT(c,REDIS_VERSION);
|
||||
|
||||
ADD_REPLY_BULK_CBUFFER_STRING_CONSTANT(c,"proto");
|
||||
|
||||
addReplyBulkCString(c,"proto");
|
||||
addReplyLongLong(c,c->resp);
|
||||
|
||||
addReplyBulkCString(c,"id");
|
||||
ADD_REPLY_BULK_CBUFFER_STRING_CONSTANT(c,"id");
|
||||
addReplyLongLong(c,c->id);
|
||||
|
||||
addReplyBulkCString(c,"mode");
|
||||
ADD_REPLY_BULK_CBUFFER_STRING_CONSTANT(c,"mode");
|
||||
if (server.sentinel_mode) addReplyBulkCString(c,"sentinel");
|
||||
else if (server.cluster_enabled) addReplyBulkCString(c,"cluster");
|
||||
else addReplyBulkCString(c,"standalone");
|
||||
|
||||
if (!server.sentinel_mode) {
|
||||
addReplyBulkCString(c,"role");
|
||||
ADD_REPLY_BULK_CBUFFER_STRING_CONSTANT(c,"role");
|
||||
addReplyBulkCString(c,server.masterhost ? "replica" : "master");
|
||||
}
|
||||
|
||||
addReplyBulkCString(c,"modules");
|
||||
ADD_REPLY_BULK_CBUFFER_STRING_CONSTANT(c,"modules");
|
||||
addReplyLoadedModules(c);
|
||||
}
|
||||
|
||||
@@ -3834,7 +3898,7 @@ void rewriteClientCommandArgument(client *c, int i, robj *newval) {
|
||||
* the caller wishes. The main usage of this function currently is
|
||||
* enforcing the client output length limits. */
|
||||
size_t getClientOutputBufferMemoryUsage(client *c) {
|
||||
if (getClientType(c) == CLIENT_TYPE_SLAVE) {
|
||||
if (unlikely(clientTypeIsSlave(c))) {
|
||||
size_t repl_buf_size = 0;
|
||||
size_t repl_node_num = 0;
|
||||
size_t repl_node_size = sizeof(listNode) + sizeof(replBufBlock);
|
||||
@@ -3856,9 +3920,10 @@ size_t getClientOutputBufferMemoryUsage(client *c) {
|
||||
* the client output buffer memory usage portion of the total. */
|
||||
size_t getClientMemoryUsage(client *c, size_t *output_buffer_mem_usage) {
|
||||
size_t mem = getClientOutputBufferMemoryUsage(c);
|
||||
|
||||
if (output_buffer_mem_usage != NULL)
|
||||
*output_buffer_mem_usage = mem;
|
||||
mem += sdsZmallocSize(c->querybuf);
|
||||
mem += c->querybuf ? sdsZmallocSize(c->querybuf) : 0;
|
||||
mem += zmalloc_size(c);
|
||||
mem += c->buf_usable_size;
|
||||
/* For efficiency (less work keeping track of the argv memory), it doesn't include the used memory
|
||||
@@ -3897,6 +3962,12 @@ int getClientType(client *c) {
|
||||
return CLIENT_TYPE_NORMAL;
|
||||
}
|
||||
|
||||
static inline int clientTypeIsSlave(client *c) {
|
||||
if (unlikely((c->flags & CLIENT_SLAVE) && !(c->flags & CLIENT_MONITOR)))
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int getClientTypeByName(char *name) {
|
||||
if (!strcasecmp(name,"normal")) return CLIENT_TYPE_NORMAL;
|
||||
else if (!strcasecmp(name,"slave")) return CLIENT_TYPE_SLAVE;
|
||||
@@ -3986,7 +4057,7 @@ int closeClientOnOutputBufferLimitReached(client *c, int async) {
|
||||
serverAssert(c->reply_bytes < SIZE_MAX-(1024*64));
|
||||
/* Note that c->reply_bytes is irrelevant for replica clients
|
||||
* (they use the global repl buffers). */
|
||||
if ((c->reply_bytes == 0 && getClientType(c) != CLIENT_TYPE_SLAVE) ||
|
||||
if ((c->reply_bytes == 0 && !clientTypeIsSlave(c)) ||
|
||||
c->flags & CLIENT_CLOSE_ASAP) return 0;
|
||||
if (checkClientOutputBufferLimits(c)) {
|
||||
sds client = catClientInfoString(sdsempty(),c);
|
||||
@@ -4213,13 +4284,6 @@ void processEventsWhileBlocked(void) {
|
||||
* ========================================================================== */
|
||||
|
||||
#define IO_THREADS_MAX_NUM 128
|
||||
#ifndef CACHE_LINE_SIZE
|
||||
#if defined(__aarch64__) && defined(__APPLE__)
|
||||
#define CACHE_LINE_SIZE 128
|
||||
#else
|
||||
#define CACHE_LINE_SIZE 64
|
||||
#endif
|
||||
#endif
|
||||
|
||||
typedef struct __attribute__((aligned(CACHE_LINE_SIZE))) threads_pending {
|
||||
redisAtomic unsigned long value;
|
||||
@@ -4423,7 +4487,7 @@ int handleClientsWithPendingWritesUsingThreads(void) {
|
||||
* buffer, to guarantee data accessing thread safe, we must put all
|
||||
* replicas client into io_threads_list[0] i.e. main thread handles
|
||||
* sending the output buffer of all replicas. */
|
||||
if (getClientType(c) == CLIENT_TYPE_SLAVE) {
|
||||
if (unlikely(clientTypeIsSlave(c))) {
|
||||
listAddNodeTail(io_threads_list[0],c);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -3161,7 +3161,13 @@ emptykey:
|
||||
/* Mark that we are loading in the global state and setup the fields
|
||||
* needed to provide loading stats. */
|
||||
void startLoading(size_t size, int rdbflags, int async) {
|
||||
/* Load the DB */
|
||||
loadingSetFlags(NULL, size, async);
|
||||
loadingFireEvent(rdbflags);
|
||||
}
|
||||
|
||||
/* Initialize stats, set loading flags and filename if provided. */
|
||||
void loadingSetFlags(char *filename, size_t size, int async) {
|
||||
rdbFileBeingLoaded = filename;
|
||||
server.loading = 1;
|
||||
if (async == 1) server.async_loading = 1;
|
||||
server.loading_start_time = time(NULL);
|
||||
@@ -3171,7 +3177,9 @@ void startLoading(size_t size, int rdbflags, int async) {
|
||||
server.rdb_last_load_keys_expired = 0;
|
||||
server.rdb_last_load_keys_loaded = 0;
|
||||
blockingOperationStarts();
|
||||
}
|
||||
|
||||
void loadingFireEvent(int rdbflags) {
|
||||
/* Fire the loading modules start event. */
|
||||
int subevent;
|
||||
if (rdbflags & RDBFLAGS_AOF_PREAMBLE)
|
||||
@@ -3187,8 +3195,8 @@ void startLoading(size_t size, int rdbflags, int async) {
|
||||
* needed to provide loading stats.
|
||||
* 'filename' is optional and used for rdb-check on error */
|
||||
void startLoadingFile(size_t size, char* filename, int rdbflags) {
|
||||
rdbFileBeingLoaded = filename;
|
||||
startLoading(size, rdbflags, 0);
|
||||
loadingSetFlags(filename, size, 0);
|
||||
loadingFireEvent(rdbflags);
|
||||
}
|
||||
|
||||
/* Refresh the absolute loading progress info */
|
||||
@@ -3702,14 +3710,21 @@ eoferr:
|
||||
return C_ERR;
|
||||
}
|
||||
|
||||
int rdbLoad(char *filename, rdbSaveInfo *rsi, int rdbflags) {
|
||||
return rdbLoadWithEmptyFunc(filename, rsi, rdbflags, NULL);
|
||||
}
|
||||
|
||||
/* Like rdbLoadRio() but takes a filename instead of a rio stream. The
|
||||
* filename is open for reading and a rio stream object created in order
|
||||
* to do the actual loading. Moreover the ETA displayed in the INFO
|
||||
* output is initialized and finalized.
|
||||
*
|
||||
* If you pass an 'rsi' structure initialized with RDB_SAVE_INFO_INIT, the
|
||||
* loading code will fill the information fields in the structure. */
|
||||
int rdbLoad(char *filename, rdbSaveInfo *rsi, int rdbflags) {
|
||||
* loading code will fill the information fields in the structure.
|
||||
*
|
||||
* If emptyDbFunc is not NULL, it will be called to flush old db or to
|
||||
* discard partial db on error. */
|
||||
int rdbLoadWithEmptyFunc(char *filename, rdbSaveInfo *rsi, int rdbflags, void (*emptyDbFunc)(void)) {
|
||||
FILE *fp;
|
||||
rio rdb;
|
||||
int retval;
|
||||
@@ -3727,12 +3742,20 @@ int rdbLoad(char *filename, rdbSaveInfo *rsi, int rdbflags) {
|
||||
if (fstat(fileno(fp), &sb) == -1)
|
||||
sb.st_size = 0;
|
||||
|
||||
startLoadingFile(sb.st_size, filename, rdbflags);
|
||||
loadingSetFlags(filename, sb.st_size, 0);
|
||||
/* Note that inside loadingSetFlags(), server.loading is set.
|
||||
* emptyDbCallback() may yield back to event-loop to reply -LOADING. */
|
||||
if (emptyDbFunc)
|
||||
emptyDbFunc(); /* Flush existing db. */
|
||||
loadingFireEvent(rdbflags);
|
||||
rioInitWithFile(&rdb,fp);
|
||||
|
||||
retval = rdbLoadRio(&rdb,rdbflags,rsi);
|
||||
|
||||
fclose(fp);
|
||||
if (retval != C_OK && emptyDbFunc)
|
||||
emptyDbFunc(); /* Clean up partial db. */
|
||||
|
||||
stopLoading(retval==C_OK);
|
||||
/* Reclaim the cache backed by rdb */
|
||||
if (retval == C_OK && !(rdbflags & RDBFLAGS_KEEP_CACHE)) {
|
||||
|
||||
@@ -136,6 +136,7 @@ uint64_t rdbLoadLen(rio *rdb, int *isencoded);
|
||||
int rdbLoadLenByRef(rio *rdb, int *isencoded, uint64_t *lenptr);
|
||||
int rdbSaveObjectType(rio *rdb, robj *o);
|
||||
int rdbLoadObjectType(rio *rdb);
|
||||
int rdbLoadWithEmptyFunc(char *filename, rdbSaveInfo *rsi, int rdbflags, void (*emptyDbFunc)(void));
|
||||
int rdbLoad(char *filename, rdbSaveInfo *rsi, int rdbflags);
|
||||
int rdbSaveBackground(int req, char *filename, rdbSaveInfo *rsi, int rdbflags);
|
||||
int rdbSaveToSlavesSockets(int req, rdbSaveInfo *rsi);
|
||||
|
||||
@@ -3264,6 +3264,9 @@ static int issueCommandRepeat(int argc, char **argv, long repeat) {
|
||||
config.cluster_reissue_command = 0;
|
||||
return REDIS_ERR;
|
||||
}
|
||||
/* Reset dbnum after reconnecting so we can re-select the previous db in cliSelect(). */
|
||||
config.dbnum = 0;
|
||||
cliSelect();
|
||||
}
|
||||
config.cluster_reissue_command = 0;
|
||||
if (config.cluster_send_asking) {
|
||||
@@ -3691,6 +3694,8 @@ static int evalMode(int argc, char **argv) {
|
||||
/* Call it */
|
||||
int eval_ldb = config.eval_ldb; /* Save it, may be reverted. */
|
||||
retval = issueCommand(argc+3-got_comma, argv2);
|
||||
for (j = 0; j < argc+3-got_comma; j++) sdsfree(argv2[j]);
|
||||
free(argv2);
|
||||
if (eval_ldb) {
|
||||
if (!config.eval_ldb) {
|
||||
/* If the debugging session ended immediately, there was an
|
||||
@@ -6076,6 +6081,7 @@ static int clusterManagerFixSlotsCoverage(char *all_slots) {
|
||||
if (!clusterManagerCheckRedisReply(n, reply, NULL)) {
|
||||
fixed = -1;
|
||||
if (reply) freeReplyObject(reply);
|
||||
if (slot_nodes) listRelease(slot_nodes);
|
||||
goto cleanup;
|
||||
}
|
||||
assert(reply->type == REDIS_REPLY_ARRAY);
|
||||
|
||||
+9
-1
@@ -35,6 +35,7 @@
|
||||
*/
|
||||
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <signal.h>
|
||||
@@ -46,8 +47,15 @@ void _serverAssert(const char *estr, const char *file, int line) {
|
||||
}
|
||||
|
||||
void _serverPanic(const char *file, int line, const char *msg, ...) {
|
||||
va_list ap;
|
||||
char fmtmsg[256];
|
||||
|
||||
va_start(ap,msg);
|
||||
vsnprintf(fmtmsg,sizeof(fmtmsg),msg,ap);
|
||||
va_end(ap);
|
||||
|
||||
fprintf(stderr, "------------------------------------------------");
|
||||
fprintf(stderr, "!!! Software Failure. Press left mouse button to continue");
|
||||
fprintf(stderr, "Guru Meditation: %s #%s:%d",msg,file,line);
|
||||
fprintf(stderr, "Guru Meditation: %s #%s:%d",fmtmsg,file,line);
|
||||
abort();
|
||||
}
|
||||
|
||||
@@ -1296,7 +1296,10 @@ REDISMODULE_API int *(*RedisModule_GetCommandKeys)(RedisModuleCtx *ctx, RedisMod
|
||||
REDISMODULE_API int *(*RedisModule_GetCommandKeysWithFlags)(RedisModuleCtx *ctx, RedisModuleString **argv, int argc, int *num_keys, int **out_flags) REDISMODULE_ATTR;
|
||||
REDISMODULE_API const char *(*RedisModule_GetCurrentCommandName)(RedisModuleCtx *ctx) REDISMODULE_ATTR;
|
||||
REDISMODULE_API int (*RedisModule_RegisterDefragFunc)(RedisModuleCtx *ctx, RedisModuleDefragFunc func) REDISMODULE_ATTR;
|
||||
REDISMODULE_API int (*RedisModule_RegisterDefragCallbacks)(RedisModuleCtx *ctx, RedisModuleDefragFunc start, RedisModuleDefragFunc end) REDISMODULE_ATTR;
|
||||
REDISMODULE_API void *(*RedisModule_DefragAlloc)(RedisModuleDefragCtx *ctx, void *ptr) REDISMODULE_ATTR;
|
||||
REDISMODULE_API void *(*RedisModule_DefragAllocRaw)(RedisModuleDefragCtx *ctx, size_t size) REDISMODULE_ATTR;
|
||||
REDISMODULE_API void (*RedisModule_DefragFreeRaw)(RedisModuleDefragCtx *ctx, void *ptr) REDISMODULE_ATTR;
|
||||
REDISMODULE_API RedisModuleString *(*RedisModule_DefragRedisModuleString)(RedisModuleDefragCtx *ctx, RedisModuleString *str) REDISMODULE_ATTR;
|
||||
REDISMODULE_API int (*RedisModule_DefragShouldStop)(RedisModuleDefragCtx *ctx) REDISMODULE_ATTR;
|
||||
REDISMODULE_API int (*RedisModule_DefragCursorSet)(RedisModuleDefragCtx *ctx, unsigned long cursor) REDISMODULE_ATTR;
|
||||
@@ -1662,7 +1665,10 @@ static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int
|
||||
REDISMODULE_GET_API(GetCommandKeysWithFlags);
|
||||
REDISMODULE_GET_API(GetCurrentCommandName);
|
||||
REDISMODULE_GET_API(RegisterDefragFunc);
|
||||
REDISMODULE_GET_API(RegisterDefragCallbacks);
|
||||
REDISMODULE_GET_API(DefragAlloc);
|
||||
REDISMODULE_GET_API(DefragAllocRaw);
|
||||
REDISMODULE_GET_API(DefragFreeRaw);
|
||||
REDISMODULE_GET_API(DefragRedisModuleString);
|
||||
REDISMODULE_GET_API(DefragShouldStop);
|
||||
REDISMODULE_GET_API(DefragCursorSet);
|
||||
|
||||
+45
-36
@@ -3,13 +3,15 @@
|
||||
* 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.
|
||||
*/
|
||||
|
||||
|
||||
#include "server.h"
|
||||
#include "cluster.h"
|
||||
#include "bio.h"
|
||||
@@ -1736,12 +1738,24 @@ void replicationSendNewlineToMaster(void) {
|
||||
}
|
||||
|
||||
/* Callback used by emptyData() while flushing away old data to load
|
||||
* the new dataset received by the master and by discardTempDb()
|
||||
* after loading succeeded or failed. */
|
||||
* the new dataset received by the master or to clear partial db if loading
|
||||
* fails. */
|
||||
void replicationEmptyDbCallback(dict *d) {
|
||||
UNUSED(d);
|
||||
if (server.repl_state == REPL_STATE_TRANSFER)
|
||||
replicationSendNewlineToMaster();
|
||||
|
||||
processEventsWhileBlocked();
|
||||
}
|
||||
|
||||
/* Function to flush old db or the partial db on error. */
|
||||
static void rdbLoadEmptyDbFunc(void) {
|
||||
serverAssert(server.loading);
|
||||
|
||||
serverLog(LL_NOTICE, "MASTER <-> REPLICA sync: Flushing old data");
|
||||
int empty_db_flags = server.repl_slave_lazy_flush ? EMPTYDB_ASYNC :
|
||||
EMPTYDB_NO_FLAGS;
|
||||
emptyData(-1, empty_db_flags, replicationEmptyDbCallback);
|
||||
}
|
||||
|
||||
/* Once we have a link with the master and the synchronization was
|
||||
@@ -1765,6 +1779,9 @@ void replicationCreateMasterClient(connection *conn, int dbid) {
|
||||
* connection. */
|
||||
server.master->flags |= CLIENT_MASTER;
|
||||
|
||||
/* Allocate a private query buffer for the master client instead of using the reusable query buffer.
|
||||
* This is done because the master's query buffer data needs to be preserved for my sub-replicas to use. */
|
||||
server.master->querybuf = sdsempty();
|
||||
server.master->authenticated = 1;
|
||||
server.master->reploff = server.master_initial_offset;
|
||||
server.master->read_reploff = server.master->reploff;
|
||||
@@ -1778,27 +1795,6 @@ void replicationCreateMasterClient(connection *conn, int dbid) {
|
||||
if (dbid != -1) selectDb(server.master,dbid);
|
||||
}
|
||||
|
||||
/* This function will try to re-enable the AOF file after the
|
||||
* master-replica synchronization: if it fails after multiple attempts
|
||||
* the replica cannot be considered reliable and exists with an
|
||||
* error. */
|
||||
void restartAOFAfterSYNC(void) {
|
||||
unsigned int tries, max_tries = 10;
|
||||
for (tries = 0; tries < max_tries; ++tries) {
|
||||
if (startAppendOnly() == C_OK) break;
|
||||
serverLog(LL_WARNING,
|
||||
"Failed enabling the AOF after successful master synchronization! "
|
||||
"Trying it again in one second.");
|
||||
sleep(1);
|
||||
}
|
||||
if (tries == max_tries) {
|
||||
serverLog(LL_WARNING,
|
||||
"FATAL: this replica instance finished the synchronization with "
|
||||
"its master, but the AOF can't be turned on. Exiting now.");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
static int useDisklessLoad(void) {
|
||||
/* compute boolean decision to use diskless load */
|
||||
int enabled = server.repl_diskless_load == REPL_DISKLESS_LOAD_SWAPDB ||
|
||||
@@ -1831,7 +1827,7 @@ redisDb *disklessLoadInitTempDb(void) {
|
||||
/* Helper function for readSyncBulkPayload() to discard our tempDb
|
||||
* when the loading succeeded or failed. */
|
||||
void disklessLoadDiscardTempDb(redisDb *tempDb) {
|
||||
discardTempDb(tempDb, replicationEmptyDbCallback);
|
||||
discardTempDb(tempDb);
|
||||
}
|
||||
|
||||
/* If we know we got an entirely different data set from our master
|
||||
@@ -2057,9 +2053,6 @@ void readSyncBulkPayload(connection *conn) {
|
||||
NULL);
|
||||
} else {
|
||||
replicationAttachToNewMaster();
|
||||
|
||||
serverLog(LL_NOTICE, "MASTER <-> REPLICA sync: Flushing old data");
|
||||
emptyData(-1,empty_db_flags,replicationEmptyDbCallback);
|
||||
}
|
||||
|
||||
/* Before loading the DB into memory we need to delete the readable
|
||||
@@ -2093,13 +2086,22 @@ void readSyncBulkPayload(connection *conn) {
|
||||
functionsLibCtxClear(functions_lib_ctx);
|
||||
}
|
||||
|
||||
loadingSetFlags(NULL, server.repl_transfer_size, asyncLoading);
|
||||
if (server.repl_diskless_load != REPL_DISKLESS_LOAD_SWAPDB) {
|
||||
serverLog(LL_NOTICE, "MASTER <-> REPLICA sync: Flushing old data");
|
||||
/* Note that inside loadingSetFlags(), server.loading is set.
|
||||
* replicationEmptyDbCallback() may yield back to event-loop to
|
||||
* reply -LOADING. */
|
||||
emptyData(-1, empty_db_flags, replicationEmptyDbCallback);
|
||||
}
|
||||
loadingFireEvent(RDBFLAGS_REPLICATION);
|
||||
|
||||
rioInitWithConn(&rdb,conn,server.repl_transfer_size);
|
||||
|
||||
/* Put the socket in blocking mode to simplify RDB transfer.
|
||||
* We'll restore it when the RDB is received. */
|
||||
connBlock(conn);
|
||||
connRecvTimeout(conn, server.repl_timeout*1000);
|
||||
startLoading(server.repl_transfer_size, RDBFLAGS_REPLICATION, asyncLoading);
|
||||
|
||||
int loadingFailed = 0;
|
||||
rdbLoadingCtx loadingCtx = { .dbarray = dbarray, .functions_lib_ctx = functions_lib_ctx };
|
||||
@@ -2120,7 +2122,6 @@ void readSyncBulkPayload(connection *conn) {
|
||||
}
|
||||
|
||||
if (loadingFailed) {
|
||||
stopLoading(0);
|
||||
cancelReplicationHandshake(1);
|
||||
rioFreeConn(&rdb, NULL);
|
||||
|
||||
@@ -2138,6 +2139,11 @@ void readSyncBulkPayload(connection *conn) {
|
||||
emptyData(-1,empty_db_flags,replicationEmptyDbCallback);
|
||||
}
|
||||
|
||||
/* Note that replicationEmptyDbCallback() may yield back to event
|
||||
* loop to reply -LOADING if flushing the db takes a long time. So,
|
||||
* stopLoading() must be called after emptyData() above. */
|
||||
stopLoading(0);
|
||||
|
||||
/* Note that there's no point in restarting the AOF on SYNC
|
||||
* failure, it'll be restarted when sync succeeds or the replica
|
||||
* gets promoted. */
|
||||
@@ -2213,7 +2219,7 @@ void readSyncBulkPayload(connection *conn) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (rdbLoad(server.rdb_filename,&rsi,RDBFLAGS_REPLICATION) != RDB_OK) {
|
||||
if (rdbLoadWithEmptyFunc(server.rdb_filename,&rsi,RDBFLAGS_REPLICATION,rdbLoadEmptyDbFunc) != RDB_OK) {
|
||||
serverLog(LL_WARNING,
|
||||
"Failed trying to load the MASTER synchronization "
|
||||
"DB from disk, check server logs.");
|
||||
@@ -2225,9 +2231,6 @@ void readSyncBulkPayload(connection *conn) {
|
||||
bg_unlink(server.rdb_filename);
|
||||
}
|
||||
|
||||
/* If disk-based RDB loading fails, remove the half-loaded dataset. */
|
||||
emptyData(-1, empty_db_flags, replicationEmptyDbCallback);
|
||||
|
||||
/* Note that there's no point in restarting the AOF on sync failure,
|
||||
it'll be restarted when sync succeeds or replica promoted. */
|
||||
return;
|
||||
@@ -2281,7 +2284,10 @@ void readSyncBulkPayload(connection *conn) {
|
||||
/* Restart the AOF subsystem now that we finished the sync. This
|
||||
* will trigger an AOF rewrite, and when done will start appending
|
||||
* to the new file. */
|
||||
if (server.aof_enabled) restartAOFAfterSYNC();
|
||||
if (server.aof_enabled) {
|
||||
serverLog(LL_NOTICE, "MASTER <-> REPLICA sync: Starting AOF after a successful sync");
|
||||
startAppendOnlyWithRetry();
|
||||
}
|
||||
return;
|
||||
|
||||
error:
|
||||
@@ -3098,7 +3104,10 @@ void replicationUnsetMaster(void) {
|
||||
|
||||
/* Restart the AOF subsystem in case we shut it down during a sync when
|
||||
* we were still a slave. */
|
||||
if (server.aof_enabled && server.aof_state == AOF_OFF) restartAOFAfterSYNC();
|
||||
if (server.aof_enabled && server.aof_state == AOF_OFF) {
|
||||
serverLog(LL_NOTICE, "Restarting AOF after becoming master");
|
||||
startAppendOnlyWithRetry();
|
||||
}
|
||||
}
|
||||
|
||||
/* This function is called when the slave lose the connection with the
|
||||
|
||||
+29
-5
@@ -2,8 +2,13 @@
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "server.h"
|
||||
@@ -734,6 +739,8 @@ long long getInstantaneousMetric(int metric) {
|
||||
*
|
||||
* The function always returns 0 as it never terminates the client. */
|
||||
int clientsCronResizeQueryBuffer(client *c) {
|
||||
/* If the client query buffer is NULL, it is using the reusable query buffer and there is nothing to do. */
|
||||
if (c->querybuf == NULL) return 0;
|
||||
size_t querybuf_size = sdsalloc(c->querybuf);
|
||||
time_t idletime = server.unixtime - c->lastinteraction;
|
||||
|
||||
@@ -743,7 +750,18 @@ int clientsCronResizeQueryBuffer(client *c) {
|
||||
/* There are two conditions to resize the query buffer: */
|
||||
if (idletime > 2) {
|
||||
/* 1) Query is idle for a long time. */
|
||||
c->querybuf = sdsRemoveFreeSpace(c->querybuf, 1);
|
||||
size_t remaining = sdslen(c->querybuf) - c->qb_pos;
|
||||
if (!(c->flags & CLIENT_MASTER) && !remaining) {
|
||||
/* If the client is not a master and no data is pending,
|
||||
* The client can safely use the reusable query buffer in the next read - free the client's querybuf. */
|
||||
sdsfree(c->querybuf);
|
||||
/* By setting the querybuf to NULL, the client will use the reusable query buffer in the next read.
|
||||
* We don't move the client to the reusable query buffer immediately, because if we allocated a private
|
||||
* query buffer for the client, it's likely that the client will use it again soon. */
|
||||
c->querybuf = NULL;
|
||||
} else {
|
||||
c->querybuf = sdsRemoveFreeSpace(c->querybuf, 1);
|
||||
}
|
||||
} else if (querybuf_size > PROTO_RESIZE_THRESHOLD && querybuf_size/2 > c->querybuf_peak) {
|
||||
/* 2) Query buffer is too big for latest peak and is larger than
|
||||
* resize threshold. Trim excess space but only up to a limit,
|
||||
@@ -759,7 +777,7 @@ int clientsCronResizeQueryBuffer(client *c) {
|
||||
|
||||
/* Reset the peak again to capture the peak memory usage in the next
|
||||
* cycle. */
|
||||
c->querybuf_peak = sdslen(c->querybuf);
|
||||
c->querybuf_peak = c->querybuf ? sdslen(c->querybuf) : 0;
|
||||
/* We reset to either the current used, or currently processed bulk size,
|
||||
* which ever is bigger. */
|
||||
if (c->bulklen != -1 && (size_t)c->bulklen + 2 > c->querybuf_peak) c->querybuf_peak = c->bulklen + 2;
|
||||
@@ -834,8 +852,9 @@ size_t ClientsPeakMemInput[CLIENTS_PEAK_MEM_USAGE_SLOTS] = {0};
|
||||
size_t ClientsPeakMemOutput[CLIENTS_PEAK_MEM_USAGE_SLOTS] = {0};
|
||||
|
||||
int clientsCronTrackExpansiveClients(client *c, int time_idx) {
|
||||
size_t in_usage = sdsZmallocSize(c->querybuf) + c->argv_len_sum +
|
||||
(c->argv ? zmalloc_size(c->argv) : 0);
|
||||
size_t qb_size = c->querybuf ? sdsZmallocSize(c->querybuf) : 0;
|
||||
size_t argv_size = c->argv ? zmalloc_size(c->argv) : 0;
|
||||
size_t in_usage = qb_size + c->argv_len_sum + argv_size;
|
||||
size_t out_usage = getClientOutputBufferMemoryUsage(c);
|
||||
|
||||
/* Track the biggest values observed so far in this slot. */
|
||||
@@ -6567,7 +6586,7 @@ void dismissMemory(void* ptr, size_t size_hint) {
|
||||
void dismissClientMemory(client *c) {
|
||||
/* Dismiss client query buffer and static reply buffer. */
|
||||
dismissMemory(c->buf, c->buf_usable_size);
|
||||
dismissSds(c->querybuf);
|
||||
if (c->querybuf) dismissSds(c->querybuf);
|
||||
/* Dismiss argv array only if we estimate it contains a big buffer. */
|
||||
if (c->argc && c->argv_len_sum/c->argc >= server.page_size) {
|
||||
for (int i = 0; i < c->argc; i++) {
|
||||
@@ -7212,6 +7231,11 @@ int main(int argc, char **argv) {
|
||||
loadDataFromDisk();
|
||||
aofOpenIfNeededOnServerStart();
|
||||
aofDelHistoryFiles();
|
||||
/* While loading data, we delay applying "appendonly" config change.
|
||||
* If there was a config change while we were inside loadDataFromDisk()
|
||||
* above, we'll apply it here. */
|
||||
applyAppendOnlyConfig();
|
||||
|
||||
if (server.cluster_enabled) {
|
||||
serverAssert(verifyClusterConfigWithData() == C_OK);
|
||||
}
|
||||
|
||||
+23
-3
@@ -388,6 +388,7 @@ extern int configOOMScoreAdjValuesDefaults[CONFIG_OOM_COUNT];
|
||||
#define CLIENT_MODULE_PREVENT_AOF_PROP (1ULL<<48) /* Module client do not want to propagate to AOF */
|
||||
#define CLIENT_MODULE_PREVENT_REPL_PROP (1ULL<<49) /* Module client do not want to propagate to replica */
|
||||
#define CLIENT_REPROCESSING_COMMAND (1ULL<<50) /* The client is re-processing the command. */
|
||||
#define CLIENT_REUSABLE_QUERYBUFFER (1ULL<<51) /* The client is using the reusable query buffer. */
|
||||
|
||||
/* Any flag that does not let optimize FLUSH SYNC to run it in bg as blocking client ASYNC */
|
||||
#define CLIENT_AVOID_BLOCKING_ASYNC_FLUSH (CLIENT_DENY_BLOCKING|CLIENT_MULTI|CLIENT_LUA_DEBUG|CLIENT_LUA_DEBUG_SYNC|CLIENT_MODULE)
|
||||
@@ -820,6 +821,8 @@ struct RedisModule {
|
||||
int blocked_clients; /* Count of RedisModuleBlockedClient in this module. */
|
||||
RedisModuleInfoFunc info_cb; /* Callback for module to add INFO fields. */
|
||||
RedisModuleDefragFunc defrag_cb; /* Callback for global data defrag. */
|
||||
RedisModuleDefragFunc defrag_start_cb; /* Callback indicating defrag started. */
|
||||
RedisModuleDefragFunc defrag_end_cb; /* Callback indicating defrag ended. */
|
||||
struct moduleLoadQueueEntry *loadmod; /* Module load arguments for config rewrite. */
|
||||
int num_commands_with_acl_categories; /* Number of commands in this module included in acl categories */
|
||||
int onload; /* Flag to identify if the call is being made from Onload (0 or 1) */
|
||||
@@ -2555,6 +2558,8 @@ robj *moduleTypeDupOrReply(client *c, robj *fromkey, robj *tokey, int todb, robj
|
||||
int moduleDefragValue(robj *key, robj *obj, int dbid);
|
||||
int moduleLateDefrag(robj *key, robj *value, unsigned long *cursor, long long endtime, int dbid);
|
||||
void moduleDefragGlobals(void);
|
||||
void moduleDefragStart(void);
|
||||
void moduleDefragEnd(void);
|
||||
void *moduleGetHandleByName(char *modulename);
|
||||
int moduleIsModuleCommand(void *module_handle, struct redisCommand *cmd);
|
||||
|
||||
@@ -2627,6 +2632,7 @@ void addReplyDouble(client *c, double d);
|
||||
void addReplyBigNum(client *c, const char *num, size_t len);
|
||||
void addReplyHumanLongDouble(client *c, long double d);
|
||||
void addReplyLongLong(client *c, long long ll);
|
||||
void addReplyLongLongFromStr(client *c, robj* str);
|
||||
void addReplyArrayLen(client *c, long length);
|
||||
void addReplyMapLen(client *c, long length);
|
||||
void addReplySetLen(client *c, long length);
|
||||
@@ -2690,6 +2696,8 @@ void initThreadedIO(void);
|
||||
client *lookupClientByID(uint64_t id);
|
||||
int authRequired(client *c);
|
||||
void putClientInPendingWriteQueue(client *c);
|
||||
/* reply macros */
|
||||
#define ADD_REPLY_BULK_CBUFFER_STRING_CONSTANT(c, str) addReplyBulkCBuffer(c, str, strlen(str))
|
||||
|
||||
/* logreqres.c - logging of requests and responses */
|
||||
void reqresReset(client *c, int free_buf);
|
||||
@@ -2740,7 +2748,7 @@ robj *listTypeGet(listTypeEntry *entry);
|
||||
unsigned char *listTypeGetValue(listTypeEntry *entry, size_t *vlen, long long *lval);
|
||||
void listTypeInsert(listTypeEntry *entry, robj *value, int where);
|
||||
void listTypeReplace(listTypeEntry *entry, robj *value);
|
||||
int listTypeEqual(listTypeEntry *entry, robj *o);
|
||||
int listTypeEqual(listTypeEntry *entry, robj *o, size_t object_len);
|
||||
void listTypeDelete(listTypeIterator *iter, listTypeEntry *entry);
|
||||
robj *listTypeDup(robj *o);
|
||||
void listTypeDelRange(robj *o, long start, long stop);
|
||||
@@ -2877,6 +2885,8 @@ const char *getFailoverStateString(void);
|
||||
/* Generic persistence functions */
|
||||
void startLoadingFile(size_t size, char* filename, int rdbflags);
|
||||
void startLoading(size_t size, int rdbflags, int async);
|
||||
void loadingSetFlags(char *filename, size_t size, int async);
|
||||
void loadingFireEvent(int rdbflags);
|
||||
void loadingAbsProgress(off_t pos);
|
||||
void loadingIncrProgress(off_t size);
|
||||
void stopLoading(int success);
|
||||
@@ -2904,9 +2914,10 @@ int rewriteAppendOnlyFileBackground(void);
|
||||
int loadAppendOnlyFiles(aofManifest *am);
|
||||
void stopAppendOnly(void);
|
||||
int startAppendOnly(void);
|
||||
void startAppendOnlyWithRetry(void);
|
||||
void applyAppendOnlyConfig(void);
|
||||
void backgroundRewriteDoneHandler(int exitcode, int bysignal);
|
||||
void killAppendOnlyChild(void);
|
||||
void restartAOFAfterSYNC(void);
|
||||
void aofLoadManifestFromDisk(void);
|
||||
void aofOpenIfNeededOnServerStart(void);
|
||||
void aofManifestFree(aofManifest *am);
|
||||
@@ -3127,6 +3138,8 @@ void checkChildrenDone(void);
|
||||
int setOOMScoreAdj(int process_class);
|
||||
void rejectCommandFormat(client *c, const char *fmt, ...);
|
||||
void *activeDefragAlloc(void *ptr);
|
||||
void *activeDefragAllocRaw(size_t size);
|
||||
void activeDefragFreeRaw(void *ptr);
|
||||
robj *activeDefragStringOb(robj* ob);
|
||||
void dismissSds(sds s);
|
||||
void dismissMemory(void* ptr, size_t size_hint);
|
||||
@@ -3355,10 +3368,12 @@ void propagateDeletion(redisDb *db, robj *key, int lazy);
|
||||
int keyIsExpired(redisDb *db, robj *key);
|
||||
long long getExpire(redisDb *db, robj *key);
|
||||
void setExpire(client *c, redisDb *db, robj *key, long long when);
|
||||
void setExpireWithDictEntry(client *c, redisDb *db, robj *key, long long when, dictEntry *kde);
|
||||
int checkAlreadyExpired(long long when);
|
||||
int parseExtendedExpireArgumentsOrReply(client *c, int *flags);
|
||||
robj *lookupKeyRead(redisDb *db, robj *key);
|
||||
robj *lookupKeyWrite(redisDb *db, robj *key);
|
||||
robj *lookupKeyWriteWithDictEntry(redisDb *db, robj *key, dictEntry **deref);
|
||||
robj *lookupKeyReadOrReply(client *c, robj *key, robj *reply);
|
||||
robj *lookupKeyWriteOrReply(client *c, robj *key, robj *reply);
|
||||
robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags);
|
||||
@@ -3378,6 +3393,7 @@ int objectSetLRUOrLFU(robj *val, long long lfu_freq, long long lru_idle,
|
||||
dictEntry *dbAdd(redisDb *db, robj *key, robj *val);
|
||||
int dbAddRDBLoad(redisDb *db, sds key, robj *val);
|
||||
void dbReplaceValue(redisDb *db, robj *key, robj *val);
|
||||
void dbReplaceValueWithDictEntry(redisDb *db, robj *key, robj *val, dictEntry *de);
|
||||
|
||||
#define SETKEY_KEEPTTL 1
|
||||
#define SETKEY_NO_SIGNAL 2
|
||||
@@ -3385,11 +3401,14 @@ void dbReplaceValue(redisDb *db, robj *key, robj *val);
|
||||
#define SETKEY_DOESNT_EXIST 8
|
||||
#define SETKEY_ADD_OR_UPDATE 16 /* Key most likely doesn't exists */
|
||||
void setKey(client *c, redisDb *db, robj *key, robj *val, int flags);
|
||||
void setKeyWithDictEntry(client *c, redisDb *db, robj *key, robj *val, int flags, dictEntry *de);
|
||||
robj *dbRandomKey(redisDb *db);
|
||||
int dbGenericDelete(redisDb *db, robj *key, int async, int flags);
|
||||
int dbSyncDelete(redisDb *db, robj *key);
|
||||
int dbDelete(redisDb *db, robj *key);
|
||||
robj *dbUnshareStringValue(redisDb *db, robj *key, robj *o);
|
||||
robj *dbUnshareStringValueWithDictEntry(redisDb *db, robj *key, robj *o, dictEntry *de);
|
||||
|
||||
|
||||
#define EMPTYDB_NO_FLAGS 0 /* No flags. */
|
||||
#define EMPTYDB_ASYNC (1<<0) /* Reclaim memory in another thread. */
|
||||
@@ -3399,7 +3418,7 @@ long long emptyDbStructure(redisDb *dbarray, int dbnum, int async, void(callback
|
||||
void flushAllDataAndResetRDB(int flags);
|
||||
long long dbTotalServerKeyCount(void);
|
||||
redisDb *initTempDb(void);
|
||||
void discardTempDb(redisDb *tempDb, void(callback)(dict*));
|
||||
void discardTempDb(redisDb *tempDb);
|
||||
|
||||
|
||||
int selectDb(client *c, int id);
|
||||
@@ -3634,6 +3653,7 @@ void scardCommand(client *c);
|
||||
void spopCommand(client *c);
|
||||
void srandmemberCommand(client *c);
|
||||
void sinterCommand(client *c);
|
||||
void smembersCommand(client *c);
|
||||
void sinterCardCommand(client *c);
|
||||
void sinterstoreCommand(client *c);
|
||||
void sunionCommand(client *c);
|
||||
|
||||
+36
-9
@@ -1985,6 +1985,21 @@ uint64_t hashTypeRemoveFromExpires(ebuckets *hexpires, robj *o) {
|
||||
return expireTime;
|
||||
}
|
||||
|
||||
int hashTypeIsFieldsWithExpire(robj *o) {
|
||||
if (o->encoding == OBJ_ENCODING_LISTPACK) {
|
||||
return 0;
|
||||
} else if (o->encoding == OBJ_ENCODING_LISTPACK_EX) {
|
||||
return EB_EXPIRE_TIME_INVALID != listpackExGetMinExpire(o);
|
||||
} else { /* o->encoding == OBJ_ENCODING_HT */
|
||||
dict *d = o->ptr;
|
||||
/* If dict doesn't holds HFE metadata */
|
||||
if (!isDictWithMetaHFE(d))
|
||||
return 0;
|
||||
dictExpireMetadata *meta = (dictExpireMetadata *) dictMetadata(d);
|
||||
return ebGetTotalItems(meta->hfe, &hashFieldExpireBucketsType) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Add hash to global HFE DS and update key for notifications.
|
||||
*
|
||||
* key - must be the same key instance that is persisted in db->dict
|
||||
@@ -2315,6 +2330,14 @@ void hdelCommand(client *c) {
|
||||
if ((o = lookupKeyWriteOrReply(c,c->argv[1],shared.czero)) == NULL ||
|
||||
checkType(c,o,OBJ_HASH)) return;
|
||||
|
||||
/* Hash field expiration is optimized to avoid frequent update global HFE DS for
|
||||
* each field deletion. Eventually active-expiration will run and update or remove
|
||||
* the hash from global HFE DS gracefully. Nevertheless, statistic "subexpiry"
|
||||
* might reflect wrong number of hashes with HFE to the user if it is the last
|
||||
* field with expiration. The following logic checks if this is indeed the last
|
||||
* field with expiration and removes it from global HFE DS. */
|
||||
int isHFE = hashTypeIsFieldsWithExpire(o);
|
||||
|
||||
for (j = 2; j < c->argc; j++) {
|
||||
if (hashTypeDelete(o,c->argv[j]->ptr,1)) {
|
||||
deleted++;
|
||||
@@ -2328,9 +2351,13 @@ void hdelCommand(client *c) {
|
||||
if (deleted) {
|
||||
signalModifiedKey(c,c->db,c->argv[1]);
|
||||
notifyKeyspaceEvent(NOTIFY_HASH,"hdel",c->argv[1],c->db->id);
|
||||
if (keyremoved)
|
||||
notifyKeyspaceEvent(NOTIFY_GENERIC,"del",c->argv[1],
|
||||
c->db->id);
|
||||
if (keyremoved) {
|
||||
notifyKeyspaceEvent(NOTIFY_GENERIC, "del", c->argv[1], c->db->id);
|
||||
} else {
|
||||
if (isHFE && (hashTypeIsFieldsWithExpire(o) == 0)) /* is it last HFE */
|
||||
ebRemove(&c->db->hexpires, &hashExpireBucketsType, o);
|
||||
}
|
||||
|
||||
server.dirty += deleted;
|
||||
}
|
||||
addReplyLongLong(c,deleted);
|
||||
@@ -2401,7 +2428,11 @@ void genericHgetallCommand(client *c, int flags) {
|
||||
|
||||
/* We return a map if the user requested keys and values, like in the
|
||||
* HGETALL case. Otherwise to use a flat array makes more sense. */
|
||||
length = hashTypeLength(o, 1 /*subtractExpiredFields*/);
|
||||
if ((length = hashTypeLength(o, 1 /*subtractExpiredFields*/)) == 0) {
|
||||
addReply(c, emptyResp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (flags & OBJ_HASH_KEY && flags & OBJ_HASH_VALUE) {
|
||||
addReplyMapLen(c, length);
|
||||
} else {
|
||||
@@ -2410,11 +2441,7 @@ void genericHgetallCommand(client *c, int flags) {
|
||||
|
||||
hi = hashTypeInitIterator(o);
|
||||
|
||||
/* Skip expired fields if the hash has an expire time set at global HFE DS. We could
|
||||
* set it to constant 1, but then it will make another lookup for each field expiration */
|
||||
int skipExpiredFields = (EB_EXPIRE_TIME_INVALID == hashTypeGetMinExpire(o, 0)) ? 0 : 1;
|
||||
|
||||
while (hashTypeNext(hi, skipExpiredFields) != C_ERR) {
|
||||
while (hashTypeNext(hi, 1 /*skipExpiredFields*/) != C_ERR) {
|
||||
if (flags & OBJ_HASH_KEY) {
|
||||
addHashIteratorCursorToReply(c, hi, OBJ_HASH_KEY);
|
||||
count++;
|
||||
|
||||
+9
-6
@@ -383,12 +383,12 @@ int listTypeReplaceAtIndex(robj *o, int index, robj *value) {
|
||||
}
|
||||
|
||||
/* Compare the given object with the entry at the current position. */
|
||||
int listTypeEqual(listTypeEntry *entry, robj *o) {
|
||||
int listTypeEqual(listTypeEntry *entry, robj *o, size_t object_len) {
|
||||
serverAssertWithInfo(NULL,o,sdsEncodedObject(o));
|
||||
if (entry->li->encoding == OBJ_ENCODING_QUICKLIST) {
|
||||
return quicklistCompare(&entry->entry,o->ptr,sdslen(o->ptr));
|
||||
return quicklistCompare(&entry->entry,o->ptr,object_len);
|
||||
} else if (entry->li->encoding == OBJ_ENCODING_LISTPACK) {
|
||||
return lpCompare(entry->lpe,o->ptr,sdslen(o->ptr));
|
||||
return lpCompare(entry->lpe,o->ptr,object_len);
|
||||
} else {
|
||||
serverPanic("Unknown list encoding");
|
||||
}
|
||||
@@ -538,8 +538,9 @@ void linsertCommand(client *c) {
|
||||
|
||||
/* Seek pivot from head to tail */
|
||||
iter = listTypeInitIterator(subject,0,LIST_TAIL);
|
||||
const size_t object_len = sdslen(c->argv[3]->ptr);
|
||||
while (listTypeNext(iter,&entry)) {
|
||||
if (listTypeEqual(&entry,c->argv[3])) {
|
||||
if (listTypeEqual(&entry,c->argv[3],object_len)) {
|
||||
listTypeInsert(&entry,c->argv[4],where);
|
||||
inserted = 1;
|
||||
break;
|
||||
@@ -999,8 +1000,9 @@ void lposCommand(client *c) {
|
||||
listTypeEntry entry;
|
||||
long llen = listTypeLength(o);
|
||||
long index = 0, matches = 0, matchindex = -1, arraylen = 0;
|
||||
const size_t ele_len = sdslen(ele->ptr);
|
||||
while (listTypeNext(li,&entry) && (maxlen == 0 || index < maxlen)) {
|
||||
if (listTypeEqual(&entry,ele)) {
|
||||
if (listTypeEqual(&entry,ele,ele_len)) {
|
||||
matches++;
|
||||
matchindex = (direction == LIST_TAIL) ? index : llen - index - 1;
|
||||
if (matches >= rank) {
|
||||
@@ -1052,8 +1054,9 @@ void lremCommand(client *c) {
|
||||
}
|
||||
|
||||
listTypeEntry entry;
|
||||
const size_t object_len = sdslen(c->argv[3]->ptr);
|
||||
while (listTypeNext(li,&entry)) {
|
||||
if (listTypeEqual(&entry,obj)) {
|
||||
if (listTypeEqual(&entry,obj,object_len)) {
|
||||
listTypeDelete(li, &entry);
|
||||
server.dirty++;
|
||||
removed++;
|
||||
|
||||
+31
-1
@@ -1244,7 +1244,7 @@ int qsortCompareSetsByRevCardinality(const void *s1, const void *s2) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* SINTER / SMEMBERS / SINTERSTORE / SINTERCARD
|
||||
/* SINTER / SINTERSTORE / SINTERCARD
|
||||
*
|
||||
* 'cardinality_only' work for SINTERCARD, only return the cardinality
|
||||
* with minimum processing and memory overheads.
|
||||
@@ -1420,6 +1420,36 @@ void sinterCommand(client *c) {
|
||||
sinterGenericCommand(c, c->argv+1, c->argc-1, NULL, 0, 0);
|
||||
}
|
||||
|
||||
/* SMEMBERS key */
|
||||
void smembersCommand(client *c) {
|
||||
setTypeIterator *si;
|
||||
char *str;
|
||||
size_t len;
|
||||
int64_t intobj;
|
||||
robj *setobj = lookupKeyRead(c->db, c->argv[1]);
|
||||
if (checkType(c,setobj,OBJ_SET)) return;
|
||||
if (!setobj) {
|
||||
addReply(c, shared.emptyset[c->resp]);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Prepare the response. */
|
||||
unsigned long length = setTypeSize(setobj);
|
||||
addReplySetLen(c,length);
|
||||
/* Iterate through the elements of the set. */
|
||||
si = setTypeInitIterator(setobj);
|
||||
|
||||
while (setTypeNext(si, &str, &len, &intobj) != -1) {
|
||||
if (str != NULL)
|
||||
addReplyBulkCBuffer(c, str, len);
|
||||
else
|
||||
addReplyBulkLongLong(c, intobj);
|
||||
length--;
|
||||
}
|
||||
setTypeReleaseIterator(si);
|
||||
serverAssert(length == 0); /* fail on corrupt data */
|
||||
}
|
||||
|
||||
/* SINTERCARD numkeys key [key ...] [LIMIT limit] */
|
||||
void sinterCardCommand(client *c) {
|
||||
long j;
|
||||
|
||||
+21
-11
@@ -1405,11 +1405,6 @@ int streamRangeHasTombstones(stream *s, streamID *start, streamID *end) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (streamCompareID(&s->first_id,&s->max_deleted_entry_id) > 0) {
|
||||
/* The latest tombstone is before the first entry. */
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (start) {
|
||||
start_id = *start;
|
||||
} else {
|
||||
@@ -1446,6 +1441,17 @@ void streamReplyWithCGLag(client *c, stream *s, streamCG *cg) {
|
||||
/* The lag of a newly-initialized stream is 0. */
|
||||
lag = 0;
|
||||
valid = 1;
|
||||
} else if (!s->length) { /* All entries deleted, now empty. */
|
||||
lag = 0;
|
||||
valid = 1;
|
||||
} else if (streamCompareID(&cg->last_id,&s->first_id) < 0 &&
|
||||
streamCompareID(&s->max_deleted_entry_id,&s->first_id) < 0)
|
||||
{
|
||||
/* When both the consumer group's last_id and the maximum tombstone are behind
|
||||
* the stream's first entry, the consumer group's lag will always be equal to
|
||||
* the number of remainin entries in the stream. */
|
||||
lag = s->length;
|
||||
valid = 1;
|
||||
} else if (cg->entries_read != SCG_INVALID_ENTRIES_READ && !streamRangeHasTombstones(s,&cg->last_id,NULL)) {
|
||||
/* No fragmentation ahead means that the group's logical reads counter
|
||||
* is valid for performing the lag calculation. */
|
||||
@@ -1502,6 +1508,10 @@ long long streamEstimateDistanceFromFirstEverEntry(stream *s, streamID *id) {
|
||||
return s->entries_added;
|
||||
}
|
||||
|
||||
/* There are fragmentations between the `id` and the stream's last-generated-id. */
|
||||
if (!streamIDEqZero(id) && streamCompareID(id,&s->max_deleted_entry_id) < 0)
|
||||
return SCG_INVALID_ENTRIES_READ;
|
||||
|
||||
int cmp_last = streamCompareID(id,&s->last_id);
|
||||
if (cmp_last == 0) {
|
||||
/* Return the exact counter of the last entry in the stream. */
|
||||
@@ -1684,10 +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,&id,NULL)) {
|
||||
/* A valid counter and no future tombstones mean we can
|
||||
* increment the read counter to keep tracking the group's
|
||||
* progress. */
|
||||
if (group->entries_read != SCG_INVALID_ENTRIES_READ && !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. */
|
||||
group->entries_read++;
|
||||
} else if (s->entries_added) {
|
||||
/* The group's counter may be invalid, so we try to obtain it. */
|
||||
@@ -2190,9 +2200,9 @@ void xreadCommand(client *c) {
|
||||
streams_arg = i+1;
|
||||
streams_count = (c->argc-streams_arg);
|
||||
if ((streams_count % 2) != 0) {
|
||||
char symbol = xreadgroup ? '>' : '$';
|
||||
const char *symbol = xreadgroup ? "ID or '>'" : "ID, '+', or '$'";
|
||||
addReplyErrorFormat(c,"Unbalanced '%s' list of streams: "
|
||||
"for each stream key an ID or '%c' must be "
|
||||
"for each stream key an %s must be "
|
||||
"specified.", c->cmd->fullname,symbol);
|
||||
return;
|
||||
}
|
||||
|
||||
+34
-37
@@ -73,7 +73,8 @@ void setGenericCommand(client *c, int flags, robj *key, robj *val, robj *expire,
|
||||
if (getGenericCommand(c) == C_ERR) return;
|
||||
}
|
||||
|
||||
found = (lookupKeyWrite(c->db,key) != NULL);
|
||||
dictEntry *de = NULL;
|
||||
found = (lookupKeyWriteWithDictEntry(c->db,key,&de) != NULL);
|
||||
|
||||
if ((flags & OBJ_SET_NX && found) ||
|
||||
(flags & OBJ_SET_XX && !found))
|
||||
@@ -88,12 +89,12 @@ void setGenericCommand(client *c, int flags, robj *key, robj *val, robj *expire,
|
||||
setkey_flags |= ((flags & OBJ_KEEPTTL) || expire) ? SETKEY_KEEPTTL : 0;
|
||||
setkey_flags |= found ? SETKEY_ALREADY_EXIST : SETKEY_DOESNT_EXIST;
|
||||
|
||||
setKey(c,c->db,key,val,setkey_flags);
|
||||
setKeyWithDictEntry(c,c->db,key,val,setkey_flags,de);
|
||||
server.dirty++;
|
||||
notifyKeyspaceEvent(NOTIFY_STRING,"set",key,c->db->id);
|
||||
|
||||
if (expire) {
|
||||
setExpire(c,c->db,key,milliseconds);
|
||||
setExpireWithDictEntry(c,c->db,key,milliseconds,de);
|
||||
/* Propagate as SET Key Value PXAT millisecond-timestamp if there is
|
||||
* EX/PX/EXAT flag. */
|
||||
if (!(flags & OBJ_PXAT)) {
|
||||
@@ -422,6 +423,7 @@ void setrangeCommand(client *c) {
|
||||
robj *o;
|
||||
long offset;
|
||||
sds value = c->argv[3]->ptr;
|
||||
const size_t value_len = sdslen(value);
|
||||
|
||||
if (getLongFromObjectOrReply(c,c->argv[2],&offset,NULL) != C_OK)
|
||||
return;
|
||||
@@ -431,19 +433,20 @@ void setrangeCommand(client *c) {
|
||||
return;
|
||||
}
|
||||
|
||||
o = lookupKeyWrite(c->db,c->argv[1]);
|
||||
dictEntry *de;
|
||||
o = lookupKeyWriteWithDictEntry(c->db,c->argv[1],&de);
|
||||
if (o == NULL) {
|
||||
/* Return 0 when setting nothing on a non-existing string */
|
||||
if (sdslen(value) == 0) {
|
||||
if (value_len == 0) {
|
||||
addReply(c,shared.czero);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Return when the resulting string exceeds allowed size */
|
||||
if (checkStringLength(c,offset,sdslen(value)) != C_OK)
|
||||
if (checkStringLength(c,offset,value_len) != C_OK)
|
||||
return;
|
||||
|
||||
o = createObject(OBJ_STRING,sdsnewlen(NULL, offset+sdslen(value)));
|
||||
o = createObject(OBJ_STRING,sdsnewlen(NULL, offset+value_len));
|
||||
dbAdd(c->db,c->argv[1],o);
|
||||
} else {
|
||||
size_t olen;
|
||||
@@ -454,22 +457,22 @@ void setrangeCommand(client *c) {
|
||||
|
||||
/* Return existing string length when setting nothing */
|
||||
olen = stringObjectLen(o);
|
||||
if (sdslen(value) == 0) {
|
||||
if (value_len == 0) {
|
||||
addReplyLongLong(c,olen);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Return when the resulting string exceeds allowed size */
|
||||
if (checkStringLength(c,offset,sdslen(value)) != C_OK)
|
||||
if (checkStringLength(c,offset,value_len) != C_OK)
|
||||
return;
|
||||
|
||||
/* Create a copy when the object is shared or encoded. */
|
||||
o = dbUnshareStringValue(c->db,c->argv[1],o);
|
||||
o = dbUnshareStringValueWithDictEntry(c->db,c->argv[1],o,de);
|
||||
}
|
||||
|
||||
if (sdslen(value) > 0) {
|
||||
o->ptr = sdsgrowzero(o->ptr,offset+sdslen(value));
|
||||
memcpy((char*)o->ptr+offset,value,sdslen(value));
|
||||
if (value_len > 0) {
|
||||
o->ptr = sdsgrowzero(o->ptr,offset+value_len);
|
||||
memcpy((char*)o->ptr+offset,value,value_len);
|
||||
signalModifiedKey(c,c->db,c->argv[1]);
|
||||
notifyKeyspaceEvent(NOTIFY_STRING,
|
||||
"setrange",c->argv[1],c->db->id);
|
||||
@@ -499,24 +502,15 @@ void getrangeCommand(client *c) {
|
||||
strlen = sdslen(str);
|
||||
}
|
||||
|
||||
/* Convert negative indexes */
|
||||
if (start < 0 && end < 0 && start > end) {
|
||||
if (start < 0) start += strlen;
|
||||
if (end < 0) end += strlen;
|
||||
if (strlen == 0 || start >= (long long)strlen || end < 0 || start > end) {
|
||||
addReply(c,shared.emptybulk);
|
||||
return;
|
||||
}
|
||||
if (start < 0) start = strlen+start;
|
||||
if (end < 0) end = strlen+end;
|
||||
if (start < 0) start = 0;
|
||||
if (end < 0) end = 0;
|
||||
if ((unsigned long long)end >= strlen) end = strlen-1;
|
||||
|
||||
/* Precondition: end >= 0 && end < strlen, so the only condition where
|
||||
* nothing can be returned is: start > end. */
|
||||
if (start > end || strlen == 0) {
|
||||
addReply(c,shared.emptybulk);
|
||||
} else {
|
||||
addReplyBulkCBuffer(c,(char*)str+start,end-start+1);
|
||||
}
|
||||
if (end >= (long long)strlen) end = strlen-1;
|
||||
addReplyBulkCBuffer(c,(char*)str+start,end-start+1);
|
||||
}
|
||||
|
||||
void mgetCommand(client *c) {
|
||||
@@ -580,8 +574,8 @@ void msetnxCommand(client *c) {
|
||||
void incrDecrCommand(client *c, long long incr) {
|
||||
long long value, oldvalue;
|
||||
robj *o, *new;
|
||||
|
||||
o = lookupKeyWrite(c->db,c->argv[1]);
|
||||
dictEntry *de;
|
||||
o = lookupKeyWriteWithDictEntry(c->db,c->argv[1],&de);
|
||||
if (checkType(c,o,OBJ_STRING)) return;
|
||||
if (getLongLongFromObjectOrReply(c,o,&value,NULL) != C_OK) return;
|
||||
|
||||
@@ -602,7 +596,7 @@ void incrDecrCommand(client *c, long long incr) {
|
||||
} else {
|
||||
new = createStringObjectFromLongLongForValue(value);
|
||||
if (o) {
|
||||
dbReplaceValue(c->db,c->argv[1],new);
|
||||
dbReplaceValueWithDictEntry(c->db,c->argv[1],new,de);
|
||||
} else {
|
||||
dbAdd(c->db,c->argv[1],new);
|
||||
}
|
||||
@@ -610,7 +604,7 @@ void incrDecrCommand(client *c, long long incr) {
|
||||
signalModifiedKey(c,c->db,c->argv[1]);
|
||||
notifyKeyspaceEvent(NOTIFY_STRING,"incrby",c->argv[1],c->db->id);
|
||||
server.dirty++;
|
||||
addReplyLongLong(c, value);
|
||||
addReplyLongLongFromStr(c,new);
|
||||
}
|
||||
|
||||
void incrCommand(client *c) {
|
||||
@@ -644,7 +638,8 @@ void incrbyfloatCommand(client *c) {
|
||||
long double incr, value;
|
||||
robj *o, *new;
|
||||
|
||||
o = lookupKeyWrite(c->db,c->argv[1]);
|
||||
dictEntry *de;
|
||||
o = lookupKeyWriteWithDictEntry(c->db,c->argv[1],&de);
|
||||
if (checkType(c,o,OBJ_STRING)) return;
|
||||
if (getLongDoubleFromObjectOrReply(c,o,&value,NULL) != C_OK ||
|
||||
getLongDoubleFromObjectOrReply(c,c->argv[2],&incr,NULL) != C_OK)
|
||||
@@ -657,7 +652,7 @@ void incrbyfloatCommand(client *c) {
|
||||
}
|
||||
new = createStringObjectFromLongDouble(value,1);
|
||||
if (o)
|
||||
dbReplaceValue(c->db,c->argv[1],new);
|
||||
dbReplaceValueWithDictEntry(c->db,c->argv[1],new,de);
|
||||
else
|
||||
dbAdd(c->db,c->argv[1],new);
|
||||
signalModifiedKey(c,c->db,c->argv[1]);
|
||||
@@ -677,7 +672,8 @@ void appendCommand(client *c) {
|
||||
size_t totlen;
|
||||
robj *o, *append;
|
||||
|
||||
o = lookupKeyWrite(c->db,c->argv[1]);
|
||||
dictEntry *de;
|
||||
o = lookupKeyWriteWithDictEntry(c->db,c->argv[1],&de);
|
||||
if (o == NULL) {
|
||||
/* Create the key */
|
||||
c->argv[2] = tryObjectEncoding(c->argv[2]);
|
||||
@@ -691,12 +687,13 @@ void appendCommand(client *c) {
|
||||
|
||||
/* "append" is an argument, so always an sds */
|
||||
append = c->argv[2];
|
||||
if (checkStringLength(c,stringObjectLen(o),sdslen(append->ptr)) != C_OK)
|
||||
const size_t append_len = sdslen(append->ptr);
|
||||
if (checkStringLength(c,stringObjectLen(o),append_len) != C_OK)
|
||||
return;
|
||||
|
||||
/* Append the value */
|
||||
o = dbUnshareStringValue(c->db,c->argv[1],o);
|
||||
o->ptr = sdscatlen(o->ptr,append->ptr,sdslen(append->ptr));
|
||||
o = dbUnshareStringValueWithDictEntry(c->db,c->argv[1],o,de);
|
||||
o->ptr = sdscatlen(o->ptr,append->ptr,append_len);
|
||||
totlen = sdslen(o->ptr);
|
||||
}
|
||||
signalModifiedKey(c,c->db,c->argv[1]);
|
||||
|
||||
+2
-2
@@ -1,2 +1,2 @@
|
||||
#define REDIS_VERSION "255.255.255"
|
||||
#define REDIS_VERSION_NUM 0x00ffffff
|
||||
#define REDIS_VERSION "7.9.224"
|
||||
#define REDIS_VERSION_NUM 0x000709e0
|
||||
|
||||
+40
-8
@@ -31,6 +31,7 @@ void zlibc_free(void *ptr) {
|
||||
#include <string.h>
|
||||
#include "zmalloc.h"
|
||||
#include "atomicvar.h"
|
||||
#include "redisassert.h"
|
||||
|
||||
#define UNUSED(x) ((void)(x))
|
||||
|
||||
@@ -67,10 +68,34 @@ void zlibc_free(void *ptr) {
|
||||
#define dallocx(ptr,flags) je_dallocx(ptr,flags)
|
||||
#endif
|
||||
|
||||
#define update_zmalloc_stat_alloc(__n) atomicIncr(used_memory,(__n))
|
||||
#define update_zmalloc_stat_free(__n) atomicDecr(used_memory,(__n))
|
||||
#define MAX_THREADS 16 /* Keep it a power of 2 so we can use '&' instead of '%'. */
|
||||
#define THREAD_MASK (MAX_THREADS - 1)
|
||||
|
||||
static redisAtomic size_t used_memory = 0;
|
||||
typedef struct used_memory_entry {
|
||||
redisAtomic long long used_memory;
|
||||
char padding[CACHE_LINE_SIZE - sizeof(long long)];
|
||||
} used_memory_entry;
|
||||
|
||||
static __attribute__((aligned(CACHE_LINE_SIZE))) used_memory_entry used_memory[MAX_THREADS];
|
||||
static redisAtomic size_t num_active_threads = 0;
|
||||
static __thread long my_thread_index = -1;
|
||||
|
||||
static inline void init_my_thread_index(void) {
|
||||
if (unlikely(my_thread_index == -1)) {
|
||||
atomicGetIncr(num_active_threads, my_thread_index, 1);
|
||||
my_thread_index &= THREAD_MASK;
|
||||
}
|
||||
}
|
||||
|
||||
static void update_zmalloc_stat_alloc(long long num) {
|
||||
init_my_thread_index();
|
||||
atomicIncr(used_memory[my_thread_index].used_memory, num);
|
||||
}
|
||||
|
||||
static void update_zmalloc_stat_free(long long num) {
|
||||
init_my_thread_index();
|
||||
atomicDecr(used_memory[my_thread_index].used_memory, num);
|
||||
}
|
||||
|
||||
static void zmalloc_default_oom(size_t size) {
|
||||
fprintf(stderr, "zmalloc: Out of memory trying to allocate %zu bytes\n",
|
||||
@@ -436,9 +461,18 @@ char *zstrdup(const char *s) {
|
||||
}
|
||||
|
||||
size_t zmalloc_used_memory(void) {
|
||||
size_t um;
|
||||
atomicGet(used_memory,um);
|
||||
return um;
|
||||
size_t local_num_active_threads;
|
||||
long long total_mem = 0;
|
||||
atomicGet(num_active_threads,local_num_active_threads);
|
||||
if (local_num_active_threads > MAX_THREADS) {
|
||||
local_num_active_threads = MAX_THREADS;
|
||||
}
|
||||
for (size_t i = 0; i < local_num_active_threads; ++i) {
|
||||
long long thread_used_mem;
|
||||
atomicGet(used_memory[i].used_memory, thread_used_mem);
|
||||
total_mem += thread_used_mem;
|
||||
}
|
||||
return total_mem;
|
||||
}
|
||||
|
||||
void zmalloc_set_oom_handler(void (*oom_handler)(size_t)) {
|
||||
@@ -655,8 +689,6 @@ size_t zmalloc_get_rss(void) {
|
||||
|
||||
#if defined(USE_JEMALLOC)
|
||||
|
||||
#include "redisassert.h"
|
||||
|
||||
/* Compute the total memory wasted in fragmentation of inside small arena bins.
|
||||
* Done by summing the memory in unused regs in all slabs of all small bins.
|
||||
*
|
||||
|
||||
@@ -32,6 +32,13 @@ proc get_one_of_my_replica {id} {
|
||||
}
|
||||
set replica_port [lindex [lindex [lindex [R $id role] 2] 0] 1]
|
||||
set replica_id_num [get_instance_id_by_port redis $replica_port]
|
||||
|
||||
# To avoid -LOADING reply, wait until replica syncs with master.
|
||||
wait_for_condition 1000 50 {
|
||||
[RI $replica_id_num master_link_status] eq {up}
|
||||
} else {
|
||||
fail "Replica did not sync in time."
|
||||
}
|
||||
return $replica_id_num
|
||||
}
|
||||
|
||||
@@ -70,6 +77,7 @@ proc test_slave_load_expired_keys {aof} {
|
||||
|
||||
# wait for replica to be in sync with master
|
||||
wait_for_condition 500 10 {
|
||||
[RI $replica_id master_link_status] eq {up} &&
|
||||
[R $replica_id dbsize] eq [R $master_id dbsize]
|
||||
} else {
|
||||
fail "replica didn't sync"
|
||||
@@ -104,8 +112,15 @@ proc test_slave_load_expired_keys {aof} {
|
||||
# start the replica again (loading an RDB or AOF file)
|
||||
restart_instance redis $replica_id
|
||||
|
||||
# Replica may start a full sync after restart, trying in a loop to avoid
|
||||
# -LOADING reply in that case.
|
||||
wait_for_condition 1000 50 {
|
||||
[catch {set replica_dbsize_3 [R $replica_id dbsize]} e] == 0
|
||||
} else {
|
||||
fail "Replica is not up."
|
||||
}
|
||||
|
||||
# make sure the keys are still there
|
||||
set replica_dbsize_3 [R $replica_id dbsize]
|
||||
assert {$replica_dbsize_3 > $replica_dbsize_0}
|
||||
|
||||
# restore settings
|
||||
@@ -113,6 +128,7 @@ proc test_slave_load_expired_keys {aof} {
|
||||
|
||||
# wait for the master to expire all keys and replica to get the DELs
|
||||
wait_for_condition 500 10 {
|
||||
[RI $replica_id master_link_status] eq {up} &&
|
||||
[R $replica_id dbsize] eq $master_dbsize_0
|
||||
} else {
|
||||
fail "keys didn't expire"
|
||||
|
||||
@@ -124,6 +124,10 @@ test "Verify health as fail for killed node" {
|
||||
}
|
||||
}
|
||||
|
||||
test "Verify that other nodes can correctly output the new master's slots" {
|
||||
assert_not_equal {} [dict get [get_node_info_from_shard [R 4 CLUSTER MYID] 8 "shard"] slots]
|
||||
}
|
||||
|
||||
set primary_id 4
|
||||
set replica_id 0
|
||||
|
||||
@@ -133,7 +137,8 @@ test "Restarting primary node" {
|
||||
|
||||
test "Instance #0 gets converted into a replica" {
|
||||
wait_for_condition 1000 50 {
|
||||
[RI $replica_id role] eq {slave}
|
||||
[RI $replica_id role] eq {slave} &&
|
||||
[RI $replica_id master_link_status] eq {up}
|
||||
} else {
|
||||
fail "Old primary was not converted into replica"
|
||||
}
|
||||
|
||||
@@ -32,6 +32,17 @@ test "Cluster nodes hard reset" {
|
||||
} else {
|
||||
set node_timeout 3000
|
||||
}
|
||||
|
||||
# Wait until slave is synced. Otherwise, it may reply -LOADING
|
||||
# for any commands below.
|
||||
if {[RI $id role] eq {slave}} {
|
||||
wait_for_condition 50 1000 {
|
||||
[RI $id master_link_status] eq {up}
|
||||
} else {
|
||||
fail "Slave were not able to sync."
|
||||
}
|
||||
}
|
||||
|
||||
catch {R $id flushall} ; # May fail for readonly slaves.
|
||||
R $id MULTI
|
||||
R $id cluster reset hard
|
||||
|
||||
@@ -655,4 +655,50 @@ tags {"aof external:skip"} {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start_server {overrides {loading-process-events-interval-bytes 1024}} {
|
||||
test "Allow changing appendonly config while loading from AOF on startup" {
|
||||
# Set AOF enabled, populate db and restart.
|
||||
r config set appendonly yes
|
||||
r config set key-load-delay 100
|
||||
r config rewrite
|
||||
populate 10000
|
||||
restart_server 0 false false
|
||||
|
||||
# Disable AOF while loading from the disk.
|
||||
assert_equal 1 [s loading]
|
||||
r config set appendonly no
|
||||
assert_equal 1 [s loading]
|
||||
|
||||
# Speed up loading, verify AOF disabled.
|
||||
r config set key-load-delay 0
|
||||
wait_done_loading r
|
||||
assert_equal {10000} [r dbsize]
|
||||
assert_equal 0 [s aof_enabled]
|
||||
}
|
||||
|
||||
test "Allow changing appendonly config while loading from RDB on startup" {
|
||||
# Set AOF disabled, populate db and restart.
|
||||
r flushall
|
||||
r config set appendonly no
|
||||
r config set key-load-delay 100
|
||||
r config rewrite
|
||||
populate 10000
|
||||
r save
|
||||
restart_server 0 false false
|
||||
|
||||
# Enable AOF while loading from the disk.
|
||||
assert_equal 1 [s loading]
|
||||
r config set appendonly yes
|
||||
assert_equal 1 [s loading]
|
||||
|
||||
# Speed up loading, verify AOF enabled, do a quick sanity.
|
||||
r config set key-load-delay 0
|
||||
wait_done_loading r
|
||||
assert_equal {10000} [r dbsize]
|
||||
assert_equal 1 [s aof_enabled]
|
||||
r set t 1
|
||||
assert_equal {1} [r get t]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +122,7 @@ foreach sanitize_dump {no yes} {
|
||||
set restore_failed false
|
||||
set report_and_restart false
|
||||
set sent {}
|
||||
set expired_subkeys [s expired_subkeys]
|
||||
# RESTORE can fail, but hopefully not terminate
|
||||
if { [catch { r restore "_$k" 0 $dump REPLACE } err] } {
|
||||
set restore_failed true
|
||||
@@ -145,7 +146,17 @@ foreach sanitize_dump {no yes} {
|
||||
# if RESTORE didn't fail or terminate, run some random traffic on the new key
|
||||
incr stat_successful_restore
|
||||
if { [ catch {
|
||||
set sent [generate_fuzzy_traffic_on_key "_$k" 1] ;# traffic for 1 second
|
||||
set type [r type "_$k"]
|
||||
if {$type eq {none}} {
|
||||
# The key has been removed due to expiration.
|
||||
# Ensure the server didn't terminate during expiration and verify
|
||||
# expire stats to confirm the key was removed due to expiration.
|
||||
r ping
|
||||
assert_morethan [s expired_subkeys] $expired_subkeys
|
||||
} else {
|
||||
set sent [generate_fuzzy_traffic_on_key "_$k" $type 1] ;# traffic for 1 second
|
||||
}
|
||||
|
||||
incr stat_traffic_commands_sent [llength $sent]
|
||||
r del "_$k" ;# in case the server terminated, here's where we'll detect it.
|
||||
if {$dbsize != [r dbsize]} {
|
||||
@@ -182,7 +193,7 @@ foreach sanitize_dump {no yes} {
|
||||
dump_server_log $srv
|
||||
}
|
||||
|
||||
puts "Server crashed (by signal: $by_signal), with payload: $printable_dump"
|
||||
puts "Server crashed (by signal: $by_signal, err: $err), with payload: $printable_dump"
|
||||
set print_commands true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -885,5 +885,17 @@ test {corrupt payload: fuzzer findings - set with duplicate elements causes sdif
|
||||
}
|
||||
} {} {logreqres:skip} ;# This test violates {"uniqueItems": true}
|
||||
|
||||
test {corrupt payload: fuzzer findings - set with invalid length causes smembers to hang} {
|
||||
start_server [list overrides [list loglevel verbose use-exit-on-panic yes crash-memcheck-enabled no] ] {
|
||||
# In the past, it generated a broken protocol and left the client hung in smembers
|
||||
r config set sanitize-dump-payload no
|
||||
assert_equal {OK} [r restore _set 0 "\x14\x16\x16\x00\x00\x00\x0c\x00\x81\x61\x02\x81\x62\x02\x81\x63\x02\x01\x01\x02\x01\x03\x01\xff\x0c\x00\x91\x00\x56\x73\xc1\x82\xd5\xbd" replace]
|
||||
assert_encoding listpack _set
|
||||
catch { r SMEMBERS _set } err
|
||||
assert_equal [count_log_message 0 "crashed by signal"] 0
|
||||
assert_equal [count_log_message 0 "ASSERTION FAILED"] 1
|
||||
}
|
||||
}
|
||||
|
||||
} ;# tags
|
||||
|
||||
|
||||
@@ -809,3 +809,27 @@ if {!$::tls} { ;# fake_redis_node doesn't support TLS
|
||||
assert_equal "a\n1\nb\n2\nc\n3" [exec {*}$cmdline ZRANGE new_zset 0 -1 WITHSCORES]
|
||||
}
|
||||
}
|
||||
|
||||
start_server {tags {"cli external:skip"}} {
|
||||
test_interactive_cli_with_prompt "db_num showed in redis-cli after reconnected" {
|
||||
run_command $fd "select 0\x0D"
|
||||
run_command $fd "set a zoo-0\x0D"
|
||||
run_command $fd "select 6\x0D"
|
||||
run_command $fd "set a zoo-6\x0D"
|
||||
r save
|
||||
|
||||
# kill server and restart
|
||||
exec kill [s process_id]
|
||||
wait_for_log_messages 0 {"*Redis is now ready to exit*"} 0 1000 10
|
||||
catch {[run_command $fd "ping\x0D"]} err
|
||||
restart_server 0 true false 0
|
||||
|
||||
# redis-cli should show '[6]' after reconnected and return 'zoo-6'
|
||||
write_cli $fd "GET a\x0D"
|
||||
after 100
|
||||
set result [format_output [read_cli $fd]]
|
||||
set regex {not connected> GET a.*"zoo-6".*127\.0\.0\.1:[0-9]*\[6\]>}
|
||||
assert_equal 1 [regexp $regex $result]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -652,7 +652,6 @@ foreach testType {Successful Aborted} {
|
||||
}
|
||||
|
||||
test {Blocked commands and configs during async-loading} {
|
||||
assert_error {LOADING*} {$replica config set appendonly no}
|
||||
assert_error {LOADING*} {$replica REPLICAOF no one}
|
||||
}
|
||||
|
||||
@@ -1457,3 +1456,143 @@ start_server {tags {"repl external:skip"}} {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach disklessload {disabled on-empty-db} {
|
||||
test "Replica should reply LOADING while flushing a large db (disklessload: $disklessload)" {
|
||||
start_server {} {
|
||||
set replica [srv 0 client]
|
||||
start_server {} {
|
||||
set master [srv 0 client]
|
||||
set master_host [srv 0 host]
|
||||
set master_port [srv 0 port]
|
||||
|
||||
$replica config set repl-diskless-load $disklessload
|
||||
|
||||
# Populate replica with many keys, master with a few keys.
|
||||
$replica debug populate 2000000
|
||||
populate 3 master 10
|
||||
|
||||
# Start the replication process...
|
||||
$replica replicaof $master_host $master_port
|
||||
|
||||
wait_for_condition 100 100 {
|
||||
[s -1 loading] eq 1
|
||||
} else {
|
||||
fail "Replica didn't get into loading mode"
|
||||
}
|
||||
|
||||
# If replica has a large db, it may take some time to discard it
|
||||
# after receiving new db from the master. In this case, replica
|
||||
# should reply -LOADING. Replica may reply -LOADING while
|
||||
# loading the new db as well. To test the first case, populated
|
||||
# replica with large amount of keys and master with a few keys.
|
||||
# Discarding old db will take a long time and loading new one
|
||||
# will be quick. So, if we receive -LOADING, most probably it is
|
||||
# when flushing the db.
|
||||
wait_for_condition 1 10000 {
|
||||
[catch {$replica ping} err] &&
|
||||
[string match *LOADING* $err]
|
||||
} else {
|
||||
# There is a chance that we may not catch LOADING response
|
||||
# if flushing db happens too fast compared to test execution
|
||||
# Then, we may consider increasing key count or introducing
|
||||
# artificial delay to db flush.
|
||||
fail "Replica did not reply LOADING."
|
||||
}
|
||||
|
||||
catch {$replica shutdown nosave}
|
||||
}
|
||||
}
|
||||
} {} {repl external:skip}
|
||||
}
|
||||
|
||||
start_server {tags {"repl external:skip"} overrides {save {}}} {
|
||||
set master [srv 0 client]
|
||||
set master_host [srv 0 host]
|
||||
set master_port [srv 0 port]
|
||||
populate 10000 master 10
|
||||
|
||||
start_server {overrides {save {} rdb-del-sync-files yes loading-process-events-interval-bytes 1024}} {
|
||||
test "Allow appendonly config change while loading rdb on slave" {
|
||||
set replica [srv 0 client]
|
||||
|
||||
# While loading rdb on slave, verify appendonly config changes are allowed
|
||||
# 1- Change appendonly config from no to yes
|
||||
$replica config set appendonly no
|
||||
$replica config set key-load-delay 100
|
||||
$replica debug populate 1000
|
||||
|
||||
# Start the replication process...
|
||||
$replica replicaof $master_host $master_port
|
||||
|
||||
wait_for_condition 10 1000 {
|
||||
[s loading] eq 1
|
||||
} else {
|
||||
fail "Replica didn't get into loading mode"
|
||||
}
|
||||
|
||||
# Change config while replica is loading data
|
||||
$replica config set appendonly yes
|
||||
assert_equal 1 [s loading]
|
||||
|
||||
# Speed up loading and verify aof is enabled
|
||||
$replica config set key-load-delay 0
|
||||
wait_done_loading $replica
|
||||
assert_equal 1 [s aof_enabled]
|
||||
|
||||
# Quick sanity for AOF
|
||||
$replica replicaof no one
|
||||
set prev [s aof_current_size]
|
||||
$replica set x 100
|
||||
assert_morethan [s aof_current_size] $prev
|
||||
|
||||
# 2- While loading rdb, change appendonly from yes to no
|
||||
$replica config set appendonly yes
|
||||
$replica config set key-load-delay 100
|
||||
$replica flushall
|
||||
|
||||
# Start the replication process...
|
||||
$replica replicaof $master_host $master_port
|
||||
|
||||
wait_for_condition 10 1000 {
|
||||
[s loading] eq 1
|
||||
} else {
|
||||
fail "Replica didn't get into loading mode"
|
||||
}
|
||||
|
||||
# Change config while replica is loading data
|
||||
$replica config set appendonly no
|
||||
assert_equal 1 [s loading]
|
||||
|
||||
# Speed up loading and verify aof is disabled
|
||||
$replica config set key-load-delay 0
|
||||
wait_done_loading $replica
|
||||
assert_equal 0 [s 0 aof_enabled]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start_server {tags {"repl external:skip"}} {
|
||||
set replica [srv 0 client]
|
||||
start_server {} {
|
||||
set master [srv 0 client]
|
||||
set master_host [srv 0 host]
|
||||
set master_port [srv 0 port]
|
||||
|
||||
test "Replica flushes db lazily when replica-lazy-flush enabled" {
|
||||
$replica config set replica-lazy-flush yes
|
||||
$replica debug populate 1000
|
||||
populate 1 master 10
|
||||
|
||||
# Start the replication process...
|
||||
$replica replicaof $master_host $master_port
|
||||
|
||||
wait_for_condition 100 100 {
|
||||
[s -1 lazyfreed_objects] >= 1000 &&
|
||||
[s -1 master_link_status] eq {up}
|
||||
} else {
|
||||
fail "Replica did not free db lazily"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "redismodule.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static RedisModuleType *FragType;
|
||||
|
||||
@@ -17,9 +18,12 @@ unsigned long int last_set_cursor = 0;
|
||||
|
||||
unsigned long int datatype_attempts = 0;
|
||||
unsigned long int datatype_defragged = 0;
|
||||
unsigned long int datatype_raw_defragged = 0;
|
||||
unsigned long int datatype_resumes = 0;
|
||||
unsigned long int datatype_wrong_cursor = 0;
|
||||
unsigned long int global_attempts = 0;
|
||||
unsigned long int defrag_started = 0;
|
||||
unsigned long int defrag_ended = 0;
|
||||
unsigned long int global_defragged = 0;
|
||||
|
||||
int global_strings_len = 0;
|
||||
@@ -47,16 +51,29 @@ static void defragGlobalStrings(RedisModuleDefragCtx *ctx)
|
||||
}
|
||||
}
|
||||
|
||||
static void defragStart(RedisModuleDefragCtx *ctx) {
|
||||
REDISMODULE_NOT_USED(ctx);
|
||||
defrag_started++;
|
||||
}
|
||||
|
||||
static void defragEnd(RedisModuleDefragCtx *ctx) {
|
||||
REDISMODULE_NOT_USED(ctx);
|
||||
defrag_ended++;
|
||||
}
|
||||
|
||||
static void FragInfo(RedisModuleInfoCtx *ctx, int for_crash_report) {
|
||||
REDISMODULE_NOT_USED(for_crash_report);
|
||||
|
||||
RedisModule_InfoAddSection(ctx, "stats");
|
||||
RedisModule_InfoAddFieldLongLong(ctx, "datatype_attempts", datatype_attempts);
|
||||
RedisModule_InfoAddFieldLongLong(ctx, "datatype_defragged", datatype_defragged);
|
||||
RedisModule_InfoAddFieldLongLong(ctx, "datatype_raw_defragged", datatype_raw_defragged);
|
||||
RedisModule_InfoAddFieldLongLong(ctx, "datatype_resumes", datatype_resumes);
|
||||
RedisModule_InfoAddFieldLongLong(ctx, "datatype_wrong_cursor", datatype_wrong_cursor);
|
||||
RedisModule_InfoAddFieldLongLong(ctx, "global_attempts", global_attempts);
|
||||
RedisModule_InfoAddFieldLongLong(ctx, "global_defragged", global_defragged);
|
||||
RedisModule_InfoAddFieldLongLong(ctx, "defrag_started", defrag_started);
|
||||
RedisModule_InfoAddFieldLongLong(ctx, "defrag_ended", defrag_ended);
|
||||
}
|
||||
|
||||
struct FragObject *createFragObject(unsigned long len, unsigned long size, int maxstep) {
|
||||
@@ -79,10 +96,13 @@ static int fragResetStatsCommand(RedisModuleCtx *ctx, RedisModuleString **argv,
|
||||
|
||||
datatype_attempts = 0;
|
||||
datatype_defragged = 0;
|
||||
datatype_raw_defragged = 0;
|
||||
datatype_resumes = 0;
|
||||
datatype_wrong_cursor = 0;
|
||||
global_attempts = 0;
|
||||
global_defragged = 0;
|
||||
defrag_started = 0;
|
||||
defrag_ended = 0;
|
||||
|
||||
RedisModule_ReplyWithSimpleString(ctx, "OK");
|
||||
return REDISMODULE_OK;
|
||||
@@ -188,6 +208,14 @@ int FragDefrag(RedisModuleDefragCtx *ctx, RedisModuleString *key, void **value)
|
||||
}
|
||||
}
|
||||
|
||||
/* Defrag the values array itself using RedisModule_DefragAllocRaw
|
||||
* and RedisModule_DefragFreeRaw for testing purposes. */
|
||||
void *new_values = RedisModule_DefragAllocRaw(ctx, o->len * sizeof(void*));
|
||||
memcpy(new_values, o->values, o->len * sizeof(void*));
|
||||
RedisModule_DefragFreeRaw(ctx, o->values);
|
||||
o->values = new_values;
|
||||
datatype_raw_defragged++;
|
||||
|
||||
last_set_cursor = 0;
|
||||
return 0;
|
||||
}
|
||||
@@ -230,6 +258,7 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
|
||||
|
||||
RedisModule_RegisterInfoFunc(ctx, FragInfo);
|
||||
RedisModule_RegisterDefragFunc(ctx, defragGlobalStrings);
|
||||
RedisModule_RegisterDefragCallbacks(ctx, defragStart, defragEnd);
|
||||
|
||||
return REDISMODULE_OK;
|
||||
}
|
||||
|
||||
@@ -217,6 +217,7 @@ proc test {name code {okpattern undefined} {tags {}}} {
|
||||
|
||||
send_data_packet $::test_server_fd testing $name
|
||||
|
||||
set failed false
|
||||
set test_start_time [clock milliseconds]
|
||||
if {[catch {set retval [uplevel 1 $code]} error]} {
|
||||
set assertion [string match "assertion:*" $error]
|
||||
@@ -231,6 +232,7 @@ proc test {name code {okpattern undefined} {tags {}}} {
|
||||
lappend ::tests_failed $details
|
||||
|
||||
incr ::num_failed
|
||||
set failed true
|
||||
send_data_packet $::test_server_fd err [join $details "\n"]
|
||||
|
||||
if {$::stop_on_failure} {
|
||||
@@ -253,10 +255,17 @@ proc test {name code {okpattern undefined} {tags {}}} {
|
||||
lappend ::tests_failed $details
|
||||
|
||||
incr ::num_failed
|
||||
set failed true
|
||||
send_data_packet $::test_server_fd err [join $details "\n"]
|
||||
}
|
||||
}
|
||||
|
||||
if {$::dump_logs && $failed} {
|
||||
foreach srv $::servers {
|
||||
dump_server_log $srv
|
||||
}
|
||||
}
|
||||
|
||||
if {$::traceleaks} {
|
||||
set output [exec leaks redis-server]
|
||||
if {![string match {*0 leaks*} $output]} {
|
||||
|
||||
@@ -698,7 +698,7 @@ proc latencyrstat_percentiles {cmd r} {
|
||||
}
|
||||
}
|
||||
|
||||
proc generate_fuzzy_traffic_on_key {key duration} {
|
||||
proc generate_fuzzy_traffic_on_key {key type duration} {
|
||||
# Commands per type, blocking commands removed
|
||||
# TODO: extract these from COMMAND DOCS, and improve to include other types
|
||||
set string_commands {APPEND BITCOUNT BITFIELD BITOP BITPOS DECR DECRBY GET GETBIT GETRANGE GETSET INCR INCRBY INCRBYFLOAT MGET MSET MSETNX PSETEX SET SETBIT SETEX SETNX SETRANGE LCS STRLEN}
|
||||
@@ -709,7 +709,6 @@ proc generate_fuzzy_traffic_on_key {key duration} {
|
||||
set stream_commands {XACK XADD XCLAIM XDEL XGROUP XINFO XLEN XPENDING XRANGE XREAD XREADGROUP XREVRANGE XTRIM}
|
||||
set commands [dict create string $string_commands hash $hash_commands zset $zset_commands list $list_commands set $set_commands stream $stream_commands]
|
||||
|
||||
set type [r type $key]
|
||||
set cmds [dict get $commands $type]
|
||||
set start_time [clock seconds]
|
||||
set sent {}
|
||||
|
||||
@@ -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 get_slot_field {slot_output shard_id node_id attrib_id} {
|
||||
return [lindex [lindex [lindex $slot_output $shard_id] $node_id] $attrib_id]
|
||||
}
|
||||
@@ -116,10 +129,11 @@ test "Verify the nodes configured with prefer hostname only show hostname for ne
|
||||
# Have everyone forget node 6 and isolate it from the cluster.
|
||||
isolate_node 6
|
||||
|
||||
# Set hostnames for the masters, now that the node is isolated
|
||||
R 0 config set cluster-announce-hostname "shard-1.com"
|
||||
R 1 config set cluster-announce-hostname "shard-2.com"
|
||||
R 2 config set cluster-announce-hostname "shard-3.com"
|
||||
set primaries 3
|
||||
for {set j 0} {$j < $primaries} {incr j} {
|
||||
# Set hostnames for the masters, now that the node is isolated
|
||||
R $j config set cluster-announce-hostname "shard-$j.com"
|
||||
}
|
||||
|
||||
# Prevent Node 0 and Node 6 from properly meeting,
|
||||
# they'll hang in the handshake phase. This allows us to
|
||||
@@ -149,9 +163,17 @@ test "Verify the nodes configured with prefer hostname only show hostname for ne
|
||||
} else {
|
||||
fail "Node did not learn about the 2 shards it can talk to"
|
||||
}
|
||||
set slot_result [R 6 CLUSTER SLOTS]
|
||||
assert_equal [lindex [get_slot_field $slot_result 0 2 3] 1] "shard-2.com"
|
||||
assert_equal [lindex [get_slot_field $slot_result 1 2 3] 1] "shard-3.com"
|
||||
wait_for_condition 50 100 {
|
||||
[lindex [get_slot_field [R 6 CLUSTER SLOTS] 0 2 3] 1] eq "shard-1.com"
|
||||
} else {
|
||||
fail "hostname for shard-1 didn't reach node 6"
|
||||
}
|
||||
|
||||
wait_for_condition 50 100 {
|
||||
[lindex [get_slot_field [R 6 CLUSTER SLOTS] 1 2 3] 1] eq "shard-2.com"
|
||||
} else {
|
||||
fail "hostname for shard-2 didn't reach node 6"
|
||||
}
|
||||
|
||||
# Also make sure we know about the isolated master, we
|
||||
# just can't reach it.
|
||||
@@ -170,10 +192,14 @@ test "Verify the nodes configured with prefer hostname only show hostname for ne
|
||||
} else {
|
||||
fail "Node did not learn about the 2 shards it can talk to"
|
||||
}
|
||||
set slot_result [R 6 CLUSTER SLOTS]
|
||||
assert_equal [lindex [get_slot_field $slot_result 0 2 3] 1] "shard-1.com"
|
||||
assert_equal [lindex [get_slot_field $slot_result 1 2 3] 1] "shard-2.com"
|
||||
assert_equal [lindex [get_slot_field $slot_result 2 2 3] 1] "shard-3.com"
|
||||
|
||||
for {set j 0} {$j < $primaries} {incr j} {
|
||||
wait_for_condition 50 100 {
|
||||
[lindex [get_slot_field [R 6 CLUSTER SLOTS] $j 2 3] 1] eq "shard-$j.com"
|
||||
} else {
|
||||
fail "hostname information for shard-$j didn't reach node 6"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "Test restart will keep hostname information" {
|
||||
|
||||
@@ -18,6 +18,9 @@ start_server {tags {"modules"} overrides {{save ""}}} {
|
||||
set info [r info defragtest_stats]
|
||||
assert {[getInfoProperty $info defragtest_datatype_attempts] > 0}
|
||||
assert_equal 0 [getInfoProperty $info defragtest_datatype_resumes]
|
||||
assert_morethan [getInfoProperty $info defragtest_datatype_raw_defragged] 0
|
||||
assert_morethan [getInfoProperty $info defragtest_defrag_started] 0
|
||||
assert_morethan [getInfoProperty $info defragtest_defrag_ended] 0
|
||||
}
|
||||
|
||||
test {Module defrag: late defrag with cursor works} {
|
||||
@@ -32,6 +35,9 @@ start_server {tags {"modules"} overrides {{save ""}}} {
|
||||
set info [r info defragtest_stats]
|
||||
assert {[getInfoProperty $info defragtest_datatype_resumes] > 10}
|
||||
assert_equal 0 [getInfoProperty $info defragtest_datatype_wrong_cursor]
|
||||
assert_morethan [getInfoProperty $info defragtest_datatype_raw_defragged] 0
|
||||
assert_morethan [getInfoProperty $info defragtest_defrag_started] 0
|
||||
assert_morethan [getInfoProperty $info defragtest_defrag_ended] 0
|
||||
}
|
||||
|
||||
test {Module defrag: global defrag works} {
|
||||
@@ -41,6 +47,8 @@ start_server {tags {"modules"} overrides {{save ""}}} {
|
||||
after 2000
|
||||
set info [r info defragtest_stats]
|
||||
assert {[getInfoProperty $info defragtest_global_attempts] > 0}
|
||||
assert_morethan [getInfoProperty $info defragtest_defrag_started] 0
|
||||
assert_morethan [getInfoProperty $info defragtest_defrag_ended] 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,9 @@ start_server {tags {"modules"}} {
|
||||
# Verify the value in the loaded rdb
|
||||
assert_equal v1 [r get k]
|
||||
|
||||
# Verify aof is still enabled after RM_RdbLoad() call
|
||||
assert_equal 1 [s aof_enabled]
|
||||
|
||||
r flushdb
|
||||
r config set rdb-key-save-delay 0
|
||||
r config set appendonly no
|
||||
|
||||
@@ -61,9 +61,9 @@ start_server {tags {"modules"}} {
|
||||
r hmset hh f1 v1 f2 v2 f3 v3
|
||||
r hpexpire hh 1 fields 3 f1 f2 f3
|
||||
after 10
|
||||
assert_equal [r scan.scan_key hh] {}
|
||||
r debug set-active-expire 1
|
||||
lsort [r scan.scan_key hh]
|
||||
} {} {needs:debug}
|
||||
} {OK} {needs:debug}
|
||||
|
||||
test {Module scan zset listpack} {
|
||||
r zadd zz 1 f1 2 f2
|
||||
|
||||
+81
-4
@@ -1,3 +1,15 @@
|
||||
#
|
||||
# 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 client_idle_sec {name} {
|
||||
set clients [split [r client list] "\r\n"]
|
||||
set c [lsearch -inline $clients *name=$name*]
|
||||
@@ -24,16 +36,45 @@ start_server {tags {"querybuf slow"}} {
|
||||
# The test will run at least 2s to check if client query
|
||||
# buffer will be resized when client idle 2s.
|
||||
test "query buffer resized correctly" {
|
||||
set rd [redis_client]
|
||||
|
||||
set rd [redis_deferring_client]
|
||||
|
||||
$rd client setname test_client
|
||||
$rd read
|
||||
|
||||
# Make sure query buff has size of 0 bytes at start as the client uses the reusable qb.
|
||||
assert {[client_query_buffer test_client] == 0}
|
||||
|
||||
# Pause cron to prevent premature shrinking (timing issue).
|
||||
r debug pause-cron 1
|
||||
|
||||
# Send partial command to client to make sure it doesn't use the reusable qb.
|
||||
$rd write "*3\r\n\$3\r\nset\r\n\$2\r\na"
|
||||
$rd flush
|
||||
# Wait for the client to start using a private query buffer.
|
||||
wait_for_condition 1000 10 {
|
||||
[client_query_buffer test_client] > 0
|
||||
} else {
|
||||
fail "client should start using a private query buffer"
|
||||
}
|
||||
|
||||
# send the rest of the command
|
||||
$rd write "a\r\n\$1\r\nb\r\n"
|
||||
$rd flush
|
||||
assert_equal {OK} [$rd read]
|
||||
|
||||
set orig_test_client_qbuf [client_query_buffer test_client]
|
||||
# Make sure query buff has less than the peak resize threshold (PROTO_RESIZE_THRESHOLD) 32k
|
||||
# but at least the basic IO reading buffer size (PROTO_IOBUF_LEN) 16k
|
||||
assert {$orig_test_client_qbuf >= 16384 && $orig_test_client_qbuf < 32768}
|
||||
set MAX_QUERY_BUFFER_SIZE [expr 32768 + 2] ; # 32k + 2, allowing for potential greedy allocation of (16k + 1) * 2 bytes for the query buffer.
|
||||
assert {$orig_test_client_qbuf >= 16384 && $orig_test_client_qbuf <= $MAX_QUERY_BUFFER_SIZE}
|
||||
|
||||
# Allow shrinking to occur
|
||||
r debug pause-cron 0
|
||||
|
||||
# Check that the initial query buffer is resized after 2 sec
|
||||
wait_for_condition 1000 10 {
|
||||
[client_idle_sec test_client] >= 3 && [client_query_buffer test_client] == 0
|
||||
[client_idle_sec test_client] >= 3 && [client_query_buffer test_client] < $orig_test_client_qbuf
|
||||
} else {
|
||||
fail "query buffer was not resized"
|
||||
}
|
||||
@@ -75,10 +116,27 @@ start_server {tags {"querybuf slow"}} {
|
||||
test "query buffer resized correctly with fat argv" {
|
||||
set rd [redis_client]
|
||||
$rd client setname test_client
|
||||
|
||||
# Pause cron to prevent premature shrinking (timing issue).
|
||||
r debug pause-cron 1
|
||||
|
||||
$rd write "*3\r\n\$3\r\nset\r\n\$1\r\na\r\n\$1000000\r\n"
|
||||
$rd flush
|
||||
|
||||
# Wait for the client to start using a private query buffer of > 1000000 size.
|
||||
wait_for_condition 1000 10 {
|
||||
[client_query_buffer test_client] > 1000000
|
||||
} else {
|
||||
fail "client should start using a private query buffer"
|
||||
}
|
||||
|
||||
after 20
|
||||
# Send the start of the arg and make sure the client is not using reusable qb for it rather a private buf of > 1000000 size.
|
||||
$rd write "a"
|
||||
$rd flush
|
||||
|
||||
r debug pause-cron 0
|
||||
|
||||
after 120
|
||||
if {[client_query_buffer test_client] < 1000000} {
|
||||
fail "query buffer should not be resized when client idle time smaller than 2s"
|
||||
}
|
||||
@@ -92,5 +150,24 @@ start_server {tags {"querybuf slow"}} {
|
||||
|
||||
$rd close
|
||||
}
|
||||
}
|
||||
|
||||
start_server {tags {"querybuf"}} {
|
||||
test "Client executes small argv commands using reusable query buffer" {
|
||||
set rd [redis_deferring_client]
|
||||
$rd client setname test_client
|
||||
$rd read
|
||||
set res [r client list]
|
||||
|
||||
# Verify that the client does not create a private query buffer after
|
||||
# executing a small parameter command.
|
||||
assert_match {*name=test_client * qbuf=0 qbuf-free=0 * cmd=client|setname *} $res
|
||||
|
||||
# The client executing the command is currently using the reusable query buffer,
|
||||
# so the size shown is that of the reusable query buffer. It will be returned
|
||||
# to the reusable query buffer after command execution.
|
||||
assert_match {*qbuf=26 qbuf-free=* cmd=client|list *} $res
|
||||
|
||||
$rd close
|
||||
}
|
||||
}
|
||||
|
||||
+3
-22
@@ -109,25 +109,9 @@ proc test_scan {type} {
|
||||
|
||||
after 2
|
||||
|
||||
# TODO: remove this in redis 8.0
|
||||
set cur 0
|
||||
set keys {}
|
||||
while 1 {
|
||||
set res [r scan $cur type "string1"]
|
||||
set cur [lindex $res 0]
|
||||
set k [lindex $res 1]
|
||||
lappend keys {*}$k
|
||||
if {$cur == 0} break
|
||||
}
|
||||
|
||||
assert_equal 0 [llength $keys]
|
||||
# make sure that expired key have been removed by scan command
|
||||
assert_equal 1000 [scan [regexp -inline {keys\=([\d]*)} [r info keyspace]] keys=%d]
|
||||
|
||||
# TODO: uncomment in redis 8.0
|
||||
#assert_error "*unknown type name*" {r scan 0 type "string1"}
|
||||
assert_error "*unknown type name*" {r scan 0 type "string1"}
|
||||
# expired key will be no touched by scan command
|
||||
#assert_equal 1001 [scan [regexp -inline {keys\=([\d]*)} [r info keyspace]] keys=%d]
|
||||
assert_equal 1001 [scan [regexp -inline {keys\=([\d]*)} [r info keyspace]] keys=%d]
|
||||
r debug set-active-expire 1
|
||||
} {OK} {needs:debug}
|
||||
|
||||
@@ -191,11 +175,8 @@ proc test_scan {type} {
|
||||
|
||||
assert_equal 1000 [llength $keys]
|
||||
|
||||
# make sure that expired key have been removed by scan command
|
||||
assert_equal 1000 [scan [regexp -inline {keys\=([\d]*)} [r info keyspace]] keys=%d]
|
||||
# TODO: uncomment in redis 8.0
|
||||
# make sure that only the expired key in the type match will been removed by scan command
|
||||
#assert_equal 1001 [scan [regexp -inline {keys\=([\d]*)} [r info keyspace]] keys=%d]
|
||||
assert_equal 1001 [scan [regexp -inline {keys\=([\d]*)} [r info keyspace]] keys=%d]
|
||||
|
||||
r debug set-active-expire 1
|
||||
} {OK} {needs:debug}
|
||||
|
||||
@@ -2069,6 +2069,14 @@ start_server {tags {"scripting"}} {
|
||||
} 1 x
|
||||
|
||||
r replicaof [srv -1 host] [srv -1 port]
|
||||
|
||||
# To avoid -LOADING reply, wait until replica syncs with master.
|
||||
wait_for_condition 50 100 {
|
||||
[s master_link_status] eq {up}
|
||||
} else {
|
||||
fail "Replica did not sync in time."
|
||||
}
|
||||
|
||||
assert_error {EXECABORT Transaction discarded because of: READONLY *} {$rr exec}
|
||||
assert_error {READONLY You can't write against a read only replica. script: *} {$rr2 exec}
|
||||
$rr close
|
||||
|
||||
@@ -560,10 +560,10 @@ start_server {tags {"external:skip needs:debug"}} {
|
||||
# Test with small hash
|
||||
r debug set-active-expire 0
|
||||
r del myhash
|
||||
r hset myhash1 f1 v1 f2 v2 f3 v3 f4 v4 f5 v5 f6 v6
|
||||
r hpexpire myhash1 1 NX FIELDS 3 f2 f4 f6
|
||||
r hset myhash f1 v1 f2 v2 f3 v3 f4 v4 f5 v5 f6 v6
|
||||
r hpexpire myhash 1 NX FIELDS 3 f2 f4 f6
|
||||
after 10
|
||||
assert_equal [lsort [r hgetall myhash1]] "f1 f3 f5 v1 v3 v5"
|
||||
assert_equal [lsort [r hgetall myhash]] "f1 f3 f5 v1 v3 v5"
|
||||
|
||||
# Test with large hash
|
||||
r del myhash
|
||||
@@ -573,6 +573,13 @@ start_server {tags {"external:skip needs:debug"}} {
|
||||
}
|
||||
after 10
|
||||
assert_equal [lsort [r hgetall myhash]] [lsort "f1 f2 f3 v1 v2 v3"]
|
||||
|
||||
# hash that all fields are expired return empty result
|
||||
r del myhash
|
||||
r hset myhash f1 v1 f2 v2 f3 v3 f4 v4 f5 v5 f6 v6
|
||||
r hpexpire myhash 1 FIELDS 6 f1 f2 f3 f4 f5 f6
|
||||
after 10
|
||||
assert_equal [r hgetall myhash] ""
|
||||
r debug set-active-expire 1
|
||||
}
|
||||
|
||||
@@ -786,6 +793,36 @@ start_server {tags {"external:skip needs:debug"}} {
|
||||
}
|
||||
}
|
||||
|
||||
test "Statistics - Hashes with HFEs ($type)" {
|
||||
r config resetstat
|
||||
r flushall
|
||||
|
||||
# hash1: 5 fields, 3 with TTL. subexpiry incr +1
|
||||
r hset myhash f1 v1 f2 v2 f3 v3 f4 v4 f5 v5
|
||||
r hpexpire myhash 150 FIELDS 3 f1 f2 f3
|
||||
assert_match [get_stat_subexpiry r] 1
|
||||
|
||||
# hash2: 5 fields, 3 with TTL. subexpiry incr +1
|
||||
r hset myhash2 f1 v1 f2 v2 f3 v3 f4 v4 f5 v5
|
||||
assert_match [get_stat_subexpiry r] 1
|
||||
r hpexpire myhash2 100 FIELDS 3 f1 f2 f3
|
||||
assert_match [get_stat_subexpiry r] 2
|
||||
|
||||
# hash3: 2 fields, 1 with TTL. HDEL field with TTL. subexpiry decr -1
|
||||
r hset myhash3 f1 v1 f2 v2
|
||||
r hpexpire myhash3 100 FIELDS 1 f2
|
||||
assert_match [get_stat_subexpiry r] 3
|
||||
r hdel myhash3 f2
|
||||
assert_match [get_stat_subexpiry r] 2
|
||||
|
||||
# Expired fields of hash1 and hash2. subexpiry decr -2
|
||||
wait_for_condition 50 50 {
|
||||
[get_stat_subexpiry r] == 0
|
||||
} else {
|
||||
fail "Hash field expiry statistics failed"
|
||||
}
|
||||
}
|
||||
|
||||
r config set hash-max-listpack-entries 512
|
||||
}
|
||||
|
||||
@@ -899,25 +936,6 @@ start_server {tags {"external:skip needs:debug"}} {
|
||||
|
||||
r config set hash-max-listpack-value 64
|
||||
}
|
||||
|
||||
test "Statistics - Hashes with HFEs" {
|
||||
r config resetstat
|
||||
r del myhash
|
||||
r hset myhash f1 v1 f2 v2 f3 v3 f4 v4 f5 v5
|
||||
r hpexpire myhash 100 FIELDS 3 f1 f2 f3
|
||||
|
||||
assert_match [get_stat_subexpiry r] 1
|
||||
r hset myhash2 f1 v1 f2 v2 f3 v3 f4 v4 f5 v5
|
||||
assert_match [get_stat_subexpiry r] 1
|
||||
r hpexpire myhash2 100 FIELDS 3 f1 f2 f3
|
||||
assert_match [get_stat_subexpiry r] 2
|
||||
|
||||
wait_for_condition 50 50 {
|
||||
[get_stat_subexpiry r] == 0
|
||||
} else {
|
||||
fail "Hash field expiry statistics failed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start_server {tags {"external:skip needs:debug"}} {
|
||||
|
||||
@@ -319,7 +319,7 @@ start_server {
|
||||
r XADD mystream 666 f v
|
||||
r XGROUP CREATE mystream mygroup $
|
||||
assert_error "ERR Unbalanced 'xreadgroup' list of streams: for each stream key an ID or '>' must be specified." {r XREADGROUP GROUP mygroup Alice COUNT 1 STREAMS mystream }
|
||||
assert_error "ERR Unbalanced 'xread' list of streams: for each stream key an ID or '$' must be specified." {r XREAD COUNT 1 STREAMS mystream }
|
||||
assert_error "ERR Unbalanced 'xread' list of streams: for each stream key an ID, '+', or '$' must be specified." {r XREAD COUNT 1 STREAMS mystream }
|
||||
}
|
||||
|
||||
test {Blocking XREAD: key deleted} {
|
||||
@@ -1239,6 +1239,74 @@ start_server {
|
||||
assert_equal [dict get $group lag] 2
|
||||
}
|
||||
|
||||
test {Consumer Group Lag with XDELs and tombstone after the last_id of consume group} {
|
||||
r DEL x
|
||||
r XGROUP CREATE x g1 $ MKSTREAM
|
||||
r XADD x 1-0 data a
|
||||
r XREADGROUP GROUP g1 alice STREAMS x > ;# Read one entry
|
||||
r XADD x 2-0 data c
|
||||
r XADD x 3-0 data d
|
||||
r XDEL x 2-0
|
||||
|
||||
# Now the latest tombstone(2-0) is before the first entry(3-0), but there is still
|
||||
# a tombstone(2-0) after the last_id(1-0) of the consume group.
|
||||
set reply [r XINFO STREAM x FULL]
|
||||
set group [lindex [dict get $reply groups] 0]
|
||||
assert_equal [dict get $group entries-read] 1
|
||||
assert_equal [dict get $group lag] {}
|
||||
|
||||
r XDEL x 1-0
|
||||
# Although there is a tombstone(2-0) after the consumer group's last_id(1-0), all
|
||||
# entries before the maximal tombstone have been deleted. This means that both the
|
||||
# last_id and the largest tombstone are behind the first entry. Therefore, tombstones
|
||||
# no longer affect the lag, which now reflects the remaining entries in the stream.
|
||||
set reply [r XINFO STREAM x FULL]
|
||||
set group [lindex [dict get $reply groups] 0]
|
||||
assert_equal [dict get $group entries-read] 1
|
||||
assert_equal [dict get $group lag] 1
|
||||
|
||||
# Now there is a tombstone(2-0) after the last_id of the consume group, so after consuming
|
||||
# entry(3-0), the group's counter will be invalid.
|
||||
r XREADGROUP GROUP g1 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] 3
|
||||
assert_equal [dict get $group lag] 0
|
||||
}
|
||||
|
||||
test {Consumer group lag with XTRIM} {
|
||||
r DEL x
|
||||
r XGROUP CREATE x mygroup $ MKSTREAM
|
||||
r XADD x 1-0 data a
|
||||
r XADD x 2-0 data b
|
||||
r XADD x 3-0 data c
|
||||
r XADD x 4-0 data d
|
||||
r XADD x 5-0 data e
|
||||
r XREADGROUP GROUP mygroup alice COUNT 1 STREAMS x >
|
||||
|
||||
set reply [r XINFO STREAM x FULL]
|
||||
set group [lindex [dict get $reply groups] 0]
|
||||
assert_equal [dict get $group entries-read] 1
|
||||
assert_equal [dict get $group lag] 4
|
||||
|
||||
# Although XTRIM doesn't update the `max-deleted-entry-id`, it always updates the
|
||||
# position of the first entry. When trimming causes the first entry to be behind
|
||||
# the consumer group's last_id, the consumer group's lag will always be equal to
|
||||
# the number of remainin entries in the stream.
|
||||
r XTRIM x MAXLEN 1
|
||||
set reply [r XINFO STREAM x FULL]
|
||||
set group [lindex [dict get $reply groups] 0]
|
||||
assert_equal [dict get $reply max-deleted-entry-id] "0-0"
|
||||
assert_equal [dict get $group entries-read] 1
|
||||
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]
|
||||
set group [lindex [dict get $reply groups] 0]
|
||||
assert_equal [dict get $group lag] 0
|
||||
}
|
||||
|
||||
test {Loading from legacy (Redis <= v6.2.x, rdb_ver < 10) persistence} {
|
||||
# The payload was DUMPed from a v5 instance after:
|
||||
# XADD x 1-0 data a
|
||||
|
||||
@@ -464,6 +464,12 @@ start_server {tags {"string"}} {
|
||||
assert_equal "" [r getrange mykey 5 3]
|
||||
assert_equal " World" [r getrange mykey 5 5000]
|
||||
assert_equal "Hello World" [r getrange mykey -5000 10000]
|
||||
assert_equal "" [r getrange mykey 0 -100]
|
||||
assert_equal "" [r getrange mykey 1 -100]
|
||||
assert_equal "" [r getrange mykey -1 -100]
|
||||
assert_equal "" [r getrange mykey -100 -99]
|
||||
assert_equal "" [r getrange mykey -100 -100]
|
||||
assert_equal "" [r getrange mykey -100 -101]
|
||||
}
|
||||
|
||||
test "GETRANGE against integer-encoded value" {
|
||||
@@ -474,6 +480,12 @@ start_server {tags {"string"}} {
|
||||
assert_equal "" [r getrange mykey 5 3]
|
||||
assert_equal "4" [r getrange mykey 3 5000]
|
||||
assert_equal "1234" [r getrange mykey -5000 10000]
|
||||
assert_equal "" [r getrange mykey 0 -100]
|
||||
assert_equal "" [r getrange mykey 1 -100]
|
||||
assert_equal "" [r getrange mykey -1 -100]
|
||||
assert_equal "" [r getrange mykey -100 -99]
|
||||
assert_equal "" [r getrange mykey -100 -100]
|
||||
assert_equal "" [r getrange mykey -100 -101]
|
||||
}
|
||||
|
||||
test "GETRANGE fuzzing" {
|
||||
|
||||
Reference in New Issue
Block a user