package com.lambdaworks.redis.cluster; import static com.google.code.tempusfugit.temporal.Duration.seconds; import static com.google.code.tempusfugit.temporal.Timeout.timeout; import static com.lambdaworks.redis.cluster.ClusterTestUtil.getNodeId; import static com.lambdaworks.redis.cluster.ClusterTestUtil.getOwnPartition; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Fail.fail; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.junit.*; import com.google.code.tempusfugit.temporal.Condition; import com.google.code.tempusfugit.temporal.WaitFor; import com.lambdaworks.Connections; import com.lambdaworks.Futures; import com.lambdaworks.Wait; import com.lambdaworks.category.SlowTests; import com.lambdaworks.redis.*; import com.lambdaworks.redis.api.async.RedisAsyncCommands; import com.lambdaworks.redis.cluster.api.StatefulRedisClusterConnection; import com.lambdaworks.redis.cluster.api.async.RedisAdvancedClusterAsyncCommands; import com.lambdaworks.redis.cluster.api.async.RedisClusterAsyncCommands; import com.lambdaworks.redis.cluster.api.sync.RedisAdvancedClusterCommands; import com.lambdaworks.redis.cluster.api.sync.RedisClusterCommands; import com.lambdaworks.redis.cluster.models.partitions.ClusterPartitionParser; import com.lambdaworks.redis.cluster.models.partitions.Partitions; import com.lambdaworks.redis.cluster.models.partitions.RedisClusterNode; /** * Test for mutable cluster setup scenarios. * * @author Mark Paluch * @since 3.0 */ @SuppressWarnings({ "unchecked" }) @SlowTests public class RedisClusterSetupTest extends AbstractTest { public static final String host = TestSettings.hostAddr(); private static RedisClusterClient clusterClient; private static RedisClient client = DefaultRedisClient.get(); private RedisClusterCommands<String, String> redis1; private RedisClusterCommands<String, String> redis2; @Rule public ClusterRule clusterRule = new ClusterRule(clusterClient, AbstractClusterTest.port5, AbstractClusterTest.port6); @BeforeClass public static void setupClient() { clusterClient = RedisClusterClient.create(RedisURI.Builder.redis(host, AbstractClusterTest.port5).build()); } @AfterClass public static void shutdownClient() { FastShutdown.shutdown(clusterClient); } @Before public void openConnection() throws Exception { redis1 = client.connect(RedisURI.Builder.redis(AbstractClusterTest.host, AbstractClusterTest.port5).build()).sync(); redis2 = client.connect(RedisURI.Builder.redis(AbstractClusterTest.host, AbstractClusterTest.port6).build()).sync(); clusterRule.clusterReset(); } @After public void closeConnection() throws Exception { redis1.close(); redis2.close(); } @Test public void clusterMeet() throws Exception { clusterRule.clusterReset(); Partitions partitionsBeforeMeet = ClusterPartitionParser.parse(redis1.clusterNodes()); assertThat(partitionsBeforeMeet.getPartitions()).hasSize(1); String result = redis1.clusterMeet(host, AbstractClusterTest.port6); assertThat(result).isEqualTo("OK"); Wait.untilEquals(2, () -> ClusterPartitionParser.parse(redis1.clusterNodes()).size()).waitOrTimeout(); Partitions partitionsAfterMeet = ClusterPartitionParser.parse(redis1.clusterNodes()); assertThat(partitionsAfterMeet.getPartitions()).hasSize(2); } @Test public void clusterForget() throws Exception { clusterRule.clusterReset(); String result = redis1.clusterMeet(host, AbstractClusterTest.port6); assertThat(result).isEqualTo("OK"); Wait.untilTrue(() -> redis1.clusterNodes().contains(redis2.clusterMyId())).waitOrTimeout(); Wait.untilTrue(() -> redis2.clusterNodes().contains(redis1.clusterMyId())).waitOrTimeout(); Wait.untilTrue(() -> { Partitions partitions = ClusterPartitionParser.parse(redis1.clusterNodes()); if (partitions.size() != 2) { return false; } for (RedisClusterNode redisClusterNode : partitions) { if (redisClusterNode.is(RedisClusterNode.NodeFlag.HANDSHAKE)) { return false; } } return true; }).waitOrTimeout(); redis1.clusterForget(redis2.clusterMyId()); Wait.untilEquals(1, () -> ClusterPartitionParser.parse(redis1.clusterNodes()).size()).waitOrTimeout(); Partitions partitionsAfterForget = ClusterPartitionParser.parse(redis1.clusterNodes()); assertThat(partitionsAfterForget.getPartitions()).hasSize(1); } @Test public void clusterDelSlots() throws Exception { ClusterSetup.setup2Masters(clusterRule); redis1.clusterDelSlots(1, 2, 5, 6); Wait.untilEquals(11996, () -> getOwnPartition(redis1).getSlots().size()).waitOrTimeout(); } @Test public void clusterSetSlots() throws Exception { ClusterSetup.setup2Masters(clusterRule); redis1.clusterSetSlotNode(6, getNodeId(redis2)); waitForSlots(redis1, 11999); waitForSlots(redis2, 4384); Partitions partitions = ClusterPartitionParser.parse(redis1.clusterNodes()); for (RedisClusterNode redisClusterNode : partitions.getPartitions()) { if (redisClusterNode.getFlags().contains(RedisClusterNode.NodeFlag.MYSELF)) { assertThat(redisClusterNode.getSlots()).contains(1, 2, 3, 4, 5).doesNotContain(6); } } } @Test public void clusterSlotMigrationImport() throws Exception { ClusterSetup.setup2Masters(clusterRule); String nodeId2 = getNodeId(redis2); assertThat(redis1.clusterSetSlotMigrating(6, nodeId2)).isEqualTo("OK"); assertThat(redis1.clusterSetSlotImporting(15000, nodeId2)).isEqualTo("OK"); assertThat(redis1.clusterSetSlotStable(6)).isEqualTo("OK"); } @Test public void clusterTopologyRefresh() throws Exception { clusterClient.setOptions( ClusterClientOptions.builder().refreshClusterView(true).refreshPeriod(5, TimeUnit.SECONDS).build()); clusterClient.reloadPartitions(); RedisAdvancedClusterAsyncCommands<String, String> clusterConnection = clusterClient.connect().async(); assertThat(clusterClient.getPartitions()).hasSize(1); ClusterSetup.setup2Masters(clusterRule); assertThat(clusterClient.getPartitions()).hasSize(2); clusterConnection.close(); } @Test public void changeTopologyWhileOperations() throws Exception { ClusterSetup.setup2Masters(clusterRule); ClusterTopologyRefreshOptions clusterTopologyRefreshOptions = ClusterTopologyRefreshOptions.builder() .enableAllAdaptiveRefreshTriggers().build(); clusterClient .setOptions(ClusterClientOptions.builder().topologyRefreshOptions(clusterTopologyRefreshOptions).build()); StatefulRedisClusterConnection<String, String> connection = clusterClient.connect(); RedisAdvancedClusterCommands<String, String> sync = connection.sync(); RedisAdvancedClusterAsyncCommands<String, String> async = connection.async(); Partitions partitions = connection.getPartitions(); assertThat(partitions.getPartitionBySlot(0).getSlots().size()).isEqualTo(12000); assertThat(partitions.getPartitionBySlot(16380).getSlots().size()).isEqualTo(4384); assertRoutedExecution(async); sync.del("A"); sync.del("t"); sync.del("p"); shiftAllSlotsToNode1(); assertRoutedExecution(async); Wait.untilTrue(() -> { if (clusterClient.getPartitions().size() == 2) { for (RedisClusterNode redisClusterNode : clusterClient.getPartitions()) { if (redisClusterNode.getSlots().size() > 16380) { return true; } } } return false; }).waitOrTimeout(); assertThat(partitions.getPartitionBySlot(0).getSlots().size()).isEqualTo(16384); assertThat(sync.get("A")).isEqualTo("value"); assertThat(sync.get("t")).isEqualTo("value"); assertThat(sync.get("p")).isEqualTo("value"); async.close(); } @Test public void slotMigrationShouldUseAsking() throws Exception { ClusterSetup.setup2Masters(clusterRule); StatefulRedisClusterConnection<String, String> connection = clusterClient.connect(); RedisAdvancedClusterCommands<String, String> sync = connection.sync(); RedisAdvancedClusterAsyncCommands<String, String> async = connection.async(); Partitions partitions = connection.getPartitions(); assertThat(partitions.getPartitionBySlot(0).getSlots().size()).isEqualTo(12000); assertThat(partitions.getPartitionBySlot(16380).getSlots().size()).isEqualTo(4384); redis1.clusterSetSlotMigrating(3300, redis2.clusterMyId()); redis2.clusterSetSlotImporting(3300, redis1.clusterMyId()); assertThat(sync.get("b")).isNull(); async.close(); } @Test public void disconnectedConnectionRejectTest() throws Exception { clusterClient.setOptions(ClusterClientOptions.builder().refreshClusterView(true).refreshPeriod(1, TimeUnit.SECONDS) .disconnectedBehavior(ClientOptions.DisconnectedBehavior.REJECT_COMMANDS).build()); RedisAdvancedClusterAsyncCommands<String, String> clusterConnection = clusterClient.connect().async(); clusterClient.setOptions(ClusterClientOptions.builder() .disconnectedBehavior(ClientOptions.DisconnectedBehavior.REJECT_COMMANDS).refreshClusterView(false).build()); ClusterSetup.setup2Masters(clusterRule); assertRoutedExecution(clusterConnection); RedisClusterNode partition1 = getOwnPartition(redis1); RedisClusterAsyncCommands<String, String> node1Connection = clusterConnection .getConnection(partition1.getUri().getHost(), partition1.getUri().getPort()); shiftAllSlotsToNode1(); suspendConnection(node1Connection); RedisFuture<String> set = clusterConnection.set("t", "value"); // 15891 set.await(5, TimeUnit.SECONDS); try { set.get(); fail("Missing RedisException"); } catch (ExecutionException e) { assertThat(e).hasRootCauseInstanceOf(RedisException.class).hasMessageContaining("not connected"); } finally { clusterConnection.close(); } } @Test public void atLeastOnceForgetNodeFailover() throws Exception { ClusterTopologyRefreshOptions refreshOptions = ClusterTopologyRefreshOptions.builder() .enablePeriodicRefresh(1, TimeUnit.SECONDS).dynamicRefreshSources(false) .build(); clusterClient.setOptions( ClusterClientOptions.builder().topologyRefreshOptions(refreshOptions).build()); RedisAdvancedClusterAsyncCommands<String, String> clusterConnection = clusterClient.connectClusterAsync(); clusterClient.setOptions(ClusterClientOptions.builder().refreshClusterView(false).build()); ClusterSetup.setup2Masters(clusterRule); assertRoutedExecution(clusterConnection); RedisClusterNode partition1 = getOwnPartition(redis1); RedisClusterNode partition2 = getOwnPartition(redis2); RedisClusterAsyncCommands<String, String> node2Connection = clusterConnection .getConnection(partition2.getUri().getHost(), partition2.getUri().getPort()); shiftAllSlotsToNode1(); suspendConnection(node2Connection); List<RedisFuture<String>> futures = new ArrayList<>(); futures.add(clusterConnection.set("t", "value")); // 15891 futures.add(clusterConnection.set("p", "value")); // 16023 clusterConnection.set("A", "value").get(1, TimeUnit.SECONDS); // 6373 for (RedisFuture<String> future : futures) { assertThat(future.isDone()).isFalse(); assertThat(future.isCancelled()).isFalse(); } redis1.clusterForget(partition2.getNodeId()); redis2.clusterForget(partition1.getNodeId()); clusterClient.setOptions(ClusterClientOptions.builder().topologyRefreshOptions(refreshOptions).build()); waitUntilOnlyOnePartition(); Wait.untilTrue(() -> Futures.areAllCompleted(futures)).waitOrTimeout(); assertRoutedExecution(clusterConnection); clusterConnection.close(); } @Test public void expireStaleNodeIdConnections() throws Exception { clusterClient.setOptions( ClusterClientOptions.builder().refreshClusterView(true).refreshPeriod(1, TimeUnit.SECONDS).build()); RedisAdvancedClusterAsyncCommands<String, String> clusterConnection = clusterClient.connectClusterAsync(); ClusterSetup.setup2Masters(clusterRule); PooledClusterConnectionProvider<String, String> clusterConnectionProvider = getPooledClusterConnectionProvider( clusterConnection); assertThat(clusterConnectionProvider.getConnectionCount()).isEqualTo(0); assertRoutedExecution(clusterConnection); assertThat(clusterConnectionProvider.getConnectionCount()).isEqualTo(2); Partitions partitions = ClusterPartitionParser.parse(redis1.clusterNodes()); for (RedisClusterNode redisClusterNode : partitions.getPartitions()) { if (!redisClusterNode.getFlags().contains(RedisClusterNode.NodeFlag.MYSELF)) { redis1.clusterForget(redisClusterNode.getNodeId()); } } partitions = ClusterPartitionParser.parse(redis2.clusterNodes()); for (RedisClusterNode redisClusterNode : partitions.getPartitions()) { if (!redisClusterNode.getFlags().contains(RedisClusterNode.NodeFlag.MYSELF)) { redis2.clusterForget(redisClusterNode.getNodeId()); } } Wait.untilEquals(1, () -> clusterClient.getPartitions().size()).waitOrTimeout(); Wait.untilEquals(1, () -> clusterConnectionProvider.getConnectionCount()).waitOrTimeout(); clusterConnection.close(); } private void assertRoutedExecution(RedisClusterAsyncCommands<String, String> clusterConnection) throws Exception { assertExecuted(clusterConnection.set("A", "value")); // 6373 assertExecuted(clusterConnection.set("t", "value")); // 15891 assertExecuted(clusterConnection.set("p", "value")); // 16023 } @Test public void doNotExpireStaleNodeIdConnections() throws Exception { clusterClient.setOptions(ClusterClientOptions.builder().closeStaleConnections(false).build()); RedisAdvancedClusterAsyncCommands<String, String> clusterConnection = clusterClient.connect().async(); ClusterSetup.setup2Masters(clusterRule); PooledClusterConnectionProvider<String, String> clusterConnectionProvider = getPooledClusterConnectionProvider( clusterConnection); assertThat(clusterConnectionProvider.getConnectionCount()).isEqualTo(0); assertRoutedExecution(clusterConnection); assertThat(clusterConnectionProvider.getConnectionCount()).isEqualTo(2); Partitions partitions = ClusterPartitionParser.parse(redis1.clusterNodes()); for (RedisClusterNode redisClusterNode : partitions.getPartitions()) { if (!redisClusterNode.getFlags().contains(RedisClusterNode.NodeFlag.MYSELF)) { redis1.clusterForget(redisClusterNode.getNodeId()); } } partitions = ClusterPartitionParser.parse(redis2.clusterNodes()); for (RedisClusterNode redisClusterNode : partitions.getPartitions()) { if (!redisClusterNode.getFlags().contains(RedisClusterNode.NodeFlag.MYSELF)) { redis2.clusterForget(redisClusterNode.getNodeId()); } } clusterClient.reloadPartitions(); assertThat(clusterConnectionProvider.getConnectionCount()).isEqualTo(2); clusterConnection.close(); } @Test public void expireStaleHostAndPortConnections() throws Exception { clusterClient.setOptions(ClusterClientOptions.builder().build()); RedisAdvancedClusterAsyncCommands<String, String> clusterConnection = clusterClient.connectClusterAsync(); ClusterSetup.setup2Masters(clusterRule); final PooledClusterConnectionProvider<String, String> clusterConnectionProvider = getPooledClusterConnectionProvider( clusterConnection); assertThat(clusterConnectionProvider.getConnectionCount()).isEqualTo(0); assertRoutedExecution(clusterConnection); assertThat(clusterConnectionProvider.getConnectionCount()).isEqualTo(2); for (RedisClusterNode redisClusterNode : clusterClient.getPartitions()) { clusterConnection.getConnection(redisClusterNode.getUri().getHost(), redisClusterNode.getUri().getPort()); clusterConnection.getConnection(redisClusterNode.getNodeId()); } assertThat(clusterConnectionProvider.getConnectionCount()).isEqualTo(4); Partitions partitions = ClusterPartitionParser.parse(redis1.clusterNodes()); for (RedisClusterNode redisClusterNode : partitions.getPartitions()) { if (!redisClusterNode.getFlags().contains(RedisClusterNode.NodeFlag.MYSELF)) { redis1.clusterForget(redisClusterNode.getNodeId()); } } partitions = ClusterPartitionParser.parse(redis2.clusterNodes()); for (RedisClusterNode redisClusterNode : partitions.getPartitions()) { if (!redisClusterNode.getFlags().contains(RedisClusterNode.NodeFlag.MYSELF)) { redis2.clusterForget(redisClusterNode.getNodeId()); } } clusterClient.reloadPartitions(); Wait.untilEquals(1, () -> clusterClient.getPartitions().size()).waitOrTimeout(); Wait.untilEquals(2L, () -> clusterConnectionProvider.getConnectionCount()).waitOrTimeout(); clusterConnection.close(); } @Test public void readFromSlaveTest() throws Exception { ClusterSetup.setup2Masters(clusterRule); RedisAdvancedClusterAsyncCommands<String, String> clusterConnection = clusterClient.connect().async(); clusterConnection.getStatefulConnection().setReadFrom(ReadFrom.SLAVE); clusterConnection.set(key, value).get(); try { clusterConnection.get(key); } catch (RedisException e) { assertThat(e).hasMessageContaining("Cannot determine a partition to read for slot"); } clusterConnection.close(); } @Test public void readFromNearestTest() throws Exception { ClusterSetup.setup2Masters(clusterRule); RedisAdvancedClusterCommands<String, String> clusterConnection = clusterClient.connect().sync(); clusterConnection.getStatefulConnection().setReadFrom(ReadFrom.NEAREST); clusterConnection.set(key, value); assertThat(clusterConnection.get(key)).isEqualTo(value); clusterConnection.close(); } protected PooledClusterConnectionProvider<String, String> getPooledClusterConnectionProvider( RedisAdvancedClusterAsyncCommands<String, String> clusterAsyncConnection) { RedisChannelHandler<String, String> channelHandler = getChannelHandler(clusterAsyncConnection); ClusterDistributionChannelWriter writer = (ClusterDistributionChannelWriter) channelHandler.getChannelWriter(); return (PooledClusterConnectionProvider<String, String>) writer.getClusterConnectionProvider(); } private RedisChannelHandler<String, String> getChannelHandler( RedisAdvancedClusterAsyncCommands<String, String> clusterAsyncConnection) { return (RedisChannelHandler<String, String>) clusterAsyncConnection.getStatefulConnection(); } private void assertExecuted(RedisFuture<String> set) throws Exception { set.get(5, TimeUnit.SECONDS); assertThat(set.getError()).isNull(); assertThat(set.get()).isEqualTo("OK"); } private void waitUntilOnlyOnePartition() throws InterruptedException, TimeoutException { Wait.untilEquals(1, () -> clusterClient.getPartitions().size()).waitOrTimeout(); } private void suspendConnection(RedisClusterAsyncCommands<String, String> asyncCommands) throws InterruptedException, TimeoutException { Connections.getConnectionWatchdog(((RedisAsyncCommands<?, ?>) asyncCommands).getStatefulConnection()) .setReconnectSuspended(true); asyncCommands.quit(); WaitFor.waitOrTimeout(() -> !asyncCommands.isOpen(), timeout(seconds(6))); } protected void shiftAllSlotsToNode1() throws InterruptedException, TimeoutException { redis1.clusterDelSlots(AbstractClusterTest.createSlots(12000, 16384)); redis2.clusterDelSlots(AbstractClusterTest.createSlots(12000, 16384)); waitForSlots(redis2, 0); final RedisClusterNode redis2Partition = getOwnPartition(redis2); WaitFor.waitOrTimeout(new Condition() { @Override public boolean isSatisfied() { Partitions partitions = ClusterPartitionParser.parse(redis1.clusterNodes()); RedisClusterNode partition = partitions.getPartitionByNodeId(redis2Partition.getNodeId()); if (!partition.getSlots().isEmpty()) { removeRemaining(partition); } return partition.getSlots().size() == 0; } private void removeRemaining(RedisClusterNode partition) { try { redis1.clusterDelSlots(toIntArray(partition.getSlots())); } catch (Exception o_O) { // ignore } } }, timeout(seconds(10))); redis1.clusterAddSlots(RedisClusterClientTest.createSlots(12000, 16384)); waitForSlots(redis1, 16384); Wait.untilTrue(clusterRule::isStable).waitOrTimeout(); } private int[] toIntArray(List<Integer> list) { return list.parallelStream().mapToInt(Integer::intValue).toArray(); } private void waitForSlots(RedisClusterCommands<String, String> connection, int slotCount) throws InterruptedException, TimeoutException { Wait.untilEquals(slotCount, () -> getOwnPartition(connection).getSlots().size()).waitOrTimeout(); } }