Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/run-tests/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/doctests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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) && \
Expand Down
246 changes: 246 additions & 0 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ var keylessCommands = map[string]struct{}{
"failover": {},
"function": {},
"hello": {},
"hotkeys": {},
"latency": {},
"lolwut": {},
"module": {},
Expand Down Expand Up @@ -151,6 +152,7 @@ const (
CmdTypeFTSearch
CmdTypeTSTimestampValue
CmdTypeTSTimestampValueSlice
CmdTypeHotKeys
)

type (
Expand Down Expand Up @@ -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
SampledCommandSelectedSlots 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-command-selected-slots-us"].(int64); ok {
Comment thread
ndyakov marked this conversation as resolved.
Outdated
result.SampledCommandSelectedSlots = 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,
SampledCommandSelectedSlots: cmd.val.SampledCommandSelectedSlots,
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

Expand Down Expand Up @@ -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)
Expand Down
10 changes: 5 additions & 5 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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"
Expand All @@ -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:
Expand All @@ -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:
Expand Down
1 change: 0 additions & 1 deletion helper/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,3 @@ func BenchmarkDigestBytes(b *testing.B) {
})
}
}

Loading
Loading