diff --git a/pom.xml b/pom.xml
index 1f0d73253d..ee57e60d2c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -91,6 +91,12 @@
2.3.2
test
+
+ org.mockito
+ mockito-core
+ 3.7.7
+ test
+
diff --git a/src/main/java/redis/clients/jedis/JedisClusterCommand.java b/src/main/java/redis/clients/jedis/JedisClusterCommand.java
index 0aa1055df4..90b94b7903 100644
--- a/src/main/java/redis/clients/jedis/JedisClusterCommand.java
+++ b/src/main/java/redis/clients/jedis/JedisClusterCommand.java
@@ -119,7 +119,7 @@ private T runWithRetries(final int slot, int attempts, boolean tryRandomNode, Je
this.connectionHandler.renewSlotCache();
}
- return runWithRetries(slot, attempts - 1, tryRandomNode, redirect);
+ return runWithRetries(slot, attempts - 1, true, null);
} catch (JedisRedirectionException jre) {
// if MOVED redirection occurred,
if (jre instanceof JedisMovedDataException) {
diff --git a/src/test/java/redis/clients/jedis/tests/commands/JedisClusterCommandTest.java b/src/test/java/redis/clients/jedis/tests/commands/JedisClusterCommandTest.java
new file mode 100644
index 0000000000..7e4a55821b
--- /dev/null
+++ b/src/test/java/redis/clients/jedis/tests/commands/JedisClusterCommandTest.java
@@ -0,0 +1,213 @@
+package redis.clients.jedis.tests.commands;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+import org.mockito.Mockito;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.JedisClusterCommand;
+import redis.clients.jedis.JedisClusterConnectionHandler;
+import redis.clients.jedis.JedisSlotBasedConnectionHandler;
+import redis.clients.jedis.exceptions.JedisAskDataException;
+import redis.clients.jedis.exceptions.JedisClusterMaxAttemptsException;
+import redis.clients.jedis.exceptions.JedisConnectionException;
+import redis.clients.jedis.exceptions.JedisMovedDataException;
+import redis.clients.jedis.exceptions.JedisNoReachableClusterNodeException;
+
+public class JedisClusterCommandTest {
+
+ @Test(expected = JedisClusterMaxAttemptsException.class)
+ public void runZeroAttempts() {
+ JedisClusterCommand testMe = new JedisClusterCommand(null, 0) {
+ @Override
+ public String execute(Jedis connection) {
+ return null;
+ }
+ };
+
+ testMe.run("");
+ }
+
+ @Test
+ public void runSuccessfulExecute() {
+ JedisClusterConnectionHandler connectionHandler = Mockito
+ .mock(JedisClusterConnectionHandler.class);
+ JedisClusterCommand testMe = new JedisClusterCommand(connectionHandler, 10) {
+ @Override
+ public String execute(Jedis connection) {
+ return "foo";
+ }
+ };
+ String actual = testMe.run("");
+ assertEquals("foo", actual);
+ }
+
+ @Test
+ public void runFailOnFirstExecSuccessOnSecondExec() {
+ JedisClusterConnectionHandler connectionHandler = Mockito
+ .mock(JedisClusterConnectionHandler.class);
+
+ JedisClusterCommand testMe = new JedisClusterCommand(connectionHandler, 10) {
+ boolean isFirstCall = true;
+
+ @Override
+ public String execute(Jedis connection) {
+ if (isFirstCall) {
+ isFirstCall = false;
+ throw new JedisConnectionException("Borkenz");
+ }
+
+ return "foo";
+ }
+ };
+
+ String actual = testMe.run("");
+ assertEquals("foo", actual);
+ }
+
+ @Test
+ public void runReconnectWithRandomConnection() {
+ JedisSlotBasedConnectionHandler connectionHandler = Mockito
+ .mock(JedisSlotBasedConnectionHandler.class);
+ // simulate failing connection
+ Mockito.when(connectionHandler.getConnectionFromSlot(Mockito.anyInt())).thenReturn(null);
+ // simulate good connection
+ Mockito.when(connectionHandler.getConnection()).thenReturn(Mockito.mock(Jedis.class));
+
+ JedisClusterCommand testMe = new JedisClusterCommand(connectionHandler, 10) {
+ @Override
+ public String execute(Jedis connection) {
+ if (connection == null) {
+ throw new JedisConnectionException("");
+ }
+ return "foo";
+ }
+ };
+
+ String actual = testMe.run("");
+ assertEquals("foo", actual);
+ }
+
+ @Test
+ public void runMovedSuccess() {
+ JedisClusterConnectionHandler connectionHandler = Mockito
+ .mock(JedisClusterConnectionHandler.class);
+
+ JedisClusterCommand testMe = new JedisClusterCommand(connectionHandler, 10) {
+ boolean isFirstCall = true;
+
+ @Override
+ public String execute(Jedis connection) {
+ if (isFirstCall) {
+ isFirstCall = false;
+
+ // Slot 0 moved
+ throw new JedisMovedDataException("", null, 0);
+ }
+
+ return "foo";
+ }
+ };
+
+ String actual = testMe.run("");
+ assertEquals("foo", actual);
+
+ Mockito.verify(connectionHandler).renewSlotCache(Mockito. any());
+ }
+
+ @Test
+ public void runAskSuccess() {
+ JedisSlotBasedConnectionHandler connectionHandler = Mockito
+ .mock(JedisSlotBasedConnectionHandler.class);
+ Jedis jedis = Mockito.mock(Jedis.class);
+ Mockito.when(connectionHandler.getConnectionFromNode(Mockito. any())).thenReturn(
+ jedis);
+
+ JedisClusterCommand testMe = new JedisClusterCommand(connectionHandler, 10) {
+ boolean isFirstCall = true;
+
+ @Override
+ public String execute(Jedis connection) {
+ if (isFirstCall) {
+ isFirstCall = false;
+
+ // Slot 0 moved
+ throw new JedisAskDataException("", null, 0);
+ }
+
+ return "foo";
+ }
+ };
+
+ String actual = testMe.run("");
+ assertEquals("foo", actual);
+ Mockito.verify(jedis).asking();
+ }
+
+ @Test
+ public void runMovedFailSuccess() {
+ // Test:
+ // First attempt is a JedisMovedDataException() move, because we asked the wrong node
+ // Second attempt is a JedisConnectionException, because this node is down
+ // In response to that, runWithTimeout() requests a random node using
+ // connectionHandler.getConnection()
+ // Third attempt works
+ JedisSlotBasedConnectionHandler connectionHandler = Mockito
+ .mock(JedisSlotBasedConnectionHandler.class);
+
+ Jedis fromGetConnectionFromSlot = Mockito.mock(Jedis.class);
+ Mockito.when(fromGetConnectionFromSlot.toString()).thenReturn("getConnectionFromSlot");
+ Mockito.when(connectionHandler.getConnectionFromSlot(Mockito.anyInt())).thenReturn(
+ fromGetConnectionFromSlot);
+
+ Jedis fromGetConnectionFromNode = Mockito.mock(Jedis.class);
+ Mockito.when(fromGetConnectionFromNode.toString()).thenReturn("getConnectionFromNode");
+ Mockito.when(connectionHandler.getConnectionFromNode(Mockito. any())).thenReturn(
+ fromGetConnectionFromNode);
+
+ Jedis fromGetConnection = Mockito.mock(Jedis.class);
+ Mockito.when(fromGetConnection.toString()).thenReturn("getConnection");
+ Mockito.when(connectionHandler.getConnection()).thenReturn(fromGetConnection);
+
+ JedisClusterCommand testMe = new JedisClusterCommand(connectionHandler, 10) {
+ @Override
+ public String execute(Jedis connection) {
+ String source = connection.toString();
+ if ("getConnectionFromSlot".equals(source)) {
+ // First attempt, report moved
+ throw new JedisMovedDataException("Moved", null, 0);
+ }
+
+ if ("getConnectionFromNode".equals(source)) {
+ // Second attempt in response to the move, report failure
+ throw new JedisConnectionException("Connection failed");
+ }
+
+ // This is the third and last case we handle
+ assert "getConnection".equals(source);
+ return "foo";
+ }
+ };
+
+ String actual = testMe.run("");
+ assertEquals("foo", actual);
+ }
+
+ @Test(expected = JedisNoReachableClusterNodeException.class)
+ public void runRethrowsJedisNoReachableClusterNodeException() {
+ JedisSlotBasedConnectionHandler connectionHandler = Mockito
+ .mock(JedisSlotBasedConnectionHandler.class);
+ Mockito.when(connectionHandler.getConnectionFromSlot(Mockito.anyInt())).thenThrow(
+ JedisNoReachableClusterNodeException.class);
+
+ JedisClusterCommand testMe = new JedisClusterCommand(connectionHandler, 10) {
+ @Override
+ public String execute(Jedis connection) {
+ return null;
+ }
+ };
+
+ testMe.run("");
+ }
+}
diff --git a/src/test/java/redis/clients/jedis/tests/demo/ClusterDemo.java b/src/test/java/redis/clients/jedis/tests/demo/ClusterDemo.java
new file mode 100644
index 0000000000..9c120233d7
--- /dev/null
+++ b/src/test/java/redis/clients/jedis/tests/demo/ClusterDemo.java
@@ -0,0 +1,33 @@
+package redis.clients.jedis.tests.demo;
+
+import java.util.HashSet;
+import java.util.Set;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+import redis.clients.jedis.JedisPoolConfig;
+
+// See: https://github.com/redis/jedis/issues/2347
+public class ClusterDemo {
+ // Expect to find a Redis cluster on this and the five following ports
+ private final static int BASE_PORT = 6379;
+
+ // This program should survive any node being down for up to 30s, and keep printing. Having
+ // printouts pause while the node is down is fine.
+ public static void main(String[] args) throws InterruptedException {
+ JedisPoolConfig poolConfig = new JedisPoolConfig();
+
+ Set nodes = new HashSet<>();
+ for (int i = 0; i < 6; i++) {
+ nodes.add(new HostAndPort("127.0.0.1", BASE_PORT + i));
+ }
+ JedisCluster cluster = new JedisCluster(nodes, 10_000, 10, poolConfig);
+
+ // noinspection InfiniteLoopStatement
+ while (true) {
+ final Long foo = cluster.incr("foo");
+ System.out.printf("foo=%d%n", foo);
+ // noinspection BusyWait
+ Thread.sleep(1000);
+ }
+ }
+}