diff --git a/src/main/java/redis/clients/jedis/BuilderFactory.java b/src/main/java/redis/clients/jedis/BuilderFactory.java index 095f3c36c3..c7349f3e89 100644 --- a/src/main/java/redis/clients/jedis/BuilderFactory.java +++ b/src/main/java/redis/clients/jedis/BuilderFactory.java @@ -997,6 +997,108 @@ public Map build(Object data) { } }; + private static final Builder>> CLUSTER_SHARD_SLOTS_RANGES = new Builder>>() { + + @Override + public List> build(Object data) { + if (null == data) { + return null; + } + + List rawSlots = (List) data; + List> slotsRanges = new ArrayList<>(); + for (int i = 0; i < rawSlots.size(); i += 2) { + slotsRanges.add(Arrays.asList(rawSlots.get(i), rawSlots.get(i + 1))); + } + return slotsRanges; + } + }; + + private static final Builder> CLUSTER_SHARD_NODE_INFO_LIST + = new Builder>() { + + final Map mappingFunctions = createDecoderMap(); + + private Map createDecoderMap() { + + Map tempMappingFunctions = new HashMap<>(); + tempMappingFunctions.put(ClusterShardNodeInfo.ID, STRING); + tempMappingFunctions.put(ClusterShardNodeInfo.ENDPOINT, STRING); + tempMappingFunctions.put(ClusterShardNodeInfo.IP, STRING); + tempMappingFunctions.put(ClusterShardNodeInfo.HOSTNAME, STRING); + tempMappingFunctions.put(ClusterShardNodeInfo.PORT, LONG); + tempMappingFunctions.put(ClusterShardNodeInfo.TLS_PORT, LONG); + tempMappingFunctions.put(ClusterShardNodeInfo.ROLE, STRING); + tempMappingFunctions.put(ClusterShardNodeInfo.REPLICATION_OFFSET, LONG); + tempMappingFunctions.put(ClusterShardNodeInfo.HEALTH, STRING); + + return tempMappingFunctions; + } + + @Override + @SuppressWarnings("unchecked") + public List build(Object data) { + if (null == data) { + return null; + } + + List response = new ArrayList<>(); + + List clusterShardNodeInfos = (List) data; + for (Object clusterShardNodeInfoObject : clusterShardNodeInfos) { + List clusterShardNodeInfo = (List) clusterShardNodeInfoObject; + Iterator iterator = clusterShardNodeInfo.iterator(); + response.add(new ClusterShardNodeInfo(createMapFromDecodingFunctions(iterator, mappingFunctions))); + } + + return response; + } + + @Override + public String toString() { + return "List"; + } + }; + + public static final Builder> CLUSTER_SHARD_INFO_LIST + = new Builder>() { + + final Map mappingFunctions = createDecoderMap(); + + private Map createDecoderMap() { + + Map tempMappingFunctions = new HashMap<>(); + tempMappingFunctions.put(ClusterShardInfo.SLOTS, CLUSTER_SHARD_SLOTS_RANGES); + tempMappingFunctions.put(ClusterShardInfo.NODES, CLUSTER_SHARD_NODE_INFO_LIST); + + return tempMappingFunctions; + } + + @Override + @SuppressWarnings("unchecked") + public List build(Object data) { + if (null == data) { + return null; + } + + List response = new ArrayList<>(); + + List clusterShardInfos = (List) data; + for (Object clusterShardInfoObject : clusterShardInfos) { + List clusterShardInfo = (List) clusterShardInfoObject; + Iterator iterator = clusterShardInfo.iterator(); + response.add(new ClusterShardInfo(createMapFromDecodingFunctions(iterator, mappingFunctions))); + } + + return response; + } + + @Override + public String toString() { + return "List"; + } + }; + public static final Builder> MODULE_LIST = new Builder>() { @Override public List build(Object data) { diff --git a/src/main/java/redis/clients/jedis/Jedis.java b/src/main/java/redis/clients/jedis/Jedis.java index 6db8c99328..181fe279a2 100644 --- a/src/main/java/redis/clients/jedis/Jedis.java +++ b/src/main/java/redis/clients/jedis/Jedis.java @@ -6386,7 +6386,7 @@ public String srandmember(final String key) { * @param key * @param count if positive, return an array of distinct elements. * If negative the behavior changes and the command is allowed to - * return the same element multiple times + * return the same element multiple times * @return A list of randomly selected elements */ @Override @@ -8758,7 +8758,7 @@ public long clusterKeySlot(final String key) { public long clusterCountFailureReports(final String nodeId) { checkIsInMultiOrPipeline(); connection.sendCommand(CLUSTER, "COUNT-FAILURE-REPORTS", nodeId); - return connection.getIntegerReply(); + return connection.getIntegerReply(); } @Override @@ -8826,12 +8826,20 @@ public String clusterFailover(ClusterFailoverOption failoverOption) { } @Override + @Deprecated public List clusterSlots() { checkIsInMultiOrPipeline(); connection.sendCommand(CLUSTER, ClusterKeyword.SLOTS); return connection.getObjectMultiBulkReply(); } + @Override + public List clusterShards() { + checkIsInMultiOrPipeline(); + connection.sendCommand(CLUSTER, ClusterKeyword.SHARDS); + return BuilderFactory.CLUSTER_SHARD_INFO_LIST.build(connection.getObjectMultiBulkReply()); + } + @Override public String clusterMyId() { checkIsInMultiOrPipeline(); diff --git a/src/main/java/redis/clients/jedis/Protocol.java b/src/main/java/redis/clients/jedis/Protocol.java index d7c36503fc..234b73bda9 100644 --- a/src/main/java/redis/clients/jedis/Protocol.java +++ b/src/main/java/redis/clients/jedis/Protocol.java @@ -357,7 +357,7 @@ public static enum ClusterKeyword implements Rawable { MEET, RESET, INFO, FAILOVER, SLOTS, NODES, REPLICAS, SLAVES, MYID, ADDSLOTS, DELSLOTS, GETKEYSINSLOT, SETSLOT, NODE, MIGRATING, IMPORTING, STABLE, FORGET, FLUSHSLOTS, KEYSLOT, COUNTKEYSINSLOT, SAVECONFIG, REPLICATE, LINKS, ADDSLOTSRANGE, DELSLOTSRANGE, BUMPEPOCH, - MYSHARDID; + MYSHARDID, SHARDS; private final byte[] raw; diff --git a/src/main/java/redis/clients/jedis/commands/ClusterCommands.java b/src/main/java/redis/clients/jedis/commands/ClusterCommands.java index da0d3fc8e0..4ad918d12b 100644 --- a/src/main/java/redis/clients/jedis/commands/ClusterCommands.java +++ b/src/main/java/redis/clients/jedis/commands/ClusterCommands.java @@ -5,6 +5,7 @@ import redis.clients.jedis.args.ClusterResetType; import redis.clients.jedis.args.ClusterFailoverOption; +import redis.clients.jedis.resps.ClusterShardInfo; public interface ClusterCommands { @@ -79,8 +80,24 @@ public interface ClusterCommands { String clusterFailover(ClusterFailoverOption failoverOption); + /** + * {@code CLUSTER SLOTS} command is deprecated since Redis 7. + * + * @deprecated Use {@link ClusterCommands#clusterShards()}. + */ + @Deprecated List clusterSlots(); + /** + * {@code CLUSTER SHARDS} returns details about the shards of the cluster. + * This command replaces the {@code CLUSTER SLOTS} command from Redis 7, + * by providing a more efficient and extensible representation of the cluster. + * + * @return a list of shards, with each shard containing two objects, 'slots' and 'nodes'. + * @see CLUSTER SHARDS + */ + List clusterShards(); + String clusterReset(); /** diff --git a/src/main/java/redis/clients/jedis/resps/ClusterShardInfo.java b/src/main/java/redis/clients/jedis/resps/ClusterShardInfo.java new file mode 100644 index 0000000000..8e7394bccc --- /dev/null +++ b/src/main/java/redis/clients/jedis/resps/ClusterShardInfo.java @@ -0,0 +1,44 @@ +package redis.clients.jedis.resps; + +import java.util.List; +import java.util.Map; + +/** + * This class holds information about a shard of the cluster with command {@code CLUSTER SHARDS}. + * They can be accessed via getters. There is also {@link ClusterShardInfo#getClusterShardInfo()} + * method that returns a generic {@link Map} in case more info are returned from the server. + */ +public class ClusterShardInfo { + + public static final String SLOTS = "slots"; + public static final String NODES = "nodes"; + + private final List> slots; + private final List nodes; + + private final Map clusterShardInfo; + + /** + * @param map contains key-value pairs with cluster shard info + */ + @SuppressWarnings("unchecked") + public ClusterShardInfo(Map map) { + slots = (List>) map.get(SLOTS); + nodes = (List) map.get(NODES); + + clusterShardInfo = map; + } + + public List> getSlots() { + return slots; + } + + public List getNodes() { + return nodes; + } + + public Map getClusterShardInfo() { + return clusterShardInfo; + } + +} diff --git a/src/main/java/redis/clients/jedis/resps/ClusterShardNodeInfo.java b/src/main/java/redis/clients/jedis/resps/ClusterShardNodeInfo.java new file mode 100644 index 0000000000..6e245de4d2 --- /dev/null +++ b/src/main/java/redis/clients/jedis/resps/ClusterShardNodeInfo.java @@ -0,0 +1,94 @@ +package redis.clients.jedis.resps; + +import java.util.Map; + +/** + * This class holds information about a node of the cluster with command {@code CLUSTER SHARDS}. + * They can be accessed via getters. There is also {@link ClusterShardNodeInfo#getClusterShardNodeInfo()} + * method that returns a generic {@link Map} in case more info are returned from the server. + */ +public class ClusterShardNodeInfo { + + public static final String ID = "id"; + public static final String ENDPOINT = "endpoint"; + public static final String IP = "ip"; + public static final String HOSTNAME = "hostname"; + public static final String PORT = "port"; + public static final String TLS_PORT = "tls-port"; + public static final String ROLE = "role"; + public static final String REPLICATION_OFFSET = "replication-offset"; + public static final String HEALTH = "health"; + + private final String id; + private final String endpoint; + private final String ip; + private final String hostname; + private final Long port; + private final Long tlsPort; + private final String role; + private final Long replicationOffset; + private final String health; + + private final Map clusterShardNodeInfo; + + /** + * @param map contains key-value pairs with node info + */ + public ClusterShardNodeInfo(Map map) { + id = (String) map.get(ID); + endpoint = (String) map.get(ENDPOINT); + ip = (String) map.get(IP); + hostname = (String) map.get(HOSTNAME); + port = (Long) map.get(PORT); + tlsPort = (Long) map.get(TLS_PORT); + role = (String) map.get(ROLE); + replicationOffset = (Long) map.get(REPLICATION_OFFSET); + health = (String) map.get(HEALTH); + + clusterShardNodeInfo = map; + } + + public String getId() { + return id; + } + + public String getEndpoint() { + return endpoint; + } + + public String getIp() { + return ip; + } + + public String getHostname() { + return hostname; + } + + public Long getPort() { + return port; + } + + public Long getTlsPort() { + return tlsPort; + } + + public String getRole() { + return role; + } + + public Long getReplicationOffset() { + return replicationOffset; + } + + public String getHealth() { + return health; + } + + public Map getClusterShardNodeInfo() { + return clusterShardNodeInfo; + } + + public boolean isSsl() { + return tlsPort != null; + } +} diff --git a/src/test/java/redis/clients/jedis/commands/jedis/ClusterCommandsTest.java b/src/test/java/redis/clients/jedis/commands/jedis/ClusterCommandsTest.java index a6654b47ca..7e5c5db875 100644 --- a/src/test/java/redis/clients/jedis/commands/jedis/ClusterCommandsTest.java +++ b/src/test/java/redis/clients/jedis/commands/jedis/ClusterCommandsTest.java @@ -3,6 +3,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import java.util.List; @@ -13,6 +14,7 @@ import org.junit.After; import org.junit.AfterClass; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; import redis.clients.jedis.HostAndPort; @@ -20,6 +22,8 @@ import redis.clients.jedis.args.ClusterResetType; import redis.clients.jedis.HostAndPorts; import redis.clients.jedis.exceptions.JedisDataException; +import redis.clients.jedis.resps.ClusterShardInfo; +import redis.clients.jedis.resps.ClusterShardNodeInfo; import redis.clients.jedis.util.JedisClusterCRC16; import redis.clients.jedis.util.JedisClusterTestUtil; @@ -48,8 +52,17 @@ public void tearDown() { node2.disconnect(); } + @BeforeClass + public static void resetRedisBefore() { + removeSlots(); + } + @AfterClass - public static void removeSlots() throws InterruptedException { + public static void resetRedisAfter() { + removeSlots(); + } + + public static void removeSlots() { try (Jedis node = new Jedis(nodeInfo1)) { node.auth("cluster"); node.clusterReset(ClusterResetType.SOFT); @@ -189,6 +202,37 @@ public void clusterSlots() { node1.clusterDelSlots(3000, 3001, 3002); } + @Test + public void clusterShards() { + assertEquals("OK", node1.clusterAddSlots(3100, 3101, 3102, 3105)); + + List shards = node1.clusterShards(); + assertNotNull(shards); + assertTrue(shards.size() > 0); + + for (ClusterShardInfo shardInfo : shards) { + assertNotNull(shardInfo); + + assertTrue(shardInfo.getSlots().size() > 1); + for (List slotRange : shardInfo.getSlots()) { + assertEquals(2, slotRange.size()); + } + + for (ClusterShardNodeInfo nodeInfo : shardInfo.getNodes()) { + assertNotNull(nodeInfo.getId()); + assertNotNull(nodeInfo.getEndpoint()); + assertNotNull(nodeInfo.getIp()); + assertNull(nodeInfo.getHostname()); + assertNotNull(nodeInfo.getPort()); + assertNull(nodeInfo.getTlsPort()); + assertNotNull(nodeInfo.getRole()); + assertNotNull(nodeInfo.getReplicationOffset()); + assertNotNull(nodeInfo.getHealth()); + } + } + node1.clusterDelSlots(3100, 3101, 3102, 3105); + } + @Test public void clusterLinks() throws InterruptedException { List> links = node1.clusterLinks(); @@ -238,4 +282,4 @@ public void ClusterBumpEpoch() { MatcherAssert.assertThat(node1.clusterBumpEpoch(), Matchers.matchesPattern("^BUMPED|STILL [0-9]+$")); } -} \ No newline at end of file +}