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 #ifndef SKIP_CMD_TIPS_TABLE
/* HOTKEYS GET tips */ /* HOTKEYS GET tips */
#define HOTKEYS_GET_Tips NULL const char *HOTKEYS_GET_Tips[] = {
"nondeterministic_output",
"request_policy:special",
"response_policy:special",
};
#endif #endif
#ifndef SKIP_CMD_KEY_SPECS_TABLE #ifndef SKIP_CMD_KEY_SPECS_TABLE
@ -7337,7 +7341,9 @@ struct COMMAND_ARG FLUSHDB_Args[] = {
#ifndef SKIP_CMD_TIPS_TABLE #ifndef SKIP_CMD_TIPS_TABLE
/* HOTKEYS RESET tips */ /* HOTKEYS RESET tips */
#define HOTKEYS_RESET_Tips NULL const char *HOTKEYS_RESET_Tips[] = {
"request_policy:special",
};
#endif #endif
#ifndef SKIP_CMD_KEY_SPECS_TABLE #ifndef SKIP_CMD_KEY_SPECS_TABLE
@ -7354,7 +7360,9 @@ struct COMMAND_ARG FLUSHDB_Args[] = {
#ifndef SKIP_CMD_TIPS_TABLE #ifndef SKIP_CMD_TIPS_TABLE
/* HOTKEYS START tips */ /* HOTKEYS START tips */
#define HOTKEYS_START_Tips NULL const char *HOTKEYS_START_Tips[] = {
"request_policy:special",
};
#endif #endif
#ifndef SKIP_CMD_KEY_SPECS_TABLE #ifndef SKIP_CMD_KEY_SPECS_TABLE
@ -7393,7 +7401,9 @@ struct COMMAND_ARG HOTKEYS_START_Args[] = {
#ifndef SKIP_CMD_TIPS_TABLE #ifndef SKIP_CMD_TIPS_TABLE
/* HOTKEYS STOP tips */ /* HOTKEYS STOP tips */
#define HOTKEYS_STOP_Tips NULL const char *HOTKEYS_STOP_Tips[] = {
"request_policy:special",
};
#endif #endif
#ifndef SKIP_CMD_KEY_SPECS_TABLE #ifndef SKIP_CMD_KEY_SPECS_TABLE
@ -7403,10 +7413,10 @@ struct COMMAND_ARG HOTKEYS_START_Args[] = {
/* HOTKEYS command table */ /* HOTKEYS command table */
struct COMMAND_STRUCT HOTKEYS_Subcommands[] = { 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("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,0,hotkeysCommand,2,CMD_ADMIN|CMD_NOSCRIPT,0,HOTKEYS_RESET_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,0,hotkeysCommand,-2,CMD_ADMIN|CMD_NOSCRIPT,0,HOTKEYS_START_Keyspecs,0,NULL,5),.args=HOTKEYS_START_Args}, {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,0,hotkeysCommand,2,CMD_ADMIN|CMD_NOSCRIPT,0,HOTKEYS_STOP_Keyspecs,0,NULL,0)}, {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} {0}
}; };

View file

@ -11,101 +11,114 @@
"ADMIN", "ADMIN",
"NOSCRIPT" "NOSCRIPT"
], ],
"command_tips": [
"NONDETERMINISTIC_OUTPUT",
"REQUEST_POLICY:SPECIAL",
"RESPONSE_POLICY:SPECIAL"
],
"reply_schema": { "reply_schema": {
"oneOf": [ "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.", "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": "object", "type": "array",
"properties": { "items": {
"tracking-active": { "type": "object",
"type": "integer", "properties": {
"description": "Whether hotkey tracking is currently active (1) or stopped (0)." "tracking-active": {
}, "type": "integer",
"sample-ratio": { "description": "Whether hotkey tracking is currently active (1) or stopped (0)."
"type": "integer",
"description": "The sampling ratio used for tracking."
},
"selected-slots": {
"type": "array",
"items": {
"type": "integer"
}, },
"description": "Array of slot numbers being tracked (empty if tracking all slots)." "sample-ratio": {
}, "type": "integer",
"sampled-command-selected-slots-us": { "description": "The sampling ratio used for tracking."
"type": "integer", },
"description": "CPU time in microseconds for sampled commands in selected slots (only present when sampling and slots are configured)." "selected-slots": {
}, "type": "array",
"all-commands-selected-slots-us": { "items": {
"type": "integer", "type": "array",
"description": "CPU time in microseconds for all commands in selected slots (only present when slots are configured)." "items": {
},
"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" "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"
}, },
{ "minItems": 1,
"type": "integer" "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)." "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)."
"additionalProperties": false },
"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", "description": "If no tracking is started",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,7 +9,7 @@ proc hotkeys_array_to_dict {arr} {
return $result return $result
} }
start_server {tags {"hotkeys"}} { start_server {tags {external:skip "hotkeys"}} {
test {HOTKEYS START - METRICS required} { test {HOTKEYS START - METRICS required} {
catch {r hotkeys start} err catch {r hotkeys start} err
assert_match "*METRICS parameter is required*" $err assert_match "*METRICS parameter is required*" $err
@ -20,7 +20,7 @@ start_server {tags {"hotkeys"}} {
r set key1 value1 r set key1 value1
assert_equal {OK} [r hotkeys stop] 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"} { if {[llength $result] > 0 && [lindex $result 0] ne "tracking-active"} {
set result [hotkeys_array_to_dict $result] set result [hotkeys_array_to_dict $result]
} }
@ -39,7 +39,7 @@ start_server {tags {"hotkeys"}} {
r set key1 value1 r set key1 value1
assert_equal {OK} [r hotkeys stop] 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"} { if {[llength $result] > 0 && [lindex $result 0] ne "tracking-active"} {
set result [hotkeys_array_to_dict $result] set result [hotkeys_array_to_dict $result]
} }
@ -58,7 +58,7 @@ start_server {tags {"hotkeys"}} {
r set key1 value1 r set key1 value1
assert_equal {OK} [r hotkeys stop] 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"} { if {[llength $result] > 0 && [lindex $result 0] ne "tracking-active"} {
set result [hotkeys_array_to_dict $result] set result [hotkeys_array_to_dict $result]
} }
@ -126,7 +126,7 @@ start_server {tags {"hotkeys"}} {
assert_equal {OK} [r hotkeys stop] 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"} { if {[llength $result] > 0 && [lindex $result 0] ne "tracking-active"} {
set result [hotkeys_array_to_dict $result] 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] assert_equal {OK} [r hotkeys start METRICS 1 CPU DURATION 1]
after 1500 after 1500
set result [r hotkeys get] set result [lindex [r hotkeys get] 0]
if {[llength $result] > 0 && [lindex $result 0] eq "tracking-active"} { if {[llength $result] > 0 && [lindex $result 0] eq "tracking-active"} {
set result [hotkeys_array_to_dict $result] 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 start METRICS 2 CPU NET]
assert_equal {OK} [r hotkeys stop] 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"} { if {[llength $result] > 0 && [lindex $result 0] ne "tracking-active"} {
set result [hotkeys_array_to_dict $result] 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 stop]
assert_equal {OK} [r hotkeys reset] assert_equal {OK} [r hotkeys reset]
# After reset, GET should return nil # After reset, GET should return nil
set result [r hotkeys get] set result [lindex [r hotkeys get] 0]
assert_equal {} $result assert_equal {} $result
} }
@ -210,7 +210,7 @@ start_server {tags {"hotkeys"}} {
} }
test {HOTKEYS GET - returns nil when not started} { test {HOTKEYS GET - returns nil when not started} {
set result [r hotkeys get] set result [lindex [r hotkeys get] 0]
assert_equal {} $result 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 start METRICS 2 CPU NET SAMPLE 5]
assert_equal {OK} [r hotkeys stop] 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"} { if {[llength $result] > 0 && [lindex $result 0] ne "tracking-active"} {
set result [hotkeys_array_to_dict $result] 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', 2)" 1 x
r eval "redis.call('set', 'x', 3)" 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"] set result [dict get $result "by-net-bytes"]
assert [dict exists $result "x"] assert [dict exists $result "x"]
assert [dict exists $result "y"] assert [dict exists $result "y"]
@ -260,7 +260,7 @@ start_server {tags {"hotkeys"}} {
r exec r exec
assert_equal {OK} [r hotkeys stop] assert_equal {OK} [r hotkeys stop]
set result [r hotkeys get] set result [lindex [r hotkeys get] 0]
assert_equal {OK} [r hotkeys reset] assert_equal {OK} [r hotkeys reset]
# Check NET metrics # Check NET metrics
@ -290,7 +290,7 @@ start_server {tags {"hotkeys"}} {
r exec r exec
assert_equal {OK} [r hotkeys stop] assert_equal {OK} [r hotkeys stop]
set result [r hotkeys get] set result [lindex [r hotkeys get] 0]
assert_equal {OK} [r hotkeys reset] assert_equal {OK} [r hotkeys reset]
# Check NET metrics - both keys should be tracked through EVAL commands # Check NET metrics - both keys should be tracked through EVAL commands
@ -306,7 +306,7 @@ start_server {tags {"hotkeys"}} {
r set key1 value1 r set key1 value1
assert_equal {OK} [r hotkeys stop] 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"} { if {[llength $result] > 0 && [lindex $result 0] ne "tracking-active"} {
set result [hotkeys_array_to_dict $result] set result [hotkeys_array_to_dict $result]
} }
@ -354,7 +354,7 @@ start_server {tags {"hotkeys"}} {
assert_equal {OK} [r hotkeys stop] assert_equal {OK} [r hotkeys stop]
set result [r hotkeys get] set result [lindex [r hotkeys get] 0]
assert_not_equal $result {} assert_not_equal $result {}
# Convert to dict if it's a flat array # 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} { test {HOTKEYS GET - RESP3 returns map with flat array values for hotkeys} {
r hello 3 r hello 3
@ -415,7 +415,7 @@ start_server {tags {"hotkeys"}} {
r set testkey testvalue r set testkey testvalue
assert_equal {OK} [r hotkeys stop] 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) # In RESP3, the outer result is a native map (dict)
assert [dict exists $result "tracking-active"] assert [dict exists $result "tracking-active"]
@ -446,6 +446,25 @@ start_server {tags {"hotkeys"}} {
assert_equal {OK} [r hotkeys reset] 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}} { 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 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 start METRICS 2 CPU NET SLOTS 2 0 5]
assert_equal {OK} [R 0 hotkeys stop] 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"} { if {[llength $result] > 0 && [lindex $result 0] ne "tracking-active"} {
set result [hotkeys_array_to_dict $result] set result [hotkeys_array_to_dict $result]
} }
set slots [dict get $result "selected-slots"] set slots [dict get $result "selected-slots"]
# Two individual slots should return two 1-element arrays
assert_equal 2 [llength $slots] assert_equal 2 [llength $slots]
assert_equal 0 [lindex $slots 0] assert_equal {0} [lindex $slots 0]
assert_equal 5 [lindex $slots 1] 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] 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 R 0 set "{06S}key1" value1
assert_equal {OK} [R 0 hotkeys stop] 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"} { if {[llength $result] > 0 && [lindex $result 0] ne "tracking-active"} {
set result [hotkeys_array_to_dict $result] 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 R 0 set "{06S}key1" value1
assert_equal {OK} [R 0 hotkeys stop] 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"} { if {[llength $result] > 0 && [lindex $result 0] ne "tracking-active"} {
set result [hotkeys_array_to_dict $result] 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] 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"} { if {[llength $result] > 0 && [lindex $result 0] ne "tracking-active"} {
set result [hotkeys_array_to_dict $result] 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] 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"} { if {[llength $result] > 0 && [lindex $result 0] ne "tracking-active"} {
set result [hotkeys_array_to_dict $result] 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]
}
}