/* * Copyright 2015-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.redis.connection; import static org.hamcrest.core.Is.*; import static org.hamcrest.core.IsCollectionContaining.*; import static org.junit.Assert.*; import static org.mockito.Mockito.*; import static org.springframework.data.redis.test.util.MockitoUtils.*; import java.util.Arrays; import java.util.HashSet; import java.util.LinkedHashSet; import org.hamcrest.core.IsInstanceOf; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.core.convert.converter.Converter; import org.springframework.core.task.SyncTaskExecutor; import org.springframework.dao.DataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.redis.ClusterRedirectException; import org.springframework.data.redis.PassThroughExceptionTranslationStrategy; import org.springframework.data.redis.TooManyClusterRedirectionsException; import org.springframework.data.redis.connection.ClusterCommandExecutor.ClusterCommandCallback; import org.springframework.data.redis.connection.ClusterCommandExecutor.MulitNodeResult; import org.springframework.data.redis.connection.ClusterCommandExecutor.MultiKeyClusterCommandCallback; import org.springframework.data.redis.connection.RedisClusterNode.LinkState; import org.springframework.data.redis.connection.RedisClusterNode.SlotRange; import org.springframework.data.redis.connection.RedisNode.NodeType; import org.springframework.scheduling.concurrent.ConcurrentTaskExecutor; /** * @author Christoph Strobl * @author Mark Paluch */ @RunWith(MockitoJUnitRunner.class) public class ClusterCommandExecutorUnitTests { static final String CLUSTER_NODE_1_HOST = "127.0.0.1"; static final String CLUSTER_NODE_2_HOST = "127.0.0.1"; static final String CLUSTER_NODE_3_HOST = "127.0.0.1"; static final int CLUSTER_NODE_1_PORT = 7379; static final int CLUSTER_NODE_2_PORT = 7380; static final int CLUSTER_NODE_3_PORT = 7381; static final RedisClusterNode CLUSTER_NODE_1 = RedisClusterNode.newRedisClusterNode() .listeningAt(CLUSTER_NODE_1_HOST, CLUSTER_NODE_1_PORT).serving(new SlotRange(0, 5460)) .withId("ef570f86c7b1a953846668debc177a3a16733420").promotedAs(NodeType.MASTER).linkState(LinkState.CONNECTED) .build(); static final RedisClusterNode CLUSTER_NODE_2 = RedisClusterNode.newRedisClusterNode() .listeningAt(CLUSTER_NODE_2_HOST, CLUSTER_NODE_2_PORT).serving(new SlotRange(5461, 10922)) .withId("0f2ee5df45d18c50aca07228cc18b1da96fd5e84").promotedAs(NodeType.MASTER).linkState(LinkState.CONNECTED) .build(); static final RedisClusterNode CLUSTER_NODE_3 = RedisClusterNode.newRedisClusterNode() .listeningAt(CLUSTER_NODE_3_HOST, CLUSTER_NODE_3_PORT).serving(new SlotRange(10923, 16383)) .withId("3b9b8192a874fa8f1f09dbc0ee20afab5738eee7").promotedAs(NodeType.MASTER).linkState(LinkState.CONNECTED) .build(); static final RedisClusterNode CLUSTER_NODE_2_LOOKUP = RedisClusterNode.newRedisClusterNode() .withId("0f2ee5df45d18c50aca07228cc18b1da96fd5e84").build(); static final RedisClusterNode UNKNOWN_CLUSTER_NODE = new RedisClusterNode("8.8.8.8", 7379, null); private ClusterCommandExecutor executor; private static final ConnectionCommandCallback<String> COMMAND_CALLBACK = new ConnectionCommandCallback<String>() { @Override public String doInCluster(Connection connection) { return connection.theWheelWeavesAsTheWheelWills(); } }; private static final Converter<Exception, DataAccessException> exceptionConverter = new Converter<Exception, DataAccessException>() { @Override public DataAccessException convert(Exception source) { if (source instanceof MovedException) { return new ClusterRedirectException(1000, ((MovedException) source).host, ((MovedException) source).port, source); } return new InvalidDataAccessApiUsageException(source.getMessage(), source); } }; private static final MultiKeyConnectionCommandCallback<String> MULTIKEY_CALLBACK = new MultiKeyConnectionCommandCallback<String>() { @Override public String doInCluster(Connection connection, byte[] key) { return connection.bloodAndAshes(key); } }; @Mock Connection con1; @Mock Connection con2; @Mock Connection con3; @Before public void setUp() { this.executor = new ClusterCommandExecutor(new MockClusterNodeProvider(), new MockClusterResourceProvider(), new PassThroughExceptionTranslationStrategy(exceptionConverter)); } @After public void tearDown() throws Exception { this.executor.destroy(); } @Test // DATAREDIS-315 public void executeCommandOnSingleNodeShouldBeExecutedCorrectly() { executor.executeCommandOnSingleNode(COMMAND_CALLBACK, CLUSTER_NODE_2); verify(con2, times(1)).theWheelWeavesAsTheWheelWills(); } @Test // DATAREDIS-315 public void executeCommandOnSingleNodeByHostAndPortShouldBeExecutedCorrectly() { executor.executeCommandOnSingleNode(COMMAND_CALLBACK, new RedisClusterNode(CLUSTER_NODE_2_HOST, CLUSTER_NODE_2_PORT)); verify(con2, times(1)).theWheelWeavesAsTheWheelWills(); } @Test // DATAREDIS-315 public void executeCommandOnSingleNodeByNodeIdShouldBeExecutedCorrectly() { executor.executeCommandOnSingleNode(COMMAND_CALLBACK, new RedisClusterNode(CLUSTER_NODE_2.id)); verify(con2, times(1)).theWheelWeavesAsTheWheelWills(); } @Test(expected = IllegalArgumentException.class) // DATAREDIS-315 public void executeCommandOnSingleNodeShouldThrowExceptionWhenNodeIsNull() { executor.executeCommandOnSingleNode(COMMAND_CALLBACK, null); } @Test(expected = IllegalArgumentException.class) // DATAREDIS-315 public void executeCommandOnSingleNodeShouldThrowExceptionWhenCommandCallbackIsNull() { executor.executeCommandOnSingleNode(null, CLUSTER_NODE_1); } @Test(expected = IllegalArgumentException.class) // DATAREDIS-315 public void executeCommandOnSingleNodeShouldThrowExceptionWhenNodeIsUnknown() { executor.executeCommandOnSingleNode(COMMAND_CALLBACK, UNKNOWN_CLUSTER_NODE); } @Test // DATAREDIS-315 public void executeCommandAsyncOnNodesShouldExecuteCommandOnGivenNodes() { ClusterCommandExecutor executor = new ClusterCommandExecutor(new MockClusterNodeProvider(), new MockClusterResourceProvider(), new PassThroughExceptionTranslationStrategy(exceptionConverter), new ConcurrentTaskExecutor(new SyncTaskExecutor())); executor.executeCommandAsyncOnNodes(COMMAND_CALLBACK, Arrays.asList(CLUSTER_NODE_1, CLUSTER_NODE_2)); verify(con1, times(1)).theWheelWeavesAsTheWheelWills(); verify(con2, times(1)).theWheelWeavesAsTheWheelWills(); verify(con3, never()).theWheelWeavesAsTheWheelWills(); } @Test // DATAREDIS-315 public void executeCommandAsyncOnNodesShouldExecuteCommandOnGivenNodesByHostAndPort() { ClusterCommandExecutor executor = new ClusterCommandExecutor(new MockClusterNodeProvider(), new MockClusterResourceProvider(), new PassThroughExceptionTranslationStrategy(exceptionConverter), new ConcurrentTaskExecutor(new SyncTaskExecutor())); executor.executeCommandAsyncOnNodes(COMMAND_CALLBACK, Arrays.asList(new RedisClusterNode(CLUSTER_NODE_1_HOST, CLUSTER_NODE_1_PORT), new RedisClusterNode(CLUSTER_NODE_2_HOST, CLUSTER_NODE_2_PORT))); verify(con1, times(1)).theWheelWeavesAsTheWheelWills(); verify(con2, times(1)).theWheelWeavesAsTheWheelWills(); verify(con3, never()).theWheelWeavesAsTheWheelWills(); } @Test // DATAREDIS-315 public void executeCommandAsyncOnNodesShouldExecuteCommandOnGivenNodesByNodeId() { ClusterCommandExecutor executor = new ClusterCommandExecutor(new MockClusterNodeProvider(), new MockClusterResourceProvider(), new PassThroughExceptionTranslationStrategy(exceptionConverter), new ConcurrentTaskExecutor(new SyncTaskExecutor())); executor.executeCommandAsyncOnNodes(COMMAND_CALLBACK, Arrays.asList(new RedisClusterNode(CLUSTER_NODE_1.id), CLUSTER_NODE_2_LOOKUP)); verify(con1, times(1)).theWheelWeavesAsTheWheelWills(); verify(con2, times(1)).theWheelWeavesAsTheWheelWills(); verify(con3, never()).theWheelWeavesAsTheWheelWills(); } @Test(expected = IllegalArgumentException.class) // DATAREDIS-315 public void executeCommandAsyncOnNodesShouldFailOnGivenUnknownNodes() { ClusterCommandExecutor executor = new ClusterCommandExecutor(new MockClusterNodeProvider(), new MockClusterResourceProvider(), new PassThroughExceptionTranslationStrategy(exceptionConverter), new ConcurrentTaskExecutor(new SyncTaskExecutor())); executor.executeCommandAsyncOnNodes(COMMAND_CALLBACK, Arrays.asList(new RedisClusterNode("unknown"), CLUSTER_NODE_2_LOOKUP)); } @Test // DATAREDIS-315 public void executeCommandOnAllNodesShouldExecuteCommandOnEveryKnownClusterNode() { ClusterCommandExecutor executor = new ClusterCommandExecutor(new MockClusterNodeProvider(), new MockClusterResourceProvider(), new PassThroughExceptionTranslationStrategy(exceptionConverter), new ConcurrentTaskExecutor(new SyncTaskExecutor())); executor.executeCommandOnAllNodes(COMMAND_CALLBACK); verify(con1, times(1)).theWheelWeavesAsTheWheelWills(); verify(con2, times(1)).theWheelWeavesAsTheWheelWills(); verify(con3, times(1)).theWheelWeavesAsTheWheelWills(); } @Test // DATAREDIS-315 public void executeCommandAsyncOnNodesShouldCompleteAndCollectErrorsOfAllNodes() { when(con1.theWheelWeavesAsTheWheelWills()).thenReturn("rand"); when(con2.theWheelWeavesAsTheWheelWills()).thenThrow(new IllegalStateException("(error) mat lost the dagger...")); when(con3.theWheelWeavesAsTheWheelWills()).thenReturn("perrin"); try { executor.executeCommandOnAllNodes(COMMAND_CALLBACK); } catch (ClusterCommandExecutionFailureException e) { assertThat(e.getCauses().size(), is(1)); assertThat(e.getCauses().iterator().next(), IsInstanceOf.instanceOf(DataAccessException.class)); } verify(con1, times(1)).theWheelWeavesAsTheWheelWills(); verify(con2, times(1)).theWheelWeavesAsTheWheelWills(); verify(con3, times(1)).theWheelWeavesAsTheWheelWills(); } @Test // DATAREDIS-315 public void executeCommandAsyncOnNodesShouldCollectResultsCorrectly() { when(con1.theWheelWeavesAsTheWheelWills()).thenReturn("rand"); when(con2.theWheelWeavesAsTheWheelWills()).thenReturn("mat"); when(con3.theWheelWeavesAsTheWheelWills()).thenReturn("perrin"); MulitNodeResult<String> result = executor.executeCommandOnAllNodes(COMMAND_CALLBACK); assertThat(result.resultsAsList(), hasItems("rand", "mat", "perrin")); } @Test // DATAREDIS-315, DATAREDIS-467 public void executeMultikeyCommandShouldRunCommandAcrossCluster() { // key-1 and key-9 map both to node1 ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class); when(con1.bloodAndAshes(captor.capture())).thenReturn("rand").thenReturn("egwene"); when(con2.bloodAndAshes(any(byte[].class))).thenReturn("mat"); when(con3.bloodAndAshes(any(byte[].class))).thenReturn("perrin"); MulitNodeResult<String> result = executor.executeMuliKeyCommand(MULTIKEY_CALLBACK, new HashSet<byte[]>( Arrays.asList("key-1".getBytes(), "key-2".getBytes(), "key-3".getBytes(), "key-9".getBytes()))); assertThat(result.resultsAsList(), hasItems("rand", "mat", "perrin", "egwene")); // check that 2 keys have been routed to node1 assertThat(captor.getAllValues().size(), is(2)); } @Test // DATAREDIS-315 public void executeCommandOnSingleNodeAndFollowRedirect() { when(con1.theWheelWeavesAsTheWheelWills()).thenThrow(new MovedException(CLUSTER_NODE_3_HOST, CLUSTER_NODE_3_PORT)); executor.executeCommandOnSingleNode(COMMAND_CALLBACK, CLUSTER_NODE_1); verify(con1, times(1)).theWheelWeavesAsTheWheelWills(); verify(con3, times(1)).theWheelWeavesAsTheWheelWills(); verify(con2, never()).theWheelWeavesAsTheWheelWills(); } @Test // DATAREDIS-315 public void executeCommandOnSingleNodeAndFollowRedirectButStopsAfterMaxRedirects() { when(con1.theWheelWeavesAsTheWheelWills()).thenThrow(new MovedException(CLUSTER_NODE_3_HOST, CLUSTER_NODE_3_PORT)); when(con3.theWheelWeavesAsTheWheelWills()).thenThrow(new MovedException(CLUSTER_NODE_2_HOST, CLUSTER_NODE_2_PORT)); when(con2.theWheelWeavesAsTheWheelWills()).thenThrow(new MovedException(CLUSTER_NODE_1_HOST, CLUSTER_NODE_1_PORT)); try { executor.setMaxRedirects(4); executor.executeCommandOnSingleNode(COMMAND_CALLBACK, CLUSTER_NODE_1); } catch (Exception e) { assertThat(e, IsInstanceOf.instanceOf(TooManyClusterRedirectionsException.class)); } verify(con1, times(2)).theWheelWeavesAsTheWheelWills(); verify(con3, times(2)).theWheelWeavesAsTheWheelWills(); verify(con2, times(1)).theWheelWeavesAsTheWheelWills(); } @Test // DATAREDIS-315 public void executeCommandOnArbitraryNodeShouldPickARandomNode() { executor.executeCommandOnArbitraryNode(COMMAND_CALLBACK); verifyInvocationsAcross("theWheelWeavesAsTheWheelWills", times(1), con1, con2, con3); } class MockClusterNodeProvider implements ClusterTopologyProvider { @Override public ClusterTopology getTopology() { return new ClusterTopology( new LinkedHashSet<RedisClusterNode>(Arrays.asList(CLUSTER_NODE_1, CLUSTER_NODE_2, CLUSTER_NODE_3))); } } class MockClusterResourceProvider implements ClusterNodeResourceProvider { @Override public Connection getResourceForSpecificNode(RedisClusterNode node) { if (CLUSTER_NODE_1.equals(node)) { return con1; } if (CLUSTER_NODE_2.equals(node)) { return con2; } if (CLUSTER_NODE_3.equals(node)) { return con3; } return null; } @Override public void returnResourceForSpecificNode(RedisClusterNode node, Object resource) { // TODO Auto-generated method stub } } static interface ConnectionCommandCallback<S> extends ClusterCommandCallback<Connection, S> { } static interface MultiKeyConnectionCommandCallback<S> extends MultiKeyClusterCommandCallback<Connection, S> { } static interface Connection { String theWheelWeavesAsTheWheelWills(); String bloodAndAshes(byte[] key); } static class MovedException extends RuntimeException { String host; int port; public MovedException(String host, int port) { this.host = host; this.port = port; } } }