Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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-21651605017-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-21651605017-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-21651605017-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-21651605017-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 int64
Comment thread
elena-kolevska marked this conversation as resolved.
Outdated
SelectedSlots []HotKeysSlotRange
SampledCommandSelectedSlotsUs int64 // Present when sample-ratio > 1 and selected-slots is not empty
AllCommandsSelectedSlotsUs int64 // Present when selected-slots is not empty
AllCommandsAllSlotsUs int64
NetBytesSampledCommandsSelectedSlots int64 // Present when sample-ratio > 1 and selected-slots is not empty
NetBytesAllCommandsSelectedSlots int64 // Present when selected-slots is not empty
NetBytesAllCommandsAllSlots int64
CollectionStartTimeUnixMs int64
CollectionDurationMs int64
UsedCPUSysMs int64
UsedCPUUserMs int64
TotalNetBytes int64
ByCPUTimeUs []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 = 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.SampledCommandSelectedSlotsUs = v
}
if v, ok := data["all-commands-selected-slots-us"].(int64); ok {
result.AllCommandsSelectedSlotsUs = v
}
if v, ok := data["all-commands-all-slots-us"].(int64); ok {
result.AllCommandsAllSlotsUs = v
}
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.CollectionStartTimeUnixMs = v
}
if v, ok := data["collection-duration-ms"].(int64); ok {
result.CollectionDurationMs = v
}
if v, ok := data["used-cpu-sys-ms"].(int64); ok {
result.UsedCPUSysMs = v
}
if v, ok := data["used-cpu-user-ms"].(int64); ok {
result.UsedCPUUserMs = v
}
if v, ok := data["total-net-bytes"].(int64); ok {
result.TotalNetBytes = v
}

if v, ok := data["by-cpu-time-us"].([]interface{}); ok {
result.ByCPUTimeUs = 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,
SampledCommandSelectedSlotsUs: cmd.val.SampledCommandSelectedSlotsUs,
AllCommandsSelectedSlotsUs: cmd.val.AllCommandsSelectedSlotsUs,
AllCommandsAllSlotsUs: cmd.val.AllCommandsAllSlotsUs,
NetBytesSampledCommandsSelectedSlots: cmd.val.NetBytesSampledCommandsSelectedSlots,
NetBytesAllCommandsSelectedSlots: cmd.val.NetBytesAllCommandsSelectedSlots,
NetBytesAllCommandsAllSlots: cmd.val.NetBytesAllCommandsAllSlots,
CollectionStartTimeUnixMs: cmd.val.CollectionStartTimeUnixMs,
CollectionDurationMs: cmd.val.CollectionDurationMs,
Comment thread
elena-kolevska marked this conversation as resolved.
Outdated
UsedCPUSysMs: cmd.val.UsedCPUSysMs,
UsedCPUUserMs: cmd.val.UsedCPUUserMs,
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.ByCPUTimeUs != nil {
val.ByCPUTimeUs = make([]HotKeysKeyEntry, len(cmd.val.ByCPUTimeUs))
copy(val.ByCPUTimeUs, cmd.val.ByCPUTimeUs)
}
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-21651605017-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-21651605017-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-21651605017-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-21651605017-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-21651605017-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