package com.lambdaworks.redis.cluster.topology; import static org.assertj.core.api.Assertions.assertThat; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; import io.netty.util.concurrent.ScheduledFuture; import org.junit.After; import org.junit.Before; import org.junit.Test; import com.lambdaworks.Wait; import com.lambdaworks.category.SlowTests; import com.lambdaworks.redis.*; import com.lambdaworks.redis.api.async.BaseRedisAsyncCommands; import com.lambdaworks.redis.cluster.AbstractClusterTest; import com.lambdaworks.redis.cluster.ClusterClientOptions; import com.lambdaworks.redis.cluster.ClusterTopologyRefreshOptions; import com.lambdaworks.redis.cluster.RedisClusterClient; 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.RedisClusterCommands; import com.lambdaworks.redis.cluster.models.partitions.Partitions; import com.lambdaworks.redis.cluster.models.partitions.RedisClusterNode; import org.springframework.test.util.ReflectionTestUtils; /** * Test for topology refreshing. * * @author Mark Paluch */ @SuppressWarnings({ "unchecked" }) @SlowTests public class TopologyRefreshTest extends AbstractTest { public static final String host = TestSettings.hostAddr(); private static RedisClient client = DefaultRedisClient.get(); private RedisClusterClient clusterClient; private RedisClusterCommands<String, String> redis1; private RedisClusterCommands<String, String> redis2; @Before public void openConnection() throws Exception { clusterClient = RedisClusterClient.create(client.getResources(), RedisURI.Builder.redis(host, AbstractClusterTest.port1).build()); redis1 = client.connect(RedisURI.Builder.redis(AbstractClusterTest.host, AbstractClusterTest.port1).build()).sync(); redis2 = client.connect(RedisURI.Builder.redis(AbstractClusterTest.host, AbstractClusterTest.port2).build()).sync(); } @After public void closeConnection() throws Exception { redis1.close(); redis2.close(); FastShutdown.shutdown(clusterClient); } @Test public void shouldUnsubscribeTopologyRefresh() throws Exception { ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder() .enablePeriodicRefresh(true) // .build(); clusterClient.setOptions(ClusterClientOptions.builder().topologyRefreshOptions(topologyRefreshOptions).build()); RedisAdvancedClusterAsyncCommands<String, String> clusterConnection = clusterClient.connect().async(); AtomicBoolean clusterTopologyRefreshActivated = (AtomicBoolean) ReflectionTestUtils .getField(clusterClient, "clusterTopologyRefreshActivated"); AtomicReference<ScheduledFuture<?>> clusterTopologyRefreshFuture = (AtomicReference) ReflectionTestUtils .getField(clusterClient, "clusterTopologyRefreshFuture"); assertThat(clusterTopologyRefreshActivated.get()).isTrue(); assertThat(clusterTopologyRefreshFuture.get()).isNotNull(); ScheduledFuture<?> scheduledFuture = clusterTopologyRefreshFuture.get(); clusterConnection.close(); FastShutdown.shutdown(clusterClient); assertThat(clusterTopologyRefreshActivated.get()).isFalse(); assertThat(clusterTopologyRefreshFuture.get()).isNull(); assertThat(scheduledFuture.isCancelled()).isTrue(); } @Test public void changeTopologyWhileOperations() throws Exception { ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder() .enablePeriodicRefresh(true)// .refreshPeriod(1, TimeUnit.SECONDS)// .build(); clusterClient.setOptions(ClusterClientOptions.builder().topologyRefreshOptions(topologyRefreshOptions).build()); RedisAdvancedClusterAsyncCommands<String, String> clusterConnection = clusterClient.connect().async(); clusterClient.getPartitions().clear(); Wait.untilTrue(() -> { return !clusterClient.getPartitions().isEmpty(); }).waitOrTimeout(); clusterConnection.close(); } @Test public void dynamicSourcesProvidesClientCountForAllNodes() throws Exception { ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.create(); clusterClient.setOptions(ClusterClientOptions.builder().topologyRefreshOptions(topologyRefreshOptions).build()); RedisAdvancedClusterAsyncCommands<String, String> clusterConnection = clusterClient.connect().async(); for (RedisClusterNode redisClusterNode : clusterClient.getPartitions()) { assertThat(redisClusterNode).isInstanceOf(RedisClusterNodeSnapshot.class); RedisClusterNodeSnapshot snapshot = (RedisClusterNodeSnapshot) redisClusterNode; assertThat(snapshot.getConnectedClients()).isNotNull().isGreaterThanOrEqualTo(0); } clusterConnection.close(); } @Test public void staticSourcesProvidesClientCountForSeedNodes() throws Exception { ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder() .dynamicRefreshSources(false).build(); clusterClient.setOptions(ClusterClientOptions.builder().topologyRefreshOptions(topologyRefreshOptions).build()); RedisAdvancedClusterAsyncCommands<String, String> clusterConnection = clusterClient.connect().async(); Partitions partitions = clusterClient.getPartitions(); RedisClusterNodeSnapshot node1 = (RedisClusterNodeSnapshot) partitions.getPartitionBySlot(0); assertThat(node1.getConnectedClients()).isGreaterThanOrEqualTo(1); RedisClusterNodeSnapshot node2 = (RedisClusterNodeSnapshot) partitions.getPartitionBySlot(15000); assertThat(node2.getConnectedClients()).isNull(); clusterConnection.close(); } @Test public void adaptiveTopologyUpdateOnDisconnectNodeIdConnection() throws Exception { runReconnectTest((clusterConnection, node) -> { RedisClusterAsyncCommands<String, String> connection = clusterConnection.getConnection(node.getUri().getHost(), node.getUri().getPort()); return connection; }); } @Test public void adaptiveTopologyUpdateOnDisconnectHostAndPortConnection() throws Exception { runReconnectTest((clusterConnection, node) -> { RedisClusterAsyncCommands<String, String> connection = clusterConnection.getConnection(node.getUri().getHost(), node.getUri().getPort()); return connection; }); } @Test public void adaptiveTopologyUpdateOnDisconnectDefaultConnection() throws Exception { runReconnectTest((clusterConnection, node) -> { return clusterConnection; }); } @Test public void adaptiveTopologyUpdateIsRateLimited() throws Exception { ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()// .adaptiveRefreshTriggersTimeout(1, TimeUnit.HOURS)// .refreshTriggersReconnectAttempts(0)// .enableAllAdaptiveRefreshTriggers()// .build(); clusterClient.setOptions(ClusterClientOptions.builder().topologyRefreshOptions(topologyRefreshOptions).build()); RedisAdvancedClusterAsyncCommands<String, String> clusterConnection = clusterClient.connect().async(); clusterClient.getPartitions().clear(); clusterConnection.quit(); Wait.untilTrue(() -> { return !clusterClient.getPartitions().isEmpty(); }).waitOrTimeout(); clusterClient.getPartitions().clear(); clusterConnection.quit(); Thread.sleep(1000); assertThat(clusterClient.getPartitions()).isEmpty(); clusterConnection.close(); } @Test public void adaptiveTopologyUpdatetUsesTimeout() throws Exception { ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()// .adaptiveRefreshTriggersTimeout(500, TimeUnit.MILLISECONDS)// .refreshTriggersReconnectAttempts(0)// .enableAllAdaptiveRefreshTriggers()// .build(); clusterClient.setOptions(ClusterClientOptions.builder().topologyRefreshOptions(topologyRefreshOptions).build()); RedisAdvancedClusterAsyncCommands<String, String> clusterConnection = clusterClient.connect().async(); clusterConnection.quit(); Thread.sleep(1000); Wait.untilTrue(() -> { return !clusterClient.getPartitions().isEmpty(); }).waitOrTimeout(); clusterClient.getPartitions().clear(); clusterConnection.quit(); Wait.untilTrue(() -> { return !clusterClient.getPartitions().isEmpty(); }).waitOrTimeout(); clusterConnection.close(); } @Test public void adaptiveTriggerDoesNotFireOnSingleReconnect() throws Exception { ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()// .enableAllAdaptiveRefreshTriggers()// .build(); clusterClient.setOptions(ClusterClientOptions.builder().topologyRefreshOptions(topologyRefreshOptions).build()); RedisAdvancedClusterAsyncCommands<String, String> clusterConnection = clusterClient.connect().async(); clusterClient.getPartitions().clear(); clusterConnection.quit(); Thread.sleep(500); assertThat(clusterClient.getPartitions()).isEmpty(); clusterConnection.close(); } @Test public void adaptiveTriggerOnMoveRedirection() throws Exception { ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()// .enableAdaptiveRefreshTrigger(ClusterTopologyRefreshOptions.RefreshTrigger.MOVED_REDIRECT)// .build(); clusterClient.setOptions(ClusterClientOptions.builder().topologyRefreshOptions(topologyRefreshOptions).build()); StatefulRedisClusterConnection<String, String> connection = clusterClient.connect(); RedisAdvancedClusterAsyncCommands<String, String> clusterConnection = connection.async(); Partitions partitions = connection.getPartitions(); RedisClusterNode node1 = partitions.getPartitionBySlot(0); RedisClusterNode node2 = partitions.getPartitionBySlot(12000); node2.getSlots().addAll(node1.getSlots()); node1.getSlots().clear(); partitions.updateCache(); assertThat(clusterClient.getPartitions().getPartitionByNodeId(node1.getNodeId()).getSlots()).hasSize(0); assertThat(clusterClient.getPartitions().getPartitionByNodeId(node2.getNodeId()).getSlots()).hasSize(16384); clusterConnection.set("b", value); // slot 3300 Wait.untilEquals(12000, new Wait.Supplier<Integer>() { @Override public Integer get() throws Exception { return clusterClient.getPartitions().getPartitionByNodeId(node1.getNodeId()).getSlots().size(); } }).waitOrTimeout(); assertThat(clusterClient.getPartitions().getPartitionByNodeId(node1.getNodeId()).getSlots()).hasSize(12000); assertThat(clusterClient.getPartitions().getPartitionByNodeId(node2.getNodeId()).getSlots()).hasSize(4384); clusterConnection.close(); } private void runReconnectTest( BiFunction<RedisAdvancedClusterAsyncCommands<String, String>, RedisClusterNode, BaseRedisAsyncCommands> function) throws Exception { ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()// .refreshTriggersReconnectAttempts(0)// .enableAllAdaptiveRefreshTriggers()// .build(); clusterClient.setOptions(ClusterClientOptions.builder().topologyRefreshOptions(topologyRefreshOptions).build()); RedisAdvancedClusterAsyncCommands<String, String> clusterConnection = clusterClient.connect().async(); RedisClusterNode node = clusterClient.getPartitions().getPartition(0); BaseRedisAsyncCommands closeable = function.apply(clusterConnection, node); clusterClient.getPartitions().clear(); closeable.quit(); Wait.untilTrue(() -> { return !clusterClient.getPartitions().isEmpty(); }).waitOrTimeout(); closeable.close(); clusterConnection.close(); } }