Merge remote-tracking branch 'upstream/unstable' into HEAD
CI / test-ubuntu-latest (push) Has been cancelled
CI / test-sanitizer-address (push) Has been cancelled
CI / build-debian-old (push) Has been cancelled
CI / build-macos-latest (push) Has been cancelled
CI / build-32bit (push) Has been cancelled
CI / build-libc-malloc (push) Has been cancelled
CI / build-centos-jemalloc (push) Has been cancelled
CI / build-old-chain-jemalloc (push) Has been cancelled
Codecov / code-coverage (push) Has been cancelled
External Server Tests / test-external-standalone (push) Has been cancelled
External Server Tests / test-external-cluster (push) Has been cancelled
External Server Tests / test-external-nodebug (push) Has been cancelled
Spellcheck / Spellcheck (push) Has been cancelled
CI / test-ubuntu-latest (push) Has been cancelled
CI / test-sanitizer-address (push) Has been cancelled
CI / build-debian-old (push) Has been cancelled
CI / build-macos-latest (push) Has been cancelled
CI / build-32bit (push) Has been cancelled
CI / build-libc-malloc (push) Has been cancelled
CI / build-centos-jemalloc (push) Has been cancelled
CI / build-old-chain-jemalloc (push) Has been cancelled
Codecov / code-coverage (push) Has been cancelled
External Server Tests / test-external-standalone (push) Has been cancelled
External Server Tests / test-external-cluster (push) Has been cancelled
External Server Tests / test-external-nodebug (push) Has been cancelled
Spellcheck / Spellcheck (push) Has been cancelled
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
SRC_DIR = src
|
||||
MODULE_VERSION = v8.1.00
|
||||
MODULE_VERSION = v8.1.90
|
||||
MODULE_REPO = https://github.com/redisbloom/redisbloom
|
||||
TARGET_MODULE = $(SRC_DIR)/bin/$(FULL_VARIANT)/redisbloom.so
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
SRC_DIR = src
|
||||
MODULE_VERSION = v8.1.00
|
||||
MODULE_VERSION = v8.1.90
|
||||
MODULE_REPO = https://github.com/redisearch/redisearch
|
||||
TARGET_MODULE = $(SRC_DIR)/bin/$(FULL_VARIANT)/search-community/redisearch.so
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
SRC_DIR = src
|
||||
MODULE_VERSION = v8.1.00
|
||||
MODULE_VERSION = v8.1.90
|
||||
MODULE_REPO = https://github.com/redisjson/redisjson
|
||||
TARGET_MODULE = $(SRC_DIR)/bin/$(FULL_VARIANT)/rejson.so
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
SRC_DIR = src
|
||||
MODULE_VERSION = v8.1.00
|
||||
MODULE_VERSION = v8.1.90
|
||||
MODULE_REPO = https://github.com/redistimeseries/redistimeseries
|
||||
TARGET_MODULE = $(SRC_DIR)/bin/$(FULL_VARIANT)/redistimeseries.so
|
||||
|
||||
|
||||
+10
@@ -1798,6 +1798,16 @@ aof-timestamp-enabled no
|
||||
#
|
||||
# cluster-compatibility-sample-ratio 0
|
||||
|
||||
# Clusters can be configured to track per-slot resource statistics,
|
||||
# which are accessible by the CLUSTER SLOT-STATS command.
|
||||
#
|
||||
# By default, the 'cluster-slot-stats-enabled' is disabled, and only 'key-count' is captured.
|
||||
# By enabling the 'cluster-slot-stats-enabled' config, the cluster will begin to capture advanced statistics.
|
||||
# These statistics can be leveraged to assess general slot usage trends, identify hot / cold slots,
|
||||
# migrate slots for a balanced cluster workload, and / or re-write application logic to better utilize slots.
|
||||
#
|
||||
# cluster-slot-stats-enabled no
|
||||
|
||||
# In order to setup your cluster make sure to read the documentation
|
||||
# available at https://redis.io web site.
|
||||
|
||||
|
||||
+1
-1
@@ -375,7 +375,7 @@ endif
|
||||
|
||||
REDIS_SERVER_NAME=redis-server$(PROG_SUFFIX)
|
||||
REDIS_SENTINEL_NAME=redis-sentinel$(PROG_SUFFIX)
|
||||
REDIS_SERVER_OBJ=threads_mngr.o memory_prefetch.o adlist.o quicklist.o ae.o anet.o dict.o ebuckets.o eventnotifier.o iothread.o mstr.o kvstore.o server.o sds.o zmalloc.o lzf_c.o lzf_d.o pqsort.o zipmap.o sha1.o ziplist.o release.o networking.o util.o object.o db.o replication.o rdb.o t_string.o t_list.o t_set.o t_zset.o t_hash.o config.o aof.o pubsub.o multi.o debug.o sort.o intset.o syncio.o cluster.o cluster_legacy.o crc16.o endianconv.o slowlog.o eval.o bio.o rio.o rand.o memtest.o syscheck.o crcspeed.o crccombine.o crc64.o bitops.o sentinel.o notify.o setproctitle.o blocked.o hyperloglog.o latency.o sparkline.o redis-check-rdb.o redis-check-aof.o geo.o lazyfree.o module.o evict.o expire.o geohash.o geohash_helper.o childinfo.o defrag.o siphash.o rax.o t_stream.o listpack.o localtime.o lolwut.o lolwut5.o lolwut6.o lolwut8.o acl.o tracking.o socket.o tls.o sha256.o timeout.o setcpuaffinity.o monotonic.o mt19937-64.o resp_parser.o call_reply.o script_lua.o script.o functions.o function_lua.o commands.o strl.o connection.o unix.o logreqres.o
|
||||
REDIS_SERVER_OBJ=threads_mngr.o memory_prefetch.o adlist.o quicklist.o ae.o anet.o dict.o ebuckets.o eventnotifier.o iothread.o mstr.o kvstore.o server.o sds.o zmalloc.o lzf_c.o lzf_d.o pqsort.o zipmap.o sha1.o ziplist.o release.o networking.o util.o object.o db.o replication.o rdb.o t_string.o t_list.o t_set.o t_zset.o t_hash.o config.o aof.o pubsub.o multi.o debug.o sort.o intset.o syncio.o cluster.o cluster_legacy.o cluster_slot_stats.o crc16.o endianconv.o slowlog.o eval.o bio.o rio.o rand.o memtest.o syscheck.o crcspeed.o crccombine.o crc64.o bitops.o sentinel.o notify.o setproctitle.o blocked.o hyperloglog.o latency.o sparkline.o redis-check-rdb.o redis-check-aof.o geo.o lazyfree.o module.o evict.o expire.o geohash.o geohash_helper.o childinfo.o defrag.o siphash.o rax.o t_stream.o listpack.o localtime.o lolwut.o lolwut5.o lolwut6.o lolwut8.o acl.o tracking.o socket.o tls.o sha256.o timeout.o setcpuaffinity.o monotonic.o mt19937-64.o resp_parser.o call_reply.o script_lua.o script.o functions.o function_lua.o commands.o strl.o connection.o unix.o logreqres.o
|
||||
REDIS_CLI_NAME=redis-cli$(PROG_SUFFIX)
|
||||
REDIS_CLI_OBJ=anet.o adlist.o dict.o redis-cli.o zmalloc.o release.o ae.o redisassert.o crcspeed.o crccombine.o crc64.o siphash.o crc16.o monotonic.o cli_common.o mt19937-64.o strl.o cli_commands.o
|
||||
REDIS_BENCHMARK_NAME=redis-benchmark$(PROG_SUFFIX)
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
#include "slowlog.h"
|
||||
#include "latency.h"
|
||||
#include "monotonic.h"
|
||||
#include "cluster_slot_stats.h"
|
||||
|
||||
/* forward declarations */
|
||||
static void unblockClientWaitingData(client *c);
|
||||
@@ -91,6 +92,7 @@ void blockClient(client *c, int btype) {
|
||||
* This function will make updates to the commandstats, slowlog and monitors.*/
|
||||
void updateStatsOnUnblock(client *c, long blocked_us, long reply_us, int had_errors){
|
||||
const ustime_t total_cmd_duration = c->duration + blocked_us + reply_us;
|
||||
clusterSlotStatsAddCpuDuration(c, total_cmd_duration);
|
||||
c->lastcmd->microseconds += total_cmd_duration;
|
||||
c->lastcmd->calls++;
|
||||
c->commands_processed++;
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
* Copyright (c) 2009-Present, Redis Ltd.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Copyright (c) 2024-present, Valkey contributors.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Licensed under your choice of (a) the Redis Source Available License 2.0
|
||||
* (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
|
||||
* GNU Affero General Public License v3 (AGPLv3).
|
||||
@@ -17,6 +20,7 @@
|
||||
|
||||
#include "server.h"
|
||||
#include "cluster.h"
|
||||
#include "cluster_slot_stats.h"
|
||||
|
||||
#include <ctype.h>
|
||||
|
||||
@@ -929,6 +933,8 @@ void clusterCommandHelp(client *c) {
|
||||
"SLOTS",
|
||||
" Return information about slots range mappings. Each range is made of:",
|
||||
" start, end, master and replicas IP addresses, ports and ids",
|
||||
"SLOT-STATS",
|
||||
" Return an array of slot usage statistics for slots assigned to the current node.",
|
||||
"SHARDS",
|
||||
" Return information about slot range mappings and the nodes associated with them.",
|
||||
NULL
|
||||
@@ -1687,3 +1693,12 @@ void readwriteCommand(client *c) {
|
||||
c->flags &= ~CLIENT_READONLY;
|
||||
addReply(c,shared.ok);
|
||||
}
|
||||
|
||||
/* Resets transient cluster stats that we expose via INFO or other means that we want
|
||||
* to reset via CONFIG RESETSTAT. The function is also used in order to
|
||||
* initialize these fields in clusterInit() at server startup. */
|
||||
void resetClusterStats(void) {
|
||||
if (!server.cluster_enabled) return;
|
||||
|
||||
clusterSlotStatResetAll();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
/*
|
||||
* Copyright (c) 2009-Present, Redis Ltd.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Copyright (c) 2024-present, Valkey contributors.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Licensed under your choice of (a) the Redis Source Available License 2.0
|
||||
* (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
|
||||
* GNU Affero General Public License v3 (AGPLv3).
|
||||
*
|
||||
* Portions of this file are available under BSD3 terms; see REDISCONTRIBUTIONS for more information.
|
||||
*/
|
||||
|
||||
#ifndef __CLUSTER_H
|
||||
#define __CLUSTER_H
|
||||
|
||||
@@ -130,6 +144,8 @@ const char *clusterNodePreferredEndpoint(clusterNode *n);
|
||||
long long clusterNodeReplOffset(clusterNode *node);
|
||||
clusterNode *clusterLookupNode(const char *name, int length);
|
||||
const char *clusterGetSecret(size_t *len);
|
||||
unsigned int countKeysInSlot(unsigned int slot);
|
||||
int getSlotOrReply(client *c, robj *o);
|
||||
|
||||
/* functions with shared implementations */
|
||||
clusterNode *getNodeByQuery(client *c, struct redisCommand *cmd, robj **argv, int argc, int *hashslot, uint64_t cmd_flags, int *error_code);
|
||||
@@ -147,6 +163,7 @@ void clusterFreeNodesSlotsInfo(clusterNode *n);
|
||||
int clusterNodeSlotInfoCount(clusterNode *n);
|
||||
uint16_t clusterNodeSlotInfoEntry(clusterNode *n, int idx);
|
||||
int clusterNodeHasSlotInfo(clusterNode *n);
|
||||
void resetClusterStats(void);
|
||||
|
||||
int clusterGetShardCount(void);
|
||||
void *clusterGetShardIterator(void);
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "server.h"
|
||||
#include "cluster.h"
|
||||
#include "cluster_legacy.h"
|
||||
#include "cluster_slot_stats.h"
|
||||
#include "endianconv.h"
|
||||
#include "connection.h"
|
||||
|
||||
@@ -1030,6 +1031,7 @@ void clusterInit(void) {
|
||||
clusterUpdateMyselfIp();
|
||||
clusterUpdateMyselfHostname();
|
||||
clusterUpdateMyselfHumanNodename();
|
||||
resetClusterStats();
|
||||
|
||||
getRandomHexChars(server.cluster->internal_secret, CLUSTER_INTERNALSECRETLEN);
|
||||
}
|
||||
@@ -5003,6 +5005,9 @@ int clusterAddSlot(clusterNode *n, int slot) {
|
||||
if (server.cluster->slots[slot]) return C_ERR;
|
||||
clusterNodeSetSlotBit(n,slot);
|
||||
server.cluster->slots[slot] = n;
|
||||
/* Make owner_not_claiming_slot flag consistent with slot ownership information. */
|
||||
bitmapClearBit(server.cluster->owner_not_claiming_slot, slot);
|
||||
clusterSlotStatReset(slot);
|
||||
return C_OK;
|
||||
}
|
||||
|
||||
@@ -5021,6 +5026,7 @@ int clusterDelSlot(int slot) {
|
||||
server.cluster->slots[slot] = NULL;
|
||||
/* Make owner_not_claiming_slot flag consistent with slot ownership information. */
|
||||
bitmapClearBit(server.cluster->owner_not_claiming_slot, slot);
|
||||
clusterSlotStatReset(slot);
|
||||
return C_OK;
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ typedef struct clusterLink {
|
||||
#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 nodeIsMaster(n) ((n)->flags & CLUSTER_NODE_MASTER)
|
||||
#define nodeIsSlave(n) ((n)->flags & CLUSTER_NODE_SLAVE)
|
||||
#define nodeInHandshake(n) ((n)->flags & CLUSTER_NODE_HANDSHAKE)
|
||||
#define nodeHasAddr(n) (!((n)->flags & CLUSTER_NODE_NOADDR))
|
||||
@@ -330,6 +331,13 @@ struct _clusterNode {
|
||||
list *fail_reports; /* List of nodes signaling this as failing */
|
||||
};
|
||||
|
||||
/* Struct used for storing slot statistics. */
|
||||
typedef struct slotStat {
|
||||
uint64_t cpu_usec; /* CPU time (in microseconds) spent on given slot */
|
||||
uint64_t network_bytes_in; /* Network ingress (in bytes) received for given slot */
|
||||
uint64_t network_bytes_out; /* Network egress (in bytes) sent for given slot */
|
||||
} slotStat;
|
||||
|
||||
struct clusterState {
|
||||
clusterNode *myself; /* This node */
|
||||
uint64_t currentEpoch;
|
||||
@@ -377,6 +385,8 @@ struct clusterState {
|
||||
* stops claiming the slot. This prevents spreading incorrect information (that
|
||||
* source still owns the slot) using UPDATE messages. */
|
||||
unsigned char owner_not_claiming_slot[CLUSTER_SLOTS / 8];
|
||||
/* Struct used for storing slot statistics, for all slots owned by the current shard. */
|
||||
slotStat slot_stats[CLUSTER_SLOTS];
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
/*
|
||||
* Copyright (c) 2009-Present, Redis Ltd.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Copyright (c) 2024-present, Valkey contributors.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Licensed under your choice of (a) the Redis Source Available License 2.0
|
||||
* (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
|
||||
* GNU Affero General Public License v3 (AGPLv3).
|
||||
*
|
||||
* Portions of this file are available under BSD3 terms; see REDISCONTRIBUTIONS for more information.
|
||||
*/
|
||||
|
||||
#include "cluster_slot_stats.h"
|
||||
|
||||
typedef enum {
|
||||
KEY_COUNT,
|
||||
CPU_USEC,
|
||||
NETWORK_BYTES_IN,
|
||||
NETWORK_BYTES_OUT,
|
||||
SLOT_STAT_COUNT,
|
||||
INVALID
|
||||
} slotStatType;
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
* CLUSTER SLOT-STATS command
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
/* Struct used to temporarily hold slot statistics for sorting. */
|
||||
typedef struct {
|
||||
int slot;
|
||||
uint64_t stat;
|
||||
} slotStatForSort;
|
||||
|
||||
static int markSlotsAssignedToMyShard(unsigned char *assigned_slots, int start_slot, int end_slot) {
|
||||
clusterNode *primary = clusterNodeGetMaster(getMyClusterNode());
|
||||
int assigned_slots_count = 0;
|
||||
for (int slot = start_slot; slot <= end_slot; slot++) {
|
||||
if (!clusterNodeCoversSlot(primary, slot)) continue;
|
||||
assigned_slots[slot]++;
|
||||
assigned_slots_count++;
|
||||
}
|
||||
return assigned_slots_count;
|
||||
}
|
||||
|
||||
static uint64_t getSlotStat(int slot, slotStatType stat_type) {
|
||||
switch (stat_type) {
|
||||
case KEY_COUNT: return countKeysInSlot(slot);
|
||||
case CPU_USEC: return server.cluster->slot_stats[slot].cpu_usec;
|
||||
case NETWORK_BYTES_IN: return server.cluster->slot_stats[slot].network_bytes_in;
|
||||
case NETWORK_BYTES_OUT: return server.cluster->slot_stats[slot].network_bytes_out;
|
||||
default: serverPanic("Invalid slot stat type %d was found.", stat_type);
|
||||
}
|
||||
}
|
||||
|
||||
/* Compare by stat in ascending order. If stat is the same, compare by slot in ascending order. */
|
||||
static int slotStatForSortAscCmp(const void *a, const void *b) {
|
||||
const slotStatForSort *entry_a = a;
|
||||
const slotStatForSort *entry_b = b;
|
||||
if (entry_a->stat == entry_b->stat) {
|
||||
return entry_a->slot - entry_b->slot;
|
||||
}
|
||||
return entry_a->stat - entry_b->stat;
|
||||
}
|
||||
|
||||
/* Compare by stat in descending order. If stat is the same, compare by slot in ascending order. */
|
||||
static int slotStatForSortDescCmp(const void *a, const void *b) {
|
||||
const slotStatForSort *entry_a = a;
|
||||
const slotStatForSort *entry_b = b;
|
||||
if (entry_b->stat == entry_a->stat) {
|
||||
return entry_a->slot - entry_b->slot;
|
||||
}
|
||||
return entry_b->stat - entry_a->stat;
|
||||
}
|
||||
|
||||
static void collectAndSortSlotStats(slotStatForSort slot_stats[], slotStatType order_by, int desc) {
|
||||
clusterNode *primary = clusterNodeGetMaster(getMyClusterNode());
|
||||
int i = 0;
|
||||
for (int slot = 0; slot < CLUSTER_SLOTS; slot++) {
|
||||
if (!clusterNodeCoversSlot(primary, slot)) continue;
|
||||
slot_stats[i].slot = slot;
|
||||
slot_stats[i].stat = getSlotStat(slot, order_by);
|
||||
i++;
|
||||
}
|
||||
qsort(slot_stats, i, sizeof(slotStatForSort), desc ? slotStatForSortDescCmp : slotStatForSortAscCmp);
|
||||
}
|
||||
|
||||
static void addReplySlotStat(client *c, int slot) {
|
||||
addReplyArrayLen(c, 2); /* Array of size 2, where 0th index represents (int) slot,
|
||||
* and 1st index represents (map) usage statistics. */
|
||||
addReplyLongLong(c, slot);
|
||||
addReplyMapLen(c, (server.cluster_slot_stats_enabled) ? SLOT_STAT_COUNT
|
||||
: 1); /* Nested map representing slot usage statistics. */
|
||||
addReplyBulkCString(c, "key-count");
|
||||
addReplyLongLong(c, countKeysInSlot(slot));
|
||||
|
||||
/* Any additional metrics aside from key-count come with a performance trade-off,
|
||||
* and are aggregated and returned based on its server config. */
|
||||
if (server.cluster_slot_stats_enabled) {
|
||||
addReplyBulkCString(c, "cpu-usec");
|
||||
addReplyLongLong(c, server.cluster->slot_stats[slot].cpu_usec);
|
||||
addReplyBulkCString(c, "network-bytes-in");
|
||||
addReplyLongLong(c, server.cluster->slot_stats[slot].network_bytes_in);
|
||||
addReplyBulkCString(c, "network-bytes-out");
|
||||
addReplyLongLong(c, server.cluster->slot_stats[slot].network_bytes_out);
|
||||
}
|
||||
}
|
||||
|
||||
/* Adds reply for the SLOTSRANGE variant.
|
||||
* Response is ordered in ascending slot number. */
|
||||
static void addReplySlotsRange(client *c, unsigned char *assigned_slots, int start_slot, int end_slot, int len) {
|
||||
addReplyArrayLen(c, len); /* Top level RESP reply format is defined as an array, due to ordering invariance. */
|
||||
|
||||
for (int slot = start_slot; slot <= end_slot; slot++) {
|
||||
if (assigned_slots[slot]) addReplySlotStat(c, slot);
|
||||
}
|
||||
}
|
||||
|
||||
static void addReplySortedSlotStats(client *c, slotStatForSort slot_stats[], long limit) {
|
||||
int num_slots_assigned = getMyShardSlotCount();
|
||||
int len = min(limit, num_slots_assigned);
|
||||
addReplyArrayLen(c, len); /* Top level RESP reply format is defined as an array, due to ordering invariance. */
|
||||
|
||||
for (int i = 0; i < len; i++) {
|
||||
addReplySlotStat(c, slot_stats[i].slot);
|
||||
}
|
||||
}
|
||||
|
||||
static int canAddNetworkBytesOut(client *c) {
|
||||
return server.cluster_slot_stats_enabled && server.cluster_enabled && c->slot != -1;
|
||||
}
|
||||
|
||||
/* Accumulates egress bytes upon sending RESP responses back to user clients. */
|
||||
void clusterSlotStatsAddNetworkBytesOutForUserClient(client *c) {
|
||||
if (!canAddNetworkBytesOut(c)) return;
|
||||
|
||||
serverAssert(c->slot >= 0 && c->slot < CLUSTER_SLOTS);
|
||||
server.cluster->slot_stats[c->slot].network_bytes_out += c->net_output_bytes_curr_cmd;
|
||||
}
|
||||
|
||||
/* Accumulates egress bytes upon sending replication stream. This only applies for primary nodes. */
|
||||
static void clusterSlotStatsUpdateNetworkBytesOutForReplication(long long len) {
|
||||
client *c = server.current_client;
|
||||
if (c == NULL || !canAddNetworkBytesOut(c)) return;
|
||||
|
||||
/* We multiply the bytes len by the number of replicas to account for us broadcasting to multiple replicas at once. */
|
||||
len *= (long long)listLength(server.slaves);
|
||||
serverAssert(c->slot >= 0 && c->slot < CLUSTER_SLOTS);
|
||||
serverAssert(nodeIsMaster(server.cluster->myself));
|
||||
/* We sometimes want to adjust the counter downwards (for example when we want to undo accounting for
|
||||
* SELECT commands that don't belong to any slot) so let's make sure we don't underflow the counter. */
|
||||
serverAssert(len >= 0 || server.cluster->slot_stats[c->slot].network_bytes_out >= (uint64_t)-len);
|
||||
server.cluster->slot_stats[c->slot].network_bytes_out += len;
|
||||
}
|
||||
|
||||
/* Increment network bytes out for replication stream. This method will increment `len` value times the active replica
|
||||
* count. */
|
||||
void clusterSlotStatsIncrNetworkBytesOutForReplication(long long len) {
|
||||
clusterSlotStatsUpdateNetworkBytesOutForReplication(len);
|
||||
}
|
||||
|
||||
/* Decrement network bytes out for replication stream.
|
||||
* This is used to remove accounting of data which doesn't belong to any particular slots e.g. SELECT command.
|
||||
* This will decrement `len` value times the active replica count. */
|
||||
void clusterSlotStatsDecrNetworkBytesOutForReplication(long long len) {
|
||||
clusterSlotStatsUpdateNetworkBytesOutForReplication(-len);
|
||||
}
|
||||
|
||||
/* Upon SPUBLISH, two egress events are triggered.
|
||||
* 1) Internal propagation, for clients that are subscribed to the current node.
|
||||
* 2) External propagation, for other nodes within the same shard (could either be a primary or replica).
|
||||
* This type is not aggregated, to stay consistent with server.stat_net_output_bytes aggregation.
|
||||
* This function covers the internal propagation component. */
|
||||
void clusterSlotStatsAddNetworkBytesOutForShardedPubSubInternalPropagation(client *c, int slot) {
|
||||
/* For a blocked client, c->slot could be pre-filled.
|
||||
* Thus c->slot is backed-up for restoration after aggregation is completed. */
|
||||
int save_slot = c->slot;
|
||||
c->slot = slot;
|
||||
if (canAddNetworkBytesOut(c)) {
|
||||
serverAssert(c->slot >= 0 && c->slot < CLUSTER_SLOTS);
|
||||
server.cluster->slot_stats[c->slot].network_bytes_out += c->net_output_bytes_curr_cmd;
|
||||
}
|
||||
/* For sharded pubsub, the client's network bytes metrics must be reset here,
|
||||
* as resetClient() is not called until subscription ends. */
|
||||
c->net_output_bytes_curr_cmd = 0;
|
||||
c->slot = save_slot;
|
||||
}
|
||||
|
||||
/* Adds reply for the ORDERBY variant.
|
||||
* Response is ordered based on the sort result. */
|
||||
static void addReplyOrderBy(client *c, slotStatType order_by, long limit, int desc) {
|
||||
slotStatForSort slot_stats[CLUSTER_SLOTS];
|
||||
collectAndSortSlotStats(slot_stats, order_by, desc);
|
||||
addReplySortedSlotStats(c, slot_stats, limit);
|
||||
}
|
||||
|
||||
/* Resets applicable slot statistics. */
|
||||
void clusterSlotStatReset(int slot) {
|
||||
/* key-count is exempt, as it is queried separately through `countKeysInSlot()`. */
|
||||
memset(&server.cluster->slot_stats[slot], 0, sizeof(slotStat));
|
||||
}
|
||||
|
||||
void clusterSlotStatResetAll(void) {
|
||||
memset(server.cluster->slot_stats, 0, sizeof(server.cluster->slot_stats));
|
||||
}
|
||||
|
||||
/* For cpu-usec accumulation, nested commands within EXEC, EVAL, FCALL are skipped.
|
||||
* This is due to their unique callstack, where the c->duration for
|
||||
* EXEC, EVAL and FCALL already includes all of its nested commands.
|
||||
* Meaning, the accumulation of cpu-usec for these nested commands
|
||||
* would equate to repeating the same calculation twice.
|
||||
*/
|
||||
static int canAddCpuDuration(client *c) {
|
||||
return server.cluster_slot_stats_enabled && /* Config should be enabled. */
|
||||
server.cluster_enabled && /* Cluster mode should be enabled. */
|
||||
c->slot != -1 && /* Command should be slot specific. */
|
||||
(!server.execution_nesting || /* Either command should not be nested, */
|
||||
(c->realcmd->flags & CMD_BLOCKING)); /* or it must be due to unblocking. */
|
||||
}
|
||||
|
||||
void clusterSlotStatsAddCpuDuration(client *c, ustime_t duration) {
|
||||
if (!canAddCpuDuration(c)) return;
|
||||
|
||||
serverAssert(c->slot >= 0 && c->slot < CLUSTER_SLOTS);
|
||||
server.cluster->slot_stats[c->slot].cpu_usec += duration;
|
||||
}
|
||||
|
||||
/* For cross-slot scripting, its caller client's slot must be invalidated,
|
||||
* such that its slot-stats aggregation is bypassed. */
|
||||
void clusterSlotStatsInvalidateSlotIfApplicable(scriptRunCtx *ctx) {
|
||||
if (!(ctx->flags & SCRIPT_ALLOW_CROSS_SLOT)) return;
|
||||
|
||||
ctx->original_client->slot = -1;
|
||||
}
|
||||
|
||||
static int canAddNetworkBytesIn(client *c) {
|
||||
/* First, cluster mode must be enabled.
|
||||
* Second, command should target a specific slot.
|
||||
* Third, blocked client is not aggregated, to avoid duplicate aggregation upon unblocking.
|
||||
* Fourth, the server is not under a MULTI/EXEC transaction, to avoid duplicate aggregation of
|
||||
* EXEC's 14 bytes RESP upon nested call()'s afterCommand(). */
|
||||
return server.cluster_enabled && server.cluster_slot_stats_enabled &&
|
||||
c->slot != -1 && !(c->flags & CLIENT_BLOCKED) && !server.in_exec;
|
||||
}
|
||||
|
||||
/* Adds network ingress bytes of the current command in execution,
|
||||
* calculated earlier within networking.c layer.
|
||||
*
|
||||
* Note: Below function should only be called once c->slot is parsed.
|
||||
* Otherwise, the aggregation will be skipped due to canAddNetworkBytesIn() check failure.
|
||||
* */
|
||||
void clusterSlotStatsAddNetworkBytesInForUserClient(client *c) {
|
||||
if (!canAddNetworkBytesIn(c)) return;
|
||||
|
||||
if (c->cmd->proc == execCommand) {
|
||||
/* Accumulate its corresponding MULTI RESP; *1\r\n$5\r\nmulti\r\n */
|
||||
c->net_input_bytes_curr_cmd += 15;
|
||||
}
|
||||
|
||||
server.cluster->slot_stats[c->slot].network_bytes_in += c->net_input_bytes_curr_cmd;
|
||||
}
|
||||
|
||||
void clusterSlotStatsCommand(client *c) {
|
||||
if (!server.cluster_enabled) {
|
||||
addReplyError(c, "This instance has cluster support disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Parse additional arguments. */
|
||||
if (c->argc == 5 && !strcasecmp(c->argv[2]->ptr, "slotsrange")) {
|
||||
/* CLUSTER SLOT-STATS SLOTSRANGE start-slot end-slot */
|
||||
int start_slot, end_slot;
|
||||
if ((start_slot = getSlotOrReply(c, c->argv[3])) == -1 ||
|
||||
(end_slot = getSlotOrReply(c, c->argv[4])) == -1) {
|
||||
return;
|
||||
}
|
||||
if (start_slot > end_slot) {
|
||||
addReplyErrorFormat(c, "Start slot number %d is greater than end slot number %d", start_slot, end_slot);
|
||||
return;
|
||||
}
|
||||
/* Initialize slot assignment array. */
|
||||
unsigned char assigned_slots[CLUSTER_SLOTS] = {0};
|
||||
int assigned_slots_count = markSlotsAssignedToMyShard(assigned_slots, start_slot, end_slot);
|
||||
addReplySlotsRange(c, assigned_slots, start_slot, end_slot, assigned_slots_count);
|
||||
|
||||
} else if (c->argc >= 4 && !strcasecmp(c->argv[2]->ptr, "orderby")) {
|
||||
/* CLUSTER SLOT-STATS ORDERBY metric [LIMIT limit] [ASC | DESC] */
|
||||
int desc = 1;
|
||||
slotStatType order_by = INVALID;
|
||||
if (!strcasecmp(c->argv[3]->ptr, "key-count")) {
|
||||
order_by = KEY_COUNT;
|
||||
} else if (!strcasecmp(c->argv[3]->ptr, "cpu-usec") && server.cluster_slot_stats_enabled) {
|
||||
order_by = CPU_USEC;
|
||||
} else if (!strcasecmp(c->argv[3]->ptr, "network-bytes-in") && server.cluster_slot_stats_enabled) {
|
||||
order_by = NETWORK_BYTES_IN;
|
||||
} else if (!strcasecmp(c->argv[3]->ptr, "network-bytes-out") && server.cluster_slot_stats_enabled) {
|
||||
order_by = NETWORK_BYTES_OUT;
|
||||
} else {
|
||||
addReplyError(c, "Unrecognized sort metric for ORDERBY.");
|
||||
return;
|
||||
}
|
||||
int i = 4; /* Next argument index, following ORDERBY */
|
||||
int limit_counter = 0, asc_desc_counter = 0;
|
||||
long limit = CLUSTER_SLOTS;
|
||||
while (i < c->argc) {
|
||||
int moreargs = c->argc > i + 1;
|
||||
if (!strcasecmp(c->argv[i]->ptr, "limit") && moreargs) {
|
||||
if (getRangeLongFromObjectOrReply(
|
||||
c, c->argv[i + 1], 1, CLUSTER_SLOTS, &limit,
|
||||
"Limit has to lie in between 1 and 16384 (maximum number of slots).") != C_OK) {
|
||||
return;
|
||||
}
|
||||
i++;
|
||||
limit_counter++;
|
||||
} else if (!strcasecmp(c->argv[i]->ptr, "asc")) {
|
||||
desc = 0;
|
||||
asc_desc_counter++;
|
||||
} else if (!strcasecmp(c->argv[i]->ptr, "desc")) {
|
||||
desc = 1;
|
||||
asc_desc_counter++;
|
||||
} else {
|
||||
addReplyErrorObject(c, shared.syntaxerr);
|
||||
return;
|
||||
}
|
||||
if (limit_counter > 1 || asc_desc_counter > 1) {
|
||||
addReplyError(c, "Multiple filters of the same type are disallowed.");
|
||||
return;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
addReplyOrderBy(c, order_by, limit, desc);
|
||||
|
||||
} else {
|
||||
addReplySubcommandSyntaxError(c);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (c) 2009-Present, Redis Ltd.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Copyright (c) 2024-present, Valkey contributors.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Licensed under your choice of (a) the Redis Source Available License 2.0
|
||||
* (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
|
||||
* GNU Affero General Public License v3 (AGPLv3).
|
||||
*
|
||||
* Portions of this file are available under BSD3 terms; see REDISCONTRIBUTIONS for more information.
|
||||
*/
|
||||
|
||||
#include "server.h"
|
||||
#include "cluster.h"
|
||||
#include "script.h"
|
||||
#include "cluster_legacy.h"
|
||||
|
||||
/* General use-cases. */
|
||||
void clusterSlotStatReset(int slot);
|
||||
void clusterSlotStatResetAll(void);
|
||||
|
||||
/* cpu-usec metric. */
|
||||
void clusterSlotStatsAddCpuDuration(client *c, ustime_t duration);
|
||||
void clusterSlotStatsInvalidateSlotIfApplicable(scriptRunCtx *ctx);
|
||||
|
||||
/* network-bytes-in metric. */
|
||||
void clusterSlotStatsAddNetworkBytesInForUserClient(client *c);
|
||||
|
||||
/* network-bytes-out metric. */
|
||||
void clusterSlotStatsAddNetworkBytesOutForUserClient(client *c);
|
||||
void clusterSlotStatsIncrNetworkBytesOutForReplication(long long len);
|
||||
void clusterSlotStatsDecrNetworkBytesOutForReplication(long long len);
|
||||
void clusterSlotStatsAddNetworkBytesOutForShardedPubSubInternalPropagation(client *c, int slot);
|
||||
+153
-3
@@ -925,6 +925,56 @@ struct COMMAND_ARG CLUSTER_SLAVES_Args[] = {
|
||||
{MAKE_ARG("node-id",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
};
|
||||
|
||||
/********** CLUSTER SLOT_STATS ********************/
|
||||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
/* CLUSTER SLOT_STATS history */
|
||||
#define CLUSTER_SLOT_STATS_History NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_TIPS_TABLE
|
||||
/* CLUSTER SLOT_STATS tips */
|
||||
const char *CLUSTER_SLOT_STATS_Tips[] = {
|
||||
"nondeterministic_output",
|
||||
"request_policy:all_shards",
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_KEY_SPECS_TABLE
|
||||
/* CLUSTER SLOT_STATS key specs */
|
||||
#define CLUSTER_SLOT_STATS_Keyspecs NULL
|
||||
#endif
|
||||
|
||||
/* CLUSTER SLOT_STATS filter slotsrange argument table */
|
||||
struct COMMAND_ARG CLUSTER_SLOT_STATS_filter_slotsrange_Subargs[] = {
|
||||
{MAKE_ARG("start-slot",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("end-slot",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
};
|
||||
|
||||
/* CLUSTER SLOT_STATS filter orderby order argument table */
|
||||
struct COMMAND_ARG CLUSTER_SLOT_STATS_filter_orderby_order_Subargs[] = {
|
||||
{MAKE_ARG("asc",ARG_TYPE_PURE_TOKEN,-1,"ASC",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("desc",ARG_TYPE_PURE_TOKEN,-1,"DESC",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
};
|
||||
|
||||
/* CLUSTER SLOT_STATS filter orderby argument table */
|
||||
struct COMMAND_ARG CLUSTER_SLOT_STATS_filter_orderby_Subargs[] = {
|
||||
{MAKE_ARG("metric",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("limit",ARG_TYPE_INTEGER,-1,"LIMIT",NULL,NULL,CMD_ARG_OPTIONAL,0,NULL)},
|
||||
{MAKE_ARG("order",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,2,NULL),.subargs=CLUSTER_SLOT_STATS_filter_orderby_order_Subargs},
|
||||
};
|
||||
|
||||
/* CLUSTER SLOT_STATS filter argument table */
|
||||
struct COMMAND_ARG CLUSTER_SLOT_STATS_filter_Subargs[] = {
|
||||
{MAKE_ARG("slotsrange",ARG_TYPE_BLOCK,-1,"SLOTSRANGE",NULL,NULL,CMD_ARG_NONE,2,NULL),.subargs=CLUSTER_SLOT_STATS_filter_slotsrange_Subargs},
|
||||
{MAKE_ARG("orderby",ARG_TYPE_BLOCK,-1,"ORDERBY",NULL,NULL,CMD_ARG_NONE,3,NULL),.subargs=CLUSTER_SLOT_STATS_filter_orderby_Subargs},
|
||||
};
|
||||
|
||||
/* CLUSTER SLOT_STATS argument table */
|
||||
struct COMMAND_ARG CLUSTER_SLOT_STATS_Args[] = {
|
||||
{MAKE_ARG("filter",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_NONE,2,NULL),.subargs=CLUSTER_SLOT_STATS_filter_Subargs},
|
||||
};
|
||||
|
||||
/********** CLUSTER SLOTS ********************/
|
||||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
@@ -976,6 +1026,7 @@ struct COMMAND_STRUCT CLUSTER_Subcommands[] = {
|
||||
{MAKE_CMD("setslot","Binds a hash slot to a node.","O(1)","3.0.0",CMD_DOC_NONE,NULL,NULL,"cluster",COMMAND_GROUP_CLUSTER,CLUSTER_SETSLOT_History,0,CLUSTER_SETSLOT_Tips,0,clusterCommand,-4,CMD_NO_ASYNC_LOADING|CMD_ADMIN|CMD_STALE,0,CLUSTER_SETSLOT_Keyspecs,0,NULL,2),.args=CLUSTER_SETSLOT_Args},
|
||||
{MAKE_CMD("shards","Returns the mapping of cluster slots to shards.","O(N) where N is the total number of cluster nodes","7.0.0",CMD_DOC_NONE,NULL,NULL,"cluster",COMMAND_GROUP_CLUSTER,CLUSTER_SHARDS_History,0,CLUSTER_SHARDS_Tips,1,clusterCommand,2,CMD_LOADING|CMD_STALE,0,CLUSTER_SHARDS_Keyspecs,0,NULL,0)},
|
||||
{MAKE_CMD("slaves","Lists the replica nodes of a master node.","O(N) where N is the number of replicas.","3.0.0",CMD_DOC_DEPRECATED,"`CLUSTER REPLICAS`","5.0.0","cluster",COMMAND_GROUP_CLUSTER,CLUSTER_SLAVES_History,0,CLUSTER_SLAVES_Tips,1,clusterCommand,3,CMD_ADMIN|CMD_STALE,0,CLUSTER_SLAVES_Keyspecs,0,NULL,1),.args=CLUSTER_SLAVES_Args},
|
||||
{MAKE_CMD("slot-stats","Return an array of slot usage statistics for slots assigned to the current node.","O(N) where N is the total number of slots based on arguments. O(N*log(N)) with ORDERBY subcommand.","8.2.0",CMD_DOC_NONE,NULL,NULL,"cluster",COMMAND_GROUP_CLUSTER,CLUSTER_SLOT_STATS_History,0,CLUSTER_SLOT_STATS_Tips,2,clusterSlotStatsCommand,-4,CMD_STALE|CMD_LOADING,0,CLUSTER_SLOT_STATS_Keyspecs,0,NULL,1),.args=CLUSTER_SLOT_STATS_Args},
|
||||
{MAKE_CMD("slots","Returns the mapping of cluster slots to nodes.","O(N) where N is the total number of Cluster nodes","3.0.0",CMD_DOC_DEPRECATED,"`CLUSTER SHARDS`","7.0.0","cluster",COMMAND_GROUP_CLUSTER,CLUSTER_SLOTS_History,2,CLUSTER_SLOTS_Tips,1,clusterCommand,2,CMD_LOADING|CMD_STALE,0,CLUSTER_SLOTS_Keyspecs,0,NULL,0)},
|
||||
{0}
|
||||
};
|
||||
@@ -9649,6 +9700,46 @@ struct COMMAND_ARG XACK_Args[] = {
|
||||
{MAKE_ARG("id",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,0,NULL)},
|
||||
};
|
||||
|
||||
/********** XACKDEL ********************/
|
||||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
/* XACKDEL history */
|
||||
#define XACKDEL_History NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_TIPS_TABLE
|
||||
/* XACKDEL tips */
|
||||
#define XACKDEL_Tips NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_KEY_SPECS_TABLE
|
||||
/* XACKDEL key specs */
|
||||
keySpec XACKDEL_Keyspecs[1] = {
|
||||
{NULL,CMD_KEY_RW|CMD_KEY_UPDATE|CMD_KEY_DELETE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}
|
||||
};
|
||||
#endif
|
||||
|
||||
/* XACKDEL condition argument table */
|
||||
struct COMMAND_ARG XACKDEL_condition_Subargs[] = {
|
||||
{MAKE_ARG("keepref",ARG_TYPE_PURE_TOKEN,-1,"KEEPREF",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("delref",ARG_TYPE_PURE_TOKEN,-1,"DELREF",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("acked",ARG_TYPE_PURE_TOKEN,-1,"ACKED",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
};
|
||||
|
||||
/* XACKDEL ids argument table */
|
||||
struct COMMAND_ARG XACKDEL_ids_Subargs[] = {
|
||||
{MAKE_ARG("numids",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("id",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,0,NULL)},
|
||||
};
|
||||
|
||||
/* XACKDEL argument table */
|
||||
struct COMMAND_ARG XACKDEL_Args[] = {
|
||||
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("group",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("condition",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,3,NULL),.subargs=XACKDEL_condition_Subargs},
|
||||
{MAKE_ARG("ids",ARG_TYPE_BLOCK,-1,"IDS",NULL,NULL,CMD_ARG_NONE,2,NULL),.subargs=XACKDEL_ids_Subargs},
|
||||
};
|
||||
|
||||
/********** XADD ********************/
|
||||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
@@ -9656,6 +9747,7 @@ struct COMMAND_ARG XACK_Args[] = {
|
||||
commandHistory XADD_History[] = {
|
||||
{"6.2.0","Added the `NOMKSTREAM` option, `MINID` trimming strategy and the `LIMIT` option."},
|
||||
{"7.0.0","Added support for the `<ms>-*` explicit ID form."},
|
||||
{"8.2.0","Added the `KEEPREF`, `DELREF` and `ACKED` options."},
|
||||
};
|
||||
#endif
|
||||
|
||||
@@ -9673,6 +9765,13 @@ keySpec XADD_Keyspecs[1] = {
|
||||
};
|
||||
#endif
|
||||
|
||||
/* XADD condition argument table */
|
||||
struct COMMAND_ARG XADD_condition_Subargs[] = {
|
||||
{MAKE_ARG("keepref",ARG_TYPE_PURE_TOKEN,-1,"KEEPREF",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("delref",ARG_TYPE_PURE_TOKEN,-1,"DELREF",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("acked",ARG_TYPE_PURE_TOKEN,-1,"ACKED",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
};
|
||||
|
||||
/* XADD trim strategy argument table */
|
||||
struct COMMAND_ARG XADD_trim_strategy_Subargs[] = {
|
||||
{MAKE_ARG("maxlen",ARG_TYPE_PURE_TOKEN,-1,"MAXLEN",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
@@ -9709,6 +9808,7 @@ struct COMMAND_ARG XADD_data_Subargs[] = {
|
||||
struct COMMAND_ARG XADD_Args[] = {
|
||||
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("nomkstream",ARG_TYPE_PURE_TOKEN,-1,"NOMKSTREAM",NULL,"6.2.0",CMD_ARG_OPTIONAL,0,NULL)},
|
||||
{MAKE_ARG("condition",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,3,NULL),.subargs=XADD_condition_Subargs},
|
||||
{MAKE_ARG("trim",ARG_TYPE_BLOCK,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,4,NULL),.subargs=XADD_trim_Subargs},
|
||||
{MAKE_ARG("id-selector",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_NONE,2,NULL),.subargs=XADD_id_selector_Subargs},
|
||||
{MAKE_ARG("data",ARG_TYPE_BLOCK,-1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,2,NULL),.subargs=XADD_data_Subargs},
|
||||
@@ -9809,6 +9909,45 @@ struct COMMAND_ARG XDEL_Args[] = {
|
||||
{MAKE_ARG("id",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,0,NULL)},
|
||||
};
|
||||
|
||||
/********** XDELEX ********************/
|
||||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
/* XDELEX history */
|
||||
#define XDELEX_History NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_TIPS_TABLE
|
||||
/* XDELEX tips */
|
||||
#define XDELEX_Tips NULL
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_KEY_SPECS_TABLE
|
||||
/* XDELEX key specs */
|
||||
keySpec XDELEX_Keyspecs[1] = {
|
||||
{NULL,CMD_KEY_RW|CMD_KEY_DELETE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}
|
||||
};
|
||||
#endif
|
||||
|
||||
/* XDELEX condition argument table */
|
||||
struct COMMAND_ARG XDELEX_condition_Subargs[] = {
|
||||
{MAKE_ARG("keepref",ARG_TYPE_PURE_TOKEN,-1,"KEEPREF",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("delref",ARG_TYPE_PURE_TOKEN,-1,"DELREF",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("acked",ARG_TYPE_PURE_TOKEN,-1,"ACKED",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
};
|
||||
|
||||
/* XDELEX ids argument table */
|
||||
struct COMMAND_ARG XDELEX_ids_Subargs[] = {
|
||||
{MAKE_ARG("numids",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("id",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,0,NULL)},
|
||||
};
|
||||
|
||||
/* XDELEX argument table */
|
||||
struct COMMAND_ARG XDELEX_Args[] = {
|
||||
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("condition",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,3,NULL),.subargs=XDELEX_condition_Subargs},
|
||||
{MAKE_ARG("ids",ARG_TYPE_BLOCK,-1,"IDS",NULL,NULL,CMD_ARG_NONE,2,NULL),.subargs=XDELEX_ids_Subargs},
|
||||
};
|
||||
|
||||
/********** XGROUP CREATE ********************/
|
||||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
@@ -10363,6 +10502,7 @@ struct COMMAND_ARG XSETID_Args[] = {
|
||||
/* XTRIM history */
|
||||
commandHistory XTRIM_History[] = {
|
||||
{"6.2.0","Added the `MINID` trimming strategy and the `LIMIT` option."},
|
||||
{"8.2.0","Added the `KEEPREF`, `DELREF` and `ACKED` options."},
|
||||
};
|
||||
#endif
|
||||
|
||||
@@ -10392,18 +10532,26 @@ struct COMMAND_ARG XTRIM_trim_operator_Subargs[] = {
|
||||
{MAKE_ARG("approximately",ARG_TYPE_PURE_TOKEN,-1,"~",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
};
|
||||
|
||||
/* XTRIM trim condition argument table */
|
||||
struct COMMAND_ARG XTRIM_trim_condition_Subargs[] = {
|
||||
{MAKE_ARG("keepref",ARG_TYPE_PURE_TOKEN,-1,"KEEPREF",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("delref",ARG_TYPE_PURE_TOKEN,-1,"DELREF",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("acked",ARG_TYPE_PURE_TOKEN,-1,"ACKED",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
};
|
||||
|
||||
/* XTRIM trim argument table */
|
||||
struct COMMAND_ARG XTRIM_trim_Subargs[] = {
|
||||
{MAKE_ARG("strategy",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_NONE,2,NULL),.subargs=XTRIM_trim_strategy_Subargs},
|
||||
{MAKE_ARG("operator",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,2,NULL),.subargs=XTRIM_trim_operator_Subargs},
|
||||
{MAKE_ARG("threshold",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("count",ARG_TYPE_INTEGER,-1,"LIMIT",NULL,"6.2.0",CMD_ARG_OPTIONAL,0,NULL)},
|
||||
{MAKE_ARG("condition",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,3,NULL),.subargs=XTRIM_trim_condition_Subargs},
|
||||
};
|
||||
|
||||
/* XTRIM argument table */
|
||||
struct COMMAND_ARG XTRIM_Args[] = {
|
||||
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("trim",ARG_TYPE_BLOCK,-1,NULL,NULL,NULL,CMD_ARG_NONE,4,NULL),.subargs=XTRIM_trim_Subargs},
|
||||
{MAKE_ARG("trim",ARG_TYPE_BLOCK,-1,NULL,NULL,NULL,CMD_ARG_NONE,5,NULL),.subargs=XTRIM_trim_Subargs},
|
||||
};
|
||||
|
||||
/********** APPEND ********************/
|
||||
@@ -11328,10 +11476,12 @@ struct COMMAND_STRUCT redisCommandTable[] = {
|
||||
{MAKE_CMD("zunionstore","Stores the union of multiple sorted sets in a key.","O(N)+O(M log(M)) with N being the sum of the sizes of the input sorted sets, and M being the number of elements in the resulting sorted set.","2.0.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZUNIONSTORE_History,0,ZUNIONSTORE_Tips,0,zunionstoreCommand,-4,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_SORTEDSET,ZUNIONSTORE_Keyspecs,2,zunionInterDiffStoreGetKeys,5),.args=ZUNIONSTORE_Args},
|
||||
/* stream */
|
||||
{MAKE_CMD("xack","Returns the number of messages that were successfully acknowledged by the consumer group member of a stream.","O(1) for each message ID processed.","5.0.0",CMD_DOC_NONE,NULL,NULL,"stream",COMMAND_GROUP_STREAM,XACK_History,0,XACK_Tips,0,xackCommand,-4,CMD_WRITE|CMD_FAST,ACL_CATEGORY_STREAM,XACK_Keyspecs,1,NULL,3),.args=XACK_Args},
|
||||
{MAKE_CMD("xadd","Appends a new message to a stream. Creates the key if it doesn't exist.","O(1) when adding a new entry, O(N) when trimming where N being the number of entries evicted.","5.0.0",CMD_DOC_NONE,NULL,NULL,"stream",COMMAND_GROUP_STREAM,XADD_History,2,XADD_Tips,1,xaddCommand,-5,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_STREAM,XADD_Keyspecs,1,NULL,5),.args=XADD_Args},
|
||||
{MAKE_CMD("xackdel","Acknowledges and deletes one or multiple messages for a stream consumer group.","O(1) for each message ID processed.","8.2.0",CMD_DOC_NONE,NULL,NULL,"stream",COMMAND_GROUP_STREAM,XACKDEL_History,0,XACKDEL_Tips,0,xackdelCommand,-6,CMD_WRITE|CMD_FAST,ACL_CATEGORY_STREAM,XACKDEL_Keyspecs,1,NULL,4),.args=XACKDEL_Args},
|
||||
{MAKE_CMD("xadd","Appends a new message to a stream. Creates the key if it doesn't exist.","O(1) when adding a new entry, O(N) when trimming where N being the number of entries evicted.","5.0.0",CMD_DOC_NONE,NULL,NULL,"stream",COMMAND_GROUP_STREAM,XADD_History,3,XADD_Tips,1,xaddCommand,-5,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_STREAM,XADD_Keyspecs,1,NULL,6),.args=XADD_Args},
|
||||
{MAKE_CMD("xautoclaim","Changes, or acquires, ownership of messages in a consumer group, as if the messages were delivered to as consumer group member.","O(1) if COUNT is small.","6.2.0",CMD_DOC_NONE,NULL,NULL,"stream",COMMAND_GROUP_STREAM,XAUTOCLAIM_History,1,XAUTOCLAIM_Tips,1,xautoclaimCommand,-6,CMD_WRITE|CMD_FAST,ACL_CATEGORY_STREAM,XAUTOCLAIM_Keyspecs,1,NULL,7),.args=XAUTOCLAIM_Args},
|
||||
{MAKE_CMD("xclaim","Changes, or acquires, ownership of a message in a consumer group, as if the message was delivered a consumer group member.","O(log N) with N being the number of messages in the PEL of the consumer group.","5.0.0",CMD_DOC_NONE,NULL,NULL,"stream",COMMAND_GROUP_STREAM,XCLAIM_History,0,XCLAIM_Tips,1,xclaimCommand,-6,CMD_WRITE|CMD_FAST,ACL_CATEGORY_STREAM,XCLAIM_Keyspecs,1,NULL,11),.args=XCLAIM_Args},
|
||||
{MAKE_CMD("xdel","Returns the number of messages after removing them from a stream.","O(1) for each single item to delete in the stream, regardless of the stream size.","5.0.0",CMD_DOC_NONE,NULL,NULL,"stream",COMMAND_GROUP_STREAM,XDEL_History,0,XDEL_Tips,0,xdelCommand,-3,CMD_WRITE|CMD_FAST,ACL_CATEGORY_STREAM,XDEL_Keyspecs,1,NULL,2),.args=XDEL_Args},
|
||||
{MAKE_CMD("xdelex","Deletes one or multiple entries from the stream.","O(1) for each single item to delete in the stream, regardless of the stream size.","8.2.0",CMD_DOC_NONE,NULL,NULL,"stream",COMMAND_GROUP_STREAM,XDELEX_History,0,XDELEX_Tips,0,xdelexCommand,-5,CMD_WRITE|CMD_FAST,ACL_CATEGORY_STREAM,XDELEX_Keyspecs,1,NULL,3),.args=XDELEX_Args},
|
||||
{MAKE_CMD("xgroup","A container for consumer groups commands.","Depends on subcommand.","5.0.0",CMD_DOC_NONE,NULL,NULL,"stream",COMMAND_GROUP_STREAM,XGROUP_History,0,XGROUP_Tips,0,NULL,-2,0,0,XGROUP_Keyspecs,0,NULL,0),.subcommands=XGROUP_Subcommands},
|
||||
{MAKE_CMD("xinfo","A container for stream introspection commands.","Depends on subcommand.","5.0.0",CMD_DOC_NONE,NULL,NULL,"stream",COMMAND_GROUP_STREAM,XINFO_History,0,XINFO_Tips,0,NULL,-2,0,0,XINFO_Keyspecs,0,NULL,0),.subcommands=XINFO_Subcommands},
|
||||
{MAKE_CMD("xlen","Return the number of messages in a stream.","O(1)","5.0.0",CMD_DOC_NONE,NULL,NULL,"stream",COMMAND_GROUP_STREAM,XLEN_History,0,XLEN_Tips,0,xlenCommand,2,CMD_READONLY|CMD_FAST,ACL_CATEGORY_STREAM,XLEN_Keyspecs,1,NULL,1),.args=XLEN_Args},
|
||||
@@ -11341,7 +11491,7 @@ struct COMMAND_STRUCT redisCommandTable[] = {
|
||||
{MAKE_CMD("xreadgroup","Returns new or historical messages from a stream for a consumer in a group. Blocks until a message is available otherwise.","For each stream mentioned: O(M) with M being the number of elements returned. If M is constant (e.g. always asking for the first 10 elements with COUNT), you can consider it O(1). On the other side when XREADGROUP blocks, XADD will pay the O(N) time in order to serve the N clients blocked on the stream getting new data.","5.0.0",CMD_DOC_NONE,NULL,NULL,"stream",COMMAND_GROUP_STREAM,XREADGROUP_History,0,XREADGROUP_Tips,0,xreadCommand,-7,CMD_BLOCKING|CMD_WRITE,ACL_CATEGORY_STREAM,XREADGROUP_Keyspecs,1,xreadGetKeys,5),.args=XREADGROUP_Args},
|
||||
{MAKE_CMD("xrevrange","Returns the messages from a stream within a range of IDs in reverse order.","O(N) with N being the number of elements returned. If N is constant (e.g. always asking for the first 10 elements with COUNT), you can consider it O(1).","5.0.0",CMD_DOC_NONE,NULL,NULL,"stream",COMMAND_GROUP_STREAM,XREVRANGE_History,1,XREVRANGE_Tips,0,xrevrangeCommand,-4,CMD_READONLY,ACL_CATEGORY_STREAM,XREVRANGE_Keyspecs,1,NULL,4),.args=XREVRANGE_Args},
|
||||
{MAKE_CMD("xsetid","An internal command for replicating stream values.","O(1)","5.0.0",CMD_DOC_NONE,NULL,NULL,"stream",COMMAND_GROUP_STREAM,XSETID_History,1,XSETID_Tips,0,xsetidCommand,-3,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_STREAM,XSETID_Keyspecs,1,NULL,4),.args=XSETID_Args},
|
||||
{MAKE_CMD("xtrim","Deletes messages from the beginning of a stream.","O(N), with N being the number of evicted entries. Constant times are very small however, since entries are organized in macro nodes containing multiple entries that can be released with a single deallocation.","5.0.0",CMD_DOC_NONE,NULL,NULL,"stream",COMMAND_GROUP_STREAM,XTRIM_History,1,XTRIM_Tips,1,xtrimCommand,-4,CMD_WRITE,ACL_CATEGORY_STREAM,XTRIM_Keyspecs,1,NULL,2),.args=XTRIM_Args},
|
||||
{MAKE_CMD("xtrim","Deletes messages from the beginning of a stream.","O(N), with N being the number of evicted entries. Constant times are very small however, since entries are organized in macro nodes containing multiple entries that can be released with a single deallocation.","5.0.0",CMD_DOC_NONE,NULL,NULL,"stream",COMMAND_GROUP_STREAM,XTRIM_History,2,XTRIM_Tips,1,xtrimCommand,-4,CMD_WRITE,ACL_CATEGORY_STREAM,XTRIM_Keyspecs,1,NULL,2),.args=XTRIM_Args},
|
||||
/* string */
|
||||
{MAKE_CMD("append","Appends a string to the value of a key. Creates the key if it doesn't exist.","O(1). The amortized time complexity is O(1) assuming the appended value is small and the already present value is of any size, since the dynamic string library used by Redis will double the free space available on every reallocation.","2.0.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,APPEND_History,0,APPEND_Tips,0,appendCommand,3,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_STRING,APPEND_Keyspecs,1,NULL,2),.args=APPEND_Args},
|
||||
{MAKE_CMD("decr","Decrements the integer value of a key by one. Uses 0 as initial value if the key doesn't exist.","O(1)","1.0.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,DECR_History,0,DECR_Tips,0,decrCommand,2,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_STRING,DECR_Keyspecs,1,NULL,1),.args=DECR_Args},
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"SLOT-STATS": {
|
||||
"summary": "Return an array of slot usage statistics for slots assigned to the current node.",
|
||||
"complexity": "O(N) where N is the total number of slots based on arguments. O(N*log(N)) with ORDERBY subcommand.",
|
||||
"group": "cluster",
|
||||
"since": "8.2.0",
|
||||
"arity": -4,
|
||||
"container": "CLUSTER",
|
||||
"function": "clusterSlotStatsCommand",
|
||||
"command_flags": [
|
||||
"STALE",
|
||||
"LOADING"
|
||||
],
|
||||
"command_tips": [
|
||||
"NONDETERMINISTIC_OUTPUT",
|
||||
"REQUEST_POLICY:ALL_SHARDS"
|
||||
],
|
||||
"reply_schema": {
|
||||
"type": "array",
|
||||
"description": "Array of nested arrays, where the inner array element represents a slot and its respective usage statistics.",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"description": "Array of size 2, where 0th index represents (int) slot and 1st index represents (map) usage statistics.",
|
||||
"minItems": 2,
|
||||
"maxItems": 2,
|
||||
"items": [
|
||||
{
|
||||
"description": "Slot Number.",
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"description": "Map of slot usage statistics.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"key-count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"cpu-usec": {
|
||||
"type": "integer"
|
||||
},
|
||||
"network-bytes-in": {
|
||||
"type": "integer"
|
||||
},
|
||||
"network-bytes-out": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"arguments": [
|
||||
{
|
||||
"name": "filter",
|
||||
"type": "oneof",
|
||||
"arguments": [
|
||||
{
|
||||
"token": "SLOTSRANGE",
|
||||
"name": "slotsrange",
|
||||
"type": "block",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "start-slot",
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"name": "end-slot",
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"token": "ORDERBY",
|
||||
"name": "orderby",
|
||||
"type": "block",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "metric",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"token": "LIMIT",
|
||||
"name": "limit",
|
||||
"type": "integer",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "order",
|
||||
"type": "oneof",
|
||||
"optional": true,
|
||||
"arguments": [
|
||||
{
|
||||
"name": "asc",
|
||||
"type": "pure-token",
|
||||
"token": "ASC"
|
||||
},
|
||||
{
|
||||
"name": "desc",
|
||||
"type": "pure-token",
|
||||
"token": "DESC"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"XACKDEL": {
|
||||
"summary": "Acknowledges and deletes one or multiple messages for a stream consumer group.",
|
||||
"complexity": "O(1) for each message ID processed.",
|
||||
"group": "stream",
|
||||
"since": "8.2.0",
|
||||
"arity": -6,
|
||||
"function": "xackdelCommand",
|
||||
"command_flags": [
|
||||
"WRITE",
|
||||
"FAST"
|
||||
],
|
||||
"acl_categories": [
|
||||
"STREAM"
|
||||
],
|
||||
"key_specs": [
|
||||
{
|
||||
"flags": [
|
||||
"RW",
|
||||
"UPDATE",
|
||||
"DELETE"
|
||||
],
|
||||
"begin_search": {
|
||||
"index": {
|
||||
"pos": 1
|
||||
}
|
||||
},
|
||||
"find_keys": {
|
||||
"range": {
|
||||
"lastkey": 0,
|
||||
"step": 1,
|
||||
"limit": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"arguments": [
|
||||
{
|
||||
"name": "key",
|
||||
"type": "key",
|
||||
"key_spec_index": 0
|
||||
},
|
||||
{
|
||||
"name": "group",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "condition",
|
||||
"type": "oneof",
|
||||
"optional": true,
|
||||
"arguments": [
|
||||
{
|
||||
"name": "keepref",
|
||||
"type": "pure-token",
|
||||
"token": "KEEPREF"
|
||||
},
|
||||
{
|
||||
"name": "delref",
|
||||
"type": "pure-token",
|
||||
"token": "DELREF"
|
||||
},
|
||||
{
|
||||
"name": "acked",
|
||||
"type": "pure-token",
|
||||
"token": "ACKED"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ids",
|
||||
"token": "IDS",
|
||||
"type": "block",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "numids",
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"type": "string",
|
||||
"multiple": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"reply_schema": {
|
||||
"description": "Array of results. Returns an array with -1 for each requested ID if the key does not exist.",
|
||||
"type": "array",
|
||||
"minItems": 0,
|
||||
"maxItems": 4294967295,
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "The id does not exist in the provided stream key.",
|
||||
"const": -1
|
||||
},
|
||||
{
|
||||
"description": "Entry was acknowledged and deleted from the stream.",
|
||||
"const": 1
|
||||
},
|
||||
{
|
||||
"description": "Entry was acknowledged but not deleted, there are still dangling references.",
|
||||
"const": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,10 @@
|
||||
[
|
||||
"7.0.0",
|
||||
"Added support for the `<ms>-*` explicit ID form."
|
||||
],
|
||||
[
|
||||
"8.2.0",
|
||||
"Added the `KEEPREF`, `DELREF` and `ACKED` options."
|
||||
]
|
||||
],
|
||||
"command_flags": [
|
||||
@@ -61,6 +65,28 @@
|
||||
"optional": true,
|
||||
"since": "6.2.0"
|
||||
},
|
||||
{
|
||||
"name": "condition",
|
||||
"type": "oneof",
|
||||
"optional": true,
|
||||
"arguments": [
|
||||
{
|
||||
"name": "keepref",
|
||||
"type": "pure-token",
|
||||
"token": "KEEPREF"
|
||||
},
|
||||
{
|
||||
"name": "delref",
|
||||
"type": "pure-token",
|
||||
"token": "DELREF"
|
||||
},
|
||||
{
|
||||
"name": "acked",
|
||||
"type": "pure-token",
|
||||
"token": "ACKED"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "trim",
|
||||
"type": "block",
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"XDELEX": {
|
||||
"summary": "Deletes one or multiple entries from the stream.",
|
||||
"complexity": "O(1) for each single item to delete in the stream, regardless of the stream size.",
|
||||
"group": "stream",
|
||||
"since": "8.2.0",
|
||||
"arity": -5,
|
||||
"function": "xdelexCommand",
|
||||
"command_flags": [
|
||||
"WRITE",
|
||||
"FAST"
|
||||
],
|
||||
"acl_categories": [
|
||||
"STREAM"
|
||||
],
|
||||
"key_specs": [
|
||||
{
|
||||
"flags": [
|
||||
"RW",
|
||||
"DELETE"
|
||||
],
|
||||
"begin_search": {
|
||||
"index": {
|
||||
"pos": 1
|
||||
}
|
||||
},
|
||||
"find_keys": {
|
||||
"range": {
|
||||
"lastkey": 0,
|
||||
"step": 1,
|
||||
"limit": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"arguments": [
|
||||
{
|
||||
"name": "key",
|
||||
"type": "key",
|
||||
"key_spec_index": 0
|
||||
},
|
||||
{
|
||||
"name": "condition",
|
||||
"type": "oneof",
|
||||
"optional": true,
|
||||
"arguments": [
|
||||
{
|
||||
"name": "keepref",
|
||||
"type": "pure-token",
|
||||
"token": "KEEPREF"
|
||||
},
|
||||
{
|
||||
"name": "delref",
|
||||
"type": "pure-token",
|
||||
"token": "DELREF"
|
||||
},
|
||||
{
|
||||
"name": "acked",
|
||||
"type": "pure-token",
|
||||
"token": "ACKED"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ids",
|
||||
"token": "IDS",
|
||||
"type": "block",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "numids",
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"type": "string",
|
||||
"multiple": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"reply_schema": {
|
||||
"description": "Array of results. Returns an array with -1 for each requested ID if the key does not exist.",
|
||||
"type": "array",
|
||||
"minItems": 0,
|
||||
"maxItems": 4294967295,
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "The id does not exist in the provided stream key.",
|
||||
"const": -1
|
||||
},
|
||||
{
|
||||
"description": "Entry was deleted from the stream.",
|
||||
"const": 1
|
||||
},
|
||||
{
|
||||
"description": "Entry was not deleted, but there are still dangling references.",
|
||||
"const": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
-16
@@ -92,23 +92,53 @@
|
||||
"pattern": "[0-9]+-[0-9]+"
|
||||
},
|
||||
{
|
||||
"description": "Consumers with pending messages",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"maxItems": 2,
|
||||
"items": [
|
||||
{
|
||||
"description": "Consumer name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Number of pending messages",
|
||||
"type": "string"
|
||||
"description": "Consuers with pending messages",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"maxItems": 2,
|
||||
"items": [
|
||||
{
|
||||
"description": "Consumer name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Number of pending messages",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "null",
|
||||
"description": "When there are no consumers with pending messages"
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Summary form, in case `start` was not given and there are no pending messages.",
|
||||
"type": "array",
|
||||
"minItems": 4,
|
||||
"maxItems": 4,
|
||||
"items": [
|
||||
{
|
||||
"description": "Total number of pending messages",
|
||||
"const": 0
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
[
|
||||
"6.2.0",
|
||||
"Added the `MINID` trimming strategy and the `LIMIT` option."
|
||||
],
|
||||
[
|
||||
"8.2.0",
|
||||
"Added the `KEEPREF`, `DELREF` and `ACKED` options."
|
||||
]
|
||||
],
|
||||
"command_flags": [
|
||||
@@ -95,6 +99,28 @@
|
||||
"type": "integer",
|
||||
"optional": true,
|
||||
"since": "6.2.0"
|
||||
},
|
||||
{
|
||||
"name": "condition",
|
||||
"type": "oneof",
|
||||
"optional": true,
|
||||
"arguments": [
|
||||
{
|
||||
"name": "keepref",
|
||||
"type": "pure-token",
|
||||
"token": "KEEPREF"
|
||||
},
|
||||
{
|
||||
"name": "delref",
|
||||
"type": "pure-token",
|
||||
"token": "DELREF"
|
||||
},
|
||||
{
|
||||
"name": "acked",
|
||||
"type": "pure-token",
|
||||
"token": "ACKED"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3114,6 +3114,8 @@ standardConfig static_configs[] = {
|
||||
createBoolConfig("aof-disable-auto-gc", NULL, MODIFIABLE_CONFIG | HIDDEN_CONFIG, server.aof_disable_auto_gc, 0, NULL, updateAofAutoGCEnabled),
|
||||
createBoolConfig("replica-ignore-disk-write-errors", NULL, MODIFIABLE_CONFIG, server.repl_ignore_disk_write_error, 0, NULL, NULL),
|
||||
createBoolConfig("hide-user-data-from-log", NULL, MODIFIABLE_CONFIG, server.hide_user_data_from_log, 0, NULL, NULL),
|
||||
createBoolConfig("lazyexpire-nested-arbitrary-keys", NULL, MODIFIABLE_CONFIG | HIDDEN_CONFIG, server.lazyexpire_nested_arbitrary_keys, 1, NULL, NULL),
|
||||
createBoolConfig("cluster-slot-stats-enabled", NULL, MODIFIABLE_CONFIG, server.cluster_slot_stats_enabled, 0, NULL, NULL),
|
||||
|
||||
/* String Configs */
|
||||
createStringConfig("aclfile", NULL, IMMUTABLE_CONFIG, ALLOW_EMPTY_STRING, server.acl_filename, "", NULL, NULL),
|
||||
@@ -3466,6 +3468,7 @@ NULL
|
||||
|
||||
void configResetStatCommand(client *c) {
|
||||
resetServerStats();
|
||||
resetClusterStats();
|
||||
resetCommandTableStats(server.commands);
|
||||
resetErrorTableStats();
|
||||
addReply(c,shared.ok);
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
* Licensed under your choice of (a) the Redis Source Available License 2.0
|
||||
* (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
|
||||
* GNU Affero General Public License v3 (AGPLv3).
|
||||
*
|
||||
* Portions of this file are available under BSD3 terms; see REDISCONTRIBUTIONS for more information.
|
||||
*/
|
||||
|
||||
#include "server.h"
|
||||
@@ -364,17 +366,32 @@ int calculateKeySlot(sds key) {
|
||||
|
||||
/* Return slot-specific dictionary for key based on key's hash slot when cluster mode is enabled, else 0.*/
|
||||
int getKeySlot(sds key) {
|
||||
if (!server.cluster_enabled) return 0;
|
||||
/* This is performance optimization that uses pre-set slot id from the current command,
|
||||
* in order to avoid calculation of the key hash.
|
||||
*
|
||||
* This optimization is only used when current_client flag `CLIENT_EXECUTING_COMMAND` is set.
|
||||
* It only gets set during the execution of command under `call` method. Other flows requesting
|
||||
* the key slot would fallback to calculateKeySlot.
|
||||
*
|
||||
* Modules and scripts executed on the primary may get replicated as multi-execs that operate on multiple slots,
|
||||
* so we must always recompute the slot for commands coming from the primary.
|
||||
*/
|
||||
if (server.current_client && server.current_client->slot >= 0 && server.current_client->flags & CLIENT_EXECUTING_COMMAND) {
|
||||
debugServerAssertWithInfo(server.current_client, NULL, calculateKeySlot(key)==server.current_client->slot);
|
||||
if (server.current_client && server.current_client->slot >= 0 && server.current_client->flags & CLIENT_EXECUTING_COMMAND &&
|
||||
!(server.current_client->flags & CLIENT_MASTER))
|
||||
{
|
||||
debugServerAssertWithInfo(server.current_client, NULL,
|
||||
(int)keyHashSlot(key, (int)sdslen(key)) == server.current_client->slot);
|
||||
return server.current_client->slot;
|
||||
}
|
||||
return calculateKeySlot(key);
|
||||
int slot = keyHashSlot(key, (int)sdslen(key));
|
||||
/* For the case of replicated commands from primary, getNodeByQuery() never gets called,
|
||||
* and thus c->slot never gets populated. That said, if this command ends up accessing a key,
|
||||
* we are able to backfill c->slot here, where the key's hash calculation is made. */
|
||||
if (server.current_client && (server.current_client->flags & CLIENT_MASTER)) {
|
||||
server.current_client->slot = slot;
|
||||
}
|
||||
return slot;
|
||||
}
|
||||
|
||||
/* Return the slot of the key in the command. IO threads use this function
|
||||
@@ -2444,6 +2461,17 @@ int keyIsExpired(redisDb *db, sds key, kvobj *kv) {
|
||||
return now > when;
|
||||
}
|
||||
|
||||
/* Check if user configuration allows key to be deleted due to expiary */
|
||||
int confAllowsExpireDel(void) {
|
||||
if (server.lazyexpire_nested_arbitrary_keys)
|
||||
return 1;
|
||||
|
||||
/* This configuration specifically targets nested commands, to align with RE's feature of replication between dbs.
|
||||
* transactions (from scripts or multi-exec) containing commands like SCAN and RANDOMKEY will execute locally, but their
|
||||
* lazy-expiration DELs may induce CROSS-SLOT on remote proxy in mode replica-of (RED-161574) */
|
||||
return !(server.execution_nesting > 1 && server.executing_client->cmd->flags & CMD_TOUCHES_ARBITRARY_KEYS);
|
||||
}
|
||||
|
||||
/* This function is called when we are going to perform some operation
|
||||
* in a given key, but such key may be already logically expired even if
|
||||
* it still exists in the database. The main way this function is called
|
||||
@@ -2503,6 +2531,11 @@ keyStatus expireIfNeeded(redisDb *db, robj *key, kvobj *kv, int flags) {
|
||||
if (!(flags & EXPIRE_FORCE_DELETE_EXPIRED)) return KEY_EXPIRED;
|
||||
}
|
||||
|
||||
/* Check if user configuration disables lazy-expire deletions in current state.
|
||||
* This will only apply if the server doesn't mandate key deletion to operate correctly (write commands). */
|
||||
if (!(flags & EXPIRE_FORCE_DELETE_EXPIRED) && !confAllowsExpireDel())
|
||||
return KEY_EXPIRED;
|
||||
|
||||
/* In some cases we're explicitly instructed to return an indication of a
|
||||
* missing key without actually deleting it, even on masters. */
|
||||
if (flags & EXPIRE_AVOID_DELETE_EXPIRED)
|
||||
|
||||
+69
-1
@@ -15,6 +15,7 @@
|
||||
#include "server.h"
|
||||
#include "atomicvar.h"
|
||||
#include "cluster.h"
|
||||
#include "cluster_slot_stats.h"
|
||||
#include "script.h"
|
||||
#include "fpconv_dtoa.h"
|
||||
#include "fmtargs.h"
|
||||
@@ -223,6 +224,8 @@ client *createClient(connection *conn) {
|
||||
listInitNode(&c->clients_pending_write_node, c);
|
||||
c->mem_usage_bucket = NULL;
|
||||
c->mem_usage_bucket_node = NULL;
|
||||
c->net_input_bytes_curr_cmd = 0;
|
||||
c->net_output_bytes_curr_cmd = 0;
|
||||
if (conn) linkClient(c);
|
||||
initClientMultiState(c);
|
||||
c->net_input_bytes = 0;
|
||||
@@ -401,6 +404,7 @@ void _addReplyToBufferOrList(client *c, const char *s, size_t len) {
|
||||
return;
|
||||
}
|
||||
|
||||
c->net_output_bytes_curr_cmd += len;
|
||||
/* We call it here because this function may affect the reply
|
||||
* buffer offset (see function comment) */
|
||||
reqresSaveClientReplyOffset(c);
|
||||
@@ -792,6 +796,7 @@ void setDeferredReply(client *c, void *node, const char *s, size_t length) {
|
||||
if (len_to_copy > length)
|
||||
len_to_copy = length;
|
||||
memcpy(prev->buf + prev->used, s, len_to_copy);
|
||||
c->net_output_bytes_curr_cmd += len_to_copy;
|
||||
prev->used += len_to_copy;
|
||||
length -= len_to_copy;
|
||||
if (length == 0) {
|
||||
@@ -807,6 +812,7 @@ void setDeferredReply(client *c, void *node, const char *s, size_t length) {
|
||||
{
|
||||
memmove(next->buf + length, next->buf, next->used);
|
||||
memcpy(next->buf, s, length);
|
||||
c->net_output_bytes_curr_cmd += length;
|
||||
next->used += length;
|
||||
listDelNode(c->reply,ln);
|
||||
} else {
|
||||
@@ -817,6 +823,7 @@ void setDeferredReply(client *c, void *node, const char *s, size_t length) {
|
||||
buf->size = usable_size - sizeof(clientReplyBlock);
|
||||
buf->used = length;
|
||||
memcpy(buf->buf, s, length);
|
||||
c->net_output_bytes_curr_cmd += length;
|
||||
listNodeValue(ln) = buf;
|
||||
c->reply_bytes += buf->size;
|
||||
|
||||
@@ -2316,6 +2323,9 @@ static inline void resetClientInternal(client *c, int free_argv) {
|
||||
c->flags |= CLIENT_REPLY_SKIP;
|
||||
c->flags &= ~CLIENT_REPLY_SKIP_NEXT;
|
||||
}
|
||||
|
||||
c->net_input_bytes_curr_cmd = 0;
|
||||
c->net_output_bytes_curr_cmd = 0;
|
||||
}
|
||||
|
||||
/* resetClient prepare the client to process the next command */
|
||||
@@ -2434,6 +2444,22 @@ int processInlineBuffer(client *c) {
|
||||
c->argv_len_sum += sdslen(argv[j]);
|
||||
}
|
||||
zfree(argv);
|
||||
|
||||
/* Per-slot network bytes-in calculation.
|
||||
*
|
||||
* We calculate and store the current command's ingress bytes under
|
||||
* c->net_input_bytes_curr_cmd, for which its per-slot aggregation is deferred
|
||||
* until c->slot is parsed later within processCommand().
|
||||
*
|
||||
* Calculation: For inline buffer, every whitespace is of length 1,
|
||||
* with the exception of the trailing '\r\n' being length 2.
|
||||
*
|
||||
* For example;
|
||||
* Command) SET key value
|
||||
* Inline) SET key value\r\n
|
||||
*/
|
||||
c->net_input_bytes_curr_cmd = (c->argv_len_sum + (c->argc - 1) + 2);
|
||||
|
||||
return C_OK;
|
||||
}
|
||||
|
||||
@@ -2507,6 +2533,7 @@ int processMultibulkBuffer(client *c) {
|
||||
/* We know for sure there is a whole line since newline != NULL,
|
||||
* so go ahead and find out the multi bulk length. */
|
||||
serverAssertWithInfo(c,NULL,c->querybuf[c->qb_pos] == '*');
|
||||
size_t multibulklen_slen = newline - (c->querybuf + 1 + c->qb_pos);
|
||||
ok = string2ll(c->querybuf+1+c->qb_pos,newline-(c->querybuf+1+c->qb_pos),&ll);
|
||||
if (!ok || ll > INT_MAX) {
|
||||
c->read_error = CLIENT_READ_INVALID_MULTIBUCK_LENGTH;
|
||||
@@ -2534,6 +2561,39 @@ int processMultibulkBuffer(client *c) {
|
||||
c->argv = zmalloc(sizeof(robj*)*c->argv_len);
|
||||
}
|
||||
c->argv_len_sum = 0;
|
||||
|
||||
/* Per-slot network bytes-in calculation.
|
||||
*
|
||||
* We calculate and store the current command's ingress bytes under
|
||||
* c->net_input_bytes_curr_cmd, for which its per-slot aggregation is deferred
|
||||
* until c->slot is parsed later within processCommand().
|
||||
*
|
||||
* Calculation: For multi bulk buffer, we accumulate four factors, namely;
|
||||
*
|
||||
* 1) multibulklen_slen + 3
|
||||
* Cumulative string length (and not the value of) of multibulklen,
|
||||
* including the first "*" byte and last "\r\n" 2 bytes from RESP.
|
||||
* 2) bulklen_slen + 3
|
||||
* Cumulative string length (and not the value of) of bulklen,
|
||||
* including +3 from RESP first "$" byte and last "\r\n" 2 bytes per argument count.
|
||||
* 3) c->argv_len_sum
|
||||
* Cumulative string length of all argument vectors.
|
||||
* 4) c->argc * 2
|
||||
* Cumulative string length of the arguments' white-spaces, for which there exists a total of
|
||||
* "\r\n" 2 bytes per argument.
|
||||
*
|
||||
* For example;
|
||||
* Command) SET key value
|
||||
* RESP) *3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n
|
||||
*
|
||||
* 1) String length of "*3\r\n" is 4, obtained from (multibulklen_slen + 3).
|
||||
* 2) String length of "$3\r\n" "$3\r\n" "$5\r\n" is 12, obtained from (bulklen_slen + 3).
|
||||
* 3) String length of "SET" "key" "value" is 11, obtained from (c->argv_len_sum).
|
||||
* 4) String length of the 3 arguments' white-spaces "\r\n" is 6, obtained from (c->argc * 2).
|
||||
*
|
||||
* The 1st component is calculated within the below line.
|
||||
* */
|
||||
c->net_input_bytes_curr_cmd += (multibulklen_slen + 3);
|
||||
}
|
||||
|
||||
serverAssertWithInfo(c,NULL,c->multibulklen > 0);
|
||||
@@ -2558,6 +2618,7 @@ int processMultibulkBuffer(client *c) {
|
||||
return C_ERR;
|
||||
}
|
||||
|
||||
size_t bulklen_slen = newline - (c->querybuf + c->qb_pos + 1);
|
||||
ok = string2ll(c->querybuf+c->qb_pos+1,newline-(c->querybuf+c->qb_pos+1),&ll);
|
||||
if (!ok || ll < 0 ||
|
||||
(!(c->flags & CLIENT_MASTER) && ll > server.proto_max_bulk_len)) {
|
||||
@@ -2597,6 +2658,8 @@ int processMultibulkBuffer(client *c) {
|
||||
}
|
||||
}
|
||||
c->bulklen = ll;
|
||||
/* Per-slot network bytes-in calculation, 2nd component. */
|
||||
c->net_input_bytes_curr_cmd += (bulklen_slen + 3);
|
||||
}
|
||||
|
||||
/* Read bulk argument */
|
||||
@@ -2638,7 +2701,11 @@ int processMultibulkBuffer(client *c) {
|
||||
}
|
||||
|
||||
/* We're done when c->multibulk == 0 */
|
||||
if (c->multibulklen == 0) return C_OK;
|
||||
if (c->multibulklen == 0) {
|
||||
/* Per-slot network bytes-in calculation, 3rd and 4th components. */
|
||||
c->net_input_bytes_curr_cmd += (c->argv_len_sum + (c->argc * 2));
|
||||
return C_OK;
|
||||
}
|
||||
|
||||
/* Still not ready to process the command */
|
||||
return C_ERR;
|
||||
@@ -2660,6 +2727,7 @@ void commandProcessed(client *c) {
|
||||
if (c->flags & CLIENT_BLOCKED) return;
|
||||
|
||||
reqresAppendResponse(c);
|
||||
clusterSlotStatsAddNetworkBytesInForUserClient(c);
|
||||
resetClientInternal(c, 0);
|
||||
|
||||
long long prev_offset = c->reploff;
|
||||
|
||||
@@ -2,13 +2,19 @@
|
||||
* Copyright (c) 2009-Present, Redis Ltd.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Copyright (c) 2024-present, Valkey contributors.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Licensed under your choice of (a) the Redis Source Available License 2.0
|
||||
* (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
|
||||
* GNU Affero General Public License v3 (AGPLv3).
|
||||
*
|
||||
* Portions of this file are available under BSD3 terms; see REDISCONTRIBUTIONS for more information.
|
||||
*/
|
||||
|
||||
#include "server.h"
|
||||
#include "cluster.h"
|
||||
#include "cluster_slot_stats.h"
|
||||
|
||||
/* Structure to hold the pubsub related metadata. Currently used
|
||||
* for pubsub and pubsubshard feature. */
|
||||
@@ -470,6 +476,8 @@ int pubsubPublishMessageInternal(robj *channel, robj *message, pubsubtype type)
|
||||
while ((entry = dictNext(iter)) != NULL) {
|
||||
client *c = dictGetKey(entry);
|
||||
addReplyPubsubMessage(c,channel,message,*type.messageBulk);
|
||||
if (server.cluster_enabled && server.cluster_slot_stats_enabled)
|
||||
clusterSlotStatsAddNetworkBytesOutForShardedPubSubInternalPropagation(c, slot);
|
||||
updateClientMemUsageAndBucket(c);
|
||||
receivers++;
|
||||
}
|
||||
|
||||
@@ -2973,6 +2973,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error)
|
||||
streamNACK *nack = streamCreateNACK(NULL);
|
||||
nack->delivery_time = rdbLoadMillisecondTime(rdb,RDB_VERSION);
|
||||
nack->delivery_count = rdbLoadLen(rdb,NULL);
|
||||
nack->cgroup_ref_node = streamLinkCGroupToEntry(s, cgroup, rawid);
|
||||
if (rioGetReadError(rdb)) {
|
||||
rdbReportReadError("Stream PEL NACK loading failed.");
|
||||
decrRefCount(o);
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
#include "server.h"
|
||||
#include "cluster.h"
|
||||
#include "cluster_slot_stats.h"
|
||||
#include "bio.h"
|
||||
#include "functions.h"
|
||||
#include "connection.h"
|
||||
@@ -392,6 +393,8 @@ void feedReplicationBuffer(char *s, size_t len) {
|
||||
|
||||
if (server.repl_backlog == NULL) return;
|
||||
|
||||
clusterSlotStatsIncrNetworkBytesOutForReplication(len);
|
||||
|
||||
while(len > 0) {
|
||||
size_t start_pos = 0; /* The position of referenced block to start sending. */
|
||||
listNode *start_node = NULL; /* Replica/backlog starts referenced node. */
|
||||
@@ -547,6 +550,11 @@ void replicationFeedSlaves(list *slaves, int dictid, robj **argv, int argc) {
|
||||
|
||||
feedReplicationBufferWithObject(selectcmd);
|
||||
|
||||
/* Although the SELECT command is not associated with any slot,
|
||||
* its per-slot network-bytes-out accumulation is made by the above function call.
|
||||
* To cancel-out this accumulation, below adjustment is made. */
|
||||
clusterSlotStatsDecrNetworkBytesOutForReplication(sdslen(selectcmd->ptr));
|
||||
|
||||
if (dictid < 0 || dictid >= PROTO_SHARED_SELECT_CMDS)
|
||||
decrRefCount(selectcmd);
|
||||
|
||||
@@ -4149,6 +4157,9 @@ void replicationSendAck(void) {
|
||||
addReplyBulkLongLong(c,server.fsynced_reploff);
|
||||
}
|
||||
c->flags &= ~CLIENT_MASTER_FORCE_REPLY;
|
||||
/* Accumulation from above replies must be reset back to 0 manually,
|
||||
* as this subroutine does not invoke resetClient(). */
|
||||
c->net_output_bytes_curr_cmd = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,20 @@
|
||||
* Copyright (c) 2009-Present, Redis Ltd.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Copyright (c) 2024-present, Valkey contributors.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Licensed under your choice of (a) the Redis Source Available License 2.0
|
||||
* (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
|
||||
* GNU Affero General Public License v3 (AGPLv3).
|
||||
*
|
||||
* Portions of this file are available under BSD3 terms; see REDISCONTRIBUTIONS for more information.
|
||||
*/
|
||||
|
||||
#include "server.h"
|
||||
#include "script.h"
|
||||
#include "cluster.h"
|
||||
#include "cluster_slot_stats.h"
|
||||
|
||||
#include <lua.h>
|
||||
#include <lauxlib.h>
|
||||
@@ -664,6 +670,7 @@ void scriptCall(scriptRunCtx *run_ctx, sds *err) {
|
||||
}
|
||||
call(c, call_flags);
|
||||
serverAssert((c->flags & CLIENT_BLOCKED) == 0);
|
||||
clusterSlotStatsInvalidateSlotIfApplicable(run_ctx);
|
||||
return;
|
||||
|
||||
error:
|
||||
|
||||
+16
-3
@@ -15,6 +15,7 @@
|
||||
#include "server.h"
|
||||
#include "monotonic.h"
|
||||
#include "cluster.h"
|
||||
#include "cluster_slot_stats.h"
|
||||
#include "slowlog.h"
|
||||
#include "bio.h"
|
||||
#include "latency.h"
|
||||
@@ -1054,7 +1055,16 @@ void removeClientFromMemUsageBucket(client *c, int allow_eviction) {
|
||||
* returns 1 if client eviction for this client is allowed, 0 otherwise.
|
||||
*/
|
||||
int updateClientMemUsageAndBucket(client *c) {
|
||||
serverAssert(pthread_equal(pthread_self(), server.main_thread_id) && c->conn);
|
||||
/* The unlikely case this function was called from a thread different
|
||||
* than the main one is a module call from a spawned thread. This is safe
|
||||
* since this call must have been made after calling
|
||||
* RedisModule_ThreadSafeContextLock i.e the module is holding the GIL. In
|
||||
* that special case we assert that at least the updated client's
|
||||
* running_tid is the main thread. The true main thread is allowed to call
|
||||
* this function on clients handled by IO-threads as it makes sure the
|
||||
* IO-threads are paused, f.e see cleintsCron() and evictClients(). */
|
||||
serverAssert((pthread_equal(pthread_self(), server.main_thread_id) ||
|
||||
c->running_tid == IOTHREAD_MAIN_THREAD_ID) && c->conn);
|
||||
int allow_eviction = clientEvictionAllowed(c);
|
||||
removeClientFromMemUsageBucket(c, allow_eviction);
|
||||
|
||||
@@ -3821,13 +3831,14 @@ void call(client *c, int flags) {
|
||||
if (!(c->flags & CLIENT_BLOCKED))
|
||||
freeClientOriginalArgv(c);
|
||||
|
||||
/* populate the per-command statistics that we show in INFO commandstats.
|
||||
* If the client is blocked we will handle latency stats and duration when it is unblocked. */
|
||||
/* Populate the per-command and per-slot statistics that we show in INFO commandstats and CLUSTER SLOT-STATS,
|
||||
* respectively. If the client is blocked we will handle latency stats and duration when it is unblocked. */
|
||||
if (update_command_stats && !(c->flags & CLIENT_BLOCKED)) {
|
||||
real_cmd->calls++;
|
||||
real_cmd->microseconds += c->duration;
|
||||
if (server.latency_tracking_enabled && !(c->flags & CLIENT_BLOCKED))
|
||||
updateCommandLatencyHistogram(&(real_cmd->latency_histogram), c->duration*1000);
|
||||
clusterSlotStatsAddCpuDuration(c, c->duration);
|
||||
}
|
||||
|
||||
/* The duration needs to be reset after each call except for a blocked command,
|
||||
@@ -3978,6 +3989,8 @@ void afterCommand(client *c) {
|
||||
/* Flush pending tracking invalidations. */
|
||||
trackingHandlePendingKeyInvalidations();
|
||||
|
||||
clusterSlotStatsAddNetworkBytesOutForUserClient(c);
|
||||
|
||||
/* Flush other pending push messages. only when we are not in nested call.
|
||||
* So the messages are not interleaved with transaction response. */
|
||||
if (!server.execution_nesting)
|
||||
|
||||
@@ -1447,6 +1447,11 @@ typedef struct client {
|
||||
|
||||
/* list node in clients_pending_write list */
|
||||
listNode clients_pending_write_node;
|
||||
/* Statistics and metrics */
|
||||
size_t net_input_bytes_curr_cmd; /* Total network input bytes read for the
|
||||
* execution of this client's current command. */
|
||||
size_t net_output_bytes_curr_cmd; /* Total network output bytes sent to this
|
||||
* client, by the current command. */
|
||||
/* Response buffer */
|
||||
size_t buf_peak; /* Peak used size of buffer in last 5 sec interval. */
|
||||
mstime_t buf_peak_last_reset_time; /* keeps the last time the buffer peak value was reset */
|
||||
@@ -1977,6 +1982,7 @@ struct redisServer {
|
||||
unsigned int max_new_tls_conns_per_cycle; /* The maximum number of tls connections that will be accepted during each invocation of the event loop. */
|
||||
unsigned int max_new_conns_per_cycle; /* The maximum number of tcp connections that will be accepted during each invocation of the event loop. */
|
||||
int cluster_compatibility_sample_ratio; /* Sampling ratio for cluster mode incompatible commands. */
|
||||
int lazyexpire_nested_arbitrary_keys; /* If disabled, avoid lazy-expire from commands that touch arbitrary keys (SCAN/RANDOMKEY) within transactions */
|
||||
|
||||
/* AOF persistence */
|
||||
int aof_enabled; /* AOF configuration */
|
||||
@@ -2249,6 +2255,7 @@ struct redisServer {
|
||||
unsigned long long cluster_link_msg_queue_limit_bytes; /* Memory usage limit on individual link msg queue */
|
||||
int cluster_drop_packet_filter; /* Debug config that allows tactically
|
||||
* dropping packets of a specific type */
|
||||
int cluster_slot_stats_enabled; /* Cluster slot usage statistics tracking enabled. */
|
||||
/* Scripting */
|
||||
unsigned int lua_arena; /* eval lua arena used in jemalloc. */
|
||||
mstime_t busy_reply_threshold; /* Script / module timeout in milliseconds */
|
||||
@@ -3620,6 +3627,7 @@ void deleteExpiredKeyAndPropagate(redisDb *db, robj *keyobj);
|
||||
void deleteEvictedKeyAndPropagate(redisDb *db, robj *keyobj, long long *key_mem_freed);
|
||||
void propagateDeletion(redisDb *db, robj *key, int lazy);
|
||||
int keyIsExpired(redisDb *db, sds key, kvobj *kv);
|
||||
int confAllowsExpireDel(void);
|
||||
long long getExpire(redisDb *db, sds key, kvobj *kv);
|
||||
kvobj *setExpire(client *c, redisDb *db, robj *key, long long when);
|
||||
kvobj *setExpireByLink(client *c, redisDb *db, sds key, long long when, dictEntryLink link);
|
||||
@@ -4051,6 +4059,7 @@ void sunsubscribeCommand(client *c);
|
||||
void watchCommand(client *c);
|
||||
void unwatchCommand(client *c);
|
||||
void clusterCommand(client *c);
|
||||
void clusterSlotStatsCommand(client *c);
|
||||
void restoreCommand(client *c);
|
||||
void migrateCommand(client *c);
|
||||
void askingCommand(client *c);
|
||||
@@ -4113,11 +4122,13 @@ void xreadCommand(client *c);
|
||||
void xgroupCommand(client *c);
|
||||
void xsetidCommand(client *c);
|
||||
void xackCommand(client *c);
|
||||
void xackdelCommand(client *c);
|
||||
void xpendingCommand(client *c);
|
||||
void xclaimCommand(client *c);
|
||||
void xautoclaimCommand(client *c);
|
||||
void xinfoCommand(client *c);
|
||||
void xdelCommand(client *c);
|
||||
void xdelexCommand(client *c);
|
||||
void xtrimCommand(client *c);
|
||||
void lolwutCommand(client *c);
|
||||
void aclCommand(client *c);
|
||||
|
||||
@@ -21,6 +21,9 @@ typedef struct stream {
|
||||
streamID max_deleted_entry_id; /* The maximal ID that was deleted. */
|
||||
uint64_t entries_added; /* All time count of elements added. */
|
||||
rax *cgroups; /* Consumer groups dictionary: name -> streamCG */
|
||||
rax *cgroups_ref; /* Index mapping message IDs to their consumer groups. */
|
||||
streamID min_cgroup_last_id; /* The minimum ID of consume group. */
|
||||
unsigned int min_cgroup_last_id_valid: 1;
|
||||
} stream;
|
||||
|
||||
/* We define an iterator to iterate stream items in an abstract way, without
|
||||
@@ -94,6 +97,7 @@ typedef struct streamNACK {
|
||||
uint64_t delivery_count; /* Number of times this message was delivered.*/
|
||||
streamConsumer *consumer; /* The consumer this message was delivered to
|
||||
in the last delivery. */
|
||||
listNode *cgroup_ref_node; /* Reference to this NACK in the cgroups_ref list. */
|
||||
} streamNACK;
|
||||
|
||||
/* Stream propagation information, passed to functions in order to propagate
|
||||
@@ -144,4 +148,6 @@ long long streamEstimateDistanceFromFirstEverEntry(stream *s, streamID *id);
|
||||
int64_t streamTrimByLength(stream *s, long long maxlen, int approx);
|
||||
int64_t streamTrimByID(stream *s, streamID minid, int approx);
|
||||
|
||||
listNode *streamLinkCGroupToEntry(stream *s, streamCG *cg, unsigned char *key);
|
||||
|
||||
#endif
|
||||
|
||||
+520
-35
@@ -33,13 +33,16 @@
|
||||
* will return NULL. */
|
||||
#define STREAM_LISTPACK_MAX_SIZE (1<<30)
|
||||
|
||||
void streamFreeCG(streamCG *cg);
|
||||
void streamFreeCGGeneric(void *cg);
|
||||
void streamFreeNACK(streamNACK *na);
|
||||
size_t streamReplyWithRangeFromConsumerPEL(client *c, stream *s, streamID *start, streamID *end, size_t count, streamConsumer *consumer);
|
||||
int streamParseStrictIDOrReply(client *c, robj *o, streamID *id, uint64_t missing_seq, int *seq_given);
|
||||
int streamParseIDOrReply(client *c, robj *o, streamID *id, uint64_t missing_seq);
|
||||
|
||||
int streamEntryIsReferenced(stream *s, streamID *id);
|
||||
void streamCleanupEntryCGroupRefs(stream *s, streamID *id);
|
||||
void streamUpdateCGroupLastId(stream *s, streamCG *cg, streamID *id);
|
||||
|
||||
/* -----------------------------------------------------------------------
|
||||
* Low level stream encoding: a radix tree of listpacks.
|
||||
* ----------------------------------------------------------------------- */
|
||||
@@ -57,6 +60,10 @@ stream *streamNew(void) {
|
||||
s->max_deleted_entry_id.ms = 0;
|
||||
s->entries_added = 0;
|
||||
s->cgroups = NULL; /* Created on demand to save memory when not used. */
|
||||
s->cgroups_ref = NULL;
|
||||
s->min_cgroup_last_id.ms = UINT64_MAX;
|
||||
s->min_cgroup_last_id.seq = UINT64_MAX;
|
||||
s->min_cgroup_last_id_valid = 0;
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -65,6 +72,8 @@ void freeStream(stream *s) {
|
||||
raxFreeWithCallback(s->rax, lpFreeGeneric);
|
||||
if (s->cgroups)
|
||||
raxFreeWithCallback(s->cgroups, streamFreeCGGeneric);
|
||||
if (s->cgroups_ref)
|
||||
raxFreeWithCallback(s->cgroups_ref, listReleaseGeneric);
|
||||
zfree(s);
|
||||
}
|
||||
|
||||
@@ -199,6 +208,7 @@ robj *streamDup(robj *o) {
|
||||
streamNACK *new_nack = streamCreateNACK(NULL);
|
||||
new_nack->delivery_time = nack->delivery_time;
|
||||
new_nack->delivery_count = nack->delivery_count;
|
||||
new_nack->cgroup_ref_node = streamLinkCGroupToEntry(new_s, new_cg, ri_cg_pel.key);
|
||||
raxInsert(new_cg->pel, ri_cg_pel.key, sizeof(streamID), new_nack, NULL);
|
||||
}
|
||||
raxStop(&ri_cg_pel);
|
||||
@@ -662,6 +672,7 @@ typedef struct {
|
||||
/* XADD + XTRIM common options */
|
||||
int trim_strategy; /* TRIM_STRATEGY_* */
|
||||
int trim_strategy_arg_idx; /* Index of the count in MAXLEN/MINID, for rewriting. */
|
||||
int delete_strategy; /* DELETE_STRATEGY_* */
|
||||
int approx_trim; /* If 1 only delete whole radix tree nodes, so
|
||||
* the trim argument is not applied verbatim. */
|
||||
long long limit; /* Maximum amount of entries to trim. If 0, no limitation
|
||||
@@ -676,6 +687,17 @@ typedef struct {
|
||||
#define TRIM_STRATEGY_MAXLEN 1
|
||||
#define TRIM_STRATEGY_MINID 2
|
||||
|
||||
typedef struct {
|
||||
int startidx; /* Starting index of IDs in argv */
|
||||
long numids; /* Number of IDs to process */
|
||||
int delete_strategy; /* DELETE_STRATEGY_* */
|
||||
} streamAckDelArgs;
|
||||
|
||||
#define DELETE_STRATEGY_NONE 0
|
||||
#define DELETE_STRATEGY_KEEPREF 1 /* Delete and keep references */
|
||||
#define DELETE_STRATEGY_DELREF 2 /* Delete from pending entries list */
|
||||
#define DELETE_STRATEGY_ACKED 3 /* Only delete messages that are acknowledged */
|
||||
|
||||
/* Trim the stream 's' according to args->trim_strategy, and return the
|
||||
* number of elements removed from the stream. The 'approx' option, if non-zero,
|
||||
* specifies that the trimming must be performed in a approximated way in
|
||||
@@ -705,6 +727,7 @@ int64_t streamTrim(stream *s, streamAddTrimArgs *args) {
|
||||
int approx = args->approx_trim;
|
||||
int64_t limit = args->limit;
|
||||
int trim_strategy = args->trim_strategy;
|
||||
int delete_strategy = args->delete_strategy;
|
||||
|
||||
if (trim_strategy == TRIM_STRATEGY_NONE)
|
||||
return 0;
|
||||
@@ -725,21 +748,27 @@ int64_t streamTrim(stream *s, streamAddTrimArgs *args) {
|
||||
if (limit && (deleted + entries) > limit)
|
||||
break;
|
||||
|
||||
/* Check if we can remove the whole node. */
|
||||
int remove_node;
|
||||
streamID master_id = {0}; /* For MINID */
|
||||
/* Check if we can remove the whole node */
|
||||
int remove_node = 0; /* Final decision flag for node removal */
|
||||
int node_eligible_for_remove = 0; /* Whether node meets the basic criteria for removal */
|
||||
streamID master_id = {0};
|
||||
/* Read the master ID from the radix tree key. */
|
||||
streamDecodeID(ri.key, &master_id);
|
||||
if (trim_strategy == TRIM_STRATEGY_MAXLEN) {
|
||||
remove_node = s->length - entries >= maxlen;
|
||||
node_eligible_for_remove = s->length - entries >= maxlen;
|
||||
} else {
|
||||
/* Read the master ID from the radix tree key. */
|
||||
streamDecodeID(ri.key, &master_id);
|
||||
|
||||
/* Read last ID. */
|
||||
streamID last_id = {0,0};
|
||||
lpGetEdgeStreamID(lp, 0, &master_id, &last_id);
|
||||
|
||||
/* We can remove the entire node id its last ID < 'id' */
|
||||
remove_node = streamCompareID(&last_id, id) < 0;
|
||||
node_eligible_for_remove = streamCompareID(&last_id, id) < 0;
|
||||
}
|
||||
|
||||
if (node_eligible_for_remove && delete_strategy == DELETE_STRATEGY_KEEPREF) {
|
||||
/* With KEEPREF strategy, we can remove the whole node directly since we don't need
|
||||
* to check or clean up consumer group references. */
|
||||
remove_node = 1;
|
||||
}
|
||||
|
||||
if (remove_node) {
|
||||
@@ -785,11 +814,9 @@ int64_t streamTrim(stream *s, streamAddTrimArgs *args) {
|
||||
int64_t seq_delta = lpGetInteger(p);
|
||||
p = lpNext(lp, p); /* Skip ID seq delta */
|
||||
|
||||
streamID currid = {0}; /* For MINID */
|
||||
if (trim_strategy == TRIM_STRATEGY_MINID) {
|
||||
currid.ms = master_id.ms + ms_delta;
|
||||
currid.seq = master_id.seq + seq_delta;
|
||||
}
|
||||
streamID currid = {0};
|
||||
currid.ms = master_id.ms + ms_delta;
|
||||
currid.seq = master_id.seq + seq_delta;
|
||||
|
||||
int stop;
|
||||
if (trim_strategy == TRIM_STRATEGY_MAXLEN) {
|
||||
@@ -813,17 +840,38 @@ int64_t streamTrim(stream *s, streamAddTrimArgs *args) {
|
||||
while(to_skip--) p = lpNext(lp,p); /* Skip the whole entry. */
|
||||
p = lpNext(lp,p); /* Skip the final lp-count field. */
|
||||
|
||||
/* Mark the entry as deleted. */
|
||||
/* Mark the entry as deleted if allowed. */
|
||||
if (!(flags & STREAM_ITEM_FLAG_DELETED)) {
|
||||
intptr_t delta = p - lp;
|
||||
flags |= STREAM_ITEM_FLAG_DELETED;
|
||||
lp = lpReplaceInteger(lp, &pcopy, flags);
|
||||
deleted_from_lp++;
|
||||
s->length--;
|
||||
p = lp + delta;
|
||||
int can_delete = 1;
|
||||
if (delete_strategy == DELETE_STRATEGY_ACKED) {
|
||||
/* Only delete entry that has been acknowledged by all consumer groups. */
|
||||
can_delete = (streamEntryIsReferenced(s, &currid) == 0);
|
||||
} else if (delete_strategy == DELETE_STRATEGY_DELREF) {
|
||||
/* Remove all consumer group references for this entry */
|
||||
streamCleanupEntryCGroupRefs(s, &currid);
|
||||
}
|
||||
|
||||
if (can_delete) {
|
||||
/* Mark the entry as deleted. */
|
||||
intptr_t delta = p - lp;
|
||||
flags |= STREAM_ITEM_FLAG_DELETED;
|
||||
lp = lpReplaceInteger(lp, &pcopy, flags);
|
||||
deleted_from_lp++;
|
||||
s->length--;
|
||||
p = lp + delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
deleted += deleted_from_lp;
|
||||
/* If this node was originally eligible for removal but we couldn't remove it upfront
|
||||
* due to delete strategy constraints, and now we've processed and deleted all entries
|
||||
* in the node, we can finally remove the entire node. */
|
||||
if (node_eligible_for_remove && deleted_from_lp == entries) {
|
||||
lpFree(lp);
|
||||
raxRemove(s->rax,ri.key,ri.key_len,NULL);
|
||||
raxSeek(&ri,">=",ri.key,ri.key_len);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Now we update the entries/deleted counters. */
|
||||
p = lpFirst(lp);
|
||||
@@ -844,6 +892,12 @@ int64_t streamTrim(stream *s, streamAddTrimArgs *args) {
|
||||
/* Update the listpack with the new pointer. */
|
||||
raxInsert(s->rax,ri.key,ri.key_len,lp,NULL);
|
||||
|
||||
/* If the node is eligible for removal but we couldn't remove it due to delete strategy
|
||||
* constraints (we need to check each entry individually), continue to the next node
|
||||
* instead of stopping here. */
|
||||
if (node_eligible_for_remove)
|
||||
continue;
|
||||
|
||||
break; /* If we are here, there was enough to delete in the current
|
||||
node, so no need to go to the next node. */
|
||||
}
|
||||
@@ -866,7 +920,8 @@ int64_t streamTrimByLength(stream *s, long long maxlen, int approx) {
|
||||
.trim_strategy = TRIM_STRATEGY_MAXLEN,
|
||||
.approx_trim = approx,
|
||||
.limit = approx ? 100 * server.stream_node_max_entries : 0,
|
||||
.maxlen = maxlen
|
||||
.maxlen = maxlen,
|
||||
.delete_strategy = DELETE_STRATEGY_KEEPREF
|
||||
};
|
||||
return streamTrim(s, &args);
|
||||
}
|
||||
@@ -877,7 +932,8 @@ int64_t streamTrimByID(stream *s, streamID minid, int approx) {
|
||||
.trim_strategy = TRIM_STRATEGY_MINID,
|
||||
.approx_trim = approx,
|
||||
.limit = approx ? 100 * server.stream_node_max_entries : 0,
|
||||
.minid = minid
|
||||
.minid = minid,
|
||||
.delete_strategy = DELETE_STRATEGY_KEEPREF
|
||||
};
|
||||
return streamTrim(s, &args);
|
||||
}
|
||||
@@ -891,6 +947,7 @@ int64_t streamTrimByID(stream *s, streamID minid, int approx) {
|
||||
static int streamParseAddOrTrimArgsOrReply(client *c, streamAddTrimArgs *args, int xadd) {
|
||||
/* Initialize arguments to defaults */
|
||||
memset(args, 0, sizeof(*args));
|
||||
args->delete_strategy = DELETE_STRATEGY_NONE;
|
||||
|
||||
/* Parse options. */
|
||||
int i = 2; /* This is the first argument position where we could
|
||||
@@ -964,6 +1021,12 @@ static int streamParseAddOrTrimArgsOrReply(client *c, streamAddTrimArgs *args, i
|
||||
}
|
||||
limit_given = 1;
|
||||
i++;
|
||||
} else if (!strcasecmp(opt,"keepref") && args->delete_strategy == DELETE_STRATEGY_NONE) {
|
||||
args->delete_strategy = DELETE_STRATEGY_KEEPREF;
|
||||
} else if (!strcasecmp(opt,"delref") && args->delete_strategy == DELETE_STRATEGY_NONE) {
|
||||
args->delete_strategy = DELETE_STRATEGY_DELREF;
|
||||
} else if (!strcasecmp(opt,"acked") && args->delete_strategy == DELETE_STRATEGY_NONE) {
|
||||
args->delete_strategy = DELETE_STRATEGY_ACKED;
|
||||
} else if (xadd && !strcasecmp(opt,"nomkstream")) {
|
||||
args->no_mkstream = 1;
|
||||
} else if (xadd) {
|
||||
@@ -1019,9 +1082,66 @@ static int streamParseAddOrTrimArgsOrReply(client *c, streamAddTrimArgs *args, i
|
||||
}
|
||||
}
|
||||
|
||||
/* Set default consumer group reference handling to KEEPREF if none was specified */
|
||||
if (args->delete_strategy == DELETE_STRATEGY_NONE)
|
||||
args->delete_strategy = DELETE_STRATEGY_KEEPREF;
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
static int streamParseAckDelArgsOrReply(client *c, int start_pos, streamAckDelArgs *args) {
|
||||
/* Initialize arguments to defaults */
|
||||
memset(args, 0, sizeof(*args));
|
||||
args->startidx = -1;
|
||||
args->delete_strategy = DELETE_STRATEGY_NONE;
|
||||
|
||||
/* Parse command options */
|
||||
int j = start_pos;
|
||||
while (j < c->argc) {
|
||||
char *opt = c->argv[j]->ptr;
|
||||
if (!strcasecmp(opt, "KEEPREF") && args->delete_strategy == DELETE_STRATEGY_NONE) {
|
||||
args->delete_strategy = DELETE_STRATEGY_KEEPREF;
|
||||
j++;
|
||||
} else if (!strcasecmp(opt, "DELREF") && args->delete_strategy == DELETE_STRATEGY_NONE) {
|
||||
args->delete_strategy = DELETE_STRATEGY_DELREF;
|
||||
j++;
|
||||
} else if (!strcasecmp(opt, "ACKED") && args->delete_strategy == DELETE_STRATEGY_NONE) {
|
||||
args->delete_strategy = DELETE_STRATEGY_ACKED;
|
||||
j++;
|
||||
} else if (!strcasecmp(opt, "IDS") && j+1 < c->argc) {
|
||||
/* Parse the number of IDs */
|
||||
if (getRangeLongFromObjectOrReply(c, c->argv[j+1], 1, LONG_MAX,
|
||||
&args->numids, "Number of IDs must be a positive integer") != C_OK)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Verify that the specified number of IDs matches the actual arguments */
|
||||
if (args->numids > (c->argc - j - 2)) {
|
||||
addReplyError(c, "The `numids` parameter must match the number of arguments");
|
||||
return 0;
|
||||
}
|
||||
|
||||
args->startidx = j + 2; /* Skip "IDS" and numids */
|
||||
j = args->startidx + args->numids;
|
||||
} else {
|
||||
addReplyErrorObject(c,shared.syntaxerr);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (args->startidx == -1) {
|
||||
addReplyError(c, "IDS option is required");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Set default consumer group reference handling to KEEPREF if none was specified */
|
||||
if (args->delete_strategy == DELETE_STRATEGY_NONE)
|
||||
args->delete_strategy = DELETE_STRATEGY_KEEPREF;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Initialize the stream iterator, so that we can call iterating functions
|
||||
* to get the next items. This requires a corresponding streamIteratorStop()
|
||||
* at the end. The 'rev' parameter controls the direction. If it's zero the
|
||||
@@ -1708,7 +1828,7 @@ size_t streamReplyWithRange(client *c, stream *s, streamID *start, streamID *end
|
||||
/* The group's counter may be invalid, so we try to obtain it. */
|
||||
group->entries_read = streamEstimateDistanceFromFirstEverEntry(s,&id);
|
||||
}
|
||||
group->last_id = id;
|
||||
streamUpdateCGroupLastId(s, group, &id);
|
||||
/* In the past, we would only set it when NOACK was specified. And in
|
||||
* #9127, XCLAIM did not propagate entries_read in ACK, which would
|
||||
* cause entries_read to be inconsistent between master and replicas,
|
||||
@@ -1770,6 +1890,8 @@ size_t streamReplyWithRange(client *c, stream *s, streamID *start, streamID *end
|
||||
nack->delivery_count = 1;
|
||||
/* Add the entry in the new consumer local PEL. */
|
||||
raxInsert(consumer->pel,buf,sizeof(buf),nack,NULL);
|
||||
} else if (group_inserted == 1 && consumer_inserted == 1) {
|
||||
nack->cgroup_ref_node = streamLinkCGroupToEntry(s, group, buf);
|
||||
} else if (group_inserted == 1 && consumer_inserted == 0) {
|
||||
serverPanic("NACK half-created. Should not be possible.");
|
||||
}
|
||||
@@ -2467,6 +2589,127 @@ cleanup: /* Cleanup. */
|
||||
* Low level implementation of consumer groups
|
||||
* ----------------------------------------------------------------------- */
|
||||
|
||||
/* Update a consumer group's last_id and handle minimum last_id tracking.
|
||||
* we will recalculate the minimum last_id when needed. */
|
||||
void streamUpdateCGroupLastId(stream *s, streamCG *cg, streamID *id) {
|
||||
/* When a consumer group's last_id is updated, we need to invalidate the cached
|
||||
* minimum last_id in two cases:
|
||||
* 1. If the consumer group's previous last_id equals the minimum last_id.
|
||||
* 2. If the new ID being set is smaller than the current minimum last_id. */
|
||||
if (s->min_cgroup_last_id_valid &&
|
||||
(streamCompareID(&cg->last_id, &s->min_cgroup_last_id) == 0 ||
|
||||
streamCompareID(id, &s->min_cgroup_last_id) < 0))
|
||||
{
|
||||
s->min_cgroup_last_id_valid = 0;
|
||||
}
|
||||
cg->last_id = *id;
|
||||
}
|
||||
|
||||
/* Link a consumer group to a stream entry in the cgroups_ref index.
|
||||
* Returns a pointer to the list node, so that it can be used for future deletion. */
|
||||
listNode *streamLinkCGroupToEntry(stream *s, streamCG *cg, unsigned char *key) {
|
||||
list *cglist;
|
||||
|
||||
if (!s->cgroups_ref)
|
||||
s->cgroups_ref = raxNew();
|
||||
|
||||
/* Try to find the list for this stream ID, create it if it doesn't exist */
|
||||
if (!raxFind(s->cgroups_ref, key, sizeof(streamID), (void**)&cglist)) {
|
||||
cglist = listCreate();
|
||||
serverAssert(raxInsert(s->cgroups_ref, key, sizeof(streamID), cglist, NULL));
|
||||
}
|
||||
|
||||
/* Add the consumer group to the list and return the list node */
|
||||
listAddNodeTail(cglist, cg);
|
||||
return listLast(cglist);
|
||||
}
|
||||
|
||||
/* Unlink a consumer group reference from the entry index for a specific stream ID.
|
||||
* This is called when a message is acknowledged or when a consumer group is deleted. */
|
||||
void streamUnlinkEntryFromCGroupRef(stream *s, streamNACK *na, unsigned char *key) {
|
||||
list *cglist;
|
||||
if (!s->cgroups_ref) return;
|
||||
if (raxFind(s->cgroups_ref, key, sizeof(streamID), (void**)&cglist)) {
|
||||
listDelNode(cglist, na->cgroup_ref_node);
|
||||
|
||||
/* If the list is now empty, remove it from the index. */
|
||||
if (listLength(cglist) == 0) {
|
||||
raxRemove(s->cgroups_ref, key, sizeof(streamID), NULL);
|
||||
listRelease(cglist);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Remove all consumer group references to a specific stream message. */
|
||||
void streamCleanupEntryCGroupRefs(stream *s, streamID *id) {
|
||||
if (!s->cgroups_ref) return;
|
||||
list *cglist;
|
||||
listIter li;
|
||||
listNode *ln;
|
||||
unsigned char buf[sizeof(streamID)];
|
||||
streamEncodeID(buf, id);
|
||||
|
||||
/* If message is not in any consumer group, nothing to do */
|
||||
if (!raxFind(s->cgroups_ref, buf, sizeof(streamID), (void **)&cglist))
|
||||
return;
|
||||
|
||||
listRewind(cglist, &li);
|
||||
while ((ln = listNext(&li))) {
|
||||
streamNACK *nack;
|
||||
streamCG *group = listNodeValue(ln);
|
||||
|
||||
/* Find the message in this consumer group's PEL */
|
||||
serverAssert(raxFind(group->pel, buf, sizeof(buf), (void **)&nack));
|
||||
|
||||
/* Remove from group and consumer PELs */
|
||||
raxRemove(group->pel, buf, sizeof(buf), NULL);
|
||||
raxRemove(nack->consumer->pel, buf, sizeof(buf), NULL);
|
||||
/* Since we're removing all references from the cgroups_ref, we can directly
|
||||
* free the NACK without unlinking it from the cgroups_ref. */
|
||||
streamFreeNACK(nack);
|
||||
}
|
||||
|
||||
raxRemove(s->cgroups_ref, buf, sizeof(streamID), NULL);
|
||||
listRelease(cglist);
|
||||
}
|
||||
|
||||
/* Check if a stream entry is still referenced by any consumer group.
|
||||
*
|
||||
* An entry is considered referenced if:
|
||||
* 1. Its ID is smaller than the minimum last_id of all consumer groups,
|
||||
* which means at least one group hasn't read it yet.
|
||||
* 2. It exists in any consumer group's PEL.
|
||||
*
|
||||
* Returns 1 if the entry is referenced, 0 if it's fully acknowledged by all groups. */
|
||||
int streamEntryIsReferenced(stream *s, streamID *id) {
|
||||
if (!s->cgroups || !raxSize(s->cgroups)) return 0;
|
||||
if (!s->min_cgroup_last_id_valid) {
|
||||
/* If the cached minimum last_id is invalid, we need to recalculate it
|
||||
* by iterating through all consumer groups to find the minimum last_id */
|
||||
s->min_cgroup_last_id_valid = 1;
|
||||
s->min_cgroup_last_id.ms = UINT64_MAX;
|
||||
s->min_cgroup_last_id.seq = UINT64_MAX;
|
||||
raxIterator ri;
|
||||
raxStart(&ri, s->cgroups);
|
||||
raxSeek(&ri, "^", NULL, 0);
|
||||
while (raxNext(&ri)) {
|
||||
streamCG *cg = ri.data;
|
||||
if (streamCompareID(&cg->last_id, &s->min_cgroup_last_id) < 0)
|
||||
s->min_cgroup_last_id = cg->last_id;
|
||||
}
|
||||
raxStop(&ri);
|
||||
}
|
||||
|
||||
/* The consume group doesn't read it. */
|
||||
if (streamCompareID(&s->min_cgroup_last_id, id) < 0)
|
||||
return 1;
|
||||
|
||||
/* Check if the message is in any consumer group's PEL */
|
||||
unsigned char buf[sizeof(streamID)];
|
||||
streamEncodeID(buf, id);
|
||||
return raxFind(s->cgroups_ref, buf, sizeof(streamID), NULL);
|
||||
}
|
||||
|
||||
/* Create a NACK entry setting the delivery count to 1 and the delivery
|
||||
* time to the current time. The NACK consumer will be set to the one
|
||||
* specified as argument of the function. */
|
||||
@@ -2475,6 +2718,7 @@ streamNACK *streamCreateNACK(streamConsumer *consumer) {
|
||||
nack->delivery_time = commandTimeSnapshot();
|
||||
nack->delivery_count = 1;
|
||||
nack->consumer = consumer;
|
||||
nack->cgroup_ref_node = NULL; /* Will be set when added to cgroups_ref */
|
||||
return nack;
|
||||
}
|
||||
|
||||
@@ -2483,6 +2727,13 @@ void streamFreeNACK(streamNACK *na) {
|
||||
zfree(na);
|
||||
}
|
||||
|
||||
/* Free a NACK entry and remove its reference from the cgroups_ref.
|
||||
* This ensures proper cleanup of the consumer group list associated with the message ID. */
|
||||
void streamDestroyNACK(stream *s, streamNACK *na, unsigned char *key) {
|
||||
streamUnlinkEntryFromCGroupRef(s, na, key);
|
||||
zfree(na);
|
||||
}
|
||||
|
||||
/* Generic version of streamFreeNACK. */
|
||||
void streamFreeNACKGeneric(void *na) {
|
||||
streamFreeNACK((streamNACK *)na);
|
||||
@@ -2517,19 +2768,35 @@ streamCG *streamCreateCG(stream *s, char *name, size_t namelen, streamID *id, lo
|
||||
streamCG *cg = zmalloc(sizeof(*cg));
|
||||
cg->pel = raxNew();
|
||||
cg->consumers = raxNew();
|
||||
cg->last_id = *id;
|
||||
cg->last_id.ms = 0;
|
||||
cg->last_id.seq = 0;
|
||||
streamUpdateCGroupLastId(s, cg, id);
|
||||
cg->entries_read = entries_read;
|
||||
raxInsert(s->cgroups,(unsigned char*)name,namelen,cg,NULL);
|
||||
return cg;
|
||||
}
|
||||
|
||||
/* Free a consumer group and all its associated data. */
|
||||
void streamFreeCG(streamCG *cg) {
|
||||
static void streamFreeCG(streamCG *cg) {
|
||||
raxFreeWithCallback(cg->pel, streamFreeNACKGeneric);
|
||||
raxFreeWithCallback(cg->consumers, streamFreeConsumerGeneric);
|
||||
zfree(cg);
|
||||
}
|
||||
|
||||
/* Destroy a consumer group and clean up all associated references. */
|
||||
void streamDestroyCG(stream *s, streamCG *cg) {
|
||||
raxIterator it;
|
||||
raxStart(&it, cg->pel);
|
||||
raxSeek(&it, "^", NULL, 0);
|
||||
while (raxNext(&it)) {
|
||||
streamNACK *nack = it.data;
|
||||
streamUnlinkEntryFromCGroupRef(s, nack, it.key);
|
||||
}
|
||||
raxStop(&it);
|
||||
|
||||
streamFreeCG(cg);
|
||||
}
|
||||
|
||||
/* Generic version of streamFreeCG. */
|
||||
void streamFreeCGGeneric(void *cg) {
|
||||
streamFreeCG((streamCG *)cg);
|
||||
@@ -2577,7 +2844,7 @@ streamConsumer *streamLookupConsumer(streamCG *cg, sds name) {
|
||||
}
|
||||
|
||||
/* Delete the consumer specified in the consumer group 'cg'. */
|
||||
void streamDelConsumer(streamCG *cg, streamConsumer *consumer) {
|
||||
void streamDelConsumer(stream *s, streamCG *cg, streamConsumer *consumer) {
|
||||
/* Iterate all the consumer pending messages, deleting every corresponding
|
||||
* entry from the global entry. */
|
||||
raxIterator ri;
|
||||
@@ -2585,8 +2852,9 @@ void streamDelConsumer(streamCG *cg, streamConsumer *consumer) {
|
||||
raxSeek(&ri,"^",NULL,0);
|
||||
while(raxNext(&ri)) {
|
||||
streamNACK *nack = ri.data;
|
||||
raxRemove(cg->pel,ri.key,ri.key_len,NULL);
|
||||
streamUnlinkEntryFromCGroupRef(s, nack, ri.key);
|
||||
streamFreeNACK(nack);
|
||||
raxRemove(cg->pel,ri.key,ri.key_len,NULL);
|
||||
}
|
||||
raxStop(&ri);
|
||||
|
||||
@@ -2728,7 +2996,7 @@ NULL
|
||||
} else if (streamParseIDOrReply(c,c->argv[4],&id,0) != C_OK) {
|
||||
return;
|
||||
}
|
||||
cg->last_id = id;
|
||||
streamUpdateCGroupLastId(s, cg, &id);
|
||||
cg->entries_read = entries_read;
|
||||
addReply(c,shared.ok);
|
||||
server.dirty++;
|
||||
@@ -2736,7 +3004,7 @@ NULL
|
||||
} else if (!strcasecmp(opt,"DESTROY") && c->argc == 4) {
|
||||
if (cg) {
|
||||
raxRemove(s->cgroups,(unsigned char*)grpname,sdslen(grpname),NULL);
|
||||
streamFreeCG(cg);
|
||||
streamDestroyCG(s, cg);
|
||||
addReply(c,shared.cone);
|
||||
server.dirty++;
|
||||
notifyKeyspaceEvent(NOTIFY_STREAM,"xgroup-destroy",
|
||||
@@ -2757,7 +3025,7 @@ NULL
|
||||
/* Delete the consumer and returns the number of pending messages
|
||||
* that were yet associated with such a consumer. */
|
||||
pending = raxSize(consumer->pel);
|
||||
streamDelConsumer(cg,consumer);
|
||||
streamDelConsumer(s,cg,consumer);
|
||||
server.dirty++;
|
||||
notifyKeyspaceEvent(NOTIFY_STREAM,"xgroup-delconsumer",
|
||||
c->argv[2],c->db->id);
|
||||
@@ -2891,7 +3159,7 @@ void xackCommand(client *c) {
|
||||
streamNACK *nack = result;
|
||||
raxRemove(group->pel,buf,sizeof(buf),NULL);
|
||||
raxRemove(nack->consumer->pel,buf,sizeof(buf),NULL);
|
||||
streamFreeNACK(nack);
|
||||
streamDestroyNACK(kv->ptr, nack, buf);
|
||||
acknowledged++;
|
||||
server.dirty++;
|
||||
}
|
||||
@@ -2901,6 +3169,115 @@ cleanup:
|
||||
if (ids != static_ids) zfree(ids);
|
||||
}
|
||||
|
||||
/* Used by xackdelCommand() */
|
||||
typedef enum XAckDelRes {
|
||||
XACKDEL_NO_ID = -1, /* ID not found in PEL. */
|
||||
XACKDEL_DELETED = 1, /* Message acknowledged and deleted. */
|
||||
XACKDEL_STILL_REFERENCED = 2, /* Message acknowledged but not deleted (still referenced). */
|
||||
} XAckDelRes;
|
||||
|
||||
/* XACKDEL <key> <group> [KEEPREF|DELREF|ACKED] [IDS <numids> <id ...>]
|
||||
* Acknowledges messages as processed and deletes them from the stream.
|
||||
*
|
||||
* Returns an array of status codes for each ID, indicating whether it
|
||||
* was deleted, still referenced, or not found. */
|
||||
void xackdelCommand(client *c) {
|
||||
stream *s = NULL;
|
||||
streamCG *group = NULL;
|
||||
kvobj *kv = lookupKeyRead(c->db, c->argv[1]);
|
||||
if (checkType(c, kv, OBJ_STREAM)) return; /* Type error. */
|
||||
|
||||
/* Parse command options */
|
||||
streamAckDelArgs args;
|
||||
if (!streamParseAckDelArgsOrReply(c, 3, &args)) return;
|
||||
|
||||
/* Reply null if the key doesn't exist or the group doesn't exist.*/
|
||||
if (!kv || !(group = streamLookupCG(kv->ptr, c->argv[2]->ptr))) {
|
||||
addReplyArrayLen(c, args.numids);
|
||||
for (int i = 0; i < args.numids; i++)
|
||||
addReplyLongLong(c, XACKDEL_NO_ID);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Start parsing the IDs, so that we abort ASAP if there is a syntax
|
||||
* error: the return value of this command cannot be an error in case
|
||||
* the client successfully acknowledged some messages, so it should be
|
||||
* executed in a "all or nothing" fashion. */
|
||||
streamID static_ids[STREAMID_STATIC_VECTOR_LEN];
|
||||
streamID *ids = static_ids;
|
||||
for (int j = 0; j < args.numids; j++) {
|
||||
if (streamParseStrictIDOrReply(c,c->argv[j+args.startidx],&ids[j],0,NULL) != C_OK)
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
s = kv->ptr;
|
||||
int first_entry = 0;
|
||||
int deleted = 0;
|
||||
addReplyArrayLen(c, args.numids);
|
||||
for (int j = 0; j < args.numids; j++) {
|
||||
int res = XACKDEL_NO_ID;
|
||||
streamID *id = &ids[j];
|
||||
unsigned char buf[sizeof(streamID)];
|
||||
streamEncodeID(buf,id);
|
||||
|
||||
/* Lookup the ID in the group PEL: it will have a reference to the
|
||||
* NACK structure that will have a reference to the consumer, so that
|
||||
* we are able to remove the entry from both PELs. */
|
||||
void *result;
|
||||
if (raxFind(group->pel,buf,sizeof(buf),&result)) {
|
||||
streamNACK *nack = result;
|
||||
raxRemove(group->pel,buf,sizeof(buf),NULL);
|
||||
raxRemove(nack->consumer->pel,buf,sizeof(buf),NULL);
|
||||
streamDestroyNACK(s, nack, buf);
|
||||
server.dirty++;
|
||||
|
||||
int can_delete = 1;
|
||||
if (args.delete_strategy == DELETE_STRATEGY_ACKED) {
|
||||
/* Only delete if acknowledged by all consumer groups */
|
||||
if (streamEntryIsReferenced(s, id))
|
||||
can_delete = 0;
|
||||
} else if (args.delete_strategy == DELETE_STRATEGY_DELREF) {
|
||||
streamCleanupEntryCGroupRefs(s, id);
|
||||
}
|
||||
|
||||
if (can_delete && streamDeleteItem(s,id)) {
|
||||
/* We want to know if the first entry in the stream was deleted
|
||||
* so we can later set the new one. */
|
||||
if (streamCompareID(id,&s->first_id) == 0) {
|
||||
first_entry = 1;
|
||||
}
|
||||
/* Update the stream's maximal tombstone if needed. */
|
||||
if (streamCompareID(id,&s->max_deleted_entry_id) > 0) {
|
||||
s->max_deleted_entry_id = *id;
|
||||
}
|
||||
deleted++;
|
||||
}
|
||||
|
||||
/* If the entry was in the PEL but not found in the stream,
|
||||
* we still consider it successfully deleted. */
|
||||
res = can_delete ? XACKDEL_DELETED : XACKDEL_STILL_REFERENCED;
|
||||
}
|
||||
addReplyLongLong(c, res);
|
||||
}
|
||||
|
||||
/* Update the stream's first ID. */
|
||||
if (deleted) {
|
||||
if (s->length == 0) {
|
||||
s->first_id.ms = 0;
|
||||
s->first_id.seq = 0;
|
||||
} else if (first_entry) {
|
||||
streamGetEdgeID(s,1,1,&s->first_id);
|
||||
}
|
||||
|
||||
/* Propagate the write. */
|
||||
signalModifiedKey(c,c->db,c->argv[1]);
|
||||
notifyKeyspaceEvent(NOTIFY_STREAM,"xdel",c->argv[1],c->db->id);
|
||||
}
|
||||
|
||||
cleanup:
|
||||
if (ids != static_ids) zfree(ids);
|
||||
}
|
||||
|
||||
/* XPENDING <key> <group> [[IDLE <idle>] <start> <stop> <count> [<consumer>]]
|
||||
*
|
||||
* If start and stop are omitted, the command just outputs information about
|
||||
@@ -3232,7 +3609,7 @@ void xclaimCommand(client *c) {
|
||||
}
|
||||
|
||||
if (streamCompareID(&last_id,&group->last_id) > 0) {
|
||||
group->last_id = last_id;
|
||||
streamUpdateCGroupLastId(o->ptr, group, &last_id);
|
||||
propagate_last_id = 1;
|
||||
}
|
||||
|
||||
@@ -3282,7 +3659,7 @@ void xclaimCommand(client *c) {
|
||||
/* Release the NACK */
|
||||
raxRemove(group->pel,buf,sizeof(buf),NULL);
|
||||
raxRemove(nack->consumer->pel,buf,sizeof(buf),NULL);
|
||||
streamFreeNACK(nack);
|
||||
streamDestroyNACK(o->ptr, nack, buf);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -3296,6 +3673,7 @@ void xclaimCommand(client *c) {
|
||||
/* Create the NACK. */
|
||||
nack = streamCreateNACK(NULL);
|
||||
raxInsert(group->pel,buf,sizeof(buf),nack,NULL);
|
||||
nack->cgroup_ref_node = streamLinkCGroupToEntry(o->ptr, group, buf);
|
||||
}
|
||||
|
||||
if (nack != NULL) {
|
||||
@@ -3471,7 +3849,7 @@ void xautoclaimCommand(client *c) {
|
||||
/* Clear this entry from the PEL, it no longer exists */
|
||||
raxRemove(group->pel,ri.key,ri.key_len,NULL);
|
||||
raxRemove(nack->consumer->pel,ri.key,ri.key_len,NULL);
|
||||
streamFreeNACK(nack);
|
||||
streamDestroyNACK(o->ptr, nack, ri.key);
|
||||
/* Remember the ID for later */
|
||||
deleted_ids[deleted_id_num++] = id;
|
||||
raxSeek(&ri,">=",ri.key,ri.key_len);
|
||||
@@ -3608,6 +3986,107 @@ cleanup:
|
||||
if (ids != static_ids) zfree(ids);
|
||||
}
|
||||
|
||||
/* Used by xdelexCommand() */
|
||||
typedef enum XDelexRes {
|
||||
XDELEX_NO_ID = -1, /* ID not found in the stream. */
|
||||
XDELEX_DELETED = 1, /* Message deleted. */
|
||||
XDELEX_STILL_REFERENCED = 2, /* Message not deleted (still referenced). */
|
||||
} XDelexRes;
|
||||
|
||||
/* XDELEX <key> [KEEPREF|DELREF|ACKED] [IDS <numids> <id ...>]
|
||||
*
|
||||
* Removes specified entries from the stream. Returns an array of status codes for
|
||||
* each ID, indicating whether it was deleted, still referenced, or not found. */
|
||||
void xdelexCommand(client *c) {
|
||||
kvobj *kv = lookupKeyWrite(c->db, c->argv[1]);
|
||||
if (checkType(c, kv, OBJ_STREAM)) return;
|
||||
|
||||
/* Parse command options */
|
||||
streamAckDelArgs args;
|
||||
if (!streamParseAckDelArgsOrReply(c, 2, &args)) return;
|
||||
|
||||
/* Non-existing keys and empty stream are the same thing. Reply null if the
|
||||
* key does not exist.*/
|
||||
if (!kv) {
|
||||
addReplyArrayLen(c, args.numids);
|
||||
for (int i = 0; i < args.numids; i++)
|
||||
addReplyLongLong(c, XDELEX_NO_ID);
|
||||
return;
|
||||
}
|
||||
|
||||
/* We need to sanity check the IDs passed to start. Even if not
|
||||
* a big issue, it is not great that the command is only partially
|
||||
* executed because at some point an invalid ID is parsed. */
|
||||
streamID static_ids[STREAMID_STATIC_VECTOR_LEN];
|
||||
streamID *ids = static_ids;
|
||||
if (args.numids > STREAMID_STATIC_VECTOR_LEN)
|
||||
ids = zmalloc(sizeof(streamID)*args.numids);
|
||||
for (int j = 0; j < args.numids; j++) {
|
||||
if (streamParseStrictIDOrReply(c,c->argv[j+args.startidx],&ids[j],0,NULL) != C_OK)
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
stream *s = kv->ptr;
|
||||
int first_entry = 0;
|
||||
int deleted = 0;
|
||||
addReplyArrayLen(c, args.numids);
|
||||
for (int j = 0; j < args.numids; j++) {
|
||||
int res = XDELEX_NO_ID;
|
||||
streamID *id = &ids[j];
|
||||
unsigned char buf[sizeof(streamID)];
|
||||
streamEncodeID(buf,id);
|
||||
|
||||
int can_delete = 1;
|
||||
if (args.delete_strategy == DELETE_STRATEGY_ACKED) {
|
||||
/* Only delete if acknowledged by all consumer groups */
|
||||
if (streamEntryIsReferenced(s, id))
|
||||
can_delete = 0;
|
||||
} else if (args.delete_strategy == DELETE_STRATEGY_DELREF) {
|
||||
streamCleanupEntryCGroupRefs(s, id);
|
||||
}
|
||||
|
||||
if (can_delete) { /* can_delete being true doesn't guarantee the ID exists */
|
||||
if (streamDeleteItem(s,id)) {
|
||||
/* We want to know if the first entry in the stream was deleted
|
||||
* so we can later set the new one. */
|
||||
if (streamCompareID(id,&s->first_id) == 0) {
|
||||
first_entry = 1;
|
||||
}
|
||||
/* Update the stream's maximal tombstone if needed. */
|
||||
if (streamCompareID(id,&s->max_deleted_entry_id) > 0) {
|
||||
s->max_deleted_entry_id = *id;
|
||||
}
|
||||
deleted++;
|
||||
res = XDELEX_DELETED;
|
||||
} else {
|
||||
/* This id doesn't exist. */
|
||||
}
|
||||
} else {
|
||||
res = XDELEX_STILL_REFERENCED;
|
||||
}
|
||||
|
||||
addReplyLongLong(c, res);
|
||||
}
|
||||
|
||||
/* Update the stream's first ID. */
|
||||
if (deleted) {
|
||||
if (s->length == 0) {
|
||||
s->first_id.ms = 0;
|
||||
s->first_id.seq = 0;
|
||||
} else if (first_entry) {
|
||||
streamGetEdgeID(s,1,1,&s->first_id);
|
||||
}
|
||||
|
||||
/* Propagate the write. */
|
||||
signalModifiedKey(c,c->db,c->argv[1]);
|
||||
notifyKeyspaceEvent(NOTIFY_STREAM,"xdel",c->argv[1],c->db->id);
|
||||
server.dirty += deleted;
|
||||
}
|
||||
|
||||
cleanup:
|
||||
if (ids != static_ids) zfree(ids);
|
||||
}
|
||||
|
||||
/* General form: XTRIM <key> [... options ...]
|
||||
*
|
||||
* List of options:
|
||||
@@ -3623,6 +4102,12 @@ cleanup:
|
||||
* count in order to demand approximated trimming
|
||||
* (like XADD MINID option).
|
||||
*
|
||||
* Consumer group reference handling (optional, defaults to KEEPREF):
|
||||
*
|
||||
* KEEPREF -- Keeps existing consumer group references
|
||||
* DELREF -- Clean up all consumer group references
|
||||
* ACKED -- Only delete messages that are acknowledged
|
||||
*
|
||||
* Other options:
|
||||
*
|
||||
* LIMIT <entries> -- The maximum number of entries to trim.
|
||||
|
||||
@@ -34,6 +34,7 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
|
||||
* XADD). */
|
||||
{"6.2.0", "Added the `NOMKSTREAM` option, `MINID` trimming strategy and the `LIMIT` option."},
|
||||
{"7.0.0", "Added support for the `<ms>-*` explicit ID form."},
|
||||
{"8.2.0", "Added the `KEEPREF`, `DELREF` and `ACKED` options."},
|
||||
{0}
|
||||
},
|
||||
.key_specs = (RedisModuleCommandKeySpec[]){
|
||||
@@ -60,6 +61,29 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
|
||||
.since = "6.2.0",
|
||||
.flags = REDISMODULE_CMD_ARG_OPTIONAL
|
||||
},
|
||||
{
|
||||
.name = "condition",
|
||||
.type = REDISMODULE_ARG_TYPE_ONEOF,
|
||||
.flags = REDISMODULE_CMD_ARG_OPTIONAL,
|
||||
.subargs = (RedisModuleCommandArg[]){
|
||||
{
|
||||
.name = "keepref",
|
||||
.type = REDISMODULE_ARG_TYPE_PURE_TOKEN,
|
||||
.token = "KEEPREF"
|
||||
},
|
||||
{
|
||||
.name = "delref",
|
||||
.type = REDISMODULE_ARG_TYPE_PURE_TOKEN,
|
||||
.token = "DELREF"
|
||||
},
|
||||
{
|
||||
.name = "acked",
|
||||
.type = REDISMODULE_ARG_TYPE_PURE_TOKEN,
|
||||
.token = "ACKED"
|
||||
},
|
||||
{0}
|
||||
}
|
||||
},
|
||||
{
|
||||
.name = "trim",
|
||||
.type = REDISMODULE_ARG_TYPE_BLOCK,
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
#
|
||||
# Copyright (c) 2009-Present, Redis Ltd.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Copyright (c) 2024-present, Valkey contributors.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Licensed under your choice of (a) the Redis Source Available License 2.0
|
||||
# (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
|
||||
# GNU Affero General Public License v3 (AGPLv3).
|
||||
#
|
||||
# Portions of this file are available under BSD3 terms; see REDISCONTRIBUTIONS for more information.
|
||||
#
|
||||
|
||||
# Cluster helper functions
|
||||
|
||||
# Check if cluster configuration is consistent.
|
||||
@@ -112,7 +126,7 @@ proc start_cluster {masters replicas options code {slot_allocator continuous_slo
|
||||
|
||||
# Configure the starting of multiple servers. Set cluster node timeout
|
||||
# aggressively since many tests depend on ping/pong messages.
|
||||
set cluster_options [list overrides [list cluster-enabled yes cluster-ping-interval 100 cluster-node-timeout 3000]]
|
||||
set cluster_options [list overrides [list cluster-enabled yes cluster-ping-interval 100 cluster-node-timeout 3000 cluster-slot-stats-enabled yes]]
|
||||
set options [concat $cluster_options $options]
|
||||
|
||||
# Cluster mode only supports a single database, so before executing the tests
|
||||
|
||||
@@ -741,7 +741,7 @@ proc generate_fuzzy_traffic_on_key {key type duration} {
|
||||
set zset_commands {ZADD ZCARD ZCOUNT ZINCRBY ZINTERSTORE ZLEXCOUNT ZPOPMAX ZPOPMIN ZRANGE ZRANGEBYLEX ZRANGEBYSCORE ZRANK ZREM ZREMRANGEBYLEX ZREMRANGEBYRANK ZREMRANGEBYSCORE ZREVRANGE ZREVRANGEBYLEX ZREVRANGEBYSCORE ZREVRANK ZSCAN ZSCORE ZUNIONSTORE ZRANDMEMBER}
|
||||
set list_commands {LINDEX LINSERT LLEN LPOP LPOS LPUSH LPUSHX LRANGE LREM LSET LTRIM RPOP RPOPLPUSH RPUSH RPUSHX}
|
||||
set set_commands {SADD SCARD SDIFF SDIFFSTORE SINTER SINTERSTORE SISMEMBER SMEMBERS SMOVE SPOP SRANDMEMBER SREM SSCAN SUNION SUNIONSTORE}
|
||||
set stream_commands {XACK XADD XCLAIM XDEL XGROUP XINFO XLEN XPENDING XRANGE XREAD XREADGROUP XREVRANGE XTRIM}
|
||||
set stream_commands {XACK XADD XCLAIM XDEL XGROUP XINFO XLEN XPENDING XRANGE XREAD XREADGROUP XREVRANGE XTRIM XDELEX XACKDEL}
|
||||
set vset_commands {VADD VREM}
|
||||
set commands [dict create string $string_commands hash $hash_commands zset $zset_commands list $list_commands set $set_commands stream $stream_commands vectorset $vset_commands]
|
||||
|
||||
|
||||
@@ -0,0 +1,988 @@
|
||||
#
|
||||
# Copyright (c) 2009-Present, Redis Ltd.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Copyright (c) 2024-present, Valkey contributors.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Licensed under your choice of (a) the Redis Source Available License 2.0
|
||||
# (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
|
||||
# GNU Affero General Public License v3 (AGPLv3).
|
||||
#
|
||||
# Portions of this file are available under BSD3 terms; see REDISCONTRIBUTIONS for more information.
|
||||
#
|
||||
|
||||
# Integration tests for CLUSTER SLOT-STATS command.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Helper functions for CLUSTER SLOT-STATS test cases.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Converts array RESP response into a dict.
|
||||
# This is useful for many test cases, where unnecessary nesting is removed.
|
||||
proc convert_array_into_dict {slot_stats} {
|
||||
set res [dict create]
|
||||
foreach slot_stat $slot_stats {
|
||||
# slot_stat is an array of size 2, where 0th index represents (int) slot,
|
||||
# and 1st index represents (map) usage statistics.
|
||||
dict set res [lindex $slot_stat 0] [lindex $slot_stat 1]
|
||||
}
|
||||
return $res
|
||||
}
|
||||
|
||||
proc get_cmdstat_usec {cmd r} {
|
||||
set cmdstatline [cmdrstat $cmd r]
|
||||
regexp "usec=(.*?),usec_per_call=(.*?),rejected_calls=0,failed_calls=0" $cmdstatline -> usec _
|
||||
return $usec
|
||||
}
|
||||
|
||||
proc initialize_expected_slots_dict {} {
|
||||
set expected_slots [dict create]
|
||||
for {set i 0} {$i < 16384} {incr i 1} {
|
||||
dict set expected_slots $i 0
|
||||
}
|
||||
return $expected_slots
|
||||
}
|
||||
|
||||
proc initialize_expected_slots_dict_with_range {start_slot end_slot} {
|
||||
assert {$start_slot <= $end_slot}
|
||||
set expected_slots [dict create]
|
||||
for {set i $start_slot} {$i <= $end_slot} {incr i 1} {
|
||||
dict set expected_slots $i 0
|
||||
}
|
||||
return $expected_slots
|
||||
}
|
||||
|
||||
proc assert_empty_slot_stats {slot_stats metrics_to_assert} {
|
||||
set slot_stats [convert_array_into_dict $slot_stats]
|
||||
dict for {slot stats} $slot_stats {
|
||||
foreach metric_name $metrics_to_assert {
|
||||
set metric_value [dict get $stats $metric_name]
|
||||
assert {$metric_value == 0}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
proc assert_empty_slot_stats_with_exception {slot_stats exception_slots metrics_to_assert} {
|
||||
set slot_stats [convert_array_into_dict $slot_stats]
|
||||
dict for {slot stats} $exception_slots {
|
||||
assert {[dict exists $slot_stats $slot]} ;# slot_stats must contain the expected slots.
|
||||
}
|
||||
dict for {slot stats} $slot_stats {
|
||||
if {[dict exists $exception_slots $slot]} {
|
||||
foreach metric_name $metrics_to_assert {
|
||||
set metric_value [dict get $exception_slots $slot $metric_name]
|
||||
assert {[dict get $stats $metric_name] == $metric_value}
|
||||
}
|
||||
} else {
|
||||
dict for {metric value} $stats {
|
||||
assert {$value == 0}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
proc assert_equal_slot_stats {slot_stats_1 slot_stats_2 deterministic_metrics non_deterministic_metrics} {
|
||||
set slot_stats_1 [convert_array_into_dict $slot_stats_1]
|
||||
set slot_stats_2 [convert_array_into_dict $slot_stats_2]
|
||||
assert {[dict size $slot_stats_1] == [dict size $slot_stats_2]}
|
||||
|
||||
dict for {slot stats_1} $slot_stats_1 {
|
||||
assert {[dict exists $slot_stats_2 $slot]}
|
||||
set stats_2 [dict get $slot_stats_2 $slot]
|
||||
|
||||
# For deterministic metrics, we assert their equality.
|
||||
foreach metric $deterministic_metrics {
|
||||
assert {[dict get $stats_1 $metric] == [dict get $stats_2 $metric]}
|
||||
}
|
||||
# For non-deterministic metrics, we assert their non-zeroness as a best-effort.
|
||||
foreach metric $non_deterministic_metrics {
|
||||
assert {([dict get $stats_1 $metric] == 0 && [dict get $stats_2 $metric] == 0) || \
|
||||
([dict get $stats_1 $metric] != 0 && [dict get $stats_2 $metric] != 0)}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
proc assert_all_slots_have_been_seen {expected_slots} {
|
||||
dict for {k v} $expected_slots {
|
||||
assert {$v == 1}
|
||||
}
|
||||
}
|
||||
|
||||
proc assert_slot_visibility {slot_stats expected_slots} {
|
||||
set slot_stats [convert_array_into_dict $slot_stats]
|
||||
dict for {slot _} $slot_stats {
|
||||
assert {[dict exists $expected_slots $slot]}
|
||||
dict set expected_slots $slot 1
|
||||
}
|
||||
|
||||
assert_all_slots_have_been_seen $expected_slots
|
||||
}
|
||||
|
||||
proc assert_slot_stats_monotonic_order {slot_stats orderby is_desc} {
|
||||
# For Tcl dict, the order of iteration is the order in which the keys were inserted into the dictionary
|
||||
# Thus, the response ordering is preserved upon calling 'convert_array_into_dict()'.
|
||||
# Source: https://www.tcl.tk/man/tcl8.6.11/TclCmd/dict.htm
|
||||
set slot_stats [convert_array_into_dict $slot_stats]
|
||||
set prev_metric -1
|
||||
dict for {_ stats} $slot_stats {
|
||||
set curr_metric [dict get $stats $orderby]
|
||||
if {$prev_metric != -1} {
|
||||
if {$is_desc == 1} {
|
||||
assert {$prev_metric >= $curr_metric}
|
||||
} else {
|
||||
assert {$prev_metric <= $curr_metric}
|
||||
}
|
||||
}
|
||||
set prev_metric $curr_metric
|
||||
}
|
||||
}
|
||||
|
||||
proc assert_slot_stats_monotonic_descent {slot_stats orderby} {
|
||||
assert_slot_stats_monotonic_order $slot_stats $orderby 1
|
||||
}
|
||||
|
||||
proc assert_slot_stats_monotonic_ascent {slot_stats orderby} {
|
||||
assert_slot_stats_monotonic_order $slot_stats $orderby 0
|
||||
}
|
||||
|
||||
proc wait_for_replica_key_exists {key key_count} {
|
||||
wait_for_condition 1000 50 {
|
||||
[R 1 exists $key] eq "$key_count"
|
||||
} else {
|
||||
fail "Test key was not replicated"
|
||||
}
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Test cases for CLUSTER SLOT-STATS cpu-usec metric correctness.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
start_cluster 1 0 {tags {external:skip cluster} overrides {cluster-slot-stats-enabled yes}} {
|
||||
|
||||
# Define shared variables.
|
||||
set key "FOO"
|
||||
set key_slot [R 0 cluster keyslot $key]
|
||||
set key_secondary "FOO2"
|
||||
set key_secondary_slot [R 0 cluster keyslot $key_secondary]
|
||||
set metrics_to_assert [list cpu-usec]
|
||||
|
||||
test "CLUSTER SLOT-STATS cpu-usec reset upon CONFIG RESETSTAT." {
|
||||
R 0 SET $key VALUE
|
||||
R 0 DEL $key
|
||||
R 0 CONFIG RESETSTAT
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
assert_empty_slot_stats $slot_stats $metrics_to_assert
|
||||
}
|
||||
R 0 CONFIG RESETSTAT
|
||||
R 0 FLUSHALL
|
||||
|
||||
test "CLUSTER SLOT-STATS cpu-usec reset upon slot migration." {
|
||||
R 0 SET $key VALUE
|
||||
|
||||
R 0 CLUSTER DELSLOTS $key_slot
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
assert_empty_slot_stats $slot_stats $metrics_to_assert
|
||||
|
||||
R 0 CLUSTER ADDSLOTS $key_slot
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
assert_empty_slot_stats $slot_stats $metrics_to_assert
|
||||
}
|
||||
R 0 CONFIG RESETSTAT
|
||||
R 0 FLUSHALL
|
||||
|
||||
test "CLUSTER SLOT-STATS cpu-usec for non-slot specific commands." {
|
||||
R 0 INFO
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
assert_empty_slot_stats $slot_stats $metrics_to_assert
|
||||
}
|
||||
R 0 CONFIG RESETSTAT
|
||||
R 0 FLUSHALL
|
||||
|
||||
test "CLUSTER SLOT-STATS cpu-usec for slot specific commands." {
|
||||
R 0 SET $key VALUE
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
set usec [get_cmdstat_usec set r]
|
||||
set expected_slot_stats [
|
||||
dict create $key_slot [
|
||||
dict create cpu-usec $usec
|
||||
]
|
||||
]
|
||||
assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
|
||||
}
|
||||
R 0 CONFIG RESETSTAT
|
||||
R 0 FLUSHALL
|
||||
|
||||
test "CLUSTER SLOT-STATS cpu-usec for blocking commands, unblocked on keyspace update." {
|
||||
# Blocking command with no timeout. Only keyspace update can unblock this client.
|
||||
set rd [redis_deferring_client]
|
||||
$rd BLPOP $key 0
|
||||
wait_for_blocked_clients_count 1
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
# When the client is blocked, no accumulation is made. This behaviour is identical to INFO COMMANDSTATS.
|
||||
assert_empty_slot_stats $slot_stats $metrics_to_assert
|
||||
|
||||
# Unblocking command.
|
||||
R 0 LPUSH $key value
|
||||
wait_for_blocked_clients_count 0
|
||||
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
set lpush_usec [get_cmdstat_usec lpush r]
|
||||
set blpop_usec [get_cmdstat_usec blpop r]
|
||||
|
||||
# Assert that both blocking and non-blocking command times have been accumulated.
|
||||
set expected_slot_stats [
|
||||
dict create $key_slot [
|
||||
dict create cpu-usec [expr $lpush_usec + $blpop_usec]
|
||||
]
|
||||
]
|
||||
assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
|
||||
}
|
||||
R 0 CONFIG RESETSTAT
|
||||
R 0 FLUSHALL
|
||||
|
||||
test "CLUSTER SLOT-STATS cpu-usec for blocking commands, unblocked on timeout." {
|
||||
# Blocking command with 0.5 seconds timeout.
|
||||
set rd [redis_deferring_client]
|
||||
$rd BLPOP $key 0.5
|
||||
|
||||
# Confirm that the client is blocked, then unblocked within 1 second.
|
||||
wait_for_blocked_clients_count 1
|
||||
wait_for_blocked_clients_count 0
|
||||
|
||||
# Assert that the blocking command time has been accumulated.
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
set blpop_usec [get_cmdstat_usec blpop r]
|
||||
set expected_slot_stats [
|
||||
dict create $key_slot [
|
||||
dict create cpu-usec $blpop_usec
|
||||
]
|
||||
]
|
||||
assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
|
||||
}
|
||||
R 0 CONFIG RESETSTAT
|
||||
R 0 FLUSHALL
|
||||
|
||||
test "CLUSTER SLOT-STATS cpu-usec for transactions." {
|
||||
set r1 [redis_client]
|
||||
$r1 MULTI
|
||||
$r1 SET $key value
|
||||
$r1 GET $key
|
||||
|
||||
# CPU metric is not accumulated until EXEC is reached. This behaviour is identical to INFO COMMANDSTATS.
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
assert_empty_slot_stats $slot_stats $metrics_to_assert
|
||||
|
||||
# Execute transaction, and assert that all nested command times have been accumulated.
|
||||
$r1 EXEC
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
set exec_usec [get_cmdstat_usec exec r]
|
||||
set expected_slot_stats [
|
||||
dict create $key_slot [
|
||||
dict create cpu-usec $exec_usec
|
||||
]
|
||||
]
|
||||
assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
|
||||
}
|
||||
R 0 CONFIG RESETSTAT
|
||||
R 0 FLUSHALL
|
||||
|
||||
test "CLUSTER SLOT-STATS cpu-usec for lua-scripts, without cross-slot keys." {
|
||||
r eval [format "#!lua
|
||||
redis.call('set', '%s', 'bar'); redis.call('get', '%s')" $key $key] 0
|
||||
|
||||
set eval_usec [get_cmdstat_usec eval r]
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
|
||||
set expected_slot_stats [
|
||||
dict create $key_slot [
|
||||
dict create cpu-usec $eval_usec
|
||||
]
|
||||
]
|
||||
assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
|
||||
}
|
||||
R 0 CONFIG RESETSTAT
|
||||
R 0 FLUSHALL
|
||||
|
||||
test "CLUSTER SLOT-STATS cpu-usec for lua-scripts, with cross-slot keys." {
|
||||
r eval [format "#!lua flags=allow-cross-slot-keys
|
||||
redis.call('set', '%s', 'bar'); redis.call('get', '%s');
|
||||
" $key $key_secondary] 0
|
||||
|
||||
# For cross-slot, we do not accumulate at all.
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
assert_empty_slot_stats $slot_stats $metrics_to_assert
|
||||
}
|
||||
R 0 CONFIG RESETSTAT
|
||||
R 0 FLUSHALL
|
||||
|
||||
test "CLUSTER SLOT-STATS cpu-usec for functions, without cross-slot keys." {
|
||||
set function_str [format "#!lua name=f1
|
||||
redis.register_function{
|
||||
function_name='f1',
|
||||
callback=function() redis.call('set', '%s', '1') redis.call('get', '%s') end
|
||||
}" $key $key]
|
||||
r function load replace $function_str
|
||||
r fcall f1 0
|
||||
|
||||
set fcall_usec [get_cmdstat_usec fcall r]
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
|
||||
set expected_slot_stats [
|
||||
dict create $key_slot [
|
||||
dict create cpu-usec $fcall_usec
|
||||
]
|
||||
]
|
||||
assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
|
||||
}
|
||||
R 0 CONFIG RESETSTAT
|
||||
R 0 FLUSHALL
|
||||
|
||||
test "CLUSTER SLOT-STATS cpu-usec for functions, with cross-slot keys." {
|
||||
set function_str [format "#!lua name=f1
|
||||
redis.register_function{
|
||||
function_name='f1',
|
||||
callback=function() redis.call('set', '%s', '1') redis.call('get', '%s') end,
|
||||
flags={'allow-cross-slot-keys'}
|
||||
}" $key $key_secondary]
|
||||
r function load replace $function_str
|
||||
r fcall f1 0
|
||||
|
||||
# For cross-slot, we do not accumulate at all.
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
assert_empty_slot_stats $slot_stats $metrics_to_assert
|
||||
}
|
||||
R 0 CONFIG RESETSTAT
|
||||
R 0 FLUSHALL
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Test cases for CLUSTER SLOT-STATS network-bytes-in.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
start_cluster 1 0 {tags {external:skip cluster} overrides {cluster-slot-stats-enabled yes}} {
|
||||
|
||||
# Define shared variables.
|
||||
set key "key"
|
||||
set key_slot [R 0 cluster keyslot $key]
|
||||
set metrics_to_assert [list network-bytes-in]
|
||||
|
||||
test "CLUSTER SLOT-STATS network-bytes-in, multi bulk buffer processing." {
|
||||
# *3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n --> 33 bytes.
|
||||
R 0 SET $key value
|
||||
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
set expected_slot_stats [
|
||||
dict create $key_slot [
|
||||
dict create network-bytes-in 33
|
||||
]
|
||||
]
|
||||
assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
|
||||
}
|
||||
R 0 CONFIG RESETSTAT
|
||||
R 0 FLUSHALL
|
||||
|
||||
test "CLUSTER SLOT-STATS network-bytes-in, in-line buffer processing." {
|
||||
set rd [redis_deferring_client]
|
||||
# SET key value\r\n --> 15 bytes.
|
||||
$rd write "SET $key value\r\n"
|
||||
$rd flush
|
||||
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
set expected_slot_stats [
|
||||
dict create $key_slot [
|
||||
dict create network-bytes-in 15
|
||||
]
|
||||
]
|
||||
|
||||
assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
|
||||
}
|
||||
R 0 CONFIG RESETSTAT
|
||||
R 0 FLUSHALL
|
||||
|
||||
test "CLUSTER SLOT-STATS network-bytes-in, blocking command." {
|
||||
set rd [redis_deferring_client]
|
||||
# *3\r\n$5\r\nblpop\r\n$3\r\nkey\r\n$1\r\n0\r\n --> 31 bytes.
|
||||
$rd BLPOP $key 0
|
||||
wait_for_blocked_clients_count 1
|
||||
|
||||
# Slot-stats must be empty here, as the client is yet to be unblocked.
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
assert_empty_slot_stats $slot_stats $metrics_to_assert
|
||||
|
||||
# *3\r\n$5\r\nlpush\r\n$3\r\nkey\r\n$5\r\nvalue\r\n --> 35 bytes.
|
||||
R 0 LPUSH $key value
|
||||
wait_for_blocked_clients_count 0
|
||||
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
set expected_slot_stats [
|
||||
dict create $key_slot [
|
||||
dict create network-bytes-in 66 ;# 31 + 35 bytes.
|
||||
]
|
||||
]
|
||||
|
||||
assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
|
||||
}
|
||||
R 0 CONFIG RESETSTAT
|
||||
R 0 FLUSHALL
|
||||
|
||||
test "CLUSTER SLOT-STATS network-bytes-in, multi-exec transaction." {
|
||||
set r [redis_client]
|
||||
# *1\r\n$5\r\nmulti\r\n --> 15 bytes.
|
||||
$r MULTI
|
||||
# *3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n --> 33 bytes.
|
||||
assert {[$r SET $key value] eq {QUEUED}}
|
||||
# *1\r\n$4\r\nexec\r\n --> 14 bytes.
|
||||
assert {[$r EXEC] eq {OK}}
|
||||
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
set expected_slot_stats [
|
||||
dict create $key_slot [
|
||||
dict create network-bytes-in 62 ;# 15 + 33 + 14 bytes.
|
||||
]
|
||||
]
|
||||
|
||||
assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
|
||||
}
|
||||
R 0 CONFIG RESETSTAT
|
||||
R 0 FLUSHALL
|
||||
|
||||
test "CLUSTER SLOT-STATS network-bytes-in, non slot specific command." {
|
||||
R 0 INFO
|
||||
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
assert_empty_slot_stats $slot_stats $metrics_to_assert
|
||||
}
|
||||
R 0 CONFIG RESETSTAT
|
||||
R 0 FLUSHALL
|
||||
|
||||
test "CLUSTER SLOT-STATS network-bytes-in, pub/sub." {
|
||||
# PUB/SUB does not get accumulated at per-slot basis,
|
||||
# as it is cluster-wide and is not slot specific.
|
||||
set rd [redis_deferring_client]
|
||||
$rd subscribe channel
|
||||
R 0 publish channel message
|
||||
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
assert_empty_slot_stats $slot_stats $metrics_to_assert
|
||||
}
|
||||
R 0 CONFIG RESETSTAT
|
||||
R 0 FLUSHALL
|
||||
}
|
||||
|
||||
start_cluster 1 1 {tags {external:skip cluster} overrides {cluster-slot-stats-enabled yes}} {
|
||||
set channel "channel"
|
||||
set key_slot [R 0 cluster keyslot $channel]
|
||||
set metrics_to_assert [list network-bytes-in]
|
||||
|
||||
# Setup replication.
|
||||
assert {[s -1 role] eq {slave}}
|
||||
wait_for_condition 1000 50 {
|
||||
[s -1 master_link_status] eq {up}
|
||||
} else {
|
||||
fail "Instance #1 master link status is not up"
|
||||
}
|
||||
R 1 readonly
|
||||
|
||||
test "CLUSTER SLOT-STATS network-bytes-in, sharded pub/sub." {
|
||||
set slot [R 0 cluster keyslot $channel]
|
||||
set primary [Rn 0]
|
||||
set replica [Rn 1]
|
||||
set replica_subcriber [redis_deferring_client -1]
|
||||
$replica_subcriber SSUBSCRIBE $channel
|
||||
# *2\r\n$10\r\nssubscribe\r\n$7\r\nchannel\r\n --> 34 bytes.
|
||||
$primary SPUBLISH $channel hello
|
||||
# *3\r\n$8\r\nspublish\r\n$7\r\nchannel\r\n$5\r\nhello\r\n --> 42 bytes.
|
||||
|
||||
set slot_stats [$primary CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
set expected_slot_stats [
|
||||
dict create $key_slot [
|
||||
dict create network-bytes-in 42
|
||||
]
|
||||
]
|
||||
assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
|
||||
|
||||
set slot_stats [$replica CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
set expected_slot_stats [
|
||||
dict create $key_slot [
|
||||
dict create network-bytes-in 34
|
||||
]
|
||||
]
|
||||
assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
|
||||
}
|
||||
R 0 CONFIG RESETSTAT
|
||||
R 0 FLUSHALL
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Test cases for CLUSTER SLOT-STATS network-bytes-out correctness.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
start_cluster 1 0 {tags {external:skip cluster}} {
|
||||
# Define shared variables.
|
||||
set key "FOO"
|
||||
set key_slot [R 0 cluster keyslot $key]
|
||||
set expected_slots_to_key_count [dict create $key_slot 1]
|
||||
set metrics_to_assert [list network-bytes-out]
|
||||
R 0 CONFIG SET cluster-slot-stats-enabled yes
|
||||
|
||||
test "CLUSTER SLOT-STATS network-bytes-out, for non-slot specific commands." {
|
||||
R 0 INFO
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
assert_empty_slot_stats $slot_stats $metrics_to_assert
|
||||
}
|
||||
R 0 CONFIG RESETSTAT
|
||||
R 0 FLUSHALL
|
||||
|
||||
test "CLUSTER SLOT-STATS network-bytes-out, for slot specific commands." {
|
||||
R 0 SET $key value
|
||||
# +OK\r\n --> 5 bytes
|
||||
|
||||
set expected_slot_stats [
|
||||
dict create $key_slot [
|
||||
dict create network-bytes-out 5
|
||||
]
|
||||
]
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
|
||||
}
|
||||
R 0 CONFIG RESETSTAT
|
||||
R 0 FLUSHALL
|
||||
|
||||
test "CLUSTER SLOT-STATS network-bytes-out, blocking commands." {
|
||||
set rd [redis_deferring_client]
|
||||
$rd BLPOP $key 0
|
||||
wait_for_blocked_clients_count 1
|
||||
|
||||
# Assert empty slot stats here, since COB is yet to be flushed due to the block.
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
assert_empty_slot_stats $slot_stats $metrics_to_assert
|
||||
|
||||
# Unblock the command.
|
||||
# LPUSH client) :1\r\n --> 4 bytes.
|
||||
# BLPOP client) *2\r\n$3\r\nkey\r\n$5\r\nvalue\r\n --> 24 bytes, upon unblocking.
|
||||
R 0 LPUSH $key value
|
||||
wait_for_blocked_clients_count 0
|
||||
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
set expected_slot_stats [
|
||||
dict create $key_slot [
|
||||
dict create network-bytes-out 28 ;# 4 + 24 bytes.
|
||||
]
|
||||
]
|
||||
assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
|
||||
}
|
||||
R 0 CONFIG RESETSTAT
|
||||
R 0 FLUSHALL
|
||||
}
|
||||
|
||||
start_cluster 1 1 {tags {external:skip cluster}} {
|
||||
|
||||
# Define shared variables.
|
||||
set key "FOO"
|
||||
set key_slot [R 0 CLUSTER KEYSLOT $key]
|
||||
set metrics_to_assert [list network-bytes-out]
|
||||
R 0 CONFIG SET cluster-slot-stats-enabled yes
|
||||
|
||||
# Setup replication.
|
||||
assert {[s -1 role] eq {slave}}
|
||||
wait_for_condition 1000 50 {
|
||||
[s -1 master_link_status] eq {up}
|
||||
} else {
|
||||
fail "Instance #1 master link status is not up"
|
||||
}
|
||||
R 1 readonly
|
||||
|
||||
test "CLUSTER SLOT-STATS network-bytes-out, replication stream egress." {
|
||||
assert_equal [R 0 SET $key VALUE] {OK}
|
||||
# Local client) +OK\r\n --> 5 bytes.
|
||||
# Replication stream) *3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n --> 33 bytes.
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
set expected_slot_stats [
|
||||
dict create $key_slot [
|
||||
dict create network-bytes-out 38 ;# 5 + 33 bytes.
|
||||
]
|
||||
]
|
||||
assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
|
||||
}
|
||||
}
|
||||
|
||||
start_cluster 1 1 {tags {external:skip cluster}} {
|
||||
|
||||
# Define shared variables.
|
||||
set channel "channel"
|
||||
set key_slot [R 0 cluster keyslot $channel]
|
||||
set channel_secondary "channel2"
|
||||
set key_slot_secondary [R 0 cluster keyslot $channel_secondary]
|
||||
set metrics_to_assert [list network-bytes-out]
|
||||
R 0 CONFIG SET cluster-slot-stats-enabled yes
|
||||
|
||||
test "CLUSTER SLOT-STATS network-bytes-out, sharded pub/sub, single channel." {
|
||||
set slot [R 0 cluster keyslot $channel]
|
||||
set publisher [Rn 0]
|
||||
set subscriber [redis_client]
|
||||
set replica [redis_deferring_client -1]
|
||||
|
||||
# Subscriber client) *3\r\n$10\r\nssubscribe\r\n$7\r\nchannel\r\n:1\r\n --> 38 bytes
|
||||
$subscriber SSUBSCRIBE $channel
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
set expected_slot_stats [
|
||||
dict create $key_slot [
|
||||
dict create network-bytes-out 38
|
||||
]
|
||||
]
|
||||
R 0 CONFIG RESETSTAT
|
||||
|
||||
# Publisher client) :1\r\n --> 4 bytes.
|
||||
# Subscriber client) *3\r\n$8\r\nsmessage\r\n$7\r\nchannel\r\n$5\r\nhello\r\n --> 42 bytes.
|
||||
assert_equal 1 [$publisher SPUBLISH $channel hello]
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
set expected_slot_stats [
|
||||
dict create $key_slot [
|
||||
dict create network-bytes-out 46 ;# 4 + 42 bytes.
|
||||
]
|
||||
]
|
||||
assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
|
||||
}
|
||||
$subscriber QUIT
|
||||
R 0 FLUSHALL
|
||||
R 0 CONFIG RESETSTAT
|
||||
|
||||
test "CLUSTER SLOT-STATS network-bytes-out, sharded pub/sub, cross-slot channels." {
|
||||
set slot [R 0 cluster keyslot $channel]
|
||||
set publisher [Rn 0]
|
||||
set subscriber [redis_client]
|
||||
set replica [redis_deferring_client -1]
|
||||
|
||||
# Stack multi-slot subscriptions against a single client.
|
||||
# For primary channel;
|
||||
# Subscriber client) *3\r\n$10\r\nssubscribe\r\n$7\r\nchannel\r\n:1\r\n --> 38 bytes
|
||||
# For secondary channel;
|
||||
# Subscriber client) *3\r\n$10\r\nssubscribe\r\n$8\r\nchannel2\r\n:1\r\n --> 39 bytes
|
||||
$subscriber SSUBSCRIBE $channel
|
||||
$subscriber SSUBSCRIBE $channel_secondary
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
set expected_slot_stats [
|
||||
dict create \
|
||||
$key_slot [ \
|
||||
dict create network-bytes-out 38
|
||||
] \
|
||||
$key_slot_secondary [ \
|
||||
dict create network-bytes-out 39
|
||||
]
|
||||
]
|
||||
R 0 CONFIG RESETSTAT
|
||||
|
||||
# For primary channel;
|
||||
# Publisher client) :1\r\n --> 4 bytes.
|
||||
# Subscriber client) *3\r\n$8\r\nsmessage\r\n$7\r\nchannel\r\n$5\r\nhello\r\n --> 42 bytes.
|
||||
# For secondary channel;
|
||||
# Publisher client) :1\r\n --> 4 bytes.
|
||||
# Subscriber client) *3\r\n$8\r\nsmessage\r\n$8\r\nchannel2\r\n$5\r\nhello\r\n --> 43 bytes.
|
||||
assert_equal 1 [$publisher SPUBLISH $channel hello]
|
||||
assert_equal 1 [$publisher SPUBLISH $channel_secondary hello]
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
set expected_slot_stats [
|
||||
dict create \
|
||||
$key_slot [ \
|
||||
dict create network-bytes-out 46 ;# 4 + 42 bytes.
|
||||
] \
|
||||
$key_slot_secondary [ \
|
||||
dict create network-bytes-out 47 ;# 4 + 43 bytes.
|
||||
]
|
||||
]
|
||||
assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
|
||||
}
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Test cases for CLUSTER SLOT-STATS key-count metric correctness.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
start_cluster 1 0 {tags {external:skip cluster} overrides {cluster-slot-stats-enabled yes}} {
|
||||
|
||||
# Define shared variables.
|
||||
set key "FOO"
|
||||
set key_slot [R 0 cluster keyslot $key]
|
||||
set metrics_to_assert [list key-count]
|
||||
set expected_slot_stats [
|
||||
dict create $key_slot [
|
||||
dict create key-count 1
|
||||
]
|
||||
]
|
||||
|
||||
test "CLUSTER SLOT-STATS contains default value upon redis-server startup" {
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
assert_empty_slot_stats $slot_stats $metrics_to_assert
|
||||
}
|
||||
|
||||
test "CLUSTER SLOT-STATS contains correct metrics upon key introduction" {
|
||||
R 0 SET $key TEST
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
|
||||
}
|
||||
|
||||
test "CLUSTER SLOT-STATS contains correct metrics upon key mutation" {
|
||||
R 0 SET $key NEW_VALUE
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
|
||||
}
|
||||
|
||||
test "CLUSTER SLOT-STATS contains correct metrics upon key deletion" {
|
||||
R 0 DEL $key
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
assert_empty_slot_stats $slot_stats $metrics_to_assert
|
||||
}
|
||||
|
||||
test "CLUSTER SLOT-STATS slot visibility based on slot ownership changes" {
|
||||
R 0 CONFIG SET cluster-require-full-coverage no
|
||||
|
||||
R 0 CLUSTER DELSLOTS $key_slot
|
||||
set expected_slots [initialize_expected_slots_dict]
|
||||
dict unset expected_slots $key_slot
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
assert {[dict size $expected_slots] == 16383}
|
||||
assert_slot_visibility $slot_stats $expected_slots
|
||||
|
||||
R 0 CLUSTER ADDSLOTS $key_slot
|
||||
set expected_slots [initialize_expected_slots_dict]
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
assert {[dict size $expected_slots] == 16384}
|
||||
assert_slot_visibility $slot_stats $expected_slots
|
||||
}
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Test cases for CLUSTER SLOT-STATS SLOTSRANGE sub-argument.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
start_cluster 1 0 {tags {external:skip cluster}} {
|
||||
|
||||
test "CLUSTER SLOT-STATS SLOTSRANGE all slots present" {
|
||||
set start_slot 100
|
||||
set end_slot 102
|
||||
set expected_slots [initialize_expected_slots_dict_with_range $start_slot $end_slot]
|
||||
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE $start_slot $end_slot]
|
||||
assert_slot_visibility $slot_stats $expected_slots
|
||||
}
|
||||
|
||||
test "CLUSTER SLOT-STATS SLOTSRANGE some slots missing" {
|
||||
set start_slot 100
|
||||
set end_slot 102
|
||||
set expected_slots [initialize_expected_slots_dict_with_range $start_slot $end_slot]
|
||||
|
||||
R 0 CLUSTER DELSLOTS $start_slot
|
||||
dict unset expected_slots $start_slot
|
||||
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE $start_slot $end_slot]
|
||||
assert_slot_visibility $slot_stats $expected_slots
|
||||
}
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Test cases for CLUSTER SLOT-STATS ORDERBY sub-argument.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
start_cluster 1 0 {tags {external:skip cluster} overrides {cluster-slot-stats-enabled yes}} {
|
||||
|
||||
set metrics [list "key-count" "cpu-usec" "network-bytes-in" "network-bytes-out"]
|
||||
|
||||
# SET keys for target hashslots, to encourage ordering.
|
||||
set hash_tags [list 0 1 2 3 4]
|
||||
set num_keys 1
|
||||
foreach hash_tag $hash_tags {
|
||||
for {set i 0} {$i < $num_keys} {incr i 1} {
|
||||
R 0 SET "$i{$hash_tag}" VALUE
|
||||
}
|
||||
incr num_keys 1
|
||||
}
|
||||
|
||||
# SET keys for random hashslots, for random noise.
|
||||
set num_keys 0
|
||||
while {$num_keys < 1000} {
|
||||
set random_key [randomInt 16384]
|
||||
R 0 SET $random_key VALUE
|
||||
incr num_keys 1
|
||||
}
|
||||
|
||||
test "CLUSTER SLOT-STATS ORDERBY DESC correct ordering" {
|
||||
foreach orderby $metrics {
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS ORDERBY $orderby DESC]
|
||||
assert_slot_stats_monotonic_descent $slot_stats $orderby
|
||||
}
|
||||
}
|
||||
|
||||
test "CLUSTER SLOT-STATS ORDERBY ASC correct ordering" {
|
||||
foreach orderby $metrics {
|
||||
set slot_stats [R 0 CLUSTER SLOT-STATS ORDERBY $orderby ASC]
|
||||
assert_slot_stats_monotonic_ascent $slot_stats $orderby
|
||||
}
|
||||
}
|
||||
|
||||
test "CLUSTER SLOT-STATS ORDERBY LIMIT correct response pagination, where limit is less than number of assigned slots" {
|
||||
R 0 FLUSHALL SYNC
|
||||
R 0 CONFIG RESETSTAT
|
||||
|
||||
foreach orderby $metrics {
|
||||
set limit 5
|
||||
set slot_stats_desc [R 0 CLUSTER SLOT-STATS ORDERBY $orderby LIMIT $limit DESC]
|
||||
set slot_stats_asc [R 0 CLUSTER SLOT-STATS ORDERBY $orderby LIMIT $limit ASC]
|
||||
set slot_stats_desc_length [llength $slot_stats_desc]
|
||||
set slot_stats_asc_length [llength $slot_stats_asc]
|
||||
assert {$limit == $slot_stats_desc_length && $limit == $slot_stats_asc_length}
|
||||
|
||||
# All slot statistics have been reset to 0, so we will order by slot in ascending order.
|
||||
set expected_slots [dict create 0 0 1 0 2 0 3 0 4 0]
|
||||
assert_slot_visibility $slot_stats_desc $expected_slots
|
||||
assert_slot_visibility $slot_stats_asc $expected_slots
|
||||
}
|
||||
}
|
||||
|
||||
test "CLUSTER SLOT-STATS ORDERBY LIMIT correct response pagination, where limit is greater than number of assigned slots" {
|
||||
R 0 CONFIG SET cluster-require-full-coverage no
|
||||
R 0 FLUSHALL SYNC
|
||||
R 0 CLUSTER FLUSHSLOTS
|
||||
R 0 CLUSTER ADDSLOTS 100 101
|
||||
|
||||
foreach orderby $metrics {
|
||||
set num_assigned_slots 2
|
||||
set limit 5
|
||||
set slot_stats_desc [R 0 CLUSTER SLOT-STATS ORDERBY $orderby LIMIT $limit DESC]
|
||||
set slot_stats_asc [R 0 CLUSTER SLOT-STATS ORDERBY $orderby LIMIT $limit ASC]
|
||||
set slot_stats_desc_length [llength $slot_stats_desc]
|
||||
set slot_stats_asc_length [llength $slot_stats_asc]
|
||||
set expected_response_length [expr min($num_assigned_slots, $limit)]
|
||||
assert {$expected_response_length == $slot_stats_desc_length && $expected_response_length == $slot_stats_asc_length}
|
||||
|
||||
set expected_slots [dict create 100 0 101 0]
|
||||
assert_slot_visibility $slot_stats_desc $expected_slots
|
||||
assert_slot_visibility $slot_stats_asc $expected_slots
|
||||
}
|
||||
}
|
||||
|
||||
test "CLUSTER SLOT-STATS ORDERBY arg sanity check." {
|
||||
# Non-existent argument.
|
||||
assert_error "ERR*" {R 0 CLUSTER SLOT-STATS ORDERBY key-count non-existent-arg}
|
||||
# Negative LIMIT.
|
||||
assert_error "ERR*" {R 0 CLUSTER SLOT-STATS ORDERBY key-count DESC LIMIT -1}
|
||||
# Non-existent ORDERBY metric.
|
||||
assert_error "ERR*" {R 0 CLUSTER SLOT-STATS ORDERBY non-existent-metric}
|
||||
# When cluster-slot-stats-enabled config is disabled, you cannot sort using advanced metrics.
|
||||
R 0 CONFIG SET cluster-slot-stats-enabled no
|
||||
set orderby "cpu-usec"
|
||||
assert_error "ERR*" {R 0 CLUSTER SLOT-STATS ORDERBY $orderby}
|
||||
set orderby "network-bytes-in"
|
||||
assert_error "ERR*" {R 0 CLUSTER SLOT-STATS ORDERBY $orderby}
|
||||
set orderby "network-bytes-out"
|
||||
assert_error "ERR*" {R 0 CLUSTER SLOT-STATS ORDERBY $orderby}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Test cases for CLUSTER SLOT-STATS replication.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
start_cluster 1 1 {tags {external:skip cluster} overrides {cluster-slot-stats-enabled yes}} {
|
||||
|
||||
# Define shared variables.
|
||||
set key "key"
|
||||
set key_slot [R 0 CLUSTER KEYSLOT $key]
|
||||
set primary [Rn 0]
|
||||
set replica [Rn 1]
|
||||
|
||||
# For replication, assertions are split between deterministic and non-deterministic metrics.
|
||||
# * For deterministic metrics, strict equality assertions are made.
|
||||
# * For non-deterministic metrics, non-zeroness assertions are made.
|
||||
# Non-zeroness as in, both primary and replica should either have some value, or no value at all.
|
||||
#
|
||||
# * key-count is deterministic between primary and its replica.
|
||||
# * cpu-usec is non-deterministic between primary and its replica.
|
||||
# * network-bytes-in is deterministic between primary and its replica.
|
||||
# * network-bytes-out will remain empty in the replica, since primary client do not receive replies, unless for replicationSendAck().
|
||||
set deterministic_metrics [list key-count network-bytes-in]
|
||||
set non_deterministic_metrics [list cpu-usec]
|
||||
set empty_metrics [list network-bytes-out]
|
||||
|
||||
# Setup replication.
|
||||
assert {[s -1 role] eq {slave}}
|
||||
wait_for_condition 1000 50 {
|
||||
[s -1 master_link_status] eq {up}
|
||||
} else {
|
||||
fail "Instance #1 master link status is not up"
|
||||
}
|
||||
R 1 readonly
|
||||
|
||||
test "CLUSTER SLOT-STATS metrics replication for new keys" {
|
||||
# *3\r\n$3\r\nset\r\n$3\r\nkey\r\n$5\r\nvalue\r\n --> 33 bytes.
|
||||
R 0 SET $key VALUE
|
||||
|
||||
set expected_slot_stats [
|
||||
dict create $key_slot [
|
||||
dict create key-count 1 network-bytes-in 33
|
||||
]
|
||||
]
|
||||
set slot_stats_master [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
assert_empty_slot_stats_with_exception $slot_stats_master $expected_slot_stats $deterministic_metrics
|
||||
|
||||
wait_for_condition 500 10 {
|
||||
[string match {*calls=1,*} [cmdrstat set $replica]]
|
||||
} else {
|
||||
fail "Replica did not receive the command."
|
||||
}
|
||||
set slot_stats_replica [R 1 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
assert_equal_slot_stats $slot_stats_master $slot_stats_replica $deterministic_metrics $non_deterministic_metrics
|
||||
assert_empty_slot_stats $slot_stats_replica $empty_metrics
|
||||
}
|
||||
R 0 CONFIG RESETSTAT
|
||||
R 1 CONFIG RESETSTAT
|
||||
|
||||
test "CLUSTER SLOT-STATS metrics replication for existing keys" {
|
||||
# *3\r\n$3\r\nset\r\n$3\r\nkey\r\n$13\r\nvalue_updated\r\n --> 42 bytes.
|
||||
R 0 SET $key VALUE_UPDATED
|
||||
|
||||
set expected_slot_stats [
|
||||
dict create $key_slot [
|
||||
dict create key-count 1 network-bytes-in 42
|
||||
]
|
||||
]
|
||||
set slot_stats_master [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
assert_empty_slot_stats_with_exception $slot_stats_master $expected_slot_stats $deterministic_metrics
|
||||
|
||||
wait_for_condition 500 10 {
|
||||
[string match {*calls=1,*} [cmdrstat set $replica]]
|
||||
} else {
|
||||
fail "Replica did not receive the command."
|
||||
}
|
||||
set slot_stats_replica [R 1 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
assert_equal_slot_stats $slot_stats_master $slot_stats_replica $deterministic_metrics $non_deterministic_metrics
|
||||
assert_empty_slot_stats $slot_stats_replica $empty_metrics
|
||||
}
|
||||
R 0 CONFIG RESETSTAT
|
||||
R 1 CONFIG RESETSTAT
|
||||
|
||||
test "CLUSTER SLOT-STATS metrics replication for deleting keys" {
|
||||
# *2\r\n$3\r\ndel\r\n$3\r\nkey\r\n --> 22 bytes.
|
||||
R 0 DEL $key
|
||||
|
||||
set expected_slot_stats [
|
||||
dict create $key_slot [
|
||||
dict create key-count 0 network-bytes-in 22
|
||||
]
|
||||
]
|
||||
set slot_stats_master [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
assert_empty_slot_stats_with_exception $slot_stats_master $expected_slot_stats $deterministic_metrics
|
||||
|
||||
wait_for_condition 500 10 {
|
||||
[string match {*calls=1,*} [cmdrstat del $replica]]
|
||||
} else {
|
||||
fail "Replica did not receive the command."
|
||||
}
|
||||
set slot_stats_replica [R 1 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
|
||||
assert_equal_slot_stats $slot_stats_master $slot_stats_replica $deterministic_metrics $non_deterministic_metrics
|
||||
assert_empty_slot_stats $slot_stats_replica $empty_metrics
|
||||
}
|
||||
R 0 CONFIG RESETSTAT
|
||||
R 1 CONFIG RESETSTAT
|
||||
}
|
||||
@@ -899,3 +899,59 @@ start_cluster 1 0 {tags {"expire external:skip cluster"}} {
|
||||
assert_equal 0 [s 0 expired_time_cap_reached_count]
|
||||
} {} {needs:debug}
|
||||
}
|
||||
|
||||
# Config lazyexpire-nested-arbitrary-keys test body
|
||||
proc conf_le_test {option mode} {
|
||||
r config set lazyexpire-nested-arbitrary-keys $option
|
||||
r debug set-active-expire 0
|
||||
r flushall
|
||||
r script LOAD {return redis.call('SCAN', 0)}
|
||||
|
||||
r set foo bar
|
||||
r pexpire foo 1
|
||||
after 2
|
||||
|
||||
set repl [attach_to_replication_stream]
|
||||
|
||||
# First two conditions hit lazy expire within a 'transaction', meaning
|
||||
# DEL propagation should be blocked if 'lazyexpire-nested-arbitrary-keys' is set.
|
||||
if {$mode == "lua"} {
|
||||
r eval "return redis.call('SCAN', 0)" 0
|
||||
} elseif {$mode == "multi"} {
|
||||
r multi
|
||||
r scan 0
|
||||
r exec
|
||||
} else {
|
||||
r scan 0
|
||||
}
|
||||
|
||||
# dummy command to verify nothing else gets into the replication stream.
|
||||
r set x 1
|
||||
|
||||
if {$option == "no" && $mode != "direct"} {
|
||||
assert_replication_stream $repl {
|
||||
{select *}
|
||||
{set x 1}
|
||||
}
|
||||
} else {
|
||||
assert_replication_stream $repl {
|
||||
{select *}
|
||||
{del foo}
|
||||
{set x 1}
|
||||
}
|
||||
}
|
||||
|
||||
close_replication_stream $repl
|
||||
r script flush
|
||||
assert_equal [r config set lazyexpire-nested-arbitrary-keys yes] {OK}
|
||||
assert_equal [r debug set-active-expire 1] {OK}
|
||||
}
|
||||
|
||||
foreach option {yes no} {
|
||||
foreach mode {direct multi lua} {
|
||||
start_server {tags {"expire"}} {
|
||||
test "Config lazyexpire-nested-arbitrary-keys ($option, $mode)" {
|
||||
conf_le_test $option $mode
|
||||
} {} {needs:debug repl}
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -134,6 +134,24 @@ start_server {tags {"modules usercall"}} {
|
||||
assert_match {*cmd=usercall.call_with_user_flag*} [dict get $entry client-info]
|
||||
}
|
||||
|
||||
test {server not crashing when MONITOR is fed from spawned thread} {
|
||||
set rd [redis_deferring_client]
|
||||
$rd monitor
|
||||
|
||||
r ACL LOG RESET
|
||||
assert_equal [r usercall.reset_user] OK
|
||||
assert_equal [r usercall.add_to_acl "~* &* +@all -set"] OK
|
||||
|
||||
r flushdb
|
||||
r set x x
|
||||
|
||||
# This is enough. This checks that we don't crash inside
|
||||
# updateClientMemUsageAndBucket
|
||||
assert_equal x [r usercall.call_with_user_bg C get x]
|
||||
|
||||
$rd close
|
||||
}
|
||||
|
||||
start_server {tags {"wait aof network external:skip"}} {
|
||||
set slave [srv 0 client]
|
||||
set slave_host [srv 0 host]
|
||||
|
||||
@@ -46,6 +46,11 @@ start_server {
|
||||
test {XREADGROUP will return only new elements} {
|
||||
r XADD mystream * a 1
|
||||
r XADD mystream * b 2
|
||||
|
||||
# Verify XPENDING returns empty results when no messages are in the PEL.
|
||||
assert_equal {0 {} {} {}} [r XPENDING mystream mygroup]
|
||||
assert_equal {} [r XPENDING mystream mygroup - + 10]
|
||||
|
||||
# XREADGROUP should return only the new elements "a 1" "b 1"
|
||||
# and not the element "foo bar" which was pre existing in the
|
||||
# stream (see previous test)
|
||||
@@ -543,6 +548,25 @@ start_server {
|
||||
assert_equal [s total_error_replies] 1
|
||||
}
|
||||
|
||||
test {XGROUP DESTROY removes all consumer group references} {
|
||||
r DEL mystream
|
||||
for {set j 0} {$j < 5} {incr j} {
|
||||
r XADD mystream $j-1 item $j
|
||||
}
|
||||
|
||||
r XGROUP CREATE mystream mygroup 0
|
||||
r XREADGROUP GROUP mygroup consumer1 STREAMS mystream >
|
||||
assert {[lindex [r XPENDING mystream mygroup] 0] == 5}
|
||||
|
||||
# Try to delete a message with ACKED - should fail because both groups have references
|
||||
assert_equal {2 2 2 2 2} [r XDELEX mystream ACKED IDS 5 0-1 1-1 2-1 3-1 4-1]
|
||||
|
||||
# Destroy one consumer group, and then we can delete all the entries with ACKED.
|
||||
r XGROUP DESTROY mystream mygroup
|
||||
assert_equal {1 1 1 1 1} [r XDELEX mystream ACKED IDS 5 0-1 1-1 2-1 3-1 4-1]
|
||||
assert_equal 0 [r XLEN mystream]
|
||||
}
|
||||
|
||||
test {RENAME can unblock XREADGROUP with data} {
|
||||
r del mystream{t}
|
||||
r XGROUP CREATE mystream{t} mygroup $ MKSTREAM
|
||||
@@ -1500,4 +1524,111 @@ start_server {
|
||||
assert_equal [r xinfo groups mystream] $grpinfo
|
||||
}
|
||||
}
|
||||
|
||||
start_server {} {
|
||||
test "XACKDEL wrong number of args" {
|
||||
assert_error {*wrong number of arguments for 'xackdel' command} {r XACKDEL}
|
||||
assert_error {*wrong number of arguments for 'xackdel' command} {r XACKDEL s}
|
||||
assert_error {*wrong number of arguments for 'xackdel' command} {r XACKDEL s g}
|
||||
}
|
||||
|
||||
test "XACKDEL should return empty array when key doesn't exist or group doesn't exist" {
|
||||
r DEL s
|
||||
assert_equal {-1 -1} [r XACKDEL s g IDS 2 1-1 2-2] ;# the key doesn't exist
|
||||
|
||||
r XADD s 1-0 f v
|
||||
assert_equal {-1 -1} [r XACKDEL s g IDS 2 1-1 2-2] ;# the key exists but the group doesn't exist
|
||||
}
|
||||
|
||||
test "XACKDEL IDS parameter validation" {
|
||||
r DEL s
|
||||
r XADD s 1-0 f v
|
||||
r XGROUP CREATE s g 0
|
||||
|
||||
# Test invalid numids
|
||||
assert_error {*Number of IDs must be a positive integer*} {r XACKDEL s g IDS abc 1-1}
|
||||
assert_error {*Number of IDs must be a positive integer*} {r XACKDEL s g IDS 0 1-1}
|
||||
assert_error {*Number of IDs must be a positive integer*} {r XACKDEL s g IDS -5 1-1}
|
||||
|
||||
# Test whether numids is equal to the number of IDs provided
|
||||
assert_error {*The `numids` parameter must match the number of arguments*} {r XACKDEL s g IDS 3 1-1 2-2}
|
||||
assert_error {*syntax error*} {r XACKDEL s g IDS 1 1-1 2-2}
|
||||
}
|
||||
|
||||
test "XACKDEL KEEPREF/DELREF/ACKED parameter validation" {
|
||||
# Test mutually exclusive options
|
||||
assert_error {*syntax error*} {r XACKDEL s g KEEPREF DELREF IDS 1 1-1}
|
||||
assert_error {*syntax error*} {r XACKDEL s g KEEPREF ACKED IDS 1 1-1}
|
||||
assert_error {*syntax error*} {r XACKDEL s g DELREF ACKED IDS 1 1-1}
|
||||
}
|
||||
|
||||
test "XACKDEL with DELREF option acknowledges will remove entry from all PELs" {
|
||||
r DEL mystream
|
||||
r XADD mystream 1-0 f v
|
||||
r XADD mystream 2-0 f v
|
||||
|
||||
# Create two consumer groups
|
||||
r XGROUP CREATE mystream group1 0
|
||||
r XGROUP CREATE mystream group2 0
|
||||
r XREADGROUP GROUP group1 consumer1 STREAMS mystream >
|
||||
r XREADGROUP GROUP group2 consumer2 STREAMS mystream >
|
||||
|
||||
# Verify the message was removed from both groups' PELs when with DELREF
|
||||
assert_equal {1 1} [r XACKDEL mystream group1 DELREF IDS 2 1-0 2-0]
|
||||
assert_equal 0 [r XLEN mystream]
|
||||
assert_equal {0 {} {} {}} [r XPENDING mystream group1]
|
||||
assert_equal {0 {} {} {}} [r XPENDING mystream group2]
|
||||
assert_equal {-1 -1} [r XACKDEL mystream group2 DELREF IDS 2 1-0 2-0]
|
||||
}
|
||||
|
||||
test "XACKDEL with ACKED option only deletes messages acknowledged by all groups" {
|
||||
r DEL mystream
|
||||
r XADD mystream 1-0 f v
|
||||
r XADD mystream 2-0 f v
|
||||
|
||||
# Create two consumer groups
|
||||
r XGROUP CREATE mystream group1 0
|
||||
r XGROUP CREATE mystream group2 0
|
||||
r XREADGROUP GROUP group1 consumer1 STREAMS mystream >
|
||||
r XREADGROUP GROUP group2 consumer2 STREAMS mystream >
|
||||
|
||||
# The message is referenced by two groups.
|
||||
# Even after one of them is ack, it still can't be deleted.
|
||||
assert_equal {2 2} [r XACKDEL mystream group1 ACKED IDS 2 1-0 2-0]
|
||||
assert_equal 2 [r XLEN mystream]
|
||||
assert_equal {0 {} {} {}} [r XPENDING mystream group1]
|
||||
assert_equal {2 1-0 2-0 {{consumer2 2}}} [r XPENDING mystream group2]
|
||||
|
||||
# When these messages are dereferenced by all groups, they can be deleted.
|
||||
assert_equal {1 1} [r XACKDEL mystream group2 ACKED IDS 2 1-0 2-0]
|
||||
assert_equal 0 [r XLEN mystream]
|
||||
assert_equal {0 {} {} {}} [r XPENDING mystream group1]
|
||||
assert_equal {0 {} {} {}} [r XPENDING mystream group2]
|
||||
}
|
||||
|
||||
test "XACKDEL with KEEPREF" {
|
||||
r DEL mystream
|
||||
r XADD mystream 1-0 f v
|
||||
r XADD mystream 2-0 f v
|
||||
|
||||
# Create two consumer groups
|
||||
r XGROUP CREATE mystream group1 0
|
||||
r XGROUP CREATE mystream group2 0
|
||||
r XREADGROUP GROUP group1 consumer1 STREAMS mystream >
|
||||
r XREADGROUP GROUP group2 consumer2 STREAMS mystream >
|
||||
|
||||
# Test XACKDEL with KEEPREF
|
||||
# XACKDEL only deletes the message from the stream
|
||||
# but does not clean up references in consumer groups' PELs
|
||||
assert_equal {1 1} [r XACKDEL mystream group1 KEEPREF IDS 2 1-0 2-0]
|
||||
assert_equal 0 [r XLEN mystream]
|
||||
assert_equal {0 {} {} {}} [r XPENDING mystream group1]
|
||||
assert_equal {2 1-0 2-0 {{consumer2 2}}} [r XPENDING mystream group2]
|
||||
|
||||
# Acknowledge remaining messages in group2
|
||||
assert_equal {1 1} [r XACKDEL mystream group2 KEEPREF IDS 2 1-0 2-0]
|
||||
assert_equal {0 {} {} {}} [r XPENDING mystream group1]
|
||||
assert_equal {0 {} {} {}} [r XPENDING mystream group2]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,6 +205,64 @@ start_server {
|
||||
}
|
||||
}
|
||||
|
||||
test {XADD with MAXLEN option and ACKED option} {
|
||||
r DEL mystream
|
||||
r XADD mystream 1-0 f v
|
||||
r XADD mystream 2-0 f v
|
||||
r XADD mystream 3-0 f v
|
||||
r XADD mystream 4-0 f v
|
||||
r XADD mystream 5-0 f v
|
||||
assert {[r XLEN mystream] == 5}
|
||||
|
||||
# Create a consumer group but don't read any messages yet
|
||||
# ACKED option should preserve all messages since none are acked.
|
||||
r XGROUP CREATE mystream mygroup 0
|
||||
r XADD mystream MAXLEN = 1 ACKED 6-0 f v
|
||||
assert {[r XLEN mystream] == 6} ;# All messages preserved + the new one
|
||||
|
||||
# Read 1 messages and acknowledge them
|
||||
# This leaves 5 messages still unacked
|
||||
set records [r XREADGROUP GROUP mygroup consumer1 COUNT 1 STREAMS mystream >]
|
||||
r XACK mystream mygroup [lindex [lindex [lindex [lindex $records 0] 1] 0] 0]
|
||||
assert {[lindex [r XPENDING mystream mygroup] 0] == 0}
|
||||
|
||||
# With 5 messages still unacked, ACKED option should preserve them
|
||||
r XADD mystream MAXLEN = 1 ACKED 7-0 f v
|
||||
assert {[r XLEN mystream] == 6} ;# 6 - 1 acked + 1 new
|
||||
|
||||
# Acknowledge all remaining messages
|
||||
set records [r XREADGROUP GROUP mygroup consumer1 STREAMS mystream >]
|
||||
set ids {}
|
||||
foreach entry [lindex [lindex $records 0] 1] {
|
||||
lappend ids [lindex $entry 0]
|
||||
}
|
||||
r XACK mystream mygroup {*}$ids
|
||||
assert {[lindex [r XPENDING mystream mygroup] 0] == 0} ;# All messages acked
|
||||
|
||||
# Now ACKED should trim to MAXLEN since all messages are acked
|
||||
r XADD mystream MAXLEN = 1 ACKED * f v
|
||||
assert {[r XLEN mystream] == 1} ;# Successfully trimmed to 1 entries
|
||||
}
|
||||
|
||||
test {XADD with MAXLEN option and DELREF option} {
|
||||
r DEL mystream
|
||||
r XADD mystream 1-0 f v
|
||||
r XADD mystream 2-0 f v
|
||||
r XADD mystream 3-0 f v
|
||||
r XADD mystream 4-0 f v
|
||||
r XADD mystream 5-0 f v
|
||||
|
||||
r XGROUP CREATE mystream mygroup 0
|
||||
r XREADGROUP GROUP mygroup consumer1 COUNT 1 STREAMS mystream >
|
||||
|
||||
# XADD with MAXLEN and DELREF should trim and remove all references
|
||||
r XADD mystream MAXLEN = 1 DELREF * f v
|
||||
assert {[r XLEN mystream] == 1}
|
||||
|
||||
# All PEL entries should be cleaned up
|
||||
assert {[lindex [r XPENDING mystream mygroup] 0] == 0}
|
||||
}
|
||||
|
||||
test {XTRIM with MINID option} {
|
||||
r DEL mystream
|
||||
r XADD mystream 1-0 f v
|
||||
@@ -1081,3 +1139,140 @@ start_server {tags {"stream"}} {
|
||||
assert_match "*wrong number of arguments for 'xinfo|help' command" $e
|
||||
}
|
||||
}
|
||||
|
||||
start_server {tags {"stream"}} {
|
||||
test "XDELEX wrong number of args" {
|
||||
assert_error {*wrong number of arguments for 'xdelex' command} {r XDELEX s DELREF}
|
||||
}
|
||||
|
||||
test "XDELEX should return empty array when key doesn't exist" {
|
||||
r DEL nonexist
|
||||
assert_equal {-1 -1} [r XDELEX nonexist IDS 2 1-1 2-2]
|
||||
}
|
||||
|
||||
test "XDELEX IDS parameter validation" {
|
||||
r DEL s
|
||||
r XADD s 1-0 f v
|
||||
r XGROUP CREATE s g 0
|
||||
|
||||
# Test invalid numids
|
||||
assert_error {*Number of IDs must be a positive integer*} {r XDELEX s IDS abc 1-1}
|
||||
assert_error {*Number of IDs must be a positive integer*} {r XDELEX s IDS 0 1-1}
|
||||
assert_error {*Number of IDs must be a positive integer*} {r XDELEX s IDS -5 1-1}
|
||||
|
||||
# Test whether numids is equal to the number of IDs provided
|
||||
assert_error {*The `numids` parameter must match the number of arguments*} {r XDELEX s IDS 3 1-1 2-2}
|
||||
assert_error {*syntax error*} {r XDELEX s IDS 1 1-1 2-2}
|
||||
|
||||
# Delete non-existent ids
|
||||
assert_equal {-1 -1} [r XDELEX s IDS 2 1-1 2-2]
|
||||
}
|
||||
|
||||
test "XDELEX KEEPREF/DELREF/ACKED parameter validation" {
|
||||
# Test mutually exclusive options
|
||||
assert_error {*syntax error*} {r XDELEX s KEEPREF DELREF IDS 1 1-1}
|
||||
assert_error {*syntax error*} {r XDELEX s KEEPREF ACKED IDS 1 1-1}
|
||||
assert_error {*syntax error*} {r XDELEX s ACKED DELREF IDS 1 1-1}
|
||||
}
|
||||
|
||||
test "XDELEX with DELREF option acknowledges will remove entry from all PELs" {
|
||||
r DEL mystream
|
||||
r XADD mystream 1-0 f v
|
||||
r XADD mystream 2-0 f v
|
||||
|
||||
# Create two consumer groups
|
||||
r XGROUP CREATE mystream group1 0
|
||||
r XGROUP CREATE mystream group2 0
|
||||
r XREADGROUP GROUP group1 consumer1 STREAMS mystream >
|
||||
r XREADGROUP GROUP group2 consumer2 STREAMS mystream >
|
||||
|
||||
# Verify the message was removed from both groups' PELs when with DELREF
|
||||
assert_equal {1 1} [r XDELEX mystream DELREF IDS 2 1-0 2-0]
|
||||
assert_equal 0 [r XLEN mystream]
|
||||
assert_equal {0 {} {} {}} [r XPENDING mystream group1]
|
||||
assert_equal {0 {} {} {}} [r XPENDING mystream group2]
|
||||
}
|
||||
|
||||
test "XDELEX with ACKED option only deletes messages acknowledged by all groups" {
|
||||
r DEL mystream
|
||||
r XADD mystream 1-0 f v
|
||||
r XADD mystream 2-0 f v
|
||||
|
||||
# Create two consumer groups
|
||||
r XGROUP CREATE mystream group1 0
|
||||
r XGROUP CREATE mystream group2 0
|
||||
r XGROUP CREATE mystream group3 0
|
||||
r XREADGROUP GROUP group1 consumer1 STREAMS mystream >
|
||||
r XREADGROUP GROUP group2 consumer2 STREAMS mystream >
|
||||
|
||||
# The message is referenced by three consumer groups:
|
||||
# - group1 and group2 have read the messages
|
||||
# - group3 hasn't read the messages yet (not delivered)
|
||||
# Even after group1 acknowledges the messages, they still can't be deleted
|
||||
r XACK mystream group1 1-0 2-0
|
||||
assert_equal {2 2} [r XDELEX mystream ACKED IDS 2 1-0 2-0]
|
||||
assert_equal 2 [r XLEN mystream]
|
||||
|
||||
# Even after both group1 and group2 acknowledge the messages, these entries
|
||||
# still can't be deleted because group3 hasn't even read them yet.
|
||||
r XACK mystream group2 1-0 2-0
|
||||
assert_equal {2 2} [r XDELEX mystream ACKED IDS 2 1-0 2-0]
|
||||
assert_equal 2 [r XLEN mystream]
|
||||
|
||||
# Now group3 reads the messages, but hasn't acknowledged them yet.
|
||||
# these entries still can't be deleted because group3 hasn't acknowledged them.
|
||||
r XREADGROUP GROUP group3 consumer3 STREAMS mystream >
|
||||
assert_equal {2 2} [r XDELEX mystream ACKED IDS 2 1-0 2-0]
|
||||
assert_equal 2 [r XLEN mystream]
|
||||
|
||||
# Now group3 acknowledges the messages. These entries can now be deleted.
|
||||
r XACK mystream group3 1-0 2-0
|
||||
r XDELEX mystream ACKED IDS 2 1-0 2-0
|
||||
assert_equal 0 [r XLEN mystream]
|
||||
}
|
||||
|
||||
test "XDELEX with ACKED option won't delete messages when new consumer groups are created" {
|
||||
r DEL mystream
|
||||
r XADD mystream 1-0 f v
|
||||
r XADD mystream 2-0 f v
|
||||
r XADD mystream 3-0 f v
|
||||
|
||||
r XGROUP CREATE mystream group1 0
|
||||
r XREADGROUP GROUP group1 consumer1 STREAMS mystream >
|
||||
|
||||
# When the group1 ack message, the message can be deleted with ACK option.
|
||||
assert_equal {3} [r XACK mystream group1 1-0 2-0 3-0]
|
||||
assert_equal {1} [r XDELEX mystream ACKED IDS 1 1-0]
|
||||
|
||||
# Create a new consumer group that hasn't read the messages yet.
|
||||
# Even if group1 ack the message, we still can't delete the message.
|
||||
r XGROUP CREATE mystream group2 0
|
||||
assert_equal {2} [r XDELEX mystream ACKED IDS 1 2-0]
|
||||
|
||||
# Now group2 reads and acknowledges the messages,
|
||||
# so we can be successfully deleted with the ACKED option.
|
||||
r XREADGROUP GROUP group2 consumer1 STREAMS mystream >
|
||||
assert_equal {2} [r XACK mystream group2 2-0 3-0]
|
||||
assert_equal {1 1} [r XDELEX mystream ACKED IDS 2 2-0 3-0]
|
||||
}
|
||||
|
||||
test "XDELEX with KEEPREF" {
|
||||
r DEL mystream
|
||||
r XADD mystream 1-0 f v
|
||||
r XADD mystream 2-0 f v
|
||||
|
||||
# Create two consumer groups
|
||||
r XGROUP CREATE mystream group1 0
|
||||
r XGROUP CREATE mystream group2 0
|
||||
r XREADGROUP GROUP group1 consumer1 STREAMS mystream >
|
||||
r XREADGROUP GROUP group2 consumer2 STREAMS mystream >
|
||||
|
||||
# Test XDELEX with KEEPREF
|
||||
# XDELEX only deletes the message from the stream
|
||||
# but does not clean up references in consumer groups' PELs
|
||||
assert_equal {1 1} [r XDELEX mystream KEEPREF IDS 2 1-0 2-0]
|
||||
assert_equal 0 [r XLEN mystream]
|
||||
assert_equal {2 1-0 2-0 {{consumer1 2}}} [r XPENDING mystream group1]
|
||||
assert_equal {2 1-0 2-0 {{consumer2 2}}} [r XPENDING mystream group2]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user