summaryrefslogtreecommitdiff
path: root/examples/redis-unstable/tests/unit/cluster/slot-stats.tcl
diff options
context:
space:
mode:
Diffstat (limited to 'examples/redis-unstable/tests/unit/cluster/slot-stats.tcl')
-rw-r--r--examples/redis-unstable/tests/unit/cluster/slot-stats.tcl1169
1 files changed, 1169 insertions, 0 deletions
diff --git a/examples/redis-unstable/tests/unit/cluster/slot-stats.tcl b/examples/redis-unstable/tests/unit/cluster/slot-stats.tcl
new file mode 100644
index 0000000..1123731
--- /dev/null
+++ b/examples/redis-unstable/tests/unit/cluster/slot-stats.tcl
@@ -0,0 +1,1169 @@
+#
+# Copyright (c) 2009-Present, Redis Ltd.
+# All rights reserved.
+#
+# Copyright (c) 2024-present, Valkey contributors.
+# All rights reserved.
+#
+# Licensed under your choice of (a) the Redis Source Available License 2.0
+# (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the
+# GNU Affero General Public License v3 (AGPLv3).
+#
+# Portions of this file are available under BSD3 terms; see REDISCONTRIBUTIONS for more information.
+#
+
+# Integration tests for CLUSTER SLOT-STATS command.
+
+# -----------------------------------------------------------------------------
+# Helper functions for CLUSTER SLOT-STATS test cases.
+# -----------------------------------------------------------------------------
+
+# Converts array RESP response into a dict.
+# This is useful for many test cases, where unnecessary nesting is removed.
+proc convert_array_into_dict {slot_stats} {
+ set res [dict create]
+ foreach slot_stat $slot_stats {
+ # slot_stat is an array of size 2, where 0th index represents (int) slot,
+ # and 1st index represents (map) usage statistics.
+ dict set res [lindex $slot_stat 0] [lindex $slot_stat 1]
+ }
+ return $res
+}
+
+proc get_cmdstat_usec {cmd r} {
+ set cmdstatline [cmdrstat $cmd r]
+ regexp "usec=(.*?),usec_per_call=(.*?),rejected_calls=0,failed_calls=0" $cmdstatline -> usec _
+ return $usec
+}
+
+proc initialize_expected_slots_dict {} {
+ set expected_slots [dict create]
+ for {set i 0} {$i < 16384} {incr i 1} {
+ dict set expected_slots $i 0
+ }
+ return $expected_slots
+}
+
+proc initialize_expected_slots_dict_with_range {start_slot end_slot} {
+ assert {$start_slot <= $end_slot}
+ set expected_slots [dict create]
+ for {set i $start_slot} {$i <= $end_slot} {incr i 1} {
+ dict set expected_slots $i 0
+ }
+ return $expected_slots
+}
+
+proc assert_empty_slot_stats {slot_stats metrics_to_assert} {
+ set slot_stats [convert_array_into_dict $slot_stats]
+ dict for {slot stats} $slot_stats {
+ foreach metric_name $metrics_to_assert {
+ set metric_value [dict get $stats $metric_name]
+ assert {$metric_value == 0}
+ }
+ }
+}
+
+proc assert_empty_slot_stats_with_exception {slot_stats exception_slots metrics_to_assert} {
+ set slot_stats [convert_array_into_dict $slot_stats]
+ dict for {slot stats} $exception_slots {
+ assert {[dict exists $slot_stats $slot]} ;# slot_stats must contain the expected slots.
+ }
+ dict for {slot stats} $slot_stats {
+ if {[dict exists $exception_slots $slot]} {
+ foreach metric_name $metrics_to_assert {
+ set metric_value [dict get $exception_slots $slot $metric_name]
+ assert {[dict get $stats $metric_name] == $metric_value}
+ }
+ } else {
+ dict for {metric value} $stats {
+ assert {$value == 0}
+ }
+ }
+ }
+}
+
+proc assert_equal_slot_stats {slot_stats_1 slot_stats_2 deterministic_metrics non_deterministic_metrics} {
+ set slot_stats_1 [convert_array_into_dict $slot_stats_1]
+ set slot_stats_2 [convert_array_into_dict $slot_stats_2]
+ assert {[dict size $slot_stats_1] == [dict size $slot_stats_2]}
+
+ dict for {slot stats_1} $slot_stats_1 {
+ assert {[dict exists $slot_stats_2 $slot]}
+ set stats_2 [dict get $slot_stats_2 $slot]
+
+ # For deterministic metrics, we assert their equality.
+ foreach metric $deterministic_metrics {
+ assert {[dict get $stats_1 $metric] == [dict get $stats_2 $metric]}
+ }
+ # For non-deterministic metrics, we assert their non-zeroness as a best-effort.
+ foreach metric $non_deterministic_metrics {
+ assert {([dict get $stats_1 $metric] == 0 && [dict get $stats_2 $metric] == 0) || \
+ ([dict get $stats_1 $metric] != 0 && [dict get $stats_2 $metric] != 0)}
+ }
+ }
+}
+
+proc assert_all_slots_have_been_seen {expected_slots} {
+ dict for {k v} $expected_slots {
+ assert {$v == 1}
+ }
+}
+
+proc assert_slot_visibility {slot_stats expected_slots} {
+ set slot_stats [convert_array_into_dict $slot_stats]
+ dict for {slot _} $slot_stats {
+ assert {[dict exists $expected_slots $slot]}
+ dict set expected_slots $slot 1
+ }
+
+ assert_all_slots_have_been_seen $expected_slots
+}
+
+proc assert_slot_stats_monotonic_order {slot_stats orderby is_desc} {
+ # For Tcl dict, the order of iteration is the order in which the keys were inserted into the dictionary
+ # Thus, the response ordering is preserved upon calling 'convert_array_into_dict()'.
+ # Source: https://www.tcl.tk/man/tcl8.6.11/TclCmd/dict.htm
+ set slot_stats [convert_array_into_dict $slot_stats]
+ set prev_metric -1
+ dict for {_ stats} $slot_stats {
+ set curr_metric [dict get $stats $orderby]
+ if {$prev_metric != -1} {
+ if {$is_desc == 1} {
+ assert {$prev_metric >= $curr_metric}
+ } else {
+ assert {$prev_metric <= $curr_metric}
+ }
+ }
+ set prev_metric $curr_metric
+ }
+}
+
+proc assert_slot_stats_monotonic_descent {slot_stats orderby} {
+ assert_slot_stats_monotonic_order $slot_stats $orderby 1
+}
+
+proc assert_slot_stats_monotonic_ascent {slot_stats orderby} {
+ assert_slot_stats_monotonic_order $slot_stats $orderby 0
+}
+
+proc wait_for_replica_key_exists {key key_count} {
+ wait_for_condition 1000 50 {
+ [R 1 exists $key] eq "$key_count"
+ } else {
+ fail "Test key was not replicated"
+ }
+}
+
+# -----------------------------------------------------------------------------
+# Test cases for CLUSTER SLOT-STATS cpu-usec metric correctness.
+# -----------------------------------------------------------------------------
+
+start_cluster 1 0 {tags {external:skip cluster} overrides {cluster-slot-stats-enabled yes}} {
+
+ # Define shared variables.
+ set key "FOO"
+ set key_slot [R 0 cluster keyslot $key]
+ set key_secondary "FOO2"
+ set key_secondary_slot [R 0 cluster keyslot $key_secondary]
+ set metrics_to_assert [list cpu-usec]
+
+ test "CLUSTER SLOT-STATS cpu-usec reset upon CONFIG RESETSTAT." {
+ R 0 SET $key VALUE
+ R 0 DEL $key
+ R 0 CONFIG RESETSTAT
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ assert_empty_slot_stats $slot_stats $metrics_to_assert
+ }
+ R 0 CONFIG RESETSTAT
+ R 0 FLUSHALL
+
+ test "CLUSTER SLOT-STATS cpu-usec reset upon slot migration." {
+ R 0 SET $key VALUE
+
+ R 0 CLUSTER DELSLOTS $key_slot
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ assert_empty_slot_stats $slot_stats $metrics_to_assert
+
+ R 0 CLUSTER ADDSLOTS $key_slot
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ assert_empty_slot_stats $slot_stats $metrics_to_assert
+ }
+ R 0 CONFIG RESETSTAT
+ R 0 FLUSHALL
+
+ test "CLUSTER SLOT-STATS cpu-usec for non-slot specific commands." {
+ R 0 INFO
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ assert_empty_slot_stats $slot_stats $metrics_to_assert
+ }
+ R 0 CONFIG RESETSTAT
+ R 0 FLUSHALL
+
+ test "CLUSTER SLOT-STATS cpu-usec for slot specific commands." {
+ R 0 SET $key VALUE
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ set usec [get_cmdstat_usec set r]
+ set expected_slot_stats [
+ dict create $key_slot [
+ dict create cpu-usec $usec
+ ]
+ ]
+ assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
+ }
+ R 0 CONFIG RESETSTAT
+ R 0 FLUSHALL
+
+ test "CLUSTER SLOT-STATS cpu-usec for blocking commands, unblocked on keyspace update." {
+ # Blocking command with no timeout. Only keyspace update can unblock this client.
+ set rd [redis_deferring_client]
+ $rd BLPOP $key 0
+ wait_for_blocked_clients_count 1
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ # When the client is blocked, no accumulation is made. This behaviour is identical to INFO COMMANDSTATS.
+ assert_empty_slot_stats $slot_stats $metrics_to_assert
+
+ # Unblocking command.
+ R 0 LPUSH $key value
+ wait_for_blocked_clients_count 0
+
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ set lpush_usec [get_cmdstat_usec lpush r]
+ set blpop_usec [get_cmdstat_usec blpop r]
+
+ # Assert that both blocking and non-blocking command times have been accumulated.
+ set expected_slot_stats [
+ dict create $key_slot [
+ dict create cpu-usec [expr $lpush_usec + $blpop_usec]
+ ]
+ ]
+ assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
+ }
+ R 0 CONFIG RESETSTAT
+ R 0 FLUSHALL
+
+ test "CLUSTER SLOT-STATS cpu-usec for blocking commands, unblocked on timeout." {
+ # Blocking command with 0.5 seconds timeout.
+ set rd [redis_deferring_client]
+ $rd BLPOP $key 0.5
+
+ # Confirm that the client is blocked, then unblocked within 1 second.
+ wait_for_blocked_clients_count 1
+ wait_for_blocked_clients_count 0
+
+ # Assert that the blocking command time has been accumulated.
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ set blpop_usec [get_cmdstat_usec blpop r]
+ set expected_slot_stats [
+ dict create $key_slot [
+ dict create cpu-usec $blpop_usec
+ ]
+ ]
+ assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
+ }
+ R 0 CONFIG RESETSTAT
+ R 0 FLUSHALL
+
+ test "CLUSTER SLOT-STATS cpu-usec for transactions." {
+ set r1 [redis_client]
+ $r1 MULTI
+ $r1 SET $key value
+ $r1 GET $key
+
+ # CPU metric is not accumulated until EXEC is reached. This behaviour is identical to INFO COMMANDSTATS.
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ assert_empty_slot_stats $slot_stats $metrics_to_assert
+
+ # Execute transaction, and assert that all nested command times have been accumulated.
+ $r1 EXEC
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ set exec_usec [get_cmdstat_usec exec r]
+ set expected_slot_stats [
+ dict create $key_slot [
+ dict create cpu-usec $exec_usec
+ ]
+ ]
+ assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
+ }
+ R 0 CONFIG RESETSTAT
+ R 0 FLUSHALL
+
+ test "CLUSTER SLOT-STATS cpu-usec for lua-scripts, without cross-slot keys." {
+ R 0 eval {#!lua
+ redis.call('set', KEYS[1], 'bar') redis.call('get', KEYS[2])
+ } 2 $key $key
+
+ set eval_usec [get_cmdstat_usec eval r]
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+
+ set expected_slot_stats [
+ dict create $key_slot [
+ dict create cpu-usec $eval_usec
+ ]
+ ]
+ assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
+ }
+ R 0 CONFIG RESETSTAT
+ R 0 FLUSHALL
+
+ test "CLUSTER SLOT-STATS cpu-usec for lua-scripts, with cross-slot keys." {
+ R 0 eval {#!lua flags=allow-cross-slot-keys
+ redis.call('set', KEYS[1], 'bar') redis.call('get', ARGV[1])
+ } 1 $key $key_secondary
+
+ # For cross-slot, we do not accumulate at all.
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ assert_empty_slot_stats $slot_stats $metrics_to_assert
+ }
+ R 0 CONFIG RESETSTAT
+ R 0 FLUSHALL
+
+ test "CLUSTER SLOT-STATS cpu-usec for functions, without cross-slot keys." {
+ R 0 function load replace {#!lua name=f1
+ redis.register_function{
+ function_name='f1',
+ callback=function(keys, args) redis.call('set', keys[1], '1') redis.call('get', keys[2]) end
+ }
+ }
+ R 0 fcall f1 2 $key $key
+
+ set fcall_usec [get_cmdstat_usec fcall r]
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+
+ set expected_slot_stats [
+ dict create $key_slot [
+ dict create cpu-usec $fcall_usec
+ ]
+ ]
+ assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
+ }
+ R 0 CONFIG RESETSTAT
+ R 0 FLUSHALL
+
+ test "CLUSTER SLOT-STATS cpu-usec for functions, with cross-slot keys." {
+ R 0 function load replace {#!lua name=f1
+ redis.register_function{
+ function_name='f1',
+ callback=function(keys, args) redis.call('set', keys[1], '1') redis.call('get', args[1]) end,
+ flags={'allow-cross-slot-keys'}
+ }
+ }
+ R 0 fcall f1 1 $key $key_secondary
+
+ # For cross-slot, we do not accumulate at all.
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ assert_empty_slot_stats $slot_stats $metrics_to_assert
+ }
+ R 0 CONFIG RESETSTAT
+ R 0 FLUSHALL
+}
+
+# -----------------------------------------------------------------------------
+# Test cases for CLUSTER SLOT-STATS network-bytes-in.
+# -----------------------------------------------------------------------------
+
+start_cluster 1 0 {tags {external:skip cluster} overrides {cluster-slot-stats-enabled yes}} {
+
+ # Define shared variables.
+ set key "key"
+ set key_slot [R 0 cluster keyslot $key]
+ set metrics_to_assert [list network-bytes-in]
+
+ test "CLUSTER SLOT-STATS network-bytes-in, multi bulk buffer processing." {
+ # *3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n --> 33 bytes.
+ R 0 SET $key value
+
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ set expected_slot_stats [
+ dict create $key_slot [
+ dict create network-bytes-in 33
+ ]
+ ]
+ assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
+ }
+ R 0 CONFIG RESETSTAT
+ R 0 FLUSHALL
+
+ test "CLUSTER SLOT-STATS network-bytes-in, in-line buffer processing." {
+ set rd [redis_deferring_client]
+ # SET key value\r\n --> 15 bytes.
+ $rd write "SET $key value\r\n"
+ $rd flush
+
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ set expected_slot_stats [
+ dict create $key_slot [
+ dict create network-bytes-in 15
+ ]
+ ]
+
+ assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
+ }
+ R 0 CONFIG RESETSTAT
+ R 0 FLUSHALL
+
+ test "CLUSTER SLOT-STATS network-bytes-in, blocking command." {
+ set rd [redis_deferring_client]
+ # *3\r\n$5\r\nblpop\r\n$3\r\nkey\r\n$1\r\n0\r\n --> 31 bytes.
+ $rd BLPOP $key 0
+ wait_for_blocked_clients_count 1
+
+ # Slot-stats must be empty here, as the client is yet to be unblocked.
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ assert_empty_slot_stats $slot_stats $metrics_to_assert
+
+ # *3\r\n$5\r\nlpush\r\n$3\r\nkey\r\n$5\r\nvalue\r\n --> 35 bytes.
+ R 0 LPUSH $key value
+ wait_for_blocked_clients_count 0
+
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ set expected_slot_stats [
+ dict create $key_slot [
+ dict create network-bytes-in 66 ;# 31 + 35 bytes.
+ ]
+ ]
+
+ assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
+ }
+ R 0 CONFIG RESETSTAT
+ R 0 FLUSHALL
+
+ test "CLUSTER SLOT-STATS network-bytes-in, multi-exec transaction." {
+ set r [redis_client]
+ # *1\r\n$5\r\nmulti\r\n --> 15 bytes.
+ $r MULTI
+ # *3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n --> 33 bytes.
+ assert {[$r SET $key value] eq {QUEUED}}
+ # *1\r\n$4\r\nexec\r\n --> 14 bytes.
+ assert {[$r EXEC] eq {OK}}
+
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ set expected_slot_stats [
+ dict create $key_slot [
+ dict create network-bytes-in 62 ;# 15 + 33 + 14 bytes.
+ ]
+ ]
+
+ assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
+ }
+ R 0 CONFIG RESETSTAT
+ R 0 FLUSHALL
+
+ test "CLUSTER SLOT-STATS network-bytes-in, non slot specific command." {
+ R 0 INFO
+
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ assert_empty_slot_stats $slot_stats $metrics_to_assert
+ }
+ R 0 CONFIG RESETSTAT
+ R 0 FLUSHALL
+
+ test "CLUSTER SLOT-STATS network-bytes-in, pub/sub." {
+ # PUB/SUB does not get accumulated at per-slot basis,
+ # as it is cluster-wide and is not slot specific.
+ set rd [redis_deferring_client]
+ $rd subscribe channel
+ R 0 publish channel message
+
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ assert_empty_slot_stats $slot_stats $metrics_to_assert
+ }
+ R 0 CONFIG RESETSTAT
+ R 0 FLUSHALL
+}
+
+start_cluster 1 1 {tags {external:skip cluster} overrides {cluster-slot-stats-enabled yes}} {
+ set channel "channel"
+ set key_slot [R 0 cluster keyslot $channel]
+ set metrics_to_assert [list network-bytes-in]
+
+ # Setup replication.
+ assert {[s -1 role] eq {slave}}
+ wait_for_condition 1000 50 {
+ [s -1 master_link_status] eq {up}
+ } else {
+ fail "Instance #1 master link status is not up"
+ }
+ R 1 readonly
+
+ test "CLUSTER SLOT-STATS network-bytes-in, sharded pub/sub." {
+ set slot [R 0 cluster keyslot $channel]
+ set primary [Rn 0]
+ set replica [Rn 1]
+ set replica_subcriber [redis_deferring_client -1]
+ $replica_subcriber SSUBSCRIBE $channel
+ # *2\r\n$10\r\nssubscribe\r\n$7\r\nchannel\r\n --> 34 bytes.
+ $primary SPUBLISH $channel hello
+ # *3\r\n$8\r\nspublish\r\n$7\r\nchannel\r\n$5\r\nhello\r\n --> 42 bytes.
+
+ set slot_stats [$primary CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ set expected_slot_stats [
+ dict create $key_slot [
+ dict create network-bytes-in 42
+ ]
+ ]
+ assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
+
+ set slot_stats [$replica CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ set expected_slot_stats [
+ dict create $key_slot [
+ dict create network-bytes-in 34
+ ]
+ ]
+ assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
+ }
+ R 0 CONFIG RESETSTAT
+ R 0 FLUSHALL
+}
+
+# -----------------------------------------------------------------------------
+# Test cases for CLUSTER SLOT-STATS network-bytes-out correctness.
+# -----------------------------------------------------------------------------
+
+start_cluster 1 0 {tags {external:skip cluster}} {
+ # Define shared variables.
+ set key "FOO"
+ set key_slot [R 0 cluster keyslot $key]
+ set expected_slots_to_key_count [dict create $key_slot 1]
+ set metrics_to_assert [list network-bytes-out]
+ R 0 CONFIG SET cluster-slot-stats-enabled yes
+
+ test "CLUSTER SLOT-STATS network-bytes-out, for non-slot specific commands." {
+ R 0 INFO
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ assert_empty_slot_stats $slot_stats $metrics_to_assert
+ }
+ R 0 CONFIG RESETSTAT
+ R 0 FLUSHALL
+
+ test "CLUSTER SLOT-STATS network-bytes-out, for slot specific commands." {
+ R 0 SET $key value
+ # +OK\r\n --> 5 bytes
+
+ set expected_slot_stats [
+ dict create $key_slot [
+ dict create network-bytes-out 5
+ ]
+ ]
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
+ }
+ R 0 CONFIG RESETSTAT
+ R 0 FLUSHALL
+
+ test "CLUSTER SLOT-STATS network-bytes-out, blocking commands." {
+ set rd [redis_deferring_client]
+ $rd BLPOP $key 0
+ wait_for_blocked_clients_count 1
+
+ # Assert empty slot stats here, since COB is yet to be flushed due to the block.
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ assert_empty_slot_stats $slot_stats $metrics_to_assert
+
+ # Unblock the command.
+ # LPUSH client) :1\r\n --> 4 bytes.
+ # BLPOP client) *2\r\n$3\r\nkey\r\n$5\r\nvalue\r\n --> 24 bytes, upon unblocking.
+ R 0 LPUSH $key value
+ wait_for_blocked_clients_count 0
+
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ set expected_slot_stats [
+ dict create $key_slot [
+ dict create network-bytes-out 28 ;# 4 + 24 bytes.
+ ]
+ ]
+ assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
+ }
+ R 0 CONFIG RESETSTAT
+ R 0 FLUSHALL
+}
+
+start_cluster 1 1 {tags {external:skip cluster}} {
+
+ # Define shared variables.
+ set key "FOO"
+ set key_slot [R 0 CLUSTER KEYSLOT $key]
+ set metrics_to_assert [list network-bytes-out]
+ R 0 CONFIG SET cluster-slot-stats-enabled yes
+
+ # Setup replication.
+ assert {[s -1 role] eq {slave}}
+ wait_for_condition 1000 50 {
+ [s -1 master_link_status] eq {up}
+ } else {
+ fail "Instance #1 master link status is not up"
+ }
+ R 1 readonly
+
+ test "CLUSTER SLOT-STATS network-bytes-out, replication stream egress." {
+ assert_equal [R 0 SET $key VALUE] {OK}
+ # Local client) +OK\r\n --> 5 bytes.
+ # Replication stream) *3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n --> 33 bytes.
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ set expected_slot_stats [
+ dict create $key_slot [
+ dict create network-bytes-out 38 ;# 5 + 33 bytes.
+ ]
+ ]
+ assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
+ }
+}
+
+start_cluster 1 1 {tags {external:skip cluster}} {
+
+ # Define shared variables.
+ set channel "channel"
+ set key_slot [R 0 cluster keyslot $channel]
+ set channel_secondary "channel2"
+ set key_slot_secondary [R 0 cluster keyslot $channel_secondary]
+ set metrics_to_assert [list network-bytes-out]
+ R 0 CONFIG SET cluster-slot-stats-enabled yes
+
+ test "CLUSTER SLOT-STATS network-bytes-out, sharded pub/sub, single channel." {
+ set slot [R 0 cluster keyslot $channel]
+ set publisher [Rn 0]
+ set subscriber [redis_client]
+ set replica [redis_deferring_client -1]
+
+ # Subscriber client) *3\r\n$10\r\nssubscribe\r\n$7\r\nchannel\r\n:1\r\n --> 38 bytes
+ $subscriber SSUBSCRIBE $channel
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ set expected_slot_stats [
+ dict create $key_slot [
+ dict create network-bytes-out 38
+ ]
+ ]
+ R 0 CONFIG RESETSTAT
+
+ # Publisher client) :1\r\n --> 4 bytes.
+ # Subscriber client) *3\r\n$8\r\nsmessage\r\n$7\r\nchannel\r\n$5\r\nhello\r\n --> 42 bytes.
+ assert_equal 1 [$publisher SPUBLISH $channel hello]
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ set expected_slot_stats [
+ dict create $key_slot [
+ dict create network-bytes-out 46 ;# 4 + 42 bytes.
+ ]
+ ]
+ assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
+ }
+ $subscriber QUIT
+ R 0 FLUSHALL
+ R 0 CONFIG RESETSTAT
+
+ test "CLUSTER SLOT-STATS network-bytes-out, sharded pub/sub, cross-slot channels." {
+ set slot [R 0 cluster keyslot $channel]
+ set publisher [Rn 0]
+ set subscriber [redis_client]
+ set replica [redis_deferring_client -1]
+
+ # Stack multi-slot subscriptions against a single client.
+ # For primary channel;
+ # Subscriber client) *3\r\n$10\r\nssubscribe\r\n$7\r\nchannel\r\n:1\r\n --> 38 bytes
+ # For secondary channel;
+ # Subscriber client) *3\r\n$10\r\nssubscribe\r\n$8\r\nchannel2\r\n:1\r\n --> 39 bytes
+ $subscriber SSUBSCRIBE $channel
+ $subscriber SSUBSCRIBE $channel_secondary
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ set expected_slot_stats [
+ dict create \
+ $key_slot [ \
+ dict create network-bytes-out 38
+ ] \
+ $key_slot_secondary [ \
+ dict create network-bytes-out 39
+ ]
+ ]
+ R 0 CONFIG RESETSTAT
+
+ # For primary channel;
+ # Publisher client) :1\r\n --> 4 bytes.
+ # Subscriber client) *3\r\n$8\r\nsmessage\r\n$7\r\nchannel\r\n$5\r\nhello\r\n --> 42 bytes.
+ # For secondary channel;
+ # Publisher client) :1\r\n --> 4 bytes.
+ # Subscriber client) *3\r\n$8\r\nsmessage\r\n$8\r\nchannel2\r\n$5\r\nhello\r\n --> 43 bytes.
+ assert_equal 1 [$publisher SPUBLISH $channel hello]
+ assert_equal 1 [$publisher SPUBLISH $channel_secondary hello]
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ set expected_slot_stats [
+ dict create \
+ $key_slot [ \
+ dict create network-bytes-out 46 ;# 4 + 42 bytes.
+ ] \
+ $key_slot_secondary [ \
+ dict create network-bytes-out 47 ;# 4 + 43 bytes.
+ ]
+ ]
+ assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
+ }
+}
+
+# -----------------------------------------------------------------------------
+# Test cases for CLUSTER SLOT-STATS key-count metric correctness.
+# -----------------------------------------------------------------------------
+
+start_cluster 1 0 {tags {external:skip cluster} overrides {cluster-slot-stats-enabled yes}} {
+
+ # Define shared variables.
+ set key "FOO"
+ set key_slot [R 0 cluster keyslot $key]
+ set metrics_to_assert [list key-count]
+ set expected_slot_stats [
+ dict create $key_slot [
+ dict create key-count 1
+ ]
+ ]
+
+ test "CLUSTER SLOT-STATS contains default value upon redis-server startup" {
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ assert_empty_slot_stats $slot_stats $metrics_to_assert
+ }
+
+ test "CLUSTER SLOT-STATS contains correct metrics upon key introduction" {
+ R 0 SET $key TEST
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
+ }
+
+ test "CLUSTER SLOT-STATS contains correct metrics upon key mutation" {
+ R 0 SET $key NEW_VALUE
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
+ }
+
+ test "CLUSTER SLOT-STATS contains correct metrics upon key deletion" {
+ R 0 DEL $key
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ assert_empty_slot_stats $slot_stats $metrics_to_assert
+ }
+
+ test "CLUSTER SLOT-STATS slot visibility based on slot ownership changes" {
+ R 0 CONFIG SET cluster-require-full-coverage no
+
+ R 0 CLUSTER DELSLOTS $key_slot
+ set expected_slots [initialize_expected_slots_dict]
+ dict unset expected_slots $key_slot
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ assert {[dict size $expected_slots] == 16383}
+ assert_slot_visibility $slot_stats $expected_slots
+
+ R 0 CLUSTER ADDSLOTS $key_slot
+ set expected_slots [initialize_expected_slots_dict]
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ assert {[dict size $expected_slots] == 16384}
+ assert_slot_visibility $slot_stats $expected_slots
+ }
+}
+
+# -----------------------------------------------------------------------------
+# Test cases for CLUSTER SLOT-STATS SLOTSRANGE sub-argument.
+# -----------------------------------------------------------------------------
+
+start_cluster 1 0 {tags {external:skip cluster}} {
+
+ test "CLUSTER SLOT-STATS SLOTSRANGE all slots present" {
+ set start_slot 100
+ set end_slot 102
+ set expected_slots [initialize_expected_slots_dict_with_range $start_slot $end_slot]
+
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE $start_slot $end_slot]
+ assert_slot_visibility $slot_stats $expected_slots
+ }
+
+ test "CLUSTER SLOT-STATS SLOTSRANGE some slots missing" {
+ set start_slot 100
+ set end_slot 102
+ set expected_slots [initialize_expected_slots_dict_with_range $start_slot $end_slot]
+
+ R 0 CLUSTER DELSLOTS $start_slot
+ dict unset expected_slots $start_slot
+
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE $start_slot $end_slot]
+ assert_slot_visibility $slot_stats $expected_slots
+ }
+}
+
+# -----------------------------------------------------------------------------
+# Test cases for CLUSTER SLOT-STATS ORDERBY sub-argument.
+# -----------------------------------------------------------------------------
+
+start_cluster 1 0 {tags {external:skip cluster} overrides {cluster-slot-stats-enabled yes}} {
+
+ set metrics [list "key-count" "memory-bytes" "cpu-usec" "network-bytes-in" "network-bytes-out"]
+
+ # SET keys for target hashslots, to encourage ordering.
+ set hash_tags [list 0 1 2 3 4]
+ set num_keys 1
+ foreach hash_tag $hash_tags {
+ for {set i 0} {$i < $num_keys} {incr i 1} {
+ R 0 SET "$i{$hash_tag}" VALUE
+ }
+ incr num_keys 1
+ }
+
+ # SET keys for random hashslots, for random noise.
+ set num_keys 0
+ while {$num_keys < 1000} {
+ set random_key [randomInt 16384]
+ R 0 SET $random_key VALUE
+ incr num_keys 1
+ }
+
+ test "CLUSTER SLOT-STATS ORDERBY DESC correct ordering" {
+ foreach orderby $metrics {
+ set slot_stats [R 0 CLUSTER SLOT-STATS ORDERBY $orderby DESC]
+ assert_slot_stats_monotonic_descent $slot_stats $orderby
+ }
+ }
+
+ test "CLUSTER SLOT-STATS ORDERBY ASC correct ordering" {
+ foreach orderby $metrics {
+ set slot_stats [R 0 CLUSTER SLOT-STATS ORDERBY $orderby ASC]
+ assert_slot_stats_monotonic_ascent $slot_stats $orderby
+ }
+ }
+
+ test "CLUSTER SLOT-STATS ORDERBY LIMIT correct response pagination, where limit is less than number of assigned slots" {
+ R 0 FLUSHALL SYNC
+ R 0 CONFIG RESETSTAT
+
+ foreach orderby $metrics {
+ set limit 5
+ set slot_stats_desc [R 0 CLUSTER SLOT-STATS ORDERBY $orderby LIMIT $limit DESC]
+ set slot_stats_asc [R 0 CLUSTER SLOT-STATS ORDERBY $orderby LIMIT $limit ASC]
+ set slot_stats_desc_length [llength $slot_stats_desc]
+ set slot_stats_asc_length [llength $slot_stats_asc]
+ assert {$limit == $slot_stats_desc_length && $limit == $slot_stats_asc_length}
+
+ # All slot statistics have been reset to 0, so we will order by slot in ascending order.
+ set expected_slots [dict create 0 0 1 0 2 0 3 0 4 0]
+ assert_slot_visibility $slot_stats_desc $expected_slots
+ assert_slot_visibility $slot_stats_asc $expected_slots
+ }
+ }
+
+ test "CLUSTER SLOT-STATS ORDERBY LIMIT correct response pagination, where limit is greater than number of assigned slots" {
+ R 0 CONFIG SET cluster-require-full-coverage no
+ R 0 FLUSHALL SYNC
+ R 0 CLUSTER FLUSHSLOTS
+ R 0 CLUSTER ADDSLOTS 100 101
+
+ foreach orderby $metrics {
+ set num_assigned_slots 2
+ set limit 5
+ set slot_stats_desc [R 0 CLUSTER SLOT-STATS ORDERBY $orderby LIMIT $limit DESC]
+ set slot_stats_asc [R 0 CLUSTER SLOT-STATS ORDERBY $orderby LIMIT $limit ASC]
+ set slot_stats_desc_length [llength $slot_stats_desc]
+ set slot_stats_asc_length [llength $slot_stats_asc]
+ set expected_response_length [expr min($num_assigned_slots, $limit)]
+ assert {$expected_response_length == $slot_stats_desc_length && $expected_response_length == $slot_stats_asc_length}
+
+ set expected_slots [dict create 100 0 101 0]
+ assert_slot_visibility $slot_stats_desc $expected_slots
+ assert_slot_visibility $slot_stats_asc $expected_slots
+ }
+ }
+
+ test "CLUSTER SLOT-STATS ORDERBY arg sanity check." {
+ # Non-existent argument.
+ assert_error "ERR*" {R 0 CLUSTER SLOT-STATS ORDERBY key-count non-existent-arg}
+ # Negative LIMIT.
+ assert_error "ERR*" {R 0 CLUSTER SLOT-STATS ORDERBY key-count DESC LIMIT -1}
+ # Non-existent ORDERBY metric.
+ assert_error "ERR*" {R 0 CLUSTER SLOT-STATS ORDERBY non-existent-metric}
+ # When cluster-slot-stats-enabled config is disabled, you cannot sort using advanced metrics.
+ R 0 CONFIG SET cluster-slot-stats-enabled no
+ set orderby "cpu-usec"
+ assert_error "ERR*" {R 0 CLUSTER SLOT-STATS ORDERBY $orderby}
+ set orderby "network-bytes-in"
+ assert_error "ERR*" {R 0 CLUSTER SLOT-STATS ORDERBY $orderby}
+ set orderby "network-bytes-out"
+ assert_error "ERR*" {R 0 CLUSTER SLOT-STATS ORDERBY $orderby}
+ set orderby "memory-bytes"
+ assert_error "ERR*" {R 0 CLUSTER SLOT-STATS ORDERBY $orderby}
+
+ # When only cpu net is enabled, memory-bytes ORDERBY should fail
+ R 0 CONFIG SET cluster-slot-stats-enabled "cpu net"
+ assert_error "ERR*" {R 0 CLUSTER SLOT-STATS ORDERBY memory-bytes}
+ }
+
+}
+
+# -----------------------------------------------------------------------------
+# Test cases for CLUSTER SLOT-STATS replication.
+# -----------------------------------------------------------------------------
+
+start_cluster 1 1 {tags {external:skip cluster} overrides {cluster-slot-stats-enabled yes}} {
+
+ # Define shared variables.
+ set key "key"
+ set key_slot [R 0 CLUSTER KEYSLOT $key]
+ set primary [Rn 0]
+ set replica [Rn 1]
+
+ # For replication, assertions are split between deterministic and non-deterministic metrics.
+ # * For deterministic metrics, strict equality assertions are made.
+ # * For non-deterministic metrics, non-zeroness assertions are made.
+ # Non-zeroness as in, both primary and replica should either have some value, or no value at all.
+ #
+ # * key-count is deterministic between primary and its replica.
+ # * cpu-usec is non-deterministic between primary and its replica.
+ # * network-bytes-in is deterministic between primary and its replica.
+ # * network-bytes-out will remain empty in the replica, since primary client do not receive replies, unless for replicationSendAck().
+ set deterministic_metrics [list key-count network-bytes-in]
+ set non_deterministic_metrics [list cpu-usec]
+ set empty_metrics [list network-bytes-out]
+
+ # Setup replication.
+ assert {[s -1 role] eq {slave}}
+ wait_for_condition 1000 50 {
+ [s -1 master_link_status] eq {up}
+ } else {
+ fail "Instance #1 master link status is not up"
+ }
+ R 1 readonly
+
+ test "CLUSTER SLOT-STATS metrics replication for new keys" {
+ # *3\r\n$3\r\nset\r\n$3\r\nkey\r\n$5\r\nvalue\r\n --> 33 bytes.
+ R 0 SET $key VALUE
+
+ set expected_slot_stats [
+ dict create $key_slot [
+ dict create key-count 1 network-bytes-in 33
+ ]
+ ]
+ set slot_stats_master [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ assert_empty_slot_stats_with_exception $slot_stats_master $expected_slot_stats $deterministic_metrics
+
+ wait_for_condition 500 10 {
+ [string match {*calls=1,*} [cmdrstat set $replica]]
+ } else {
+ fail "Replica did not receive the command."
+ }
+ set slot_stats_replica [R 1 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ assert_equal_slot_stats $slot_stats_master $slot_stats_replica $deterministic_metrics $non_deterministic_metrics
+ assert_empty_slot_stats $slot_stats_replica $empty_metrics
+ }
+ R 0 CONFIG RESETSTAT
+ R 1 CONFIG RESETSTAT
+
+ test "CLUSTER SLOT-STATS metrics replication for existing keys" {
+ # *3\r\n$3\r\nset\r\n$3\r\nkey\r\n$13\r\nvalue_updated\r\n --> 42 bytes.
+ R 0 SET $key VALUE_UPDATED
+
+ set expected_slot_stats [
+ dict create $key_slot [
+ dict create key-count 1 network-bytes-in 42
+ ]
+ ]
+ set slot_stats_master [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ assert_empty_slot_stats_with_exception $slot_stats_master $expected_slot_stats $deterministic_metrics
+
+ wait_for_condition 500 10 {
+ [string match {*calls=1,*} [cmdrstat set $replica]]
+ } else {
+ fail "Replica did not receive the command."
+ }
+ set slot_stats_replica [R 1 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ assert_equal_slot_stats $slot_stats_master $slot_stats_replica $deterministic_metrics $non_deterministic_metrics
+ assert_empty_slot_stats $slot_stats_replica $empty_metrics
+ }
+ R 0 CONFIG RESETSTAT
+ R 1 CONFIG RESETSTAT
+
+ test "CLUSTER SLOT-STATS metrics replication for deleting keys" {
+ # *2\r\n$3\r\ndel\r\n$3\r\nkey\r\n --> 22 bytes.
+ R 0 DEL $key
+
+ set expected_slot_stats [
+ dict create $key_slot [
+ dict create key-count 0 network-bytes-in 22
+ ]
+ ]
+ set slot_stats_master [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ assert_empty_slot_stats_with_exception $slot_stats_master $expected_slot_stats $deterministic_metrics
+
+ wait_for_condition 500 10 {
+ [string match {*calls=1,*} [cmdrstat del $replica]]
+ } else {
+ fail "Replica did not receive the command."
+ }
+ set slot_stats_replica [R 1 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ assert_equal_slot_stats $slot_stats_master $slot_stats_replica $deterministic_metrics $non_deterministic_metrics
+ assert_empty_slot_stats $slot_stats_replica $empty_metrics
+ }
+ R 0 CONFIG RESETSTAT
+ R 1 CONFIG RESETSTAT
+}
+
+start_cluster 2 2 {tags {external:skip cluster} overrides {cluster-slot-stats-enabled yes}} {
+ test "CLUSTER SLOT-STATS reset upon atomic slot migration" {
+ # key on slot-0
+ set key0 "{06S}mykey0"
+ set key0_slot [R 0 CLUSTER KEYSLOT $key0]
+ R 0 SET $key0 VALUE
+
+ # Migrate slot-0 to node-1
+ R 1 CLUSTER MIGRATION IMPORT 0 0
+ wait_for_condition 1000 10 {
+ [CI 0 cluster_slot_migration_active_tasks] == 0 &&
+ [CI 1 cluster_slot_migration_active_tasks] == 0
+ } else {
+ fail "ASM tasks did not complete"
+ }
+
+ set expected_slot_stats [
+ dict create \
+ $key0_slot [ \
+ dict create key-count 1 \
+ dict create cpu-usec 0 \
+ dict create network-bytes-in 0 \
+ dict create network-bytes-out 0 \
+ ]
+ ]
+ set metrics_to_assert [list key-count cpu-usec network-bytes-in network-bytes-out]
+
+ # Verify metrics are reset except key-count
+ set slot_stats [R 1 CLUSTER SLOT-STATS SLOTSRANGE 0 0]
+ assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
+
+ # Migrate slot-0 back to node-0
+ R 0 CLUSTER MIGRATION IMPORT 0 0
+ wait_for_condition 1000 10 {
+ [CI 0 cluster_slot_migration_active_tasks] == 0 &&
+ [CI 1 cluster_slot_migration_active_tasks] == 0
+ } else {
+ fail "ASM tasks did not complete"
+ }
+
+ # Verify metrics are reset except key-count
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 0]
+ assert_empty_slot_stats_with_exception $slot_stats $expected_slot_stats $metrics_to_assert
+ }
+}
+
+# -----------------------------------------------------------------------------
+# Test cases for CLUSTER SLOT-STATS memory-bytes field presence.
+# -----------------------------------------------------------------------------
+
+start_cluster 1 0 {tags {external:skip cluster} overrides {cluster-slot-stats-enabled yes}} {
+ # Define shared variables.
+ set key "FOO"
+ set key_slot [R 0 cluster keyslot $key]
+
+ test "CLUSTER SLOT-STATS memory-bytes field present when cluster-slot-stats-enabled set on startup" {
+ R 0 SET $key VALUE
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ set slot_stats [convert_array_into_dict $slot_stats]
+
+ # Verify memory-bytes field is present
+ assert {[dict exists $slot_stats $key_slot]}
+ set stats [dict get $slot_stats $key_slot]
+ assert {[dict exists $stats memory-bytes]}
+ assert {[dict get $stats memory-bytes] > 0}
+ }
+
+ test "CLUSTER SLOT-STATS net mem combination shows only net and mem stats" {
+ R 0 CONFIG SET cluster-slot-stats-enabled "net mem"
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ set slot_stats [convert_array_into_dict $slot_stats]
+
+ set stats [dict get $slot_stats $key_slot]
+ assert {[dict exists $stats memory-bytes]}
+ assert {[dict exists $stats network-bytes-in]}
+ assert {[dict exists $stats network-bytes-out]}
+ assert {![dict exists $stats cpu-usec]}
+ }
+
+ test "CLUSTER SLOT-STATS cpu mem combination shows only cpu and mem stats" {
+ R 0 CONFIG SET cluster-slot-stats-enabled "cpu mem"
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ set slot_stats [convert_array_into_dict $slot_stats]
+
+ set stats [dict get $slot_stats $key_slot]
+ assert {[dict exists $stats memory-bytes]}
+ assert {[dict exists $stats cpu-usec]}
+ assert {![dict exists $stats network-bytes-in]}
+ assert {![dict exists $stats network-bytes-out]}
+
+ # Restore to yes for subsequent tests
+ R 0 CONFIG SET cluster-slot-stats-enabled yes
+ }
+
+ test "CLUSTER SLOT-STATS memory-bytes field not present after disabling cluster-slot-stats-enabled" {
+ R 0 CONFIG SET cluster-slot-stats-enabled no
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ set slot_stats [convert_array_into_dict $slot_stats]
+
+ # Verify memory-bytes field is not present after disabling config
+ # (memory tracking is disabled when MEM flag is removed)
+ assert {[dict exists $slot_stats $key_slot]}
+ set stats [dict get $slot_stats $key_slot]
+ assert {![dict exists $stats memory-bytes]}
+
+ # Verify other stats fields are not present
+ assert {![dict exists $stats cpu-usec]}
+ assert {![dict exists $stats network-bytes-in]}
+ assert {![dict exists $stats network-bytes-out]}
+ }
+
+ test "CLUSTER SLOT-STATS memory tracking cannot be re-enabled after being disabled" {
+ # Once memory tracking is disabled, it cannot be re-enabled at runtime
+ assert_error "ERR*memory tracking cannot be enabled at runtime*" {R 0 CONFIG SET cluster-slot-stats-enabled yes}
+ assert_error "ERR*memory tracking cannot be enabled at runtime*" {R 0 CONFIG SET cluster-slot-stats-enabled mem}
+
+ # But cpu and net can still be enabled
+ R 0 CONFIG SET cluster-slot-stats-enabled "cpu net"
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ set slot_stats [convert_array_into_dict $slot_stats]
+
+ assert {[dict exists $slot_stats $key_slot]}
+ set stats [dict get $slot_stats $key_slot]
+ assert {![dict exists $stats memory-bytes]}
+ assert {[dict exists $stats cpu-usec]}
+ assert {[dict exists $stats network-bytes-in]}
+ assert {[dict exists $stats network-bytes-out]}
+ }
+}
+
+start_cluster 1 0 {tags {external:skip cluster} overrides {cluster-slot-stats-enabled no}} {
+ # Define shared variables.
+ set key "FOO"
+ set key_slot [R 0 cluster keyslot $key]
+
+ test "CLUSTER SLOT-STATS memory-bytes field not present when cluster-slot-stats-enabled not set on startup" {
+ R 0 SET $key VALUE
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ set slot_stats [convert_array_into_dict $slot_stats]
+
+ # Verify memory-bytes field is not present
+ assert {[dict exists $slot_stats $key_slot]}
+ set stats [dict get $slot_stats $key_slot]
+ assert {![dict exists $stats memory-bytes]}
+
+ # Only key-count should be present
+ assert {[dict exists $stats key-count]}
+ assert {[dict get $stats key-count] == 1}
+ }
+
+ test "CLUSTER SLOT-STATS enabling mem at runtime fails when not enabled at startup" {
+ # Trying to enable memory tracking at runtime should fail
+ assert_error "ERR*memory tracking cannot be enabled at runtime*" {R 0 CONFIG SET cluster-slot-stats-enabled mem}
+ assert_error "ERR*memory tracking cannot be enabled at runtime*" {R 0 CONFIG SET cluster-slot-stats-enabled yes}
+ assert_error "ERR*memory tracking cannot be enabled at runtime*" {R 0 CONFIG SET cluster-slot-stats-enabled "cpu net mem"}
+ }
+
+ test "CLUSTER SLOT-STATS enabling cpu and net at runtime works" {
+ R 0 CONFIG SET cluster-slot-stats-enabled "cpu net"
+ set slot_stats [R 0 CLUSTER SLOT-STATS SLOTSRANGE 0 16383]
+ set slot_stats [convert_array_into_dict $slot_stats]
+
+ # Verify memory-bytes field is still not present
+ assert {[dict exists $slot_stats $key_slot]}
+ set stats [dict get $slot_stats $key_slot]
+ assert {![dict exists $stats memory-bytes]}
+
+ # Other stats fields should now be present
+ assert {[dict exists $stats cpu-usec]}
+ assert {[dict exists $stats network-bytes-in]}
+ assert {[dict exists $stats network-bytes-out]}
+ }
+}