Change reply schema for hotkeys get to use map instead of flat array (#14749)

Follow #14680
Reply of `HOTKEYS GET` is an unordered collection of key-value pairs. It
is more reasonable to be a map in resp3 instead of flat array.
This commit is contained in:
Mincho Paskalev 2026-01-29 11:21:05 +02:00 committed by GitHub
parent 319153fe46
commit 591fc90263
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 141 additions and 33 deletions

View file

@ -14,31 +14,98 @@
"reply_schema": {
"oneOf": [
{
"description": "Flat array with various metrics(tracking-activated, 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": {
"oneOf": [
{
"type": "string"
},
{
"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"
},
{
"type": "array",
"items": {
"oneOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
}
}
]
}
"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"
},
{
"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

@ -474,7 +474,7 @@ void hotkeysCommand(client *c) {
int has_selected_slots = (server.hotkeys->numslots > 0);
int has_sampling = (server.hotkeys->sample_ratio > 1);
int total_len = 14;
int total_len = 7;
void *arraylenptr = addReplyDeferredLen(c);
/* tracking-active */
@ -497,7 +497,7 @@ void hotkeysCommand(client *c) {
addReplyBulkCString(c, "sampled-command-selected-slots-us");
addReplyLongLong(c, server.hotkeys->time_sampled_commands_selected_slots);
total_len += 2;
total_len++;
}
/* all-commands-selected-slots-us (conditional) */
@ -505,7 +505,7 @@ void hotkeysCommand(client *c) {
addReplyBulkCString(c, "all-commands-selected-slots-us");
addReplyLongLong(c, server.hotkeys->time_all_commands_selected_slots);
total_len += 2;
++total_len;
}
/* all-commands-all-slots-us */
@ -517,7 +517,7 @@ void hotkeysCommand(client *c) {
addReplyBulkCString(c, "net-bytes-sampled-commands-selected-slots");
addReplyLongLong(c, server.hotkeys->net_bytes_sampled_commands_selected_slots);
total_len += 2;
++total_len;
}
/* net-bytes-all-commands-selected-slots (conditional) */
@ -526,7 +526,7 @@ void hotkeysCommand(client *c) {
addReplyLongLong(c,
server.hotkeys->net_bytes_all_commands_selected_slots);
total_len += 2;
++total_len;
}
/* net-bytes-all-commands-all-slots */
@ -550,7 +550,7 @@ void hotkeysCommand(client *c) {
addReplyBulkCString(c, "total-cpu-time-sys-ms");
addReplyLongLong(c, total_cpu_sys_msec);
total_len += 4;
total_len += 2;
}
/* total-net-bytes - only if NET tracking is enabled */
@ -558,7 +558,7 @@ void hotkeysCommand(client *c) {
addReplyBulkCString(c, "total-net-bytes");
addReplyLongLong(c, total_net_bytes);
total_len += 2;
++total_len;
}
/* by-cpu-time-us - only if CPU tracking is enabled */
@ -573,7 +573,7 @@ void hotkeysCommand(client *c) {
}
zfree(cpu);
total_len += 2;
++total_len;
}
/* by-net-bytes - only if NET tracking is enabled */
@ -588,10 +588,10 @@ void hotkeysCommand(client *c) {
}
zfree(net);
total_len += 2;
++total_len;
}
setDeferredArrayLen(c, arraylenptr, total_len);
setDeferredMapLen(c, arraylenptr, total_len);
} else if (!strcasecmp(sub, "RESET")) {
/* HOTKEYS RESET */

View file

@ -351,6 +351,47 @@ start_server {tags {"hotkeys"}} {
}
}
start_server {tags {"hotkeys"}} {
test {HOTKEYS GET - RESP3 returns map with flat array values for hotkeys} {
r hello 3
assert_equal {OK} [r hotkeys start METRICS 2 CPU NET]
r set testkey testvalue
assert_equal {OK} [r hotkeys stop]
set result [r hotkeys get]
# In RESP3, the outer result is a native map (dict)
assert [dict exists $result "tracking-active"]
assert [dict exists $result "sample-ratio"]
assert [dict exists $result "selected-slots"]
assert [dict exists $result "by-cpu-time-us"]
assert [dict exists $result "by-net-bytes"]
# Verify by-cpu-time-us is a flat array [key1, val1, key2, val2, ...]
set cpu_array [dict get $result "by-cpu-time-us"]
# Flat array length should be even (key-value pairs)
assert {[llength $cpu_array] % 2 == 0}
# First element is the key name (string), second is the value (integer)
set first_key [lindex $cpu_array 0]
set first_val [lindex $cpu_array 1]
assert_equal "testkey" $first_key
assert {[string is integer $first_val]}
# Verify by-net-bytes is a flat array [key1, val1, key2, val2, ...]
set net_array [dict get $result "by-net-bytes"]
# Flat array length should be even (key-value pairs)
assert {[llength $net_array] % 2 == 0}
# First element is the key name (string), second is the value (integer)
set first_key [lindex $net_array 0]
set first_val [lindex $net_array 1]
assert_equal "testkey" $first_key
assert {[string is integer $first_val]}
assert_equal {OK} [r hotkeys reset]
}
}
start_cluster 1 0 {tags {external:skip cluster hotkeys}} {
test {HOTKEYS START - with SLOTS parameter in cluster mode} {