8000 HOTKEYS - Fix slots parsing by a-TODO-rov · Pull Request #4426 · redis/jedis · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
8000
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ PATH := ./redis-git/src:${PATH}

# Supported test env versions
SUPPORTED_TEST_ENV_VERSIONS := 8.6 8.4 8.2 8.0 7.4 7.2 6.2
DEFAULT_TEST_ENV_VERSION := 8.4
DEFAULT_TEST_ENV_VERSION := 8.6
REDIS_ENV_WORK_DIR := $(or ${REDIS_ENV_WORK_DIR},/tmp/redis-env-work)
CLIENT_LIBS_TEST_IMAGE := redislabs/client-libs-test:8.2.2
TOXIPROXY_IMAGE := ghcr.io/shopify/toxiproxy:2.8.0
Expand Down
30 changes: 21 additions & 9 deletions src/main/java/redis/clients/jedis/resps/HotkeysInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public class HotkeysInfo implements Serializable {
public static final String TRACKING_ACTIVE = "tracking-active";
public static final String SAMPLE_RATIO = "sample-ratio";
public static final String SELECTED_SLOTS = "selected-slots";
public static final String SAMPLED_COMMAND_SELECTED_SLOTS_US = "sampled-command-selected-slots-us";
public static final String SAMPLED_COMMANDS_SELECTED_SLOTS_US = "sampled-commands-selected-slots-us";
public static final String ALL_COMMANDS_SELECTED_SLOTS_US = "all-commands-selected-slots-us";
public static final String ALL_COMMANDS_ALL_SLOTS_US = "all-commands-all-slots-us";
public static final String NET_BYTES_SAMPLED_COMMANDS_SELECTED_SLOTS = "net-bytes-sampled-commands-selected-slots";
Expand Down Expand Up @@ -88,9 +88,12 @@ public long getSampleRatio() {
}

/**
* Returns the selected slot ranges. Each element is an int array of [start, end] representing a
* slot range (inclusive).
* @return list of slot ranges, empty if all slots are selected
* Returns the selected slots. Each element is an int array that can be:
* <ul>
* <li>A single slot: {@code [slot]} (array with 1 element)</li>
* <li>A slot range: {@code [start, end]} (array with 2 elements, inclusive)</li>
* </ul>
* @return list of slot entries, empty if all slots are selected
*/
public List<int[]> getSelectedSlots() {
return selectedSlots;
Expand Down Expand Up @@ -172,8 +175,12 @@ private static Map<String, Long> parseKeyValueMap(Object data) {
}

/**
* Parse selected-slots which is an array of [start, end] ranges. Example: [[0, 16383]] means all
* slots.
* Parse selected-slots which is an array of slot entries. Each entry can be:
* <ul>
* <li>A single slot: [slot] (array with 1 element)</li>
* <li>A slot range: [start, end] (array with 2 elements)</li>
* </ul>
* Example: [[0, 2], [100]] means slots 0-2 (range) and slot 100 (single).
*/
@SuppressWarnings("unchecked")
private static List<int[]> parseSlotRanges(Object data) {
Expand All @@ -188,7 +195,12 @@ private static List<int[]> parseSlotRanges(Object data) {
for (Object item : list) {
if (item instanceof List) {
List<?> range = (List<?>) item;
if (range.size() == 2) {
if (range.size() == 1) {
// Single slot
int slot = LONG.build(range.get(0)).intValue();
result.add(new int[] { slot });
} else if (range.size() == 2) {
// Slot range
int start = LONG.build(range.get(0)).intValue();
int end = LONG.build(range.get(1)).intValue();
result.add(new int[] { start, end });
Expand Down Expand Up @@ -252,7 +264,7 @@ public HotkeysInfo build(Object data) {
case SELECTED_SLOTS:
selectedSlots = parseSlotRanges(value);
break;
case SAMPLED_COMMAND_SELECTED_SLOTS_US:
case SAMPLED_COMMANDS_SELECTED_SLOTS_US:
sampledCommandSelectedSlotsUs = LONG.build(value);
break;
case ALL_COMMANDS_SELECTED_SLOTS_US:
Expand Down Expand Up @@ -308,7 +320,7 @@ public HotkeysInfo build(Object data) {
case SELECTED_SLOTS:
selectedSlots = parseSlotRanges(value);
break;
case SAMPLED_COMMAND_SELECTED_SLOTS_US:
case SAMPLED_COMMANDS_SELECTED_SLOTS_US:
sampledCommandSelectedSlotsUs = LONG.build(value);
break;
case ALL_COMMANDS_SELECTED_SLOTS_US:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,39 @@
package redis.clients.jedis.commands.jedis;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.List;
import java.util.Map;

import io.redis.test.annotations.ConditionalOnEnv;
import io.redis.test.annotations.EnabledOnCommand;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Tag;

import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.RedisClient;
import redis.clients.jedis.args.HotkeysMetric;
import redis.clients.jedis.params.HotkeysParams;
import redis.clients.jedis.resps.HotkeysInfo;
import redis.clients.jedis.util.JedisClusterCRC16;
import redis.clients.jedis.util.TestEnvUtil;

/**
* Tests that HOTKEYS commands are not supported in cluster mode.
* <p>
* The HOTKEYS command is a node-local operation that tracks hot keys on a single Redis instance. In
* a Redis Cluster, keys are distributed across multiple nodes, and there is no built-in mechanism
* to aggregate hotkeys data across all nodes. Therefore, HOTKEYS commands are intentionally
* disabled in cluster mode to avoid confusion and incorrect results.
* <p>
* Users who need hotkeys functionality in a cluster environment should connect directly to
* individual nodes and run HOTKEYS commands on each node separately.
*/
@Tag("integration")
@EnabledOnCommand("HOTKEYS")
Expand All @@ -39,4 +60,120 @@ public void hotkeysResetNotSupportedInCluster() {
public void hotkeysGetNotSupportedInCluster() {
assertThrows(UnsupportedOperationException.class, () -> cluster.hotkeysGet());
}

// Test slots - consecutive slots (server groups them as a range) and a non-consecutive slot
private static final int SLOT_0 = 0;
private static final int SLOT_1 = 1;
private static final int SLOT_2 = 2;
private static final int SLOT_100 = 100; // Non-consecutive slot to test multiple ranges

// Keys with hash tags that hash to slots 0, 1, 2
// The hash tag content was found by iterating JedisClusterCRC16.getSlot()
private static final String KEY_SLOT_0 = "key{3560}"; // {3560} hashes to slot 0
private static final String KEY_SLOT_1 = "key{22179}"; // {22179} hashes to slot 1
private static final String KEY_SLOT_2 = "key{48756}"; // {48756} hashes to slot 2

/**
* Tests HOTKEYS with SLOTS parameter by connecting directly to a single cluster node using a
* standalone RedisClient. Verifies all response fields are correctly parsed.
*/
@Test
public void hotkeysWithSlotsOnSingleClusterNode() {
// Verify our pre-computed keys hash to the expected slots
assertEquals(SLOT_0, JedisClusterCRC16.getSlot(KEY_SLOT_0));
assertEquals(SLOT_1, JedisClusterCRC16.getSlot(KEY_SLOT_1));
assertEquals(SLOT_2, JedisClusterCRC16.getSlot(KEY_SLOT_2));

HostAndPort nodeHostAndPort = endpoint.getHostsAndPorts().get(0);

try (RedisClient client = RedisClient.builder().hostAndPort(nodeHostAndPort)
.clientConfig(endpoint.getClientConfigBuilder().build()).build()) {

// Clean up any previous state
client.hotkeysStop();
client.hotkeysReset();

// Start hotkeys tracking with consecutive slots (0, 1, 2) and a non-consecutive slot (100)
// Server should group consecutive slots into a range and keep non-consecutive as single slot
String result = client
.hotkeysStart(HotkeysParams.hotkeysParams().metrics(HotkeysMetric.CPU, HotkeysMetric.NET)
.sample(2).slots(SLOT_0, SLOT_1, SLOT_2, SLOT_100));
assertEquals("OK", result);

// Generate traffic on keys that hash to slots 0, 1, 2
String[] keys = { KEY_SLOT_0, KEY_SLOT_1, KEY_SLOT_2 };
for (int i = 0; i < 50; i++) {
for (String key : keys) {
client.set(key, "value" + i);
client.get(key);
}
}

HotkeysInfo info = client.hotkeysGet();
assertNotNull(info);

// Verify tracking state
assertTrue(info.isTrackingActive());
assertEquals(2, info.getSampleRatio());

// Verify selected slots - should have 2 entries:
// 1. Range [0, 2] for consecutive slots 0, 1, 2
// 2. Single slot [100] for non-consecutive slot
List<int[]> selectedSlots = info.getSelectedSlots();
assertNotNull(selectedSlots);
assertEquals(2, selectedSlots.size());
// First entry: range [0, 2]
assertEquals(2, selectedSlots.get(0).length);
assertEquals(SLOT_0, selectedSlots.get(0)[0]);
assertEquals(SLOT_2, selectedSlots.get(0)[1]);
// Second entry: single slot [100]
assertEquals(1, selectedSlots.get(1).length);
assertEquals(SLOT_100, selectedSlots.get(1)[0]);

// Verify slot-specific CPU metrics (only present when SLOTS is used)
assertNotNull(info.getSampledCommandSelectedSlotsUs());
assertThat(info.getSampledCommandSelectedSlotsUs(), greaterThan(0L));
assertNotNull(info.getAllCommandsSelectedSlotsUs());
assertThat(info.getAllCommandsSelectedSlotsUs(), greaterThan(0L));
assertThat(info.getAllCommandsAllSlotsUs(), greaterThan(0L));

// Verify slot-specific network bytes metrics
assertNotNull(info.getNetBytesSampledCommandsSelectedSlots());
assertThat(info.getNetBytesSampledCommandsSelectedSlots(), greaterThan(0L));
assertNotNull(info.getNetBytesAllCommandsSelectedSlots());
assertThat(info.getNetBytesAllCommandsSelectedSlots(), greaterThan(0L));
assertThat(info.getNetBytesAllCommandsAllSlots(), greaterThan(0L));

// Verify timing fields
assertThat(info.getCollectionStartTimeUnixMs(), greaterThan(0L));
assertThat(info.getCollectionDurationMs(), greaterThanOrEqualTo(0L));
assertThat(info.getTotalCpuTimeUserMs(), greaterThanOrEqualTo(0L));
assertThat(info.getTotalCpuTimeSysMs(), greaterThanOrEqualTo(0L));
assertThat(info.getTotalNetBytes(), greaterThan(0L));

// Verify key metrics maps contain our 3 keys with values > 0
Map<String, Long> byCpuTimeUs = info.getByCpuTimeUs();
assertNotNull(byCpuTimeUs);
assertEquals(3, byCpuTimeUs.size());
assertTrue(byCpuTimeUs.containsKey(KEY_SLOT_0));
assertTrue(byCpuTimeUs.containsKey(KEY_SLOT_1));
assertTrue(byCpuTimeUs.containsKey(KEY_SLOT_2));
assertThat(byCpuTimeUs.get(KEY_SLOT_0), greaterThan(0L));
assertThat(byCpuTimeUs.get(KEY_SLOT_1), greaterThan(0L));
assertThat(byCpuTimeUs.get(KEY_SLOT_2), greaterThan(0L));

Map<String, Long> byNetBytes = info.getByNetBytes();
assertNotNull(byNetBytes);
assertEquals(3, byNetBytes.size());
assertTrue(byNetBytes.containsKey(KEY_SLOT_0));
assertTrue(byNetBytes.containsKey(KEY_SLOT_1));
assertTrue(byNetBytes.containsKey(KEY_SLOT_2));
assertThat(byNetBytes.get(KEY_SLOT_0), greaterThan(0L));
assertThat(byNetBytes.get(KEY_SLOT_1), greaterThan(0L));
assertThat(byNetBytes.get(KEY_SLOT_2), greaterThan(0L));

client.hotkeysStop();
client.hotkeysReset();
}
}
}
4 changes: 2 additions & 2 deletions src/test/resources/env/.env
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
REDIS_VERSION=8.4.0
REDIS_STACK_VERSION=8.4.0
REDIS_VERSION=8.6.0
REDIS_STACK_VERSION=8.6.0
CLIENT_LIBS_TEST_IMAGE=redislabs/client-libs-test
REDIS_ENV_CONF_DIR=./
REDIS_MODULES_DIR=/tmp
Expand Down
4 changes: 2 additions & 2 deletions src/test/resources/env/.env.v8.6
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
REDIS_VERSION=custom-21651605017-debian-amd64
REDIS_STACK_VERSION=custom-21651605017-debian-amd64
REDIS_VERSION=8.6.0
REDIS_STACK_VERSION=8.6.0
CLIENT_LIBS_TEST_IMAGE=redislabs/client-libs-test
REDIS_ENV_CONF_DIR=./
REDIS_MODULES_DIR=/tmp
Expand Down
Loading
0