package com.lambdaworks.redis.cluster.topology;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.concurrent.TimeUnit;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import com.lambdaworks.redis.RedisException;
import com.lambdaworks.redis.RedisURI;
import com.lambdaworks.redis.api.StatefulRedisConnection;
import com.lambdaworks.redis.api.async.RedisAsyncCommands;
import com.lambdaworks.redis.cluster.RedisClusterClient;
import com.lambdaworks.redis.cluster.models.partitions.Partitions;
import com.lambdaworks.redis.cluster.models.partitions.RedisClusterNode;
import com.lambdaworks.redis.codec.RedisCodec;
import com.lambdaworks.redis.protocol.CommandType;
import com.lambdaworks.redis.resource.ClientResources;
import com.lambdaworks.redis.resource.DnsResolvers;
/**
* @author Mark Paluch
*/
@RunWith(MockitoJUnitRunner.class)
public class ClusterTopologyRefreshTest {
public final static long COMMAND_TIMEOUT_NS = TimeUnit.MILLISECONDS.toNanos(10);
public static final String NODE_1_VIEW = "1 127.0.0.1:7380 master,myself - 0 1401258245007 2 disconnected 8000-11999\n"
+ "2 127.0.0.1:7381 master - 111 1401258245007 222 connected 7000 12000 12002-16383\n";
public static final String NODE_2_VIEW = "1 127.0.0.1:7380 master - 0 1401258245007 2 disconnected 8000-11999\n"
+ "2 127.0.0.1:7381 master,myself - 111 1401258245007 222 connected 7000 12000 12002-16383\n";
private ClusterTopologyRefresh sut;
@Mock
private RedisClusterClient client;
@Mock
private StatefulRedisConnection<String, String> connection;
@Mock
private ClientResources clientResources;
@Mock
private NodeConnectionFactory nodeConnectionFactory;
@Mock
private StatefulRedisConnection<String, String> connection1;
@Mock
private RedisAsyncCommands<String, String> asyncCommands1;
@Mock
private StatefulRedisConnection<String, String> connection2;
@Mock
private RedisAsyncCommands<String, String> asyncCommands2;
@Before
public void before() throws Exception {
when(clientResources.dnsResolver()).thenReturn(DnsResolvers.JVM_DEFAULT);
when(connection1.async()).thenReturn(asyncCommands1);
when(connection2.async()).thenReturn(asyncCommands2);
when(connection1.dispatch(any())).thenAnswer(invocation -> {
TimedAsyncCommand command = (TimedAsyncCommand) invocation.getArguments()[0];
if (command.getType() == CommandType.CLUSTER) {
command.getOutput().set(ByteBuffer.wrap(NODE_1_VIEW.getBytes()));
command.complete();
}
if (command.getType() == CommandType.CLIENT) {
command.getOutput().set(ByteBuffer.wrap("c1\nc2\n".getBytes()));
command.complete();
}
command.encodedAtNs = 10;
command.completedAtNs = 50;
return command;
});
when(connection2.dispatch(any())).thenAnswer(invocation -> {
TimedAsyncCommand command = (TimedAsyncCommand) invocation.getArguments()[0];
if (command.getType() == CommandType.CLUSTER) {
command.getOutput().set(ByteBuffer.wrap(NODE_2_VIEW.getBytes()));
command.complete();
}
if (command.getType() == CommandType.CLIENT) {
command.getOutput().set(ByteBuffer.wrap("".getBytes()));
command.complete();
}
command.encodedAtNs = 10;
command.completedAtNs = 20;
return command;
});
sut = new ClusterTopologyRefresh(nodeConnectionFactory, clientResources);
}
@Test
public void getNodeSpecificViewsNode1IsFasterThanNode2() throws Exception {
Requests requests = createClusterNodesRequests(1, NODE_1_VIEW);
requests = createClusterNodesRequests(2, NODE_2_VIEW).mergeWith(requests);
Requests clientRequests = createClientListRequests(1, "c1\nc2\n").mergeWith(createClientListRequests(2, "c1\nc2\n"));
NodeTopologyViews nodeSpecificViews = sut.getNodeSpecificViews(requests, clientRequests, COMMAND_TIMEOUT_NS);
Collection<Partitions> values = nodeSpecificViews.toMap().values();
assertThat(values).hasSize(2);
for (Partitions value : values) {
assertThat(value).extracting("nodeId").containsExactly("1", "2");
}
}
@Test
public void getNodeSpecificViewTestingNoAddrFilter() throws Exception {
String nodes1 = "n1 10.37.110.63:7000 slave n3 0 1452553664848 43 connected\n"
+ "n2 10.37.110.68:7000 slave n6 0 1452553664346 45 connected\n"
+ "badSlave :0 slave,fail,noaddr n5 1449160058028 1449160053146 46 disconnected\n"
+ "n3 10.37.110.69:7000 master - 0 1452553662842 43 connected 3829-6787 7997-9999\n"
+ "n4 10.37.110.62:7000 slave n3 0 1452553663844 43 connected\n"
+ "n5 10.37.110.70:7000 myself,master - 0 0 46 connected 10039-14999\n"
+ "n6 10.37.110.65:7000 master - 0 1452553663844 45 connected 0-3828 6788-7996 10000-10038 15000-16383";
Requests clusterNodesRequests = createClusterNodesRequests(1, nodes1);
Requests clientRequests = createClientListRequests(1, "c1\nc2\n");
NodeTopologyViews nodeSpecificViews = sut.getNodeSpecificViews(clusterNodesRequests, clientRequests,
COMMAND_TIMEOUT_NS);
List<Partitions> values = new ArrayList<>(nodeSpecificViews.toMap().values());
assertThat(values).hasSize(1);
for (Partitions value : values) {
assertThat(value).extracting("nodeId").containsOnly("n1", "n2", "n3", "n4", "n5", "n6");
}
RedisClusterNodeSnapshot firstPartition = (RedisClusterNodeSnapshot) values.get(0).getPartition(0);
RedisClusterNodeSnapshot selfPartition = (RedisClusterNodeSnapshot) values.get(0).getPartition(4);
assertThat(firstPartition.getConnectedClients()).isEqualTo(2);
assertThat(selfPartition.getConnectedClients()).isNull();
}
@Test
public void getNodeSpecificViewsNode2IsFasterThanNode1() throws Exception {
Requests clusterNodesRequests = createClusterNodesRequests(5, NODE_1_VIEW);
clusterNodesRequests = createClusterNodesRequests(1, NODE_2_VIEW).mergeWith(clusterNodesRequests);
Requests clientRequests = createClientListRequests(5, "c1\nc2\n").mergeWith(createClientListRequests(1, "c1\nc2\n"));
NodeTopologyViews nodeSpecificViews = sut.getNodeSpecificViews(clusterNodesRequests, clientRequests,
COMMAND_TIMEOUT_NS);
List<Partitions> values = new ArrayList<>(nodeSpecificViews.toMap().values());
assertThat(values).hasSize(2);
for (Partitions value : values) {
assertThat(value).extracting("nodeId").containsExactly("2", "1");
}
}
@Test
public void shouldAttemptToConnectOnlyOnce() throws Exception {
List<RedisURI> seed = Arrays.asList(RedisURI.create("127.0.0.1", 7380), RedisURI.create("127.0.0.1", 7381));
when(nodeConnectionFactory.connectToNode(any(RedisCodec.class), eq(new InetSocketAddress("127.0.0.1", 7380))))
.thenReturn((StatefulRedisConnection) connection1);
when(nodeConnectionFactory.connectToNode(any(RedisCodec.class), eq(new InetSocketAddress("127.0.0.1", 7381))))
.thenThrow(new RedisException("connection failed"));
sut.loadViews(seed, true);
verify(nodeConnectionFactory).connectToNode(any(RedisCodec.class), eq(new InetSocketAddress("127.0.0.1", 7380)));
verify(nodeConnectionFactory).connectToNode(any(RedisCodec.class), eq(new InetSocketAddress("127.0.0.1", 7381)));
}
@Test
public void shouldShouldDiscoverNodes() throws Exception {
List<RedisURI> seed = Arrays.asList(RedisURI.create("127.0.0.1", 7380));
when(nodeConnectionFactory.connectToNode(any(RedisCodec.class), eq(new InetSocketAddress("127.0.0.1", 7380))))
.thenReturn((StatefulRedisConnection) connection1);
when(nodeConnectionFactory.connectToNode(any(RedisCodec.class), eq(new InetSocketAddress("127.0.0.1", 7381))))
.thenReturn((StatefulRedisConnection) connection2);
sut.loadViews(seed, true);
verify(nodeConnectionFactory).connectToNode(any(RedisCodec.class), eq(new InetSocketAddress("127.0.0.1", 7380)));
verify(nodeConnectionFactory).connectToNode(any(RedisCodec.class), eq(new InetSocketAddress("127.0.0.1", 7381)));
}
@Test
public void shouldShouldNotDiscoverNodes() throws Exception {
List<RedisURI> seed = Arrays.asList(RedisURI.create("127.0.0.1", 7380));
when(nodeConnectionFactory.connectToNode(any(RedisCodec.class), eq(new InetSocketAddress("127.0.0.1", 7380))))
.thenReturn((StatefulRedisConnection) connection1);
sut.loadViews(seed, false);
verify(nodeConnectionFactory).connectToNode(any(RedisCodec.class), eq(new InetSocketAddress("127.0.0.1", 7380)));
verifyNoMoreInteractions(nodeConnectionFactory);
}
@Test
public void shouldNotFailOnDuplicateSeedNodes() throws Exception {
List<RedisURI> seed = Arrays.asList(RedisURI.create("127.0.0.1", 7380), RedisURI.create("127.0.0.1", 7381),
RedisURI.create("127.0.0.1", 7381));
when(nodeConnectionFactory.connectToNode(any(RedisCodec.class), eq(new InetSocketAddress("127.0.0.1", 7380))))
.thenReturn((StatefulRedisConnection) connection1);
when(nodeConnectionFactory.connectToNode(any(RedisCodec.class), eq(new InetSocketAddress("127.0.0.1", 7381))))
.thenReturn((StatefulRedisConnection) connection2);
sut.loadViews(seed, true);
verify(nodeConnectionFactory).connectToNode(any(RedisCodec.class), eq(new InetSocketAddress("127.0.0.1", 7380)));
verify(nodeConnectionFactory).connectToNode(any(RedisCodec.class), eq(new InetSocketAddress("127.0.0.1", 7381)));
}
@Test
public void undiscoveredAdditionalNodesShouldBeLastUsingClientCount() throws Exception {
List<RedisURI> seed = Arrays.asList(RedisURI.create("127.0.0.1", 7380));
when(nodeConnectionFactory.connectToNode(any(RedisCodec.class), eq(new InetSocketAddress("127.0.0.1", 7380))))
.thenReturn((StatefulRedisConnection) connection1);
Map<RedisURI, Partitions> partitionsMap = sut.loadViews(seed, false);
Partitions partitions = partitionsMap.values().iterator().next();
List<RedisClusterNode> nodes = TopologyComparators.sortByClientCount(partitions);
assertThat(nodes).hasSize(2).extracting(RedisClusterNode::getUri).containsSequence(seed.get(0),
RedisURI.create("127.0.0.1", 7381));
}
@Test
public void discoveredAdditionalNodesShouldBeOrderedUsingClientCount() throws Exception {
List<RedisURI> seed = Arrays.asList(RedisURI.create("127.0.0.1", 7380));
when(nodeConnectionFactory.connectToNode(any(RedisCodec.class), eq(new InetSocketAddress("127.0.0.1", 7380))))
.thenReturn((StatefulRedisConnection) connection1);
when(nodeConnectionFactory.connectToNode(any(RedisCodec.class), eq(new InetSocketAddress("127.0.0.1", 7381))))
.thenReturn((StatefulRedisConnection) connection2);
Map<RedisURI, Partitions> partitionsMap = sut.loadViews(seed, true);
Partitions partitions = partitionsMap.values().iterator().next();
List<RedisClusterNode> nodes = TopologyComparators.sortByClientCount(partitions);
assertThat(nodes).hasSize(2).extracting(RedisClusterNode::getUri).containsSequence(RedisURI.create("127.0.0.1", 7381),
seed.get(0));
}
@Test
public void undiscoveredAdditionalNodesShouldBeLastUsingLatency() throws Exception {
List<RedisURI> seed = Arrays.asList(RedisURI.create("127.0.0.1", 7380));
when(nodeConnectionFactory.connectToNode(any(RedisCodec.class), eq(new InetSocketAddress("127.0.0.1", 7380))))
.thenReturn((StatefulRedisConnection) connection1);
Map<RedisURI, Partitions> partitionsMap = sut.loadViews(seed, false);
Partitions partitions = partitionsMap.values().iterator().next();
List<RedisClusterNode> nodes = TopologyComparators.sortByLatency(partitions);
assertThat(nodes).hasSize(2).extracting(RedisClusterNode::getUri).containsSequence(seed.get(0),
RedisURI.create("127.0.0.1", 7381));
}
@Test
public void discoveredAdditionalNodesShouldBeOrderedUsingLatency() throws Exception {
List<RedisURI> seed = Arrays.asList(RedisURI.create("127.0.0.1", 7380));
when(nodeConnectionFactory.connectToNode(any(RedisCodec.class), eq(new InetSocketAddress("127.0.0.1", 7380))))
.thenReturn((StatefulRedisConnection) connection1);
when(nodeConnectionFactory.connectToNode(any(RedisCodec.class), eq(new InetSocketAddress("127.0.0.1", 7381))))
.thenReturn((StatefulRedisConnection) connection2);
Map<RedisURI, Partitions> partitionsMap = sut.loadViews(seed, true);
Partitions partitions = partitionsMap.values().iterator().next();
List<RedisClusterNode> nodes = TopologyComparators.sortByLatency(partitions);
assertThat(nodes).hasSize(2).extracting(RedisClusterNode::getUri).containsSequence(RedisURI.create("127.0.0.1", 7381),
seed.get(0));
}
protected Requests createClusterNodesRequests(int duration, String nodes) {
RedisURI redisURI = RedisURI.create("redis://localhost:" + duration);
Connections connections = new Connections();
connections.addConnection(redisURI, connection);
Requests requests = connections.requestTopology();
TimedAsyncCommand<String, String, String> command = requests.rawViews.get(redisURI);
command.getOutput().set(ByteBuffer.wrap(nodes.getBytes()));
command.complete();
command.encodedAtNs = 0;
command.completedAtNs = duration;
return requests;
}
protected Requests createClientListRequests(int duration, String response) {
RedisURI redisURI = RedisURI.create("redis://localhost:" + duration);
Connections connections = new Connections();
connections.addConnection(redisURI, connection);
Requests requests = connections.requestTopology();
TimedAsyncCommand<String, String, String> command = requests.rawViews.get(redisURI);
command.getOutput().set(ByteBuffer.wrap(response.getBytes()));
command.complete();
return requests;
}
}