Add cmd tips for HOTKEYS. Return err when hotkeys START specifies invalid slots (#14761)
Some checks are pending
CI / test-ubuntu-latest (push) Waiting to run
CI / test-sanitizer-address (push) Waiting to run
CI / build-debian-old (push) Waiting to run
CI / build-macos-latest (push) Waiting to run
CI / build-32bit (push) Waiting to run
CI / build-libc-malloc (push) Waiting to run
CI / build-centos-jemalloc (push) Waiting to run
CI / build-old-chain-jemalloc (push) Waiting to run
Codecov / code-coverage (push) Waiting to run
External Server Tests / test-external-standalone (push) Waiting to run
External Server Tests / test-external-cluster (push) Waiting to run
External Server Tests / test-external-nodebug (push) Waiting to run
Reply-schemas linter / reply-schemas-linter (push) Waiting to run
Spellcheck / Spellcheck (push) Waiting to run

- When passing slots not within the range of a node to `HOTKEYS START
SLOTS ...` the hotkey command now returns error.
- Changed the cmd tips for the HOTKEYS subcommands so that they reflect
the special nature of the cmd in cluster mode - i.e command should be
issued against a single node only. Clients should not care about cluster
management and aggregation of results.
- Change reply schema to return Array of the maps. For a single node
this will return array of 1 element. Getting results from multiple nodes
will make it easy to concatenate the elements into one array.
This commit is contained in:
Mincho Paskalev 2026-02-03 17:54:32 +02:00 committed by GitHub
parent 02700f11cd
commit b5a37c0e42
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 374 additions and 164 deletions

View file

@ -7320,7 +7320,11 @@ struct COMMAND_ARG FLUSHDB_Args[] = {
#ifndef SKIP_CMD_TIPS_TABLE
/* HOTKEYS GET tips */
#define HOTKEYS_GET_Tips NULL
const char *HOTKEYS_GET_Tips[] = {
"nondeterministic_output",
"request_policy:special",
"response_policy:special",
};
#endif
#ifndef SKIP_CMD_KEY_SPECS_TABLE
@ -7337,7 +7341,9 @@ struct COMMAND_ARG FLUSHDB_Args[] = {
#ifndef SKIP_CMD_TIPS_TABLE
/* HOTKEYS RESET tips */
#define HOTKEYS_RESET_Tips NULL
const char *HOTKEYS_RESET_Tips[] = {
"request_policy:special",
};
#endif
#ifndef SKIP_CMD_KEY_SPECS_TABLE
@ -7354,7 +7360,9 @@ struct COMMAND_ARG FLUSHDB_Args[] = {
#ifndef SKIP_CMD_TIPS_TABLE
/* HOTKEYS START tips */
#define HOTKEYS_START_Tips NULL
const char *HOTKEYS_START_Tips[] = {
"request_policy:special",
};
#endif
#ifndef SKIP_CMD_KEY_SPECS_TABLE
@ -7393,7 +7401,9 @@ struct COMMAND_ARG HOTKEYS_START_Args[] = {
#ifndef SKIP_CMD_TIPS_TABLE
/* HOTKEYS STOP tips */
#define HOTKEYS_STOP_Tips NULL
const char *HOTKEYS_STOP_Tips[] = {
"request_policy:special",
};
#endif
#ifndef SKIP_CMD_KEY_SPECS_TABLE
@ -7403,10 +7413,10 @@ struct COMMAND_ARG HOTKEYS_START_Args[] = {
/* HOTKEYS command table */
struct COMMAND_STRUCT HOTKEYS_Subcommands[] = {
{MAKE_CMD("get","Returns lists of top K hotkeys depending on metrics chosen in HOTKEYS START command.","O(K) where K is the number of hotkeys returned.","8.6.0",CMD_DOC_NONE,NULL,NULL,"server",COMMAND_GROUP_SERVER,HOTKEYS_GET_History,0,HOTKEYS_GET_Tips,0,hotkeysCommand,2,CMD_ADMIN|CMD_NOSCRIPT,0,HOTKEYS_GET_Keyspecs,0,NULL,0)},
{MAKE_CMD("reset","Release the resources used for hotkey tracking.","O(1)","8.6.0",CMD_DOC_NONE,NULL,NULL,"server",COMMAND_GROUP_SERVER,HOTKEYS_RESET_History,0,HOTKEYS_RESET_Tips,0,hotkeysCommand,2,CMD_ADMIN|CMD_NOSCRIPT,0,HOTKEYS_RESET_Keyspecs,0,NULL,0)},
{MAKE_CMD("start","Starts hotkeys tracking.","O(1)","8.6.0",CMD_DOC_NONE,NULL,NULL,"server",COMMAND_GROUP_SERVER,HOTKEYS_START_History,0,HOTKEYS_START_Tips,0,hotkeysCommand,-2,CMD_ADMIN|CMD_NOSCRIPT,0,HOTKEYS_START_Keyspecs,0,NULL,5),.args=HOTKEYS_START_Args},
{MAKE_CMD("stop","Stops hotkeys tracking.","O(1)","8.6.0",CMD_DOC_NONE,NULL,NULL,"server",COMMAND_GROUP_SERVER,HOTKEYS_STOP_History,0,HOTKEYS_STOP_Tips,0,hotkeysCommand,2,CMD_ADMIN|CMD_NOSCRIPT,0,HOTKEYS_STOP_Keyspecs,0,NULL,0)},
{MAKE_CMD("get","Returns lists of top K hotkeys depending on metrics chosen in HOTKEYS START command.","O(K) where K is the number of hotkeys returned.","8.6.0",CMD_DOC_NONE,NULL,NULL,"server",COMMAND_GROUP_SERVER,HOTKEYS_GET_History,0,HOTKEYS_GET_Tips,3,hotkeysCommand,2,CMD_ADMIN|CMD_NOSCRIPT,0,HOTKEYS_GET_Keyspecs,0,NULL,0)},
{MAKE_CMD("reset","Release the resources used for hotkey tracking.","O(1)","8.6.0",CMD_DOC_NONE,NULL,NULL,"server",COMMAND_GROUP_SERVER,HOTKEYS_RESET_History,0,HOTKEYS_RESET_Tips,1,hotkeysCommand,2,CMD_ADMIN|CMD_NOSCRIPT,0,HOTKEYS_RESET_Keyspecs,0,NULL,0)},
{MAKE_CMD("start","Starts hotkeys tracking.","O(1)","8.6.0",CMD_DOC_NONE,NULL,NULL,"server",COMMAND_GROUP_SERVER,HOTKEYS_START_History,0,HOTKEYS_START_Tips,1,hotkeysCommand,-2,CMD_ADMIN|CMD_NOSCRIPT,0,HOTKEYS_START_Keyspecs,0,NULL,5),.args=HOTKEYS_START_Args},
{MAKE_CMD("stop","Stops hotkeys tracking.","O(1)","8.6.0",CMD_DOC_NONE,NULL,NULL,"server",COMMAND_GROUP_SERVER,HOTKEYS_STOP_History,0,HOTKEYS_STOP_Tips,1,hotkeysCommand,2,CMD_ADMIN|CMD_NOSCRIPT,0,HOTKEYS_STOP_Keyspecs,0,NULL,0)},
{0}
};

View file

@ -11,101 +11,114 @@
"ADMIN",
"NOSCRIPT"
],
"command_tips": [
"NONDETERMINISTIC_OUTPUT",
"REQUEST_POLICY:SPECIAL",
"RESPONSE_POLICY:SPECIAL"
],
"reply_schema": {
"oneOf": [
{
"description": "Map with various metrics (tracking-active, sample-ratio, selected-slots, time/network statistics), collection info (collection-start-time-unix-ms, collection-duration-ms, total-cpu-time-user-ms, total-cpu-time-sys-ms, total-net-bytes), and the requested lists of Top-K hotkeys (available metrics: by-cpu-time-us, by-net-bytes) where at most K hotkeys are returned.",
"type": "object",
"properties": {
"tracking-active": {
"type": "integer",
"description": "Whether hotkey tracking is currently active (1) or stopped (0)."
},
"sample-ratio": {
"type": "integer",
"description": "The sampling ratio used for tracking."
},
"selected-slots": {
"type": "array",
"items": {
"type": "integer"
"description": "Array of maps with various metrics (tracking-active, sample-ratio, selected-slots, time/network statistics), collection info (collection-start-time-unix-ms, collection-duration-ms, total-cpu-time-user-ms, total-cpu-time-sys-ms, total-net-bytes), and the requested lists of Top-K hotkeys (available metrics: by-cpu-time-us, by-net-bytes) where at most K hotkeys are returned.",
"type": "array",
"items": {
"type": "object",
"properties": {
"tracking-active": {
"type": "integer",
"description": "Whether hotkey tracking is currently active (1) or stopped (0)."
},
"description": "Array of slot numbers being tracked (empty if tracking all slots)."
},
"sampled-command-selected-slots-us": {
"type": "integer",
"description": "CPU time in microseconds for sampled commands in selected slots (only present when sampling and slots are configured)."
},
"all-commands-selected-slots-us": {
"type": "integer",
"description": "CPU time in microseconds for all commands in selected slots (only present when slots are configured)."
},
"all-commands-all-slots-us": {
"type": "integer",
"description": "CPU time in microseconds for all commands across all slots."
},
"net-bytes-sampled-commands-selected-slots": {
"type": "integer",
"description": "Network bytes for sampled commands in selected slots (only present when sampling and slots are configured)."
},
"net-bytes-all-commands-selected-slots": {
"type": "integer",
"description": "Network bytes for all commands in selected slots (only present when slots are configured)."
},
"net-bytes-all-commands-all-slots": {
"type": "integer",
"description": "Network bytes for all commands across all slots."
},
"collection-start-time-unix-ms": {
"type": "integer",
"description": "Unix timestamp in milliseconds when collection started."
},
"collection-duration-ms": {
"type": "integer",
"description": "Duration of collection in milliseconds."
},
"total-cpu-time-user-ms": {
"type": "integer",
"description": "Total user CPU time in milliseconds (only present when CPU tracking is enabled)."
},
"total-cpu-time-sys-ms": {
"type": "integer",
"description": "Total system CPU time in milliseconds (only present when CPU tracking is enabled)."
},
"total-net-bytes": {
"type": "integer",
"description": "Total network bytes (only present when NET tracking is enabled)."
},
"by-cpu-time-us": {
"type": "array",
"items": {
"oneOf": [
{
"type": "string"
},
{
"sample-ratio": {
"type": "integer",
"description": "The sampling ratio used for tracking."
},
"selected-slots": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": "integer"
}
]
},
"description": "Flat array of key-value pairs (key1, cpu_time1, key2, cpu_time2, ...) for top-K hotkeys by CPU time in microseconds (only present when CPU tracking is enabled)."
},
"by-net-bytes": {
"type": "array",
"items": {
"oneOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
"minItems": 1,
"maxItems": 2
},
"description": "Array of slot ranges. Each element is an array: single-element [slot] for individual slots, or two-element [start, end] for inclusive ranges."
},
"description": "Flat array of key-value pairs (key1, bytes1, key2, bytes2, ...) for top-K hotkeys by network bytes (only present when NET tracking is enabled)."
}
},
"additionalProperties": false
"sampled-command-selected-slots-us": {
"type": "integer",
"description": "CPU time in microseconds for sampled commands in selected slots (only present when sampling and slots are configured)."
},
"all-commands-selected-slots-us": {
"type": "integer",
"description": "CPU time in microseconds for all commands in selected slots (only present when slots are configured)."
},
"all-commands-all-slots-us": {
"type": "integer",
"description": "CPU time in microseconds for all commands across all slots."
},
"net-bytes-sampled-commands-selected-slots": {
"type": "integer",
"description": "Network bytes for sampled commands in selected slots (only present when sampling and slots are configured)."
},
"net-bytes-all-commands-selected-slots": {
"type": "integer",
"description": "Network bytes for all commands in selected slots (only present when slots are configured)."
},
"net-bytes-all-commands-all-slots": {
"type": "integer",
"description": "Network bytes for all commands across all slots."
},
"collection-start-time-unix-ms": {
"type": "integer",
"description": "Unix timestamp in milliseconds when collection started."
},
"collection-duration-ms": {
"type": "integer",
"description": "Duration of collection in milliseconds."
},
"total-cpu-time-user-ms": {
"type": "integer",
"description": "Total user CPU time in milliseconds (only present when CPU tracking is enabled)."
},
"total-cpu-time-sys-ms": {
"type": "integer",
"description": "Total system CPU time in milliseconds (only present when CPU tracking is enabled)."
},
"total-net-bytes": {
"type": "integer",
"description": "Total network bytes (only present when NET tracking is enabled)."
},
"by-cpu-time-us": {
"type": "array",
"items": {
"oneOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"description": "Flat array of key-value pairs (key1, cpu_time1, key2, cpu_time2, ...) for top-K hotkeys by CPU time in microseconds (only present when CPU tracking is enabled)."
},
"by-net-bytes": {
"type": "array",
"items": {
"oneOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"description": "Flat array of key-value pairs (key1, bytes1, key2, bytes2, ...) for top-K hotkeys by network bytes (only present when NET tracking is enabled)."
}
},
"additionalProperties": false
}
},
{
"description": "If no tracking is started",

View file

@ -11,6 +11,9 @@
"ADMIN",
"NOSCRIPT"
],
"command_tips": [
"REQUEST_POLICY:SPECIAL"
],
"reply_schema": {
"const": "OK"
}

View file

@ -11,6 +11,9 @@
"ADMIN",
"NOSCRIPT"
],
"command_tips": [
"REQUEST_POLICY:SPECIAL"
],
"reply_schema": {
"const": "OK"
},

View file

@ -11,6 +11,9 @@
"ADMIN",
"NOSCRIPT"
],
"command_tips": [
"REQUEST_POLICY:SPECIAL"
],
"reply_schema": {
"const": "OK"
}

View file

@ -18,12 +18,17 @@ static inline int nearestNextPowerOf2(unsigned int count) {
return 1 << (32 - __builtin_clz(count-1));
}
/* Comparison function for qsort to sort slot indices */
static inline int slotCompare(const void *a, const void *b) {
return (*(const int *)a) - (*(const int *)b);
}
/* Initialize the hotkeys structure and start tracking. If tracking keys in
* specific slots is desired the user should pass along an already allocated and
* populated slots array. The hotkeys structure takes ownership of the array and
* will free it upon release. On failure the slots memory is released. */
* populated slotRangeArray. The hotkeys structure takes ownership of the array
* and will free it upon release. On failure the slots memory is released. */
hotkeyStats *hotkeyStatsCreate(int count, int duration, int sample_ratio,
int *slots, int slots_count, uint64_t tracked_metrics)
slotRangeArray *slots, uint64_t tracked_metrics)
{
serverAssert(tracked_metrics & (HOTKEYS_TRACK_CPU | HOTKEYS_TRACK_NET));
@ -44,7 +49,6 @@ hotkeyStats *hotkeyStatsCreate(int count, int duration, int sample_ratio,
hotkeys->duration = duration;
hotkeys->sample_ratio = sample_ratio;
hotkeys->slots = slots;
hotkeys->numslots = slots_count;
hotkeys->active = 1;
hotkeys->keys_result = (getKeysResult)GETKEYS_RESULT_INIT;
hotkeys->start = server.mstime;
@ -62,20 +66,17 @@ void hotkeyStatsRelease(hotkeyStats *hotkeys) {
if (!hotkeys) return;
if (hotkeys->cpu) chkTopKRelease(hotkeys->cpu);
if (hotkeys->net) chkTopKRelease(hotkeys->net);
zfree(hotkeys->slots);
slotRangeArrayFree(hotkeys->slots);
getKeysFreeResult(&hotkeys->keys_result);
zfree(hotkeys);
}
/* Helper function for hotkey tracking to check if a slot is in the selected
* slots list. If numslots is 0 then all slots are selected. */
* slots list. If slots is NULL then all slots are selected. */
static inline int isSlotSelected(hotkeyStats *hotkeys, int slot) {
if (hotkeys->numslots == 0) return 1;
for (int i = 0; i < hotkeys->numslots; i++) {
if (hotkeys->slots[i] == slot) return 1;
}
return 0;
if (hotkeys->slots == NULL) return 1;
return slotRangeArrayContains(hotkeys->slots, slot);
}
/* Preparation for updates of the hotkeyStats for the current command, f.e
@ -192,9 +193,9 @@ size_t hotkeysGetMemoryUsage(hotkeyStats *hotkeys) {
if (hotkeys->net) {
memory_usage += chkTopKGetMemoryUsage(hotkeys->net);
}
/* Add memory for slots array if present */
/* Add memory for slotRangeArray if present */
if (hotkeys->slots) {
memory_usage += sizeof(int) * hotkeys->numslots;
memory_usage += sizeof(slotRangeArray) + sizeof(slotRange) * hotkeys->slots->num_ranges;
}
return memory_usage;
@ -212,8 +213,41 @@ static int64_t time_diff_ms(struct timeval a, struct timeval b) {
return sec * 1000 + usec / 1000;
}
/* Helper function to output a slotRangeArray as array of arrays.
* Single slots become 1-element arrays, ranges become 2-element arrays. */
static void addReplySlotRangeArray(client *c, slotRangeArray *slots) {
addReplyArrayLen(c, slots->num_ranges);
for (int i = 0; i < slots->num_ranges; i++) {
if (slots->ranges[i].start == slots->ranges[i].end) {
/* Single slot */
addReplyArrayLen(c, 1);
addReplyLongLong(c, slots->ranges[i].start);
} else {
/* Range */
addReplyArrayLen(c, 2);
addReplyLongLong(c, slots->ranges[i].start);
addReplyLongLong(c, slots->ranges[i].end);
}
}
}
/* Helper function to output selected-slots as array of arrays.
* If slots is NULL, outputs the local node's slot ranges (all slots in non-cluster mode). */
static void addReplySelectedSlots(client *c, hotkeyStats *hotkeys) {
if (hotkeys->slots == NULL) {
/* No specific slots selected - return the local node's slot ranges */
slotRangeArray *slots = clusterGetLocalSlotRanges();
addReplySlotRangeArray(c, slots);
slotRangeArrayFree(slots);
return;
}
/* Slots are already stored as a sorted/merged slotRangeArray */
addReplySlotRangeArray(c, hotkeys->slots);
}
/* HOTKEYS command implementation
*
*
* HOTKEYS START
* <METRICS count [CPU] [NET]>
* [COUNT k]
@ -300,8 +334,7 @@ void hotkeysCommand(client *c) {
int count = 10; /* default */
long duration = 0; /* default: no auto-stop */
int sample_ratio = 1; /* default: track every key */
int slots_count = 0;
int *slots = NULL;
slotRangeArray *slots = NULL;
while (j < c->argc) {
int moreargs = (c->argc-1) - j;
if (moreargs && !strcasecmp(c->argv[j]->ptr, "COUNT")) {
@ -309,7 +342,7 @@ void hotkeysCommand(client *c) {
if (getRangeLongFromObjectOrReply(c, c->argv[j+1], 1, 64,
&count_val, "COUNT must be between 1 and 64") != C_OK)
{
zfree(slots);
slotRangeArrayFree(slots);
return;
}
count = (int)count_val;
@ -320,7 +353,7 @@ void hotkeysCommand(client *c) {
if (getRangeLongFromObjectOrReply(c, c->argv[j+1], 1, 1000000,
&duration, "DURATION must be between 1 and 1000000") != C_OK)
{
zfree(slots);
slotRangeArrayFree(slots);
return;
}
duration *= 1000;
@ -330,7 +363,7 @@ void hotkeysCommand(client *c) {
if (getRangeLongFromObjectOrReply(c, c->argv[j+1], 1, INT_MAX,
&ratio_val, "SAMPLE ratio must be positive") != C_OK)
{
zfree(slots);
slotRangeArrayFree(slots);
return;
}
sample_ratio = (int)ratio_val;
@ -343,7 +376,7 @@ void hotkeysCommand(client *c) {
if (slots) {
addReplyError(c, "SLOTS parameter already specified");
zfree(slots);
slotRangeArrayFree(slots);
return;
}
long slots_count_val;
@ -355,41 +388,59 @@ void hotkeysCommand(client *c) {
{
return;
}
slots_count = (int)slots_count_val;
int slots_count = (int)slots_count_val;
/* Parse slot numbers */
if (j + 1 + slots_count >= c->argc) {
addReplyError(c, "not enough slot numbers provided");
return;
}
slots = zmalloc(sizeof(int) * slots_count);
/* Collect slots into a temporary array for sorting */
int *temp_slots = zmalloc(sizeof(int) * slots_count);
for (int i = 0; i < slots_count; i++) {
long slot_val;
if ((slot_val = getSlotOrReply(c, c->argv[j+2+i])) == -1) {
zfree(slots);
zfree(temp_slots);
return;
}
/* Check for duplicate slot indices */
for (int k = 0; k < i; ++k) {
if (slots[k] == slot_val) {
if (!clusterNodeCoversSlot(getMyClusterNode(), slot_val)) {
addReplyErrorFormat(c, "slot %ld not handled by this node", slot_val);
zfree(temp_slots);
return;
}
/* Check for duplicate slot */
for (int k = 0; k < i; k++) {
if (temp_slots[k] == slot_val) {
addReplyError(c, "duplicate slot number");
zfree(slots);
zfree(temp_slots);
return;
}
}
slots[i] = (int)slot_val;
temp_slots[i] = (int)slot_val;
}
/* Sort the slots array */
qsort(temp_slots, slots_count, sizeof(int), slotCompare);
/* Build slotRangeArray from sorted slots */
for (int i = 0; i < slots_count; i++) {
slots = slotRangeArrayAppend(slots, temp_slots[i]);
}
zfree(temp_slots);
j += 2 + slots_count;
} else {
addReplyError(c, "syntax error");
if (slots) zfree(slots);
slotRangeArrayFree(slots);
return;
}
}
hotkeyStats *hotkeys = hotkeyStatsCreate(count, duration, sample_ratio,
slots, slots_count, tracked_metrics);
slots, tracked_metrics);
hotkeyStatsRelease(server.hotkeys);
server.hotkeys = hotkeys;
@ -472,11 +523,15 @@ void hotkeysCommand(client *c) {
}
}
int has_selected_slots = (server.hotkeys->numslots > 0);
int has_selected_slots = (server.hotkeys->slots != NULL);
int has_sampling = (server.hotkeys->sample_ratio > 1);
/* We return an array of map for easy aggregation of results from
* different nodes. */
addReplyArrayLen(c, 1);
int total_len = 7;
void *arraylenptr = addReplyDeferredLen(c);
void *maplenptr = addReplyDeferredLen(c);
/* tracking-active */
addReplyBulkCString(c, "tracking-active");
@ -486,12 +541,9 @@ void hotkeysCommand(client *c) {
addReplyBulkCString(c, "sample-ratio");
addReplyLongLong(c, server.hotkeys->sample_ratio);
/* selected-slots */
/* selected-slots - array of arrays with merged ranges */
addReplyBulkCString(c, "selected-slots");
addReplyArrayLen(c, server.hotkeys->numslots);
for (int i = 0; i < server.hotkeys->numslots; i++) {
addReplyLongLong(c, server.hotkeys->slots[i]);
}
addReplySelectedSlots(c, server.hotkeys);
/* sampled-command-selected-slots-us (conditional) */
if (has_sampling && has_selected_slots) {
@ -592,7 +644,7 @@ void hotkeysCommand(client *c) {
++total_len;
}
setDeferredMapLen(c, arraylenptr, total_len);
setDeferredMapLen(c, maplenptr, total_len);
} else if (!strcasecmp(sub, "RESET")) {
/* HOTKEYS RESET */

View file

@ -2538,10 +2538,9 @@ struct hotkeyStats {
struct chkTopK *net;
mstime_t start; /* Initial time point for wall time tracking */
/* Only keys from selected slots will be tracked. If slots are not
* initialized - all keys are tracked. */
int *slots;
int numslots;
/* Only keys from selected slots will be tracked. If slots is NULL,
* all keys are tracked. Stored as a sorted slotRangeArray. */
struct slotRangeArray *slots;
/* Statistics counters. */
uint64_t time_sampled_commands_selected_slots; /* microseconds */
@ -4132,7 +4131,7 @@ int validateHexDigest(client *c, const sds digest);
/* Hotkey tracking */
hotkeyStats *hotkeyStatsCreate(int count, int duration, int sample_ratio,
int *slots, int slots_count, uint64_t tracked_metrics);
struct slotRangeArray *slots, uint64_t tracked_metrics);
void hotkeyStatsRelease(hotkeyStats *hotkeys);
void hotkeyStatsPreCurrentCmd(hotkeyStats *hotkeys, client *c);
void hotkeyStatsUpdateCurrentCmd(hotkeyStats *hotkeys, hotkeyMetrics metrics);

View file

@ -9,7 +9,7 @@ proc hotkeys_array_to_dict {arr} {
return $result
}
start_server {tags {"hotkeys"}} {
start_server {tags {external:skip "hotkeys"}} {
test {HOTKEYS START - METRICS required} {
catch {r hotkeys start} err
assert_match "*METRICS parameter is required*" $err
@ -20,7 +20,7 @@ start_server {tags {"hotkeys"}} {
r set key1 value1
assert_equal {OK} [r hotkeys stop]
set result [r hotkeys get]
set result [lindex [r hotkeys get] 0]
if {[llength $result] > 0 && [lindex $result 0] ne "tracking-active"} {
set result [hotkeys_array_to_dict $result]
}
@ -39,7 +39,7 @@ start_server {tags {"hotkeys"}} {
r set key1 value1
assert_equal {OK} [r hotkeys stop]
set result [r hotkeys get]
set result [lindex [r hotkeys get] 0]
if {[llength $result] > 0 && [lindex $result 0] ne "tracking-active"} {
set result [hotkeys_array_to_dict $result]
}
@ -58,7 +58,7 @@ start_server {tags {"hotkeys"}} {
r set key1 value1
assert_equal {OK} [r hotkeys stop]
set result [r hotkeys get]
set result [lindex [r hotkeys get] 0]
if {[llength $result] > 0 && [lindex $result 0] ne "tracking-active"} {
set result [hotkeys_array_to_dict $result]
}
@ -126,7 +126,7 @@ start_server {tags {"hotkeys"}} {
assert_equal {OK} [r hotkeys stop]
set result [r hotkeys get]
set result [lindex [r hotkeys get] 0]
if {[llength $result] > 0 && [lindex $result 0] ne "tracking-active"} {
set result [hotkeys_array_to_dict $result]
}
@ -154,7 +154,7 @@ start_server {tags {"hotkeys"}} {
assert_equal {OK} [r hotkeys start METRICS 1 CPU DURATION 1]
after 1500
set result [r hotkeys get]
set result [lindex [r hotkeys get] 0]
if {[llength $result] > 0 && [lindex $result 0] eq "tracking-active"} {
set result [hotkeys_array_to_dict $result]
}
@ -183,7 +183,7 @@ start_server {tags {"hotkeys"}} {
assert_equal {OK} [r hotkeys start METRICS 2 CPU NET]
assert_equal {OK} [r hotkeys stop]
set result [r hotkeys get]
set result [lindex [r hotkeys get] 0]
if {[llength $result] > 0 && [lindex $result 0] ne "tracking-active"} {
set result [hotkeys_array_to_dict $result]
}
@ -197,7 +197,7 @@ start_server {tags {"hotkeys"}} {
assert_equal {OK} [r hotkeys stop]
assert_equal {OK} [r hotkeys reset]
# After reset, GET should return nil
set result [r hotkeys get]
set result [lindex [r hotkeys get] 0]
assert_equal {} $result
}
@ -210,7 +210,7 @@ start_server {tags {"hotkeys"}} {
}
test {HOTKEYS GET - returns nil when not started} {
set result [r hotkeys get]
set result [lindex [r hotkeys get] 0]
assert_equal {} $result
}
@ -218,7 +218,7 @@ start_server {tags {"hotkeys"}} {
assert_equal {OK} [r hotkeys start METRICS 2 CPU NET SAMPLE 5]
assert_equal {OK} [r hotkeys stop]
set result [r hotkeys get]
set result [lindex [r hotkeys get] 0]
if {[llength $result] > 0 && [lindex $result 0] ne "tracking-active"} {
set result [hotkeys_array_to_dict $result]
}
@ -234,7 +234,7 @@ start_server {tags {"hotkeys"}} {
r eval "redis.call('set', 'x', 2)" 1 x
r eval "redis.call('set', 'x', 3)" 1 x
set result [r hotkeys get]
set result [lindex [r hotkeys get] 0]
set result [dict get $result "by-net-bytes"]
assert [dict exists $result "x"]
assert [dict exists $result "y"]
@ -260,7 +260,7 @@ start_server {tags {"hotkeys"}} {
r exec
assert_equal {OK} [r hotkeys stop]
set result [r hotkeys get]
set result [lindex [r hotkeys get] 0]
assert_equal {OK} [r hotkeys reset]
# Check NET metrics
@ -290,7 +290,7 @@ start_server {tags {"hotkeys"}} {
r exec
assert_equal {OK} [r hotkeys stop]
set result [r hotkeys get]
set result [lindex [r hotkeys get] 0]
assert_equal {OK} [r hotkeys reset]
# Check NET metrics - both keys should be tracked through EVAL commands
@ -306,7 +306,7 @@ start_server {tags {"hotkeys"}} {
r set key1 value1
assert_equal {OK} [r hotkeys stop]
set result [r hotkeys get]
set result [lindex [r hotkeys get] 0]
if {[llength $result] > 0 && [lindex $result 0] ne "tracking-active"} {
set result [hotkeys_array_to_dict $result]
}
@ -354,7 +354,7 @@ start_server {tags {"hotkeys"}} {
assert_equal {OK} [r hotkeys stop]
set result [r hotkeys get]
set result [lindex [r hotkeys get] 0]
assert_not_equal $result {}
# Convert to dict if it's a flat array
@ -407,7 +407,7 @@ start_server {tags {"hotkeys"}} {
}
}
start_server {tags {"hotkeys"}} {
start_server {tags {external:skip "hotkeys"}} {
test {HOTKEYS GET - RESP3 returns map with flat array values for hotkeys} {
r hello 3
@ -415,7 +415,7 @@ start_server {tags {"hotkeys"}} {
r set testkey testvalue
assert_equal {OK} [r hotkeys stop]
set result [r hotkeys get]
set result [lindex [r hotkeys get] 0]
# In RESP3, the outer result is a native map (dict)
assert [dict exists $result "tracking-active"]
@ -446,6 +446,25 @@ start_server {tags {"hotkeys"}} {
assert_equal {OK} [r hotkeys reset]
}
test {HOTKEYS GET - selected-slots returns full range in non-cluster mode} {
assert_equal {OK} [r hotkeys start METRICS 1 CPU]
assert_equal {OK} [r hotkeys stop]
set result [lindex [r hotkeys get] 0]
if {[llength $result] > 0 && [lindex $result 0] ne "tracking-active"} {
set result [hotkeys_array_to_dict $result]
}
set slots [dict get $result "selected-slots"]
# Should return single range [[0, 16383]]
assert_equal 1 [llength $slots]
set range [lindex $slots 0]
assert_equal 2 [llength $range]
assert_equal 0 [lindex $range 0]
assert_equal 16383 [lindex $range 1]
assert_equal {OK} [r hotkeys reset]
}
}
start_cluster 1 0 {tags {external:skip cluster hotkeys}} {
@ -471,18 +490,72 @@ start_cluster 1 0 {tags {external:skip cluster hotkeys}} {
assert_match "*SLOTS parameter already specified*" $err
}
test {HOTKEYS GET - selected-slots field} {
test {HOTKEYS START - Error: invalid slot - negative value} {
catch {R 0 hotkeys start METRICS 1 CPU SLOTS 1 -1} err
assert_match "*Invalid or out of range slot*" $err
}
test {HOTKEYS START - Error: invalid slot - out of range} {
catch {R 0 hotkeys start METRICS 1 CPU SLOTS 1 16384} err
assert_match "*Invalid or out of range slot*" $err
}
test {HOTKEYS START - Error: invalid slot - non-integer} {
catch {R 0 hotkeys start METRICS 1 CPU SLOTS 1 abc} err
assert_match "*Invalid or out of range slot*" $err
}
test {HOTKEYS GET - selected-slots field with individual slots} {
assert_equal {OK} [R 0 hotkeys start METRICS 2 CPU NET SLOTS 2 0 5]
assert_equal {OK} [R 0 hotkeys stop]
set result [R 0 hotkeys get]
set result [lindex [R 0 hotkeys get] 0]
if {[llength $result] > 0 && [lindex $result 0] ne "tracking-active"} {
set result [hotkeys_array_to_dict $result]
}
set slots [dict get $result "selected-slots"]
# Two individual slots should return two 1-element arrays
assert_equal 2 [llength $slots]
assert_equal 0 [lindex $slots 0]
assert_equal 5 [lindex $slots 1]
assert_equal {0} [lindex $slots 0]
assert_equal {5} [lindex $slots 1]
assert_equal {OK} [R 0 hotkeys reset]
}
test {HOTKEYS GET - selected-slots with unordered input slots are sorted} {
# Slots 10,5,1,0,6,2 should become [[0,2], [5,6], [10]]
assert_equal {OK} [R 0 hotkeys start METRICS 1 CPU SLOTS 6 10 5 1 0 6 2]
assert_equal {OK} [R 0 hotkeys stop]
set result [lindex [R 0 hotkeys get] 0]
if {[llength $result] > 0 && [lindex $result 0] ne "tracking-active"} {
set result [hotkeys_array_to_dict $result]
}
set slots [dict get $result "selected-slots"]
assert_equal 3 [llength $slots]
assert_equal {0 2} [lindex $slots 0]
assert_equal {5 6} [lindex $slots 1]
assert_equal {10} [lindex $slots 2]
assert_equal {OK} [R 0 hotkeys reset]
}
test {HOTKEYS GET - selected-slots returns node's slot ranges when no SLOTS specified in cluster mode} {
# In a 1-node cluster, the node owns all slots [0-16383]
assert_equal {OK} [R 0 hotkeys start METRICS 1 CPU]
assert_equal {OK} [R 0 hotkeys stop]
set result [lindex [R 0 hotkeys get] 0]
if {[llength $result] > 0 && [lindex $result 0] ne "tracking-active"} {
set result [hotkeys_array_to_dict $result]
}
set slots [dict get $result "selected-slots"]
# 1-node cluster owns all slots, should return [[0, 16383]]
assert_equal 1 [llength $slots]
set range [lindex $slots 0]
assert_equal 2 [llength $range]
assert_equal 0 [lindex $range 0]
assert_equal 16383 [lindex $range 1]
assert_equal {OK} [R 0 hotkeys reset]
}
@ -492,7 +565,7 @@ start_cluster 1 0 {tags {external:skip cluster hotkeys}} {
R 0 set "{06S}key1" value1
assert_equal {OK} [R 0 hotkeys stop]
set result [R 0 hotkeys get]
set result [lindex [R 0 hotkeys get] 0]
if {[llength $result] > 0 && [lindex $result 0] ne "tracking-active"} {
set result [hotkeys_array_to_dict $result]
}
@ -511,7 +584,7 @@ start_cluster 1 0 {tags {external:skip cluster hotkeys}} {
R 0 set "{06S}key1" value1
assert_equal {OK} [R 0 hotkeys stop]
set result [R 0 hotkeys get]
set result [lindex [R 0 hotkeys get] 0]
if {[llength $result] > 0 && [lindex $result 0] ne "tracking-active"} {
set result [hotkeys_array_to_dict $result]
}
@ -546,7 +619,7 @@ start_cluster 1 0 {tags {external:skip cluster hotkeys}} {
assert_equal {OK} [R 0 hotkeys stop]
set result [R 0 hotkeys get]
set result [lindex [R 0 hotkeys get] 0]
if {[llength $result] > 0 && [lindex $result 0] ne "tracking-active"} {
set result [hotkeys_array_to_dict $result]
}
@ -585,7 +658,7 @@ start_cluster 1 0 {tags {external:skip cluster hotkeys}} {
assert_equal {OK} [R 0 hotkeys stop]
set result [R 0 hotkeys get]
set result [lindex [R 0 hotkeys get] 0]
if {[llength $result] > 0 && [lindex $result 0] ne "tracking-active"} {
set result [hotkeys_array_to_dict $result]
}
@ -618,3 +691,57 @@ start_cluster 1 0 {tags {external:skip cluster hotkeys}} {
}
}
start_cluster 2 0 {tags {external:skip cluster hotkeys}} {
test {HOTKEYS START - Error: slot not handled by this node} {
# In a 2-master cluster, each node handles half the slots.
# Node 0 handles slots 0-8191, Node 1 handles slots 8192-16383.
# Try to use a slot that belongs to node 1 on node 0.
catch {R 0 hotkeys start METRICS 1 CPU SLOTS 1 8192} err
assert_match "*slot 8192 not handled by this node*" $err
catch {R 1 hotkeys start METRICS 1 CPU SLOTS 1 0} err
assert_match "*slot 0 not handled by this node*" $err
}
test {HOTKEYS GET - selected-slots returns each node's slot ranges in multi-node cluster} {
# In a 2-master cluster:
# Node 0 handles slots 0-8191
# Node 1 handles slots 8192-16383
# Test node 0
assert_equal {OK} [R 0 hotkeys start METRICS 1 CPU]
assert_equal {OK} [R 0 hotkeys stop]
set result [lindex [R 0 hotkeys get] 0]
if {[llength $result] > 0 && [lindex $result 0] ne "tracking-active"} {
set result [hotkeys_array_to_dict $result]
}
set slots [dict get $result "selected-slots"]
# Node 0 should return [[0, 8191]]
assert_equal 1 [llength $slots]
set range [lindex $slots 0]
assert_equal 2 [llength $range]
assert_equal 0 [lindex $range 0]
assert_equal 8191 [lindex $range 1]
assert_equal {OK} [R 0 hotkeys reset]
# Test node 1
assert_equal {OK} [R 1 hotkeys start METRICS 1 CPU]
assert_equal {OK} [R 1 hotkeys stop]
set result [lindex [R 1 hotkeys get] 0]
if {[llength $result] > 0 && [lindex $result 0] ne "tracking-active"} {
set result [hotkeys_array_to_dict $result]
}
set slots [dict get $result "selected-slots"]
# Node 1 should return [[8192, 16383]]
assert_equal 1 [llength $slots]
set range [lindex $slots 0]
assert_equal 2 [llength $range]
assert_equal 8192 [lindex $range 0]
assert_equal 16383 [lindex $range 1]
assert_equal {OK} [R 1 hotkeys reset]
}
}