diff --git a/src/commands.def b/src/commands.def index f464491d5..af7201453 100644 --- a/src/commands.def +++ b/src/commands.def @@ -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} }; diff --git a/src/commands/hotkeys-get.json b/src/commands/hotkeys-get.json index 039f34054..3e1b1d126 100644 --- a/src/commands/hotkeys-get.json +++ b/src/commands/hotkeys-get.json @@ -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", diff --git a/src/commands/hotkeys-reset.json b/src/commands/hotkeys-reset.json index 816104e1c..685b7b27b 100644 --- a/src/commands/hotkeys-reset.json +++ b/src/commands/hotkeys-reset.json @@ -11,6 +11,9 @@ "ADMIN", "NOSCRIPT" ], + "command_tips": [ + "REQUEST_POLICY:SPECIAL" + ], "reply_schema": { "const": "OK" } diff --git a/src/commands/hotkeys-start.json b/src/commands/hotkeys-start.json index 92fb0c7fc..fec52c99c 100644 --- a/src/commands/hotkeys-start.json +++ b/src/commands/hotkeys-start.json @@ -11,6 +11,9 @@ "ADMIN", "NOSCRIPT" ], + "command_tips": [ + "REQUEST_POLICY:SPECIAL" + ], "reply_schema": { "const": "OK" }, diff --git a/src/commands/hotkeys-stop.json b/src/commands/hotkeys-stop.json index 856f9f9b7..70c7f2548 100644 --- a/src/commands/hotkeys-stop.json +++ b/src/commands/hotkeys-stop.json @@ -11,6 +11,9 @@ "ADMIN", "NOSCRIPT" ], + "command_tips": [ + "REQUEST_POLICY:SPECIAL" + ], "reply_schema": { "const": "OK" } diff --git a/src/hotkeys.c b/src/hotkeys.c index 16032737d..9713898e4 100644 --- a/src/hotkeys.c +++ b/src/hotkeys.c @@ -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 * * [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 */ diff --git a/src/server.h b/src/server.h index 836216fb0..e6a28a621 100644 --- a/src/server.h +++ b/src/server.h @@ -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); diff --git a/tests/unit/hotkeys.tcl b/tests/unit/hotkeys.tcl index 887a5d4f2..5d44ddd5e 100644 --- a/tests/unit/hotkeys.tcl +++ b/tests/unit/hotkeys.tcl @@ -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] + } +}