ClusterHotkeysCommandsTest.java

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")
@ConditionalOnEnv(value = TestEnvUtil.ENV_OSS_DOCKER, enabled = true)
public class ClusterHotkeysCommandsTest extends ClusterJedisCommandsTestBase {

  @Test
  public void hotkeysStartNotSupportedInCluster() {
    assertThrows(UnsupportedOperationException.class,
      () -> cluster.hotkeysStart(HotkeysParams.hotkeysParams().metrics(HotkeysMetric.CPU)));
  }

  @Test
  public void hotkeysStopNotSupportedInCluster() {
    assertThrows(UnsupportedOperationException.class, () -> cluster.hotkeysStop());
  }

  @Test
  public void hotkeysResetNotSupportedInCluster() {
    assertThrows(UnsupportedOperationException.class, () -> cluster.hotkeysReset());
  }

  @Test
  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();
    }
  }
}