feat(commands): Add support for Redis HOTKEYS commands (#3695)

* Add support for Redis HOTKEYS commands

* Skip hotkeys tests for redis version lower than 8.6

* chore(image): use proper ci image for 8.6

* feat(command): hotkeys wip

* fix(tests): cluster hotkeys

* chore(lint): fmt

* Ensure HOTKEYS commands are only available on standalone Client and NOT on ClusterClient, Ring, or pooled clients

* fix(hotkeys): address pr comments

* chore(ci): use latest build

* fix(hotkeys): fix typo in response

---------

Co-authored-by: Nedyalko Dyakov <[email protected]>
Co-authored-by: Nedyalko Dyakov <[email protected]>
Co-authored-by: Elena Kolevska <[email protected]>
This commit is contained in:
ofekshenawa
2026-02-11 18:08:28 +02:00
committed by GitHub
co-authored by Nedyalko Dyakov Nedyalko Dyakov Elena Kolevska
parent 34f4568e4f
commit 33ca5cb864
9 changed files with 591 additions and 10 deletions
+1 -1
View File
@@ -24,7 +24,7 @@ runs:
# Mapping of redis version to redis testing containers
declare -A redis_version_mapping=(
["8.6.x"]="custom-21183968220-debian-amd64"
["8.6.x"]="custom-21860421418-debian-amd64"
["8.4.x"]="8.4.0"
["8.2.x"]="8.2.1-pre"
["8.0.x"]="8.0.2"
+1 -1
View File
@@ -47,7 +47,7 @@ jobs:
# Mapping of redis version to redis testing containers
declare -A redis_version_mapping=(
["8.6.x"]="custom-21183968220-debian-amd64"
["8.6.x"]="custom-21860421418-debian-amd64"
["8.4.x"]="8.4.0"
["8.2.x"]="8.2.1-pre"
["8.0.x"]="8.0.2"
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
services:
redis-stack:
image: redislabs/client-libs-test:custom-21183968220-debian-amd64
image: redislabs/client-libs-test:custom-21860421418-debian-amd64
env:
TLS_ENABLED: no
REDIS_CLUSTER: no
+1 -1
View File
@@ -2,7 +2,7 @@ GO_MOD_DIRS := $(shell find . -type f -name 'go.mod' -exec dirname {} \; | sort)
REDIS_VERSION ?= 8.6
RE_CLUSTER ?= false
RCE_DOCKER ?= true
CLIENT_LIBS_TEST_IMAGE ?= redislabs/client-libs-test:custom-21183968220-debian-amd64
CLIENT_LIBS_TEST_IMAGE ?= redislabs/client-libs-test:custom-21860421418-debian-amd64
docker.start:
export RE_CLUSTER=$(RE_CLUSTER) && \
+246
View File
@@ -37,6 +37,7 @@ var keylessCommands = map[string]struct{}{
"failover": {},
"function": {},
"hello": {},
"hotkeys": {},
"latency": {},
"lolwut": {},
"module": {},
@@ -151,6 +152,7 @@ const (
CmdTypeFTSearch
CmdTypeTSTimestampValue
CmdTypeTSTimestampValueSlice
CmdTypeHotKeys
)
type (
@@ -4910,6 +4912,243 @@ func (cmd *LatencyCmd) Clone() Cmder {
//-----------------------------------------------------------------------
// HotKeysSlotRange represents a slot or slot range in the response.
// Single element slice = individual slot, two element slice = slot range [start, end].
type HotKeysSlotRange []int64
// HotKeysKeyEntry represents a hot key entry with its metric value.
type HotKeysKeyEntry struct {
Key string
Value interface{} // Can be int64 or string
}
// HotKeysResult represents the response data from HOTKEYS GET command.
// Field names match the Redis response format.
type HotKeysResult struct {
TrackingActive bool
SampleRatio uint8
SelectedSlots []HotKeysSlotRange
SampledCommandsSelectedSlots time.Duration // Present when sample-ratio > 1 and selected-slots is not empty
AllCommandsSelectedSlots time.Duration // Present when selected-slots is not empty
AllCommandsAllSlots time.Duration
NetBytesSampledCommandsSelectedSlots int64 // Present when sample-ratio > 1 and selected-slots is not empty
NetBytesAllCommandsSelectedSlots int64 // Present when selected-slots is not empty
NetBytesAllCommandsAllSlots int64
CollectionStartTime time.Time
CollectionDuration time.Duration
UsedCPUSys time.Duration
UsedCPUUser time.Duration
TotalNetBytes int64
ByCPUTime []HotKeysKeyEntry
ByNetBytes []HotKeysKeyEntry
}
type HotKeysCmd struct {
baseCmd
val *HotKeysResult
}
var _ Cmder = (*HotKeysCmd)(nil)
func NewHotKeysCmd(ctx context.Context, args ...interface{}) *HotKeysCmd {
return &HotKeysCmd{
baseCmd: baseCmd{
ctx: ctx,
args: args,
cmdType: CmdTypeHotKeys,
},
}
}
func (cmd *HotKeysCmd) SetVal(val *HotKeysResult) {
cmd.val = val
}
func (cmd *HotKeysCmd) Val() *HotKeysResult {
return cmd.val
}
func (cmd *HotKeysCmd) Result() (*HotKeysResult, error) {
return cmd.val, cmd.err
}
func (cmd *HotKeysCmd) String() string {
return cmdString(cmd, cmd.val)
}
func (cmd *HotKeysCmd) readReply(rd *proto.Reader) error {
// HOTKEYS GET response is wrapped in an array for aggregation support
arrayLen, err := rd.ReadArrayLen()
if err != nil {
return err
}
if arrayLen == 0 {
// Empty array means no tracking was started or after reset
cmd.val = nil
return nil
}
// Read the first (and typically only) element which is a map
n, err := rd.ReadMapLen()
if err != nil {
return err
}
result := &HotKeysResult{}
data := make(map[string]interface{}, n)
for i := 0; i < n; i++ {
k, err := rd.ReadString()
if err != nil {
return err
}
v, err := rd.ReadReply()
if err != nil {
if err == Nil {
data[k] = Nil
continue
}
if err, ok := err.(proto.RedisError); ok {
data[k] = err
continue
}
return err
}
data[k] = v
}
if v, ok := data["tracking-active"].(int64); ok {
result.TrackingActive = v == 1
}
if v, ok := data["sample-ratio"].(int64); ok {
result.SampleRatio = uint8(v)
}
if v, ok := data["selected-slots"].([]interface{}); ok {
result.SelectedSlots = make([]HotKeysSlotRange, 0, len(v))
for _, slot := range v {
switch s := slot.(type) {
case int64:
// Single slot
result.SelectedSlots = append(result.SelectedSlots, HotKeysSlotRange{s})
case []interface{}:
// Slot range
slotRange := make(HotKeysSlotRange, 0, len(s))
for _, sr := range s {
if val, ok := sr.(int64); ok {
slotRange = append(slotRange, val)
}
}
result.SelectedSlots = append(result.SelectedSlots, slotRange)
}
}
}
if v, ok := data["sampled-commands-selected-slots-us"].(int64); ok {
result.SampledCommandsSelectedSlots = time.Duration(v) * time.Microsecond
}
if v, ok := data["all-commands-selected-slots-us"].(int64); ok {
result.AllCommandsSelectedSlots = time.Duration(v) * time.Microsecond
}
if v, ok := data["all-commands-all-slots-us"].(int64); ok {
result.AllCommandsAllSlots = time.Duration(v) * time.Microsecond
}
if v, ok := data["net-bytes-sampled-commands-selected-slots"].(int64); ok {
result.NetBytesSampledCommandsSelectedSlots = v
}
if v, ok := data["net-bytes-all-commands-selected-slots"].(int64); ok {
result.NetBytesAllCommandsSelectedSlots = v
}
if v, ok := data["net-bytes-all-commands-all-slots"].(int64); ok {
result.NetBytesAllCommandsAllSlots = v
}
if v, ok := data["collection-start-time-unix-ms"].(int64); ok {
result.CollectionStartTime = time.UnixMilli(v)
}
if v, ok := data["collection-duration-ms"].(int64); ok {
result.CollectionDuration = time.Duration(v) * time.Millisecond
}
if v, ok := data["used-cpu-sys-ms"].(int64); ok {
result.UsedCPUSys = time.Duration(v) * time.Millisecond
}
if v, ok := data["used-cpu-user-ms"].(int64); ok {
result.UsedCPUUser = time.Duration(v) * time.Millisecond
}
if v, ok := data["total-net-bytes"].(int64); ok {
result.TotalNetBytes = v
}
if v, ok := data["by-cpu-time-us"].([]interface{}); ok {
result.ByCPUTime = parseHotKeysKeyEntries(v)
}
if v, ok := data["by-net-bytes"].([]interface{}); ok {
result.ByNetBytes = parseHotKeysKeyEntries(v)
}
cmd.val = result
return nil
}
// parseHotKeysKeyEntries parses the key-value pairs from HOTKEYS GET response.
func parseHotKeysKeyEntries(v []interface{}) []HotKeysKeyEntry {
entries := make([]HotKeysKeyEntry, 0, len(v)/2)
for i := 0; i < len(v); i += 2 {
if i+1 < len(v) {
key, keyOk := v[i].(string)
if keyOk {
entries = append(entries, HotKeysKeyEntry{
Key: key,
Value: v[i+1], // Can be int64 or string
})
}
}
}
return entries
}
func (cmd *HotKeysCmd) Clone() Cmder {
var val *HotKeysResult
if cmd.val != nil {
val = &HotKeysResult{
TrackingActive: cmd.val.TrackingActive,
SampleRatio: cmd.val.SampleRatio,
SampledCommandsSelectedSlots: cmd.val.SampledCommandsSelectedSlots,
AllCommandsSelectedSlots: cmd.val.AllCommandsSelectedSlots,
AllCommandsAllSlots: cmd.val.AllCommandsAllSlots,
NetBytesSampledCommandsSelectedSlots: cmd.val.NetBytesSampledCommandsSelectedSlots,
NetBytesAllCommandsSelectedSlots: cmd.val.NetBytesAllCommandsSelectedSlots,
NetBytesAllCommandsAllSlots: cmd.val.NetBytesAllCommandsAllSlots,
CollectionStartTime: cmd.val.CollectionStartTime,
CollectionDuration: cmd.val.CollectionDuration,
UsedCPUSys: cmd.val.UsedCPUSys,
UsedCPUUser: cmd.val.UsedCPUUser,
TotalNetBytes: cmd.val.TotalNetBytes,
}
if cmd.val.SelectedSlots != nil {
val.SelectedSlots = make([]HotKeysSlotRange, len(cmd.val.SelectedSlots))
for i, sr := range cmd.val.SelectedSlots {
val.SelectedSlots[i] = make(HotKeysSlotRange, len(sr))
copy(val.SelectedSlots[i], sr)
}
}
if cmd.val.ByCPUTime != nil {
val.ByCPUTime = make([]HotKeysKeyEntry, len(cmd.val.ByCPUTime))
copy(val.ByCPUTime, cmd.val.ByCPUTime)
}
if cmd.val.ByNetBytes != nil {
val.ByNetBytes = make([]HotKeysKeyEntry, len(cmd.val.ByNetBytes))
copy(val.ByNetBytes, cmd.val.ByNetBytes)
}
}
return &HotKeysCmd{
baseCmd: cmd.cloneBaseCmd(),
val: val,
}
}
//-----------------------------------------------------------------------
type MapStringInterfaceCmd struct {
baseCmd
@@ -7481,6 +7720,13 @@ func ExtractCommandValue(cmd interface{}) (interface{}, error) {
}); ok {
return slowLogCmd.Val(), slowLogCmd.Err()
}
case CmdTypeHotKeys:
if hotKeysCmd, ok := cmd.(interface {
Val() *HotKeysResult
Err() error
}); ok {
return hotKeysCmd.Val(), hotKeysCmd.Err()
}
case CmdTypeKeyValues:
if keyValuesCmd, ok := cmd.(interface {
Val() (string, []string)
+5 -5
View File
@@ -2,7 +2,7 @@
services:
redis:
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:custom-21183968220-debian-amd64}
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:custom-21860421418-debian-amd64}
platform: linux/amd64
container_name: redis-standalone
environment:
@@ -23,7 +23,7 @@ services:
- all
osscluster:
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:custom-21183968220-debian-amd64}
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:custom-21860421418-debian-amd64}
platform: linux/amd64
container_name: redis-osscluster
environment:
@@ -40,7 +40,7 @@ services:
- all
sentinel-cluster:
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:custom-21183968220-debian-amd64}
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:custom-21860421418-debian-amd64}
platform: linux/amd64
container_name: redis-sentinel-cluster
network_mode: "host"
@@ -60,7 +60,7 @@ services:
- all
sentinel:
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:custom-21183968220-debian-amd64}
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:custom-21860421418-debian-amd64}
platform: linux/amd64
container_name: redis-sentinel
depends_on:
@@ -84,7 +84,7 @@ services:
- all
ring-cluster:
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:custom-21183968220-debian-amd64}
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:custom-21860421418-debian-amd64}
platform: linux/amd64
container_name: redis-ring-cluster
environment:
-1
View File
@@ -93,4 +93,3 @@ func BenchmarkDigestBytes(b *testing.B) {
})
}
}
+122
View File
@@ -0,0 +1,122 @@
package redis
import (
"context"
"errors"
"strings"
)
// HOTKEYS commands are only available on standalone *Client instances.
// They are NOT available on ClusterClient, Ring, or UniversalClient because
// HOTKEYS is a stateful command requiring session affinity - all operations
// (START, GET, STOP, RESET) must be sent to the same Redis node.
//
// If you are using UniversalClient and need HOTKEYS functionality, you must
// type assert to *Client first:
//
// if client, ok := universalClient.(*redis.Client); ok {
// result, err := client.HotKeysStart(ctx, args)
// // ...
// }
// HotKeysMetric represents the metrics that can be tracked by the HOTKEYS command.
type HotKeysMetric string
const (
// HotKeysMetricCPU tracks CPU time spent on the key (in microseconds).
HotKeysMetricCPU HotKeysMetric = "CPU"
// HotKeysMetricNET tracks network bytes used by the key (ingress + egress + replication).
HotKeysMetricNET HotKeysMetric = "NET"
)
// HotKeysStartArgs contains the arguments for the HOTKEYS START command.
// This command is only available on standalone clients due to its stateful nature
// requiring session affinity. It must NOT be used on cluster or pooled clients.
type HotKeysStartArgs struct {
// Metrics to track. At least one must be specified.
Metrics []HotKeysMetric
// Count is the number of top keys to report.
// Default: 10, Min: 10, Max: 64
Count uint8
// Duration is the auto-stop tracking after this many seconds.
// Default: 0 (no auto-stop)
Duration int64
// Sample is the sample ratio - track keys with probability 1/sample.
// Default: 1 (track every key), Min: 1
Sample int64
// Slots specifies specific hash slots to track (0-16383).
// All specified slots must be hosted by the receiving node.
// If not specified, all slots are tracked.
Slots []uint16
}
// ErrHotKeysNoMetrics is returned when HotKeysStart is called without any metrics specified.
var ErrHotKeysNoMetrics = errors.New("redis: at least one metric must be specified for HOTKEYS START")
// HotKeysStart starts collecting hotkeys data.
// At least one metric must be specified in args.Metrics.
// This command is only available on standalone clients.
func (c *Client) HotKeysStart(ctx context.Context, args *HotKeysStartArgs) *StatusCmd {
cmdArgs := make([]interface{}, 0, 16)
cmdArgs = append(cmdArgs, "hotkeys", "start")
// Validate that at least one metric is specified
if len(args.Metrics) == 0 {
cmd := NewStatusCmd(ctx, cmdArgs...)
cmd.SetErr(ErrHotKeysNoMetrics)
return cmd
}
cmdArgs = append(cmdArgs, "metrics", len(args.Metrics))
for _, metric := range args.Metrics {
cmdArgs = append(cmdArgs, strings.ToLower(string(metric)))
}
if args.Count > 0 {
cmdArgs = append(cmdArgs, "count", args.Count)
}
if args.Duration > 0 {
cmdArgs = append(cmdArgs, "duration", args.Duration)
}
if args.Sample > 0 {
cmdArgs = append(cmdArgs, "sample", args.Sample)
}
if len(args.Slots) > 0 {
cmdArgs = append(cmdArgs, "slots", len(args.Slots))
for _, slot := range args.Slots {
cmdArgs = append(cmdArgs, slot)
}
}
cmd := NewStatusCmd(ctx, cmdArgs...)
_ = c.Process(ctx, cmd)
return cmd
}
// HotKeysStop stops the ongoing hotkeys collection session.
// This command is only available on standalone clients.
func (c *Client) HotKeysStop(ctx context.Context) *StatusCmd {
cmd := NewStatusCmd(ctx, "hotkeys", "stop")
_ = c.Process(ctx, cmd)
return cmd
}
// HotKeysReset discards the last hotkeys collection session results.
// Returns an error if tracking is currently active.
// This command is only available on standalone clients.
func (c *Client) HotKeysReset(ctx context.Context) *StatusCmd {
cmd := NewStatusCmd(ctx, "hotkeys", "reset")
_ = c.Process(ctx, cmd)
return cmd
}
// HotKeysGet retrieves the results of the ongoing or last hotkeys collection session.
// This command is only available on standalone clients.
func (c *Client) HotKeysGet(ctx context.Context) *HotKeysCmd {
cmd := NewHotKeysCmd(ctx, "hotkeys", "get")
_ = c.Process(ctx, cmd)
return cmd
}
+214
View File
@@ -0,0 +1,214 @@
package redis_test
import (
"context"
"time"
. "github.com/bsm/ginkgo/v2"
. "github.com/bsm/gomega"
"github.com/redis/go-redis/v9"
)
var _ = Describe("HotKeys Commands", func() {
ctx := context.TODO()
var client *redis.Client
BeforeEach(func() {
client = redis.NewClient(redisOptions())
Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())
})
AfterEach(func() {
Expect(client.Close()).NotTo(HaveOccurred())
})
Describe("HOTKEYS", func() {
It("should start, get, stop, and reset hotkeys tracking", func() {
SkipBeforeRedisVersion(8.6, "HOTKEYS commands require Redis >= 8.6")
startArgs := &redis.HotKeysStartArgs{
Metrics: []redis.HotKeysMetric{redis.HotKeysMetricCPU, redis.HotKeysMetricNET},
Count: 10,
Duration: 0,
Sample: 1,
}
start := client.HotKeysStart(ctx, startArgs)
Expect(start.Err()).NotTo(HaveOccurred())
Expect(start.Val()).To(Equal("OK"))
for i := 0; i < 100; i++ {
client.Set(ctx, "hotkey1", "value1", 0)
client.Get(ctx, "hotkey1")
}
for i := 0; i < 50; i++ {
client.Set(ctx, "hotkey2", "value2", 0)
client.Get(ctx, "hotkey2")
}
for i := 0; i < 25; i++ {
client.Set(ctx, "hotkey3", "value3", 0)
client.Get(ctx, "hotkey3")
}
time.Sleep(100 * time.Millisecond)
get := client.HotKeysGet(ctx)
Expect(get.Err()).NotTo(HaveOccurred())
result := get.Val()
Expect(result).NotTo(BeNil())
Expect(result.TrackingActive).To(BeTrue())
Expect(result.SampleRatio).To(Equal(uint8(1)))
// Verify that collection start time is set (not zero)
Expect(result.CollectionStartTime.IsZero()).To(BeFalse())
// Verify that we have some CPU time data after running commands
Expect(result.AllCommandsAllSlots).To(BeNumerically(">", 0))
// Verify that we have hot keys data (we ran 350 commands on 3 keys)
Expect(len(result.ByCPUTime)).To(BeNumerically(">", 0))
Expect(len(result.ByNetBytes)).To(BeNumerically(">", 0))
// Verify that the first hot key entry has a non-empty key
if len(result.ByCPUTime) > 0 {
Expect(result.ByCPUTime[0].Key).NotTo(BeEmpty())
Expect(result.ByCPUTime[0].Value).NotTo(BeNil())
}
stop := client.HotKeysStop(ctx)
Expect(stop.Err()).NotTo(HaveOccurred())
Expect(stop.Val()).To(Equal("OK"))
get2 := client.HotKeysGet(ctx)
Expect(get2.Err()).NotTo(HaveOccurred())
result2 := get2.Val()
Expect(result2).NotTo(BeNil())
Expect(result2.TrackingActive).To(BeFalse())
// After stopping, collection duration should be set
Expect(result2.CollectionDuration).To(BeNumerically(">", 0))
reset := client.HotKeysReset(ctx)
Expect(reset.Err()).NotTo(HaveOccurred())
Expect(reset.Val()).To(Equal("OK"))
})
It("should start hotkeys tracking with CPU metric only", func() {
SkipBeforeRedisVersion(8.6, "HOTKEYS commands require Redis >= 8.6")
startArgs := &redis.HotKeysStartArgs{
Metrics: []redis.HotKeysMetric{redis.HotKeysMetricCPU},
Count: 5,
}
start := client.HotKeysStart(ctx, startArgs)
Expect(start.Err()).NotTo(HaveOccurred())
Expect(start.Val()).To(Equal("OK"))
stop := client.HotKeysStop(ctx)
Expect(stop.Err()).NotTo(HaveOccurred())
})
It("should start hotkeys tracking with NET metric only", func() {
SkipBeforeRedisVersion(8.6, "HOTKEYS commands require Redis >= 8.6")
startArgs := &redis.HotKeysStartArgs{
Metrics: []redis.HotKeysMetric{redis.HotKeysMetricNET},
Count: 5,
}
start := client.HotKeysStart(ctx, startArgs)
Expect(start.Err()).NotTo(HaveOccurred())
Expect(start.Val()).To(Equal("OK"))
stop := client.HotKeysStop(ctx)
Expect(stop.Err()).NotTo(HaveOccurred())
})
It("should start hotkeys tracking with duration", func() {
SkipBeforeRedisVersion(8.6, "HOTKEYS commands require Redis >= 8.6")
startArgs := &redis.HotKeysStartArgs{
Metrics: []redis.HotKeysMetric{redis.HotKeysMetricCPU, redis.HotKeysMetricNET},
Count: 10,
Duration: 2,
}
start := client.HotKeysStart(ctx, startArgs)
Expect(start.Err()).NotTo(HaveOccurred())
Expect(start.Val()).To(Equal("OK"))
time.Sleep(3 * time.Second)
get := client.HotKeysGet(ctx)
Expect(get.Err()).NotTo(HaveOccurred())
result := get.Val()
Expect(result).NotTo(BeNil())
Expect(result.TrackingActive).To(BeFalse())
})
It("should start hotkeys tracking with sampling", func() {
SkipBeforeRedisVersion(8.6, "HOTKEYS commands require Redis >= 8.6")
startArgs := &redis.HotKeysStartArgs{
Metrics: []redis.HotKeysMetric{redis.HotKeysMetricCPU, redis.HotKeysMetricNET},
Count: 10,
Sample: 10,
}
start := client.HotKeysStart(ctx, startArgs)
Expect(start.Err()).NotTo(HaveOccurred())
Expect(start.Val()).To(Equal("OK"))
stop := client.HotKeysStop(ctx)
Expect(stop.Err()).NotTo(HaveOccurred())
})
It("should error when using slots in non-cluster mode", func() {
SkipBeforeRedisVersion(8.6, "HOTKEYS commands require Redis >= 8.6")
startArgs := &redis.HotKeysStartArgs{
Metrics: []redis.HotKeysMetric{redis.HotKeysMetricCPU, redis.HotKeysMetricNET},
Count: 10,
Slots: []uint16{0, 1, 2, 100, 200},
}
start := client.HotKeysStart(ctx, startArgs)
Expect(start.Err()).To(HaveOccurred())
Expect(start.Err().Error()).To(ContainSubstring("SLOTS parameter cannot be used in non-cluster mode"))
})
It("should error when no metrics are specified", func() {
startArgs := &redis.HotKeysStartArgs{
Count: 10,
}
start := client.HotKeysStart(ctx, startArgs)
Expect(start.Err()).To(HaveOccurred())
Expect(start.Err()).To(Equal(redis.ErrHotKeysNoMetrics))
})
It("should error when starting tracking while already active", func() {
SkipBeforeRedisVersion(8.6, "HOTKEYS commands require Redis >= 8.6")
startArgs := &redis.HotKeysStartArgs{
Metrics: []redis.HotKeysMetric{redis.HotKeysMetricCPU},
Count: 10,
}
start1 := client.HotKeysStart(ctx, startArgs)
Expect(start1.Err()).NotTo(HaveOccurred())
start2 := client.HotKeysStart(ctx, startArgs)
Expect(start2.Err()).To(HaveOccurred())
stop := client.HotKeysStop(ctx)
Expect(stop.Err()).NotTo(HaveOccurred())
})
It("should error when resetting while tracking is active", func() {
SkipBeforeRedisVersion(8.6, "HOTKEYS commands require Redis >= 8.6")
startArgs := &redis.HotKeysStartArgs{
Metrics: []redis.HotKeysMetric{redis.HotKeysMetricCPU},
Count: 10,
}
start := client.HotKeysStart(ctx, startArgs)
Expect(start.Err()).NotTo(HaveOccurred())
reset := client.HotKeysReset(ctx)
Expect(reset.Err()).To(HaveOccurred())
stop := client.HotKeysStop(ctx)
Expect(stop.Err()).NotTo(HaveOccurred())
})
})
})