From c5047f40c6f100dc4a6cfbe641b17a13fb1b08c8 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 28 Jul 2015 08:58:39 +0200 Subject: [PATCH] DATAREDIS-315 - Add initial support for redis cluster (jedis/lettuce). Cluster support is based on the very same building blocks as non clustered communication. RedisClusterConnection and extension to RedisConnection handles the communication with the Redis Cluster and translates errors into the Spring DAO exception hierarchy. Redirects for to a specific keys to the corresponding slot serving node are handled by the driver libraries, higher level functions like collecting information accross nodes, or sending commands to all nodes in the cluster that are covered by RedisClusterConnection utilizing a ClusterCommandExecutor distributing commands accross the cluster and collecting results. RedisTemplate provides access to cluster specific operations via the ClusterOperations interface that can be obtained via RedisTemplate.opsForCluster(). This allows to execute commands explicitly on a single node within the cluster while retaining de-/serialization features configured for the template. Original pull request: #158. --- pom.xml | 2 +- src/asciidoc/reference/redis-cluster.adoc | 136 + src/main/asciidoc/index.adoc | 3 +- src/main/asciidoc/reference/redis.adoc | 4 - .../data/redis/ClusterRedirectException.java | 79 + .../redis/ClusterStateFailureExeption.java | 51 + .../TooManyClusterRedirectionsException.java | 49 + ...usterCommandExecutionFailureException.java | 64 + .../connection/ClusterCommandExecutor.java | 352 ++ .../data/redis/connection/ClusterInfo.java | 173 + .../ClusterNodeResourceProvider.java | 43 + .../redis/connection/ClusterSlotHashUtil.java | 137 + .../redis/connection/ClusterTopology.java | 169 + .../connection/ClusterTopologyProvider.java | 33 + .../DefaultStringRedisConnection.java | 18 + .../connection/RedisClusterCommands.java | 167 + .../connection/RedisClusterConfiguration.java | 211 + .../connection/RedisClusterConnection.java | 155 + .../redis/connection/RedisClusterNode.java | 342 ++ .../connection/RedisConnectionFactory.java | 10 + .../data/redis/connection/RedisNode.java | 128 +- .../redis/connection/RedisServerCommands.java | 26 + .../redis/connection/convert/Converters.java | 132 + .../jedis/JedisClusterConnection.java | 3864 +++++++++++++++++ .../connection/jedis/JedisConnection.java | 37 + .../jedis/JedisConnectionFactory.java | 109 +- .../connection/jedis/JedisConverters.java | 46 +- .../jedis/JedisExceptionConverter.java | 22 +- .../connection/jredis/JredisConnection.java | 47 + .../jredis/JredisConnectionFactory.java | 10 + .../lettuce/LettuceClusterConnection.java | 1618 +++++++ .../connection/lettuce/LettuceConnection.java | 65 +- .../lettuce/LettuceConnectionFactory.java | 104 +- .../connection/lettuce/LettuceConverters.java | 91 + .../redis/connection/srp/SrpConnection.java | 19 + .../connection/srp/SrpConnectionFactory.java | 10 + .../redis/connection/util/ByteArraySet.java | 141 + .../data/redis/core/AbstractOperations.java | 21 +- .../data/redis/core/ClusterOperations.java | 145 + .../redis/core/DefaultClusterOperations.java | 336 ++ .../data/redis/core/RedisClusterCallback.java | 39 + .../data/redis/core/RedisOperations.java | 10 +- .../data/redis/core/RedisTemplate.java | 8 + .../redis/RedisTestProfileValueSource.java | 30 +- .../ClusterCommandExecutorUnitTests.java | 374 ++ .../connection/ClusterConnectionTests.java | 896 ++++ .../connection/ClusterSlotHashUtilsTests.java | 137 + .../connection/ClusterTestVariables.java | 61 + .../RedisClusterConfigurationUnitTests.java | 156 + .../connection/RedisConnectionUnitTests.java | 10 + .../convert/ConvertersUnitTests.java | 155 + .../jedis/JedisClusterConnectionTests.java | 2310 ++++++++++ .../JedisClusterConnectionUnitTests.java | 361 ++ .../JedisConnectionFactoryUnitTests.java | 53 +- .../jedis/JedisConvertersUnitTests.java | 1 - .../JedisExceptionConverterUnitTests.java | 86 + .../LettuceClusterConnectionTests.java | 2302 ++++++++++ .../LettuceClusterConnectionUnitTests.java | 423 ++ .../LettuceConnectionFactoryUnitTests.java | 46 + .../lettuce/LettuceConvertersUnitTests.java | 52 +- .../DefaultClusterOperationsUnitTests.java | 376 ++ .../core/MultithreadedRedisTemplateTests.java | 12 +- .../redis/core/RedisClusterTemplateTests.java | 241 + .../data/redis/core/RedisTemplateTests.java | 53 +- .../test/util/MinimumRedisVersionRule.java | 38 +- .../data/redis/test/util/MockitoUtils.java | 104 + .../data/redis/test/util/RedisClientRule.java | 2 +- .../redis/test/util/RedisClusterRule.java | 87 + .../redis/test/util/RedisSentinelRule.java | 17 +- 69 files changed, 17505 insertions(+), 104 deletions(-) create mode 100644 src/asciidoc/reference/redis-cluster.adoc create mode 100644 src/main/java/org/springframework/data/redis/ClusterRedirectException.java create mode 100644 src/main/java/org/springframework/data/redis/ClusterStateFailureExeption.java create mode 100644 src/main/java/org/springframework/data/redis/TooManyClusterRedirectionsException.java create mode 100644 src/main/java/org/springframework/data/redis/connection/ClusterCommandExecutionFailureException.java create mode 100644 src/main/java/org/springframework/data/redis/connection/ClusterCommandExecutor.java create mode 100644 src/main/java/org/springframework/data/redis/connection/ClusterInfo.java create mode 100644 src/main/java/org/springframework/data/redis/connection/ClusterNodeResourceProvider.java create mode 100644 src/main/java/org/springframework/data/redis/connection/ClusterSlotHashUtil.java create mode 100644 src/main/java/org/springframework/data/redis/connection/ClusterTopology.java create mode 100644 src/main/java/org/springframework/data/redis/connection/ClusterTopologyProvider.java create mode 100644 src/main/java/org/springframework/data/redis/connection/RedisClusterCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/RedisClusterConfiguration.java create mode 100644 src/main/java/org/springframework/data/redis/connection/RedisClusterConnection.java create mode 100644 src/main/java/org/springframework/data/redis/connection/RedisClusterNode.java create mode 100644 src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterConnection.java create mode 100644 src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnection.java create mode 100644 src/main/java/org/springframework/data/redis/connection/util/ByteArraySet.java create mode 100644 src/main/java/org/springframework/data/redis/core/ClusterOperations.java create mode 100644 src/main/java/org/springframework/data/redis/core/DefaultClusterOperations.java create mode 100644 src/main/java/org/springframework/data/redis/core/RedisClusterCallback.java create mode 100644 src/test/java/org/springframework/data/redis/connection/ClusterCommandExecutorUnitTests.java create mode 100644 src/test/java/org/springframework/data/redis/connection/ClusterConnectionTests.java create mode 100644 src/test/java/org/springframework/data/redis/connection/ClusterSlotHashUtilsTests.java create mode 100644 src/test/java/org/springframework/data/redis/connection/ClusterTestVariables.java create mode 100644 src/test/java/org/springframework/data/redis/connection/RedisClusterConfigurationUnitTests.java create mode 100644 src/test/java/org/springframework/data/redis/connection/convert/ConvertersUnitTests.java create mode 100644 src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionTests.java create mode 100644 src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionUnitTests.java create mode 100644 src/test/java/org/springframework/data/redis/connection/jedis/JedisExceptionConverterUnitTests.java create mode 100644 src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionTests.java create mode 100644 src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionUnitTests.java create mode 100644 src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryUnitTests.java create mode 100644 src/test/java/org/springframework/data/redis/core/DefaultClusterOperationsUnitTests.java create mode 100644 src/test/java/org/springframework/data/redis/core/RedisClusterTemplateTests.java create mode 100644 src/test/java/org/springframework/data/redis/test/util/MockitoUtils.java create mode 100644 src/test/java/org/springframework/data/redis/test/util/RedisClusterRule.java diff --git a/pom.xml b/pom.xml index 137d6a729..281705c37 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ 1.9.2 1.4.8 2.2 - 3.3.1.Final + 3.4.Final 2.8.0 0.7 06052013 diff --git a/src/asciidoc/reference/redis-cluster.adoc b/src/asciidoc/reference/redis-cluster.adoc new file mode 100644 index 000000000..281458962 --- /dev/null +++ b/src/asciidoc/reference/redis-cluster.adoc @@ -0,0 +1,136 @@ +[[cluster]] += Redis Cluster + +Working with http://redis.io/topics/cluster-spec[Redis Cluster] requires a Redis Server version 3.0+ and provides a very own set of features and capabilities. Please refer to the http://redis.io/topics/cluster-tutorial[Cluster Tutorial] for more information. + +TIP: Redis Cluster is only supported by <> and <>. + +== Enabling Redis Cluster + +Cluster support is based on the very same building blocks as non clustered communication. `RedisClusterConnection` and extension to `RedisConnection` handles the communication with the Redis Cluster and translates errors into the Spring DAO exception hierarchy. +`RedisClusterConnection` 's are created via the `RedisConnectionFactory` which has to be set up with the according `RedisClusterConfiguration`. + +.Sample RedisConnectionFactory Configuration for Redis Cluster +==== +[source,java] +---- +@Configuration +public class AppConfig { + + /* + * spring.redis.cluster.nodes[0] = 127.0.0.1:7379 + * spring.redis.cluster.nodes[1] = 127.0.0.1:7380 + * ... + */ + @Value("${spring.redis.cluster.nodes}") + private Collection initialClusterNodes; + + public @Bean RedisConnectionFactory connectionFactory() { + + return new JedisConnectionFactory( + new RedisClusterConfiguration(initialClusterNodes)); + } +} +---- +==== + +NOTE: The initial configuration points driver libraries to an initial set of cluster nodes. Changes resulting from live cluster reconfiguration will only be kept in the native driver and not be written back to the configuration. + +== Working With Redis Cluster Connection + +As mentioned above Redis Cluster behaves different from single node Redis or even a Sentinel monitored master slave environment. This is reasoned by the automatic sharding that maps a key to one of 16384 slots which are distributed across the nodes. Therefore commands that involve more than one key must assert that all keys map to the exact same slot in order to avoid cross slot execution errors. +Further on, hence a single cluster node, only servers a dedicated set of keys, commands issued against one particular server only return results for those keys served by the server. As a very simple example take the `KEYS` command. When issued to a server in cluster environment it only returns the keys served by the node the request is sent to and not necessarily all keys within the cluster. So to get all keys in cluster environment it is necessary to read the keys from at least all known master nodes. + +While redirects for to a specific keys to the corresponding slot serving node are handled by the driver libraries, higher level functions like collecting information across nodes, or sending commands to all nodes in the cluster that are covered by `RedisClusterConnection`. Picking up the keys example from just before, this means, that the `keys(pattern)` method picks up every master node in cluster and simultaneously executes the `KEYS` command on every single one, while picking up the results and returning the cumulated set of keys. To just request the keys of a single node `RedisClusterConnection` provides overloads for those (like `keys(node, pattern)` ). + +.Sample of Running Commands Across the Cluster +==== +[source,text] +---- +redis-cli@127.0.0.1:7379 > cluster nodes + +6b38bb... 127.0.0.1:7379 master - 0 0 25 connected 0-5460 <1> +7bb78c... 127.0.0.1:7380 master - 0 1449730618304 2 connected 5461-10922 <2> +164888... 127.0.0.1:7381 master - 0 1449730618304 3 connected 10923-16383 <3> +b8b5ee... 127.0.0.1:7382 slave 6b38bb... 0 1449730618304 25 connected <4> +---- + +[source,java] +---- +RedisClusterConnection connection = connectionFactory.getClusterConnnection(); + +connection.set("foo", value); <5> +connection.set("bar", value); <6> + +connection.keys("*"); <7> + +connection.keys(NODE_7379, "*"); <8> +connection.keys(NODE_7380, "*"); <9> +connection.keys(NODE_7381, "*"); <10> +connection.keys(NODE_7382, "*"); <11> +---- +<1> Master node serving slots 0 to 5460 replicated to slave at 7382 +<2> Master node serving slots 5461 to 10922 +<3> Master node serving slots 10923 to 16383 +<4> Slave node holding replicates of master at 7379 +<5> Request routed to node at 7381 serving slot 12182 +<6> Request routed to node at 7379 serving slot 5061 +<7> Request routed to nodes at 7379, 7380, 7381 -> [foo, bar] +<8> Request routed to node at 7379 -> [bar] +<9> Request routed to node at 7380 -> [] +<10> Request routed to node at 7381 -> [foo] +<11> Request routed to node at 7382 -> [bar] +==== + +Cross slot requests such as `MGET` are automatically served by the native driver library when all keys map to the same slot. However once this is not the case `RedisClusterConnection` executes multiple parallel `GET` commands against the slot serving nodes and again returns a cumulated result. Obviously this is less performing than the single slot execution and therefore should be used with care. In doubt please consider pinning keys to the same slot by providing a prefix in curly brackets like `{my-prefix}.foo` and `{my-prefix}.bar` which will both map to the same slot number. + +.Sample of Cross Slot Request Handling +==== +[source,text] +---- +redis-cli@127.0.0.1:7379 > cluster nodes + +6b38bb... 127.0.0.1:7379 master - 0 0 25 connected 0-5460 <1> +7bb... +---- + +[source,java] +---- +RedisClusterConnection connection = connectionFactory.getClusterConnnection(); + +connection.set("foo", value); // slot: 12182 +connection.set("{foo}.bar", value); // slot: 12182 +connection.set("bar", value); // slot: 5461 + +connection.mGet("foo", "{foo}.bar"); <2> + +connection.mGet("foo", "bar"); <3> +---- +<1> Same Configuration as in the sample before. +<2> Keys map to same slot -> 127.0.0.1:7381 MGET foo {foo}.bar +<3> Keys map to different slots and get split up into single slot ones routed to the according nodes + + -> 127.0.0.1:7379 GET bar + + -> 127.0.0.1:7381 GET foo +==== + +TIP: The above provides simple examples to demonstrate the general strategy followed by Spring Data Redis. Be aware that some operations might require loading huge amounts of data into memory in order to compute the desired command. Additionally not all cross slot requests can safely be ported to multiple single slot requests and will error if misused (eg. `PFCOUNT` ). + +== Working With RedisTemplate and ClusterOperations + +Please refer to the section <> to read about general purpose, configuration and usage of `RedisTemplate`. + +WARNING: Please be careful when setting up `RedisTemplate#keySerializer` using any of the Json `RedisSerializers` as changing json structure has immediate influence on hash slot calculation. + +`RedisTemplate` provides access to cluster specific operations via the `ClusterOperations` interface that can be obtained via `RedisTemplate.opsForCluster()`. This allows to execute commands explicitly on a single node within the cluster while retaining de-/serialization features configured for the template and provides administrative commands such as `CLUSTER MEET` or more high level operations for eg. resharding. + + +.Accessing RedisClusterConnection via RedisTemplate +==== +[source,text] +---- +ClusterOperations clusterOps = redisTemplate.opsForCluster(); +clusterOps.shutdown(NODE_7379); <1> +---- +<1> Shut down node at 7379 and cross fingers there is a slave in place that can take over. +==== + diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index cade2e5b4..f3997a7f1 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -33,6 +33,7 @@ include::introduction/getting-started.adoc[] :leveloffset: +1 include::reference/introduction.adoc[] include::reference/redis.adoc[] +include::reference/redis-cluster.adoc[] :leveloffset: -1 [[appendixes]] @@ -43,4 +44,4 @@ include::reference/redis.adoc[] include::appendix/introduction.adoc[] include::appendix/appendix-schema.adoc[] include::appendix/appendix-command-reference.adoc[] -:leveloffset: -1 \ No newline at end of file +:leveloffset: -1 diff --git a/src/main/asciidoc/reference/redis.adoc b/src/main/asciidoc/reference/redis.adoc index ec93bb711..b366c2788 100644 --- a/src/main/asciidoc/reference/redis.adoc +++ b/src/main/asciidoc/reference/redis.adoc @@ -427,8 +427,4 @@ NOTE: By default `RedisCacheManager` will not participate in any ongoing transac NOTE: By default `RedisCacheManager` does not prefix keys for cache regions, which can lead to an unexpected growth of a `ZSET` used to maintain known keys. It's highly recommended to enable the usage of prefixes in order to avoid this unexpected growth and potential key clashes using more than one cache region. -[[redis:future]] -== Roadmap ahead - -Spring Data Redis project is in its early stages. We are interested in feedback, knowing what your use cases are, what are the common patters you encounter so that the Redis module better serves your needs. Do contact us using the channels <> above, we are interested in hearing from you! diff --git a/src/main/java/org/springframework/data/redis/ClusterRedirectException.java b/src/main/java/org/springframework/data/redis/ClusterRedirectException.java new file mode 100644 index 000000000..a8cb91553 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/ClusterRedirectException.java @@ -0,0 +1,79 @@ +/* + * Copyright 2015 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; + +import org.springframework.dao.DataRetrievalFailureException; + +/** + * {@link ClusterRedirectException} indicates that a requested slot is not served by the targeted server but can be + * obtained on another one. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class ClusterRedirectException extends DataRetrievalFailureException { + + private static final long serialVersionUID = -857075813794333965L; + + private final int slot; + private final String host; + private final int port; + + /** + * Creates new {@link ClusterRedirectException}. + * + * @param slot the slot to redirect to. + * @param targetHost the host to redirect to. + * @param targetPort the port on the host. + * @param e the root cause from the data access API in use + */ + public ClusterRedirectException(int slot, String targetHost, int targetPort, Throwable e) { + + super(String.format("Redirect: slot %s to %s:%s.", slot, targetHost, targetPort), e); + + this.slot = slot; + this.host = targetHost; + this.port = targetPort; + } + + /** + * Get slot to go for. + * + * @return + */ + public int getSlot() { + return slot; + } + + /** + * Get host serving the slot. + * + * @return + */ + public String getTargetHost() { + return host; + } + + /** + * Get port on host serving the slot. + * + * @return + */ + public int getTargetPort() { + return port; + } + +} diff --git a/src/main/java/org/springframework/data/redis/ClusterStateFailureExeption.java b/src/main/java/org/springframework/data/redis/ClusterStateFailureExeption.java new file mode 100644 index 000000000..35a766cbf --- /dev/null +++ b/src/main/java/org/springframework/data/redis/ClusterStateFailureExeption.java @@ -0,0 +1,51 @@ +/* + * Copyright 2015 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; + +import org.springframework.dao.DataAccessResourceFailureException; + +/** + * {@link DataAccessResourceFailureException} indicating the current local snapshot of cluster state does no longer + * represent the actual remote state. This can happen nodes are removed from cluster, slots get migrated to other nodes + * and so on. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class ClusterStateFailureExeption extends DataAccessResourceFailureException { + + private static final long serialVersionUID = 333399051713240852L; + + /** + * Creates new {@link ClusterStateFailureExeption}. + * + * @param msg + */ + public ClusterStateFailureExeption(String msg) { + super(msg); + } + + /** + * Creates new {@link ClusterStateFailureExeption}. + * + * @param msg + * @param cause + */ + public ClusterStateFailureExeption(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/src/main/java/org/springframework/data/redis/TooManyClusterRedirectionsException.java b/src/main/java/org/springframework/data/redis/TooManyClusterRedirectionsException.java new file mode 100644 index 000000000..3d9df7076 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/TooManyClusterRedirectionsException.java @@ -0,0 +1,49 @@ +/* + * Copyright 2015 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; + +import org.springframework.dao.DataRetrievalFailureException; + +/** + * {@link DataRetrievalFailureException} thrown when following cluster redirects exceeds the max number of edges. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class TooManyClusterRedirectionsException extends DataRetrievalFailureException { + + private static final long serialVersionUID = -2818933672669154328L; + + /** + * Creates new {@link TooManyClusterRedirectionsException}. + * + * @param msg + */ + public TooManyClusterRedirectionsException(String msg) { + super(msg); + } + + /** + * Creates new {@link TooManyClusterRedirectionsException}. + * + * @param msg + * @param cause + */ + public TooManyClusterRedirectionsException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/src/main/java/org/springframework/data/redis/connection/ClusterCommandExecutionFailureException.java b/src/main/java/org/springframework/data/redis/connection/ClusterCommandExecutionFailureException.java new file mode 100644 index 000000000..6f32b8289 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/ClusterCommandExecutionFailureException.java @@ -0,0 +1,64 @@ +/* + * Copyright 2015 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 java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.springframework.dao.UncategorizedDataAccessException; + +/** + * Exception thrown when at least one call to a clustered redis environment fails. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class ClusterCommandExecutionFailureException extends UncategorizedDataAccessException { + + private static final long serialVersionUID = 5727044227040368955L; + + private final Collection causes; + + /** + * Creates new {@link ClusterCommandExecutionFailureException}. + * + * @param cause must not be {@literal null}. + */ + public ClusterCommandExecutionFailureException(Throwable cause) { + this(Collections.singletonList(cause)); + } + + /** + * Creates new {@link ClusterCommandExecutionFailureException}. + * + * @param causes must not be {@literal empty}. + */ + public ClusterCommandExecutionFailureException(List causes) { + + super(causes.get(0).getMessage(), causes.get(0)); + this.causes = causes; + } + + /** + * Get the collected errors. + * + * @return never {@literal null}. + */ + public Collection getCauses() { + return causes; + } +} diff --git a/src/main/java/org/springframework/data/redis/connection/ClusterCommandExecutor.java b/src/main/java/org/springframework/data/redis/connection/ClusterCommandExecutor.java new file mode 100644 index 000000000..bd70718fb --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/ClusterCommandExecutor.java @@ -0,0 +1,352 @@ +/* + * Copyright 2015 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 java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.core.task.AsyncTaskExecutor; +import org.springframework.dao.DataAccessException; +import org.springframework.data.redis.ClusterRedirectException; +import org.springframework.data.redis.ExceptionTranslationStrategy; +import org.springframework.data.redis.TooManyClusterRedirectionsException; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.util.Assert; + +/** + * {@link ClusterCommandExecutor} takes care of running commands across the known cluster nodes. By providing an + * {@link AsyncTaskExecutor} the execution behavior can be influenced. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class ClusterCommandExecutor implements DisposableBean { + + private AsyncTaskExecutor executor; + private final ClusterTopologyProvider topologyProvider; + private final ClusterNodeResourceProvider resourceProvider; + private final ExceptionTranslationStrategy exceptionTranslationStrategy; + private int maxRedirects = 5; + + /** + * Create a new instance of {@link ClusterCommandExecutor}. + * + * @param topologyProvider must not be {@literal null}. + * @param resourceProvider must not be {@literal null}. + * @param exceptionTranslation must not be {@literal null}. + */ + public ClusterCommandExecutor(ClusterTopologyProvider topologyProvider, ClusterNodeResourceProvider resourceProvider, + ExceptionTranslationStrategy exceptionTranslation) { + + Assert.notNull(topologyProvider); + Assert.notNull(resourceProvider); + Assert.notNull(exceptionTranslation); + + this.topologyProvider = topologyProvider; + this.resourceProvider = resourceProvider; + this.exceptionTranslationStrategy = exceptionTranslation; + } + + /** + * @param topologyProvider must not be {@literal null}. + * @param resourceProvider must not be {@literal null}. + * @param exceptionTranslation must not be {@literal null}. + * @param executor can be {@literal null}. + */ + public ClusterCommandExecutor(ClusterTopologyProvider topologyProvider, ClusterNodeResourceProvider resourceProvider, + ExceptionTranslationStrategy exceptionTranslation, AsyncTaskExecutor executor) { + + this(topologyProvider, resourceProvider, exceptionTranslation); + this.executor = executor; + } + + { + if (executor == null) { + ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor(); + threadPoolTaskExecutor.initialize(); + this.executor = threadPoolTaskExecutor; + } + } + + /** + * Run {@link ClusterCommandCallback} on a random node. + * + * @param cmd must not be {@literal null}. + * @return + */ + public T executeCommandOnArbitraryNode(ClusterCommandCallback cmd) { + + Assert.notNull(cmd, "ClusterCommandCallback must not be null!"); + List nodes = new ArrayList(getClusterTopology().getActiveNodes()); + return executeCommandOnSingleNode(cmd, nodes.get(new Random().nextInt(nodes.size()))); + } + + /** + * Run {@link ClusterCommandCallback} on given {@link RedisClusterNode}. + * + * @param cmd must not be {@literal null}. + * @param node must not be {@literal null}. + * @throws IllegalArgumentException in case no resource can be acquired for given node. + * @return + */ + public T executeCommandOnSingleNode(ClusterCommandCallback cmd, RedisClusterNode node) { + return executeCommandOnSingleNode(cmd, node, 0); + } + + private T executeCommandOnSingleNode(ClusterCommandCallback cmd, RedisClusterNode node, int redirectCount) { + + Assert.notNull(cmd, "ClusterCommandCallback must not be null!"); + Assert.notNull(node, "RedisClusterNode must not be null!"); + + if (redirectCount > maxRedirects) { + throw new TooManyClusterRedirectionsException( + String + .format( + "Cannot follow Cluster Redirects over more than %s legs. Please consider increasing the number of redirects to follow. Current value is: %s.", + redirectCount, maxRedirects)); + } + + S client = this.resourceProvider.getResourceForSpecificNode(node); + Assert.notNull(client, "Could not acquire resource for node. Is your cluster info up to date?"); + + try { + return cmd.doInCluster(client); + } catch (RuntimeException ex) { + + RuntimeException translatedException = convertToDataAccessExeption(ex); + if (translatedException instanceof ClusterRedirectException) { + ClusterRedirectException cre = (ClusterRedirectException) translatedException; + return executeCommandOnSingleNode(cmd, + topologyProvider.getTopology().lookup(cre.getTargetHost(), cre.getTargetPort()), redirectCount + 1); + } else { + throw translatedException != null ? translatedException : ex; + } + } finally { + this.resourceProvider.returnResourceForSpecificNode(node, client); + } + } + + /** + * Run {@link ClusterCommandCallback} on all reachable master nodes. + * + * @param cmd + * @return + * @throws ClusterCommandExecutionFailureException + */ + public Map executeCommandOnAllNodes(final ClusterCommandCallback cmd) { + return executeCommandAsyncOnNodes(cmd, getClusterTopology().getActiveMasterNodes()); + } + + /** + * @param callback + * @param nodes + * @return + * @throws ClusterCommandExecutionFailureException + */ + public java.util.Map executeCommandAsyncOnNodes( + final ClusterCommandCallback callback, Iterable nodes) { + + Assert.notNull(callback, "Callback must not be null!"); + Assert.notNull(nodes, "Nodes must not be null!"); + + Map> futures = new LinkedHashMap>(); + for (final RedisClusterNode node : nodes) { + + futures.put(node, executor.submit(new Callable() { + + @Override + public T call() throws Exception { + return executeCommandOnSingleNode(callback, node); + } + })); + } + + return collectResults(futures); + } + + private Map collectResults(Map> futures) { + + boolean done = false; + + Map result = new HashMap(); + Map exceptions = new HashMap(); + while (!done) { + + done = true; + for (Map.Entry> entry : futures.entrySet()) { + + if (!entry.getValue().isDone() && !entry.getValue().isCancelled()) { + done = false; + } else { + if (!result.containsKey(entry.getKey()) && !exceptions.containsKey(entry.getKey())) { + try { + result.put(entry.getKey(), entry.getValue().get()); + } catch (ExecutionException e) { + + RuntimeException ex = convertToDataAccessExeption((Exception) e.getCause()); + exceptions.put(entry.getKey(), ex != null ? ex : e.getCause()); + } catch (InterruptedException e) { + + RuntimeException ex = convertToDataAccessExeption((Exception) e.getCause()); + exceptions.put(entry.getKey(), ex != null ? ex : e.getCause()); + } + } + } + } + try { + Thread.sleep(10); + } catch (InterruptedException e) { + + done = true; + Thread.currentThread().interrupt(); + } + } + + if (!exceptions.isEmpty()) { + throw new ClusterCommandExecutionFailureException(new ArrayList(exceptions.values())); + } + return result; + } + + /** + * Run {@link MultiKeyClusterCommandCallback} with on a curated set of nodes serving one or more keys. + * + * @param cmd + * @return + * @throws ClusterCommandExecutionFailureException + */ + public Map executeMuliKeyCommand(final MultiKeyClusterCommandCallback cmd, + Iterable keys) { + + Map> nodeKeyMap = new HashMap>(); + + for (byte[] key : keys) { + for (RedisClusterNode node : getClusterTopology().getKeyServingNodes(key)) { + + if (nodeKeyMap.containsKey(node)) { + nodeKeyMap.get(node).add(key); + } else { + Set keySet = new LinkedHashSet(); + keySet.add(key); + nodeKeyMap.put(node, keySet); + } + } + } + + Map> futures = new LinkedHashMap>(); + for (final Entry> entry : nodeKeyMap.entrySet()) { + + if (entry.getKey().isMaster()) { + for (final byte[] key : entry.getValue()) { + futures.put(entry.getKey(), executor.submit(new Callable() { + + @Override + public T call() throws Exception { + return (T) executeMultiKeyCommandOnSingleNode(cmd, entry.getKey(), key); + } + })); + } + } + } + return collectResults(futures); + } + + private T executeMultiKeyCommandOnSingleNode(MultiKeyClusterCommandCallback cmd, RedisClusterNode node, + byte[] key) { + + Assert.notNull(cmd, "MultiKeyCommandCallback must not be null!"); + Assert.notNull(node, "RedisClusterNode must not be null!"); + Assert.notNull(key, "Keys for execution must not be null!"); + + S client = this.resourceProvider.getResourceForSpecificNode(node); + Assert.notNull(client, "Could not acquire resource for node. Is your cluster info up to date?"); + + try { + return cmd.doInCluster(client, key); + } catch (RuntimeException ex) { + + RuntimeException translatedException = convertToDataAccessExeption(ex); + throw translatedException != null ? translatedException : ex; + } finally { + this.resourceProvider.returnResourceForSpecificNode(node, client); + } + } + + private ClusterTopology getClusterTopology() { + return this.topologyProvider.getTopology(); + } + + private DataAccessException convertToDataAccessExeption(Exception e) { + return exceptionTranslationStrategy.translate(e); + } + + /** + * Set the maximum number of redirects to follow on {@code MOVED} or {@code ASK}. + * + * @param maxRedirects set to zero to suspend redirects. + */ + public void setMaxRedirects(int maxRedirects) { + this.maxRedirects = maxRedirects; + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.DisposableBean#destroy() + */ + @Override + public void destroy() throws Exception { + + if (executor instanceof DisposableBean) { + ((DisposableBean) executor).destroy(); + } + } + + /** + * Callback interface for Redis 'low level' code using the cluster client directly. To be used with + * {@link ClusterCommandExecutor} execution methods. + * + * @author Christoph Strobl + * @param native driver connection + * @param + * @since 1.7 + */ + public static interface ClusterCommandCallback { + S doInCluster(T client); + } + + /** + * Callback interface for Redis 'low level' code using the cluster client to execute multi key commands. + * + * @author Christoph Strobl + * @param native driver connection + * @param + */ + public static interface MultiKeyClusterCommandCallback { + S doInCluster(T client, byte[] key); + } + +} diff --git a/src/main/java/org/springframework/data/redis/connection/ClusterInfo.java b/src/main/java/org/springframework/data/redis/connection/ClusterInfo.java new file mode 100644 index 000000000..354719b52 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/ClusterInfo.java @@ -0,0 +1,173 @@ +/* + * Copyright 2015 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 java.util.Properties; + +import org.springframework.data.redis.core.types.RedisClientInfo.INFO; +import org.springframework.util.Assert; + +/** + * {@link ClusterInfo} gives access to cluster information such as {@code cluster_state} and + * {@code cluster_slots_assigned} provided by the {@code CLUSTER INFO} command. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class ClusterInfo { + + public static enum Info { + STATE("cluster_state"), SLOTS_ASSIGNED("cluster_slots_assigned"), SLOTS_OK("cluster_slots_ok"), SLOTS_PFAIL( + "cluster_slots_pfail"), SLOTS_FAIL("cluster_slots_fail"), KNOWN_NODES("cluster_known_nodes"), SIZE( + "cluster_size"), CURRENT_EPOCH("cluster_current_epoch"), MY_EPOCH("cluster_my_epoch"), MESSAGES_SENT( + "cluster_stats_messages_sent"), MESSAGES_RECEIVED("cluster_stats_messages_received"); + + String key; + + Info(String key) { + this.key = key; + } + } + + private final Properties clusterProperties; + + /** + * Creates new {@link ClusterInfo} for given {@link Properties}. + * + * @param clusterProperties must not be {@literal null}. + */ + public ClusterInfo(Properties clusterProperties) { + + Assert.notNull(clusterProperties, "ClusterProperties must not be null!"); + this.clusterProperties = clusterProperties; + } + + /** + * @see Info#STATE + * @return + */ + public String getState() { + return get(Info.STATE); + } + + /** + * @see Info#SLOTS_ASSIGNED + * @return + */ + public Long getSlotsAssigned() { + return getLongValueOf(Info.SLOTS_ASSIGNED); + } + + /** + * @see Info#SLOTS_OK + * @return + */ + public Long getSlotsOk() { + return getLongValueOf(Info.SLOTS_OK); + } + + /** + * @see Info#SLOTS_PFAIL + * @return + */ + public Long getSlotsPfail() { + return getLongValueOf(Info.SLOTS_PFAIL); + } + + /** + * @see Info#SLOTS_FAIL + * @return + */ + public Long getSlotsFail() { + return getLongValueOf(Info.SLOTS_FAIL); + } + + /** + * @see Info#KNOWN_NODES + * @return + */ + public Long getKnownNodes() { + return getLongValueOf(Info.KNOWN_NODES); + } + + /** + * @see Info#SIZE + * @return + */ + public Long getClusterSize() { + return getLongValueOf(Info.SIZE); + } + + /** + * @see Info#CURRENT_EPOCH + * @return + */ + public Long getCurrentEpoch() { + return getLongValueOf(Info.CURRENT_EPOCH); + } + + /** + * @see Info#MESSAGES_SENT + * @return + */ + public Long getMessagesSent() { + return getLongValueOf(Info.MESSAGES_SENT); + } + + /** + * @see Info#MESSAGES_RECEIVED + * @return + */ + public Long getMessagesReceived() { + return getLongValueOf(Info.MESSAGES_RECEIVED); + } + + /** + * @param info must not be null + * @return {@literal null} if no entry found for requested {@link INFO}. + */ + public String get(Info info) { + + Assert.notNull(info, "Cannot retrieve cluster information for 'null'."); + return get(info.key); + } + + /** + * @param key must not be {@literal null} or {@literal empty}. + * @return {@literal null} if no entry found for requested {@code key}. + */ + public String get(String key) { + + Assert.hasText(key, "Cannot get cluster information for 'empty' / 'null' key."); + return this.clusterProperties.getProperty(key); + } + + private Long getLongValueOf(Info info) { + + String value = get(info); + return value == null ? null : Long.valueOf(value); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return this.clusterProperties.toString(); + } + +} diff --git a/src/main/java/org/springframework/data/redis/connection/ClusterNodeResourceProvider.java b/src/main/java/org/springframework/data/redis/connection/ClusterNodeResourceProvider.java new file mode 100644 index 000000000..379b74b9d --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/ClusterNodeResourceProvider.java @@ -0,0 +1,43 @@ +/* + * Copyright 2015 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; + +/** + * {@link ClusterNodeResourceProvider} provides access to low level client api to directly execute operations against a + * Redis instance. + * + * @author Christoph Strobl + * @since 1.7 + */ +public interface ClusterNodeResourceProvider { + + /** + * Get the client resource for the given node. + * + * @param node must not be {@literal null}. + * @return + */ + S getResourceForSpecificNode(RedisClusterNode node); + + /** + * Return the resource object for the given node. This can mean free up resources or return elements back to a pool. + * + * @param node must not be {@literal null}. + * @param resource must not be {@literal null}. + */ + void returnResourceForSpecificNode(RedisClusterNode node, Object resource); + +} diff --git a/src/main/java/org/springframework/data/redis/connection/ClusterSlotHashUtil.java b/src/main/java/org/springframework/data/redis/connection/ClusterSlotHashUtil.java new file mode 100644 index 000000000..2ddc22412 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/ClusterSlotHashUtil.java @@ -0,0 +1,137 @@ +/* + * Copyright 2015 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 org.springframework.util.Assert; + +/** + * @author Christoph Strobl + * @since 1.7 + */ +public final class ClusterSlotHashUtil { + + private static final int SLOT_COUNT = 16384; + + private static final byte SUBKEY_START = '{'; + private static final byte SUBKEY_END = '}'; + + private static final int[] LOOKUP_TABLE = { 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, 0x8108, + 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF, 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, + 0x62D6, 0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, 0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, + 0x74C7, 0x44A4, 0x5485, 0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D, 0x3653, 0x2672, 0x1611, + 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4, 0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, 0x48C4, + 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823, 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, + 0xB92B, 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12, 0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, + 0x8B58, 0xBB3B, 0xAB1A, 0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41, 0xEDAE, 0xFD8F, 0xCDEC, + 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49, 0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70, 0xFF9F, + 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78, 0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, + 0xE16F, 0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067, 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, + 0xD31C, 0xE37F, 0xF35E, 0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256, 0xB5EA, 0xA5CB, 0x95A8, + 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D, 0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xA7DB, + 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C, 0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, + 0x5634, 0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB, 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, + 0x08E1, 0x3882, 0x28A3, 0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, 0x4A75, 0x5A54, 0x6A37, + 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92, 0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9, 0x7C26, + 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1, 0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, + 0x9FF8, 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0 }; + + private ClusterSlotHashUtil() { + + } + + /** + * @param keys must not be {@literal null}. + * @return + */ + public static boolean isSameSlotForAllKeys(byte[]... keys) { + + Assert.notNull(keys, "Keys must not be null!"); + + if (keys.length <= 1) { + return true; + } + + int slot = calculateSlot(keys[0]); + for (int i = 1; i < keys.length; i++) { + if (slot != calculateSlot(keys[i])) { + return false; + } + } + return true; + } + + /** + * Calculate the slot from the given key. + * + * @param key must not be {@literal null} or empty. + * @return + */ + public static int calculateSlot(String key) { + + Assert.hasText(key, "Key must not be null or empty!"); + return calculateSlot(key.getBytes()); + } + + /** + * Calculate the slot from the given key. + * + * @param key must not be {@literal null}. + * @return + */ + public static int calculateSlot(byte[] key) { + + Assert.notNull(key, "Key must not be null!"); + + byte[] finalKey = key; + int start = indexOf(key, SUBKEY_START); + if (start != -1) { + int end = indexOf(key, start + 1, SUBKEY_END); + if (end != -1 && end != start + 1) { + + finalKey = new byte[end - (start + 1)]; + System.arraycopy(key, start + 1, finalKey, 0, finalKey.length); + } + } + return crc16(finalKey) % SLOT_COUNT; + } + + private static int indexOf(byte[] haystack, byte needle) { + return indexOf(haystack, 0, needle); + } + + private static int indexOf(byte[] haystack, int start, byte needle) { + + for (int i = start; i < haystack.length; i++) { + + if (haystack[i] == needle) { + return i; + } + } + + return -1; + } + + private static int crc16(byte[] bytes) { + + int crc = 0x0000; + + for (byte b : bytes) { + crc = ((crc << 8) ^ LOOKUP_TABLE[((crc >>> 8) ^ (b & 0xFF)) & 0xFF]); + } + return crc & 0xFFFF; + } + +} diff --git a/src/main/java/org/springframework/data/redis/connection/ClusterTopology.java b/src/main/java/org/springframework/data/redis/connection/ClusterTopology.java new file mode 100644 index 000000000..70b5f3483 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/ClusterTopology.java @@ -0,0 +1,169 @@ +/* + * Copyright 2015 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 java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.data.redis.ClusterStateFailureExeption; +import org.springframework.util.Assert; + +/** + * {@link ClusterTopology} holds snapshot like information about {@link RedisClusterNode}s. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class ClusterTopology { + + private final Set nodes; + + /** + * Creates new instance of {@link ClusterTopology}. + * + * @param nodes can be {@literal null}. + */ + public ClusterTopology(Set nodes) { + this.nodes = nodes != null ? nodes : Collections. emptySet(); + } + + /** + * Get all {@link RedisClusterNode}s. + * + * @return never {@literal null}. + */ + public Set getNodes() { + return Collections.unmodifiableSet(nodes); + } + + /** + * Get all nodes (master and slave) in cluster where {@code link-state} is {@literal connected} and {@code flags} does + * not contain {@literal fail} or {@literal fail?}. + * + * @return never {@literal null}. + */ + public Set getActiveNodes() { + + Set activeNodes = new LinkedHashSet(nodes.size()); + for (RedisClusterNode node : nodes) { + if (node.isConnected() && !node.isMarkedAsFail()) { + activeNodes.add(node); + } + } + return activeNodes; + } + + /** + * Get all master nodes in cluster where {@code link-state} is {@literal connected} and {@code flags} does not contain + * {@literal fail} or {@literal fail?}. + * + * @return never {@literal null}. + */ + public Set getActiveMasterNodes() { + + Set activeMasterNodes = new LinkedHashSet(nodes.size()); + for (RedisClusterNode node : nodes) { + if (node.isMaster() && node.isConnected() && !node.isMarkedAsFail()) { + activeMasterNodes.add(node); + } + } + return activeMasterNodes; + } + + /** + * Get all master nodes in cluster. + * + * @return never {@literal null}. + */ + public Set getMasterNodes() { + + Set masterNodes = new LinkedHashSet(nodes.size()); + for (RedisClusterNode node : nodes) { + if (node.isMaster()) { + masterNodes.add(node); + } + } + return masterNodes; + } + + /** + * Get the {@link RedisClusterNode}s (master and slave) serving s specific slot. + * + * @param slot + * @return never {@literal null}. + */ + public Set getSlotServingNodes(int slot) { + + Set slotServingNodes = new LinkedHashSet(nodes.size()); + for (RedisClusterNode node : nodes) { + if (node.servesSlot(slot)) { + slotServingNodes.add(node); + } + } + return slotServingNodes; + } + + /** + * Get the {@link RedisClusterNode} that is the current master serving the given key. + * + * @param key must not be {@literal null}. + * @return + * @throws ClusterStateFailureExeption + */ + public RedisClusterNode getKeyServingMasterNode(byte[] key) { + + Assert.notNull(key, "Key for node lookup must not be null!"); + + int slot = ClusterSlotHashUtil.calculateSlot(key); + for (RedisClusterNode node : nodes) { + if (node.isMaster() && node.servesSlot(slot)) { + return node; + } + } + throw new ClusterStateFailureExeption(String.format("Could not find master node serving slot %s for key '%s',", + slot, key)); + } + + /** + * Get the {@link RedisClusterNode} matching given {@literal host} and {@literal port}. + * + * @param host must not be {@literal null}. + * @param port + * @return + * @throws ClusterStateFailureExeption + */ + public RedisClusterNode lookup(String host, int port) { + + for (RedisClusterNode node : nodes) { + if (host.equals(node.getHost()) && port == node.getPort()) { + return node; + } + } + throw new ClusterStateFailureExeption(String.format( + "Could not find node at %s:%s. Is your cluster info up to date?", host, port)); + } + + /** + * @param key must not be {@literal null}. + * @return {@literal null}. + */ + public Set getKeyServingNodes(byte[] key) { + + Assert.notNull(key, "Key must not be null for Cluster Node lookup."); + return getSlotServingNodes(ClusterSlotHashUtil.calculateSlot(key)); + } +} diff --git a/src/main/java/org/springframework/data/redis/connection/ClusterTopologyProvider.java b/src/main/java/org/springframework/data/redis/connection/ClusterTopologyProvider.java new file mode 100644 index 000000000..b3fb569d4 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/ClusterTopologyProvider.java @@ -0,0 +1,33 @@ +/* + * Copyright 2015 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; + +/** + * {@link ClusterTopologyProvider} manages the current cluster topology and makes sure to refresh cluster information. + * + * @author Christoph Strobl + * @since 1.7 + */ +public interface ClusterTopologyProvider { + + /** + * Get the current known {@link ClusterTopology}. + * + * @return never {@null}. + */ + ClusterTopology getTopology(); + +} diff --git a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java index 751d00d9c..702948003 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java @@ -2688,4 +2688,22 @@ public class DefaultStringRedisConnection implements StringRedisConnection { return byteSetToStringSet.convert(results); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#migrate(byte[], org.springframework.data.redis.connection.RedisNode, int, org.springframework.data.redis.connection.RedisServerCommands.MigrateOption) + */ + @Override + public void migrate(byte[] key, RedisNode target, int dbIndex, MigrateOption option) { + delegate.migrate(key, target, dbIndex, option); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#migrate(byte[], org.springframework.data.redis.connection.RedisNode, int, org.springframework.data.redis.connection.RedisServerCommands.MigrateOption, long) + */ + @Override + public void migrate(byte[] key, RedisNode target, int dbIndex, MigrateOption option, long timeout) { + delegate.migrate(key, target, dbIndex, option, timeout); + } + } diff --git a/src/main/java/org/springframework/data/redis/connection/RedisClusterCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisClusterCommands.java new file mode 100644 index 000000000..ef6df7ff7 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/RedisClusterCommands.java @@ -0,0 +1,167 @@ +/* + * Copyright 2015 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 java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.springframework.data.redis.connection.RedisClusterNode.SlotRange; + +/** + * Interface for the {@literal cluster} commands supported by Redis. + * + * @author Christoph Strobl + * @since 1.7 + */ +public interface RedisClusterCommands { + + /** + * Retrieve cluster node information such as {@literal id}, {@literal host}, {@literal port} and {@literal slots}. + * + * @return never {@literal null}. + */ + Iterable clusterGetClusterNodes(); + + /** + * Retrieve information about connected slaves for given master node. + * + * @param node must not be {@literal null}. + * @return never {@literal null}. + */ + Collection clusterGetSlaves(RedisClusterNode master); + + /** + * Retrieve information about masters and their connected slaves. + * + * @return never {@literal null}. + */ + Map> clusterGetMasterSlaveMap(); + + /** + * Find the slot for a given {@code key}. + * + * @param key must not be {@literal null}. + * @return + */ + Integer clusterGetSlotForKey(byte[] key); + + /** + * Find the {@link RedisClusterNode} serving given {@literal slot}. + * + * @param slot + * @return + */ + RedisClusterNode clusterGetNodeForSlot(int slot); + + /** + * Find the {@link RedisClusterNode} serving given {@literal key}. + * + * @param key must not be {@literal null}. + * @return + */ + RedisClusterNode clusterGetNodeForKey(byte[] key); + + /** + * Get cluster information. + * + * @return + */ + ClusterInfo clusterGetClusterInfo(); + + /** + * Assign slots to given {@link RedisClusterNode}. + * + * @param node must not be {@literal null}. + * @param slots + */ + void clusterAddSlots(RedisClusterNode node, int... slots); + + /** + * Assign {@link SlotRange#getSlotsArray()} to given {@link RedisClusterNode}. + * + * @param node must not be {@literal null}. + * @param range must not be {@literal null}. + */ + void clusterAddSlots(RedisClusterNode node, SlotRange range); + + /** + * Count the number of keys assigned to one {@literal slot}. + * + * @param slot + * @return + */ + Long clusterCountKeysInSlot(int slot); + + /** + * Remove slots from {@link RedisClusterNode}. + * + * @param node must not be {@literal null}. + * @param slots + */ + void clusterDeleteSlots(RedisClusterNode node, int... slots); + + /** + * Removes {@link SlotRange#getSlotsArray()} from given {@link RedisClusterNode}. + * + * @param node must not be {@literal null}. + * @param range must not be {@literal null}. + */ + void clusterDeleteSlotsInRange(RedisClusterNode node, SlotRange range); + + /** + * Remove given {@literal node} from cluster. + * + * @param node must not be {@literal null}. + */ + void clusterForget(RedisClusterNode node); + + /** + * Add given {@literal node} to cluster. + * + * @param node must not be {@literal null}. + */ + void clusterMeet(RedisClusterNode node); + + /** + * @param node must not be {@literal null}. + * @param slot + * @param mode must not be{@literal null}. + */ + void clusterSetSlot(RedisClusterNode node, int slot, AddSlots mode); + + /** + * Get {@literal keys} served by slot. + * + * @param slot + * @param count must not be {@literal null}. + * @return + */ + List clusterGetKeysInSlot(int slot, Integer count); + + /** + * Assign a {@literal slave} to given {@literal master}. + * + * @param master must not be {@literal null}. + * @param slave must not be {@literal null}. + */ + void clusterReplicate(RedisClusterNode master, RedisClusterNode slave); + + public enum AddSlots { + MIGRATING, IMPORTING, STABLE, NODE + } + +} diff --git a/src/main/java/org/springframework/data/redis/connection/RedisClusterConfiguration.java b/src/main/java/org/springframework/data/redis/connection/RedisClusterConfiguration.java new file mode 100644 index 000000000..43cf45d98 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/RedisClusterConfiguration.java @@ -0,0 +1,211 @@ +/* + * Copyright 2015 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.springframework.util.Assert.*; +import static org.springframework.util.StringUtils.*; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.util.NumberUtils; +import org.springframework.util.StringUtils; + +/** + * Configuration class used for setting up {@link RedisConnection} via {@link RedisConnectionFactory} using connecting + * to Redis Cluster. Useful when setting up a high availability Redis + * environment. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class RedisClusterConfiguration { + + private static final String REDIS_CLUSTER_NODES_CONFIG_PROPERTY = "spring.redis.cluster.nodes"; + private static final String REDIS_CLUSTER_TIMEOUT_CONFIG_PROPERTY = "spring.redis.cluster.timeout"; + private static final String REDIS_CLUSTER_MAX_REDIRECTS_CONFIG_PROPERTY = "spring.redis.cluster.max-redirects"; + + private Set clusterNodes; + private Long clusterTimeout; + private Integer maxRedirects; + + /** + * Creates new {@link RedisClusterConfiguration}. + */ + public RedisClusterConfiguration() { + this(new MapPropertySource("RedisClusterConfiguration", Collections. emptyMap())); + } + + /** + * Creates {@link RedisClusterConfiguration} for given hostPort combinations. + * + *
+	 * clusterHostAndPorts[0] = 127.0.0.1:23679
+	 * clusterHostAndPorts[1] = 127.0.0.1:23680
+	 * ...
+	 * 
+	 * 
+	 * 
+	 * @param cluster must not be
+	 * {@literal null}.
+	 */
+	public RedisClusterConfiguration(Collection clusterNodes) {
+		this(new MapPropertySource("RedisClusterConfiguration", asMap(clusterNodes, -1, -1)));
+	}
+
+	/**
+	 * Creates {@link RedisClusterConfiguration} looking up values in given {@link PropertySource}.
+	 * 
+	 * 
+	 * 
+	 * spring.redis.cluster.nodes=127.0.0.1:23679,127.0.0.1:23680,127.0.0.1:23681
+	 * spring.redis.cluster.timeout=5
+	 * spring.redis.cluster.max-redirects=3
+	 * 
+	 * 
+ * + * @param propertySource must not be {@literal null}. + */ + public RedisClusterConfiguration(PropertySource propertySource) { + + notNull(propertySource, "PropertySource must not be null!"); + + this.clusterNodes = new LinkedHashSet(); + + if (propertySource.containsProperty(REDIS_CLUSTER_NODES_CONFIG_PROPERTY)) { + appendClusterNodes(commaDelimitedListToSet(propertySource.getProperty(REDIS_CLUSTER_NODES_CONFIG_PROPERTY) + .toString())); + } + if (propertySource.containsProperty(REDIS_CLUSTER_TIMEOUT_CONFIG_PROPERTY)) { + this.clusterTimeout = NumberUtils.parseNumber(propertySource.getProperty(REDIS_CLUSTER_TIMEOUT_CONFIG_PROPERTY) + .toString(), Long.class); + } + if (propertySource.containsProperty(REDIS_CLUSTER_MAX_REDIRECTS_CONFIG_PROPERTY)) { + this.maxRedirects = NumberUtils.parseNumber( + propertySource.getProperty(REDIS_CLUSTER_MAX_REDIRECTS_CONFIG_PROPERTY).toString(), Integer.class); + } + } + + /** + * Set {@literal cluster nodes} to connect to. + * + * @param nodes must not be {@literal null}. + */ + public void setClusterNodes(Iterable nodes) { + + notNull(nodes, "Cannot set cluster nodes to 'null'."); + + this.clusterNodes.clear(); + + for (RedisNode clusterNode : nodes) { + addClusterNode(clusterNode); + } + } + + /** + * Returns an {@link Collections#unmodifiableSet(Set)} of {@literal cluster nodes}. + * + * @return {@link Set} of nodes. Never {@literal null}. + */ + public Set getClusterNodes() { + return Collections.unmodifiableSet(clusterNodes); + } + + /** + * Add a cluster node to configuration. + * + * @param node must not be {@literal null}. + */ + public void addClusterNode(RedisNode node) { + + notNull(node, "ClusterNode must not be 'null'."); + this.clusterNodes.add(node); + } + + /** + * @return + */ + public RedisClusterConfiguration clusterNode(RedisNode node) { + this.clusterNodes.add(node); + return this; + } + + /** + * @return + */ + public Long getClusterTimeout() { + return clusterTimeout != null && clusterTimeout > Long.MIN_VALUE ? clusterTimeout : null; + } + + /** + * @return + */ + public Integer getMaxRedirects() { + return maxRedirects != null && maxRedirects > Integer.MIN_VALUE ? maxRedirects : null; + } + + /** + * @param host + * @param port + * @return + */ + public RedisClusterConfiguration clusterNode(String host, Integer port) { + return clusterNode(new RedisNode(host, port)); + } + + private void appendClusterNodes(Set hostAndPorts) { + + for (String hostAndPort : hostAndPorts) { + addClusterNode(readHostAndPortFromString(hostAndPort)); + } + } + + private RedisNode readHostAndPortFromString(String hostAndPort) { + + String[] args = split(hostAndPort, ":"); + + notNull(args, "HostAndPort need to be seperated by ':'."); + isTrue(args.length == 2, "Host and Port String needs to specified as host:port"); + return new RedisNode(args[0], Integer.valueOf(args[1]).intValue()); + } + + /** + * @param master must not be {@literal null} or empty. + * @param clusterHostAndPorts must not be {@literal null}. + * @return + */ + private static Map asMap(Collection clusterHostAndPorts, long timeout, int redirects) { + + notNull(clusterHostAndPorts, "ClusterHostAndPorts must not be null!"); + + Map map = new HashMap(); + map.put(REDIS_CLUSTER_NODES_CONFIG_PROPERTY, StringUtils.collectionToCommaDelimitedString(clusterHostAndPorts)); + if (timeout >= 0) { + map.put(REDIS_CLUSTER_TIMEOUT_CONFIG_PROPERTY, Long.valueOf(timeout)); + } + if (redirects >= 0) { + map.put(REDIS_CLUSTER_MAX_REDIRECTS_CONFIG_PROPERTY, Integer.valueOf(redirects)); + } + + return map; + } +} diff --git a/src/main/java/org/springframework/data/redis/connection/RedisClusterConnection.java b/src/main/java/org/springframework/data/redis/connection/RedisClusterConnection.java new file mode 100644 index 000000000..274cc7b1c --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/RedisClusterConnection.java @@ -0,0 +1,155 @@ +/* + * Copyright 2015 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 java.util.List; +import java.util.Properties; +import java.util.Set; + +import org.springframework.data.redis.core.types.RedisClientInfo; + +/** + * {@link RedisClusterConnection} allows sending commands to dedicated nodes within the cluster. + * + * @author Christoph Strobl + * @since 1.7 + */ +public interface RedisClusterConnection extends RedisConnection, RedisClusterCommands { + + /** + * @param node must not be {@literal null}. + * @return + * @see RedisConnectionCommands#ping() + */ + String ping(RedisClusterNode node); + + /** + * @param node must not be {@literal null}. + * @see RedisServerCommands#bgReWriteAof() + */ + void bgReWriteAof(RedisClusterNode node); + + /** + * @param node must not be {@literal null}. + * @see RedisServerCommands#bgSave() + */ + void bgSave(RedisClusterNode node); + + /** + * @param node must not be {@literal null}. + * @return + * @see RedisServerCommands#lastSave() + */ + Long lastSave(RedisClusterNode node); + + /** + * @param node must not be {@literal null}. + * @see RedisServerCommands#save() + */ + void save(RedisClusterNode node); + + /** + * @param node must not be {@literal null}. + * @return + * @see RedisServerCommands#dbSize() + */ + Long dbSize(RedisClusterNode node); + + /** + * @param node must not be {@literal null}. + * @see RedisServerCommands#flushDb() + */ + void flushDb(RedisClusterNode node); + + /** + * @param node must not be {@literal null}. + * @see RedisServerCommands#flushAll() + */ + void flushAll(RedisClusterNode node); + + /** + * @param node must not be {@literal null}. + * @return + * @see RedisServerCommands#info() + */ + Properties info(RedisClusterNode node); + + /** + * @param node must not be {@literal null}. + * @param section + * @return + * @see RedisServerCommands#info(String) + */ + Properties info(RedisClusterNode node, String section); + + /** + * @param node must not be {@literal null}. + * @param pattern must not be {@literal null}. + * @return + * @see RedisKeyCommands#keys(byte[]) + */ + Set keys(RedisClusterNode node, byte[] pattern); + + /** + * @param node must not be {@literal null}. + * @return + * @see RedisKeyCommands#randomKey() + */ + byte[] randomKey(RedisClusterNode node); + + /** + * @param node must not be {@literal null}. + * @see RedisServerCommands#shutdown() + */ + void shutdown(RedisClusterNode node); + + /** + * @param node must not be {@literal null}. + * @param pattern + * @return + * @see RedisServerCommands#getConfig(String) + */ + List getConfig(RedisClusterNode node, String pattern); + + /** + * @param node must not be {@literal null}. + * @param param + * @param value + * @see RedisServerCommands#setConfig(String, String) + */ + void setConfig(RedisClusterNode node, String param, String value); + + /** + * @param node must not be {@literal null}. + * @see RedisServerCommands#resetConfigStats() + */ + void resetConfigStats(RedisClusterNode node); + + /** + * @param node must not be {@literal null}. + * @return + * @see RedisServerCommands#time() + */ + Long time(RedisClusterNode node); + + /** + * @param node must not be {@literal null}. + * @return + * @see RedisServerCommands#getClientList() + */ + public List getClientList(RedisClusterNode node); + +} diff --git a/src/main/java/org/springframework/data/redis/connection/RedisClusterNode.java b/src/main/java/org/springframework/data/redis/connection/RedisClusterNode.java new file mode 100644 index 000000000..417d8b0d7 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/RedisClusterNode.java @@ -0,0 +1,342 @@ +/* + * Copyright 2015 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 java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * Representation of a Redis server within the cluster. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class RedisClusterNode extends RedisNode { + + private SlotRange slotRange; + private LinkState linkState; + private Set flags; + + protected RedisClusterNode() { + super(); + } + + /** + * Creates new {@link RedisClusterNode} with empty {@link SlotRange}. + * + * @param host must not be {@literal null}. + * @param port + */ + public RedisClusterNode(String host, int port) { + this(host, port, new SlotRange(Collections. emptySet())); + } + + /** + * Creates new {@link RedisClusterNode} with given {@link SlotRange}. + * + * @param host must not be {@literal null}. + * @param port + * @param slotRange can be {@literal null}. + */ + public RedisClusterNode(String host, int port, SlotRange slotRange) { + + super(host, port); + this.slotRange = slotRange != null ? slotRange : new SlotRange(Collections. emptySet()); + } + + /** + * Get the served {@link SlotRange}. + * + * @return never {@literal null}. + */ + public SlotRange getSlotRange() { + return slotRange; + } + + /** + * @param slot + * @return true if slot is covered. + */ + public boolean servesSlot(int slot) { + return slotRange.contains(slot); + } + + /** + * @return + */ + public LinkState getLinkState() { + return linkState; + } + + /** + * @return true if node is connected to cluster. + */ + public boolean isConnected() { + return LinkState.CONNECTED.equals(linkState); + } + + /** + * @return never {@literal null}. + */ + public Set getFlags() { + return flags == null ? Collections. emptySet() : flags; + } + + /** + * @return true if node is marked as failing. + */ + public boolean isMarkedAsFail() { + + if (!CollectionUtils.isEmpty(flags)) { + return flags.contains(Flag.FAIL) || flags.contains(Flag.PFAIL); + } + return false; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisNode#toString() + */ + @Override + public String toString() { + return super.toString(); + } + + /** + * Get {@link RedisClusterNodeBuilder} for creating new {@link RedisClusterNode}. + * + * @return never {@literal null}. + */ + public static RedisClusterNodeBuilder newRedisClusterNode() { + return new RedisClusterNodeBuilder(); + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + public static class SlotRange { + + private final Set range; + + /** + * @param lowerBound must not be {@literal null}. + * @param upperBound must not be {@literal null}. + */ + public SlotRange(Integer lowerBound, Integer upperBound) { + + Assert.notNull(lowerBound, "LowerBound must not be null!"); + Assert.notNull(upperBound, "UpperBound must not be null!"); + + this.range = new LinkedHashSet(); + for (int i = lowerBound; i <= upperBound; i++) { + this.range.add(i); + } + } + + public SlotRange(Collection range) { + this.range = CollectionUtils.isEmpty(range) ? Collections. emptySet() + : new LinkedHashSet(range); + } + + @Override + public String toString() { + return range.toString(); + } + + /** + * @param slot + * @return true when slot is part of the range. + */ + public boolean contains(int slot) { + return range.contains(slot); + } + + /** + * @return + */ + public Set getSlots() { + return Collections.unmodifiableSet(range); + } + + public int[] getSlotsArray() { + + int[] slots = new int[range.size()]; + int pos = 0; + + for (Integer value : range) { + slots[pos++] = value.intValue(); + } + + return slots; + } + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + public static enum LinkState { + CONNECTED, DISCONNECTED + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + public static enum Flag { + + MYSELF("myself"), MASTER("master"), SLAVE("slave"), FAIL("fail"), PFAIL("fail?"), HANDSHAKE("handshake"), NOADDR( + "noaddr"), NOFLAGS("noflags"); + + private String raw; + + Flag(String raw) { + this.raw = raw; + } + + public String getRaw() { + return raw; + } + + } + + /** + * Builder for creating new {@link RedisClusterNode}. + * + * @author Christoph Strobl + * @since 1.7 + */ + public static class RedisClusterNodeBuilder extends RedisNodeBuilder { + + Set flags; + LinkState linkState; + SlotRange slotRange; + + public RedisClusterNodeBuilder() { + + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisNode.RedisNodeBuilder#listeningAt(java.lang.String, int) + */ + @Override + public RedisClusterNodeBuilder listeningAt(String host, int port) { + super.listeningAt(host, port); + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisNode.RedisNodeBuilder#withName(java.lang.String) + */ + @Override + public RedisClusterNodeBuilder withName(String name) { + super.withName(name); + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisNode.RedisNodeBuilder#withId(java.lang.String) + */ + @Override + public RedisClusterNodeBuilder withId(String id) { + super.withId(id); + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisNode.RedisNodeBuilder#promotedAs(org.springframework.data.redis.connection.RedisNode.NodeType) + */ + @Override + public RedisClusterNodeBuilder promotedAs(NodeType nodeType) { + super.promotedAs(nodeType); + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisNode.RedisNodeBuilder#slaveOf(java.lang.String) + */ + public RedisClusterNodeBuilder slaveOf(String masterId) { + super.slaveOf(masterId); + return this; + } + + /** + * Set flags for node. + * + * @param flags + * @return + */ + public RedisClusterNodeBuilder withFlags(Set flags) { + + this.flags = flags; + return this; + } + + /** + * Set {@link SlotRange}. + * + * @param range + * @return + */ + public RedisClusterNodeBuilder serving(SlotRange range) { + + this.slotRange = range; + return this; + } + + /** + * Set {@link LinkState}. + * + * @param linkState + * @return + */ + public RedisClusterNodeBuilder linkState(LinkState linkState) { + this.linkState = linkState; + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisNode.RedisNodeBuilder#build() + */ + @Override + public RedisClusterNode build() { + + RedisNode base = super.build(); + + RedisClusterNode node = new RedisClusterNode(base.getHost(), base.getPort(), slotRange); + node.id = base.id; + node.type = base.type; + node.masterId = base.masterId; + node.name = base.name; + node.flags = flags; + node.linkState = linkState; + return node; + } + } + +} diff --git a/src/main/java/org/springframework/data/redis/connection/RedisConnectionFactory.java b/src/main/java/org/springframework/data/redis/connection/RedisConnectionFactory.java index a123273db..39d4ecaef 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisConnectionFactory.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisConnectionFactory.java @@ -33,6 +33,15 @@ public interface RedisConnectionFactory extends PersistenceExceptionTranslator { */ RedisConnection getConnection(); + /** + * Provides a suitable connection for interacting with Redis Cluster. + * + * @return + * @throws + * @since 1.7 + */ + RedisClusterConnection getClusterConnection(); + /** * Specifies if pipelined results should be converted to the expected data type. If false, results of * {@link RedisConnection#closePipeline()} and {RedisConnection#exec()} will be of the type returned by the underlying @@ -45,6 +54,7 @@ public interface RedisConnectionFactory extends PersistenceExceptionTranslator { /** * Provides a suitable connection for interacting with Redis Sentinel. + * * @return connection for interacting with Redis Sentinel. * @since 1.4 */ diff --git a/src/main/java/org/springframework/data/redis/connection/RedisNode.java b/src/main/java/org/springframework/data/redis/connection/RedisNode.java index 819b7799c..e77e2da8b 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisNode.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisNode.java @@ -21,14 +21,16 @@ import org.springframework.util.ObjectUtils; /** * @author Christoph Strobl * @author Thomas Darimont - * * @since 1.4 */ public class RedisNode implements NamedNode { - private String name; - private String host; - private int port; + String id; + String name; + String host; + int port; + NodeType type; + String masterId; /** * Creates a new {@link RedisNode} with the given {@code host}, {@code port}. @@ -37,9 +39,9 @@ public class RedisNode implements NamedNode { * @param port */ public RedisNode(String host, int port) { - - Assert.notNull(host,"host must not be null!"); - + + Assert.notNull(host, "host must not be null!"); + this.host = host; this.port = port; } @@ -67,6 +69,56 @@ public class RedisNode implements NamedNode { this.name = name; } + /** + * @return + * @since 1.7 + */ + public String getMasterId() { + return masterId; + } + + /** + * @return + * @since 1.7 + */ + public String getId() { + return id; + } + + /** + * @return + * @since 1.7 + */ + public NodeType getType() { + return type; + } + + /** + * @return + * @since 1.7 + */ + public boolean isMaster() { + return ObjectUtils.nullSafeEquals(NodeType.MASTER, getType()); + } + + /** + * @return + * @since 1.7 + */ + public boolean isSlave() { + return ObjectUtils.nullSafeEquals(NodeType.SLAVE, getType()); + } + + /** + * Get {@link RedisNodeBuilder} for creating new {@link RedisNode}. + * + * @return never {@literal null}. + * @since 1.7 + */ + public static RedisNodeBuilder newRedisNode() { + return new RedisNodeBuilder(); + } + @Override public String toString() { return asString(); @@ -110,7 +162,16 @@ public class RedisNode implements NamedNode { /** * @author Christoph Strobl + * @since 1.7 + */ + public enum NodeType { + MASTER, SLAVE + } + + /** + * Builder for creating new {@link RedisNode}. * + * @author Christoph Strobl * @since 1.4 */ public static class RedisNodeBuilder { @@ -121,11 +182,21 @@ public class RedisNode implements NamedNode { node = new RedisNode(); } + /** + * Define node name. + */ public RedisNodeBuilder withName(String name) { node.name = name; return this; } + /** + * Set host and port of server. + * + * @param host must not be {@literal null}. + * @param port + * @return + */ public RedisNodeBuilder listeningAt(String host, int port) { Assert.hasText(host, "Hostname must not be empty or null."); @@ -134,6 +205,49 @@ public class RedisNode implements NamedNode { return this; } + /** + * Set id of server. + * + * @param id + * @return + */ + public RedisNodeBuilder withId(String id) { + + node.id = id; + return this; + } + + /** + * Set server role. + * + * @param nodeType + * @return + * @since 1.7 + */ + public RedisNodeBuilder promotedAs(NodeType type) { + + node.type = type; + return this; + } + + /** + * Set the id of the master node. + * + * @param masterId + * @return + * @since 1.7 + */ + public RedisNodeBuilder slaveOf(String masterId) { + + node.masterId = masterId; + return this; + } + + /** + * Get the {@link RedisNode}. + * + * @return + */ public RedisNode build() { return this.node; } diff --git a/src/main/java/org/springframework/data/redis/connection/RedisServerCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisServerCommands.java index c24d595fd..6c35ef54d 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisServerCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisServerCommands.java @@ -33,6 +33,13 @@ public interface RedisServerCommands { SAVE, NOSAVE; } + /** + * @since 1.7 + */ + public enum MigrateOption { + COPY, REPLACE + } + /** * Start an {@literal Append Only File} rewrite process on server. *

@@ -229,4 +236,23 @@ public interface RedisServerCommands { * @since 1.3 */ void slaveOfNoOne(); + + /** + * @param key must not be {@literal null}. + * @param target must not be {@literal null}. + * @param dbIndex + * @param option can be {@literal null}. Defaulted to {@link MigrateOption#COPY}. + * @since 1.7 + */ + void migrate(byte[] key, RedisNode target, int dbIndex, MigrateOption option); + + /** + * @param key must not be {@literal null}. + * @param target must not be {@literal null}. + * @param dbIndex + * @param option can be {@literal null}. Defaulted to {@link MigrateOption#COPY}. + * @param timeout + * @since 1.7 + */ + void migrate(byte[] key, RedisNode target, int dbIndex, MigrateOption option, long timeout); } diff --git a/src/main/java/org/springframework/data/redis/connection/convert/Converters.java b/src/main/java/org/springframework/data/redis/connection/convert/Converters.java index fa7bedeb3..edf6c439f 100644 --- a/src/main/java/org/springframework/data/redis/connection/convert/Converters.java +++ b/src/main/java/org/springframework/data/redis/connection/convert/Converters.java @@ -16,6 +16,10 @@ package org.springframework.data.redis.connection.convert; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Properties; @@ -23,8 +27,16 @@ import java.util.Set; import org.springframework.core.convert.converter.Converter; import org.springframework.data.redis.connection.DataType; +import org.springframework.data.redis.connection.RedisClusterNode; +import org.springframework.data.redis.connection.RedisClusterNode.Flag; +import org.springframework.data.redis.connection.RedisClusterNode.LinkState; +import org.springframework.data.redis.connection.RedisClusterNode.RedisClusterNodeBuilder; +import org.springframework.data.redis.connection.RedisClusterNode.SlotRange; +import org.springframework.data.redis.connection.RedisNode.NodeType; import org.springframework.data.redis.connection.RedisZSetCommands.Tuple; +import org.springframework.util.CollectionUtils; import org.springframework.util.NumberUtils; +import org.springframework.util.StringUtils; /** * Common type converters @@ -40,6 +52,93 @@ abstract public class Converters { private static final Converter LONG_TO_BOOLEAN = new LongToBooleanConverter(); private static final Converter STRING_TO_DATA_TYPE = new StringToDataTypeConverter(); private static final Converter, Properties> MAP_TO_PROPERTIES = MapToPropertiesConverter.INSTANCE; + private static final Converter STRING_TO_CLUSTER_NODE_CONVERTER; + private static final Map flagLookupMap; + + static { + + flagLookupMap = new LinkedHashMap(Flag.values().length, 1); + for (Flag flag : Flag.values()) { + flagLookupMap.put(flag.getRaw(), flag); + } + + STRING_TO_CLUSTER_NODE_CONVERTER = new Converter() { + + static final int ID_INDEX = 0; + static final int HOST_PORT_INDEX = 1; + static final int FLAGS_INDEX = 2; + static final int MASTER_ID_INDEX = 3; + static final int LINK_STATE_INDEX = 7; + static final int SLOTS_INDEX = 8; + + @Override + public RedisClusterNode convert(String source) { + + String[] args = source.split(" "); + String[] hostAndPort = StringUtils.split(args[HOST_PORT_INDEX], ":"); + + SlotRange range = parseSlotRange(args); + Set flags = parseFlags(args); + + RedisClusterNodeBuilder nodeBuilder = RedisClusterNode.newRedisClusterNode() + .listeningAt(hostAndPort[0], Integer.valueOf(hostAndPort[1])) // + .withId(args[ID_INDEX]) // + .promotedAs(flags.contains(Flag.MASTER) ? NodeType.MASTER : NodeType.SLAVE) // + .serving(range) // + .withFlags(flags) // + .linkState(parseLinkState(args)); + + if (!args[MASTER_ID_INDEX].isEmpty() && !args[MASTER_ID_INDEX].startsWith("-")) { + nodeBuilder.slaveOf(args[MASTER_ID_INDEX]); + } + + return nodeBuilder.build(); + } + + private Set parseFlags(String[] args) { + + String raw = args[FLAGS_INDEX]; + + Set flags = new LinkedHashSet(8, 1); + if (StringUtils.hasText(raw)) { + for (String flag : raw.split(",")) { + flags.add(flagLookupMap.get(flag)); + } + } + return flags; + } + + private LinkState parseLinkState(String[] args) { + + String raw = args[LINK_STATE_INDEX]; + + if (StringUtils.hasText(raw)) { + return LinkState.valueOf(raw.toUpperCase()); + } + return LinkState.DISCONNECTED; + } + + private SlotRange parseSlotRange(String[] args) { + + SlotRange range = new SlotRange(Collections. emptySet()); + if (args.length > SLOTS_INDEX && !args[SLOTS_INDEX].startsWith("[")) { + + String raw = args[SLOTS_INDEX]; + if (raw.contains("-")) { + String[] slotRange = StringUtils.split(raw, "-"); + + if (slotRange != null) { + range = new RedisClusterNode.SlotRange(Integer.valueOf(slotRange[0]), Integer.valueOf(slotRange[1])); + } + } else { + range = new SlotRange(Integer.valueOf(raw), Integer.valueOf(raw)); + } + } + return range; + } + + }; + } public static Converter stringToProps() { return STRING_TO_PROPS; @@ -73,6 +172,39 @@ abstract public class Converters { return (source ? ONE : ZERO); } + /** + * Converts the result of a single line of {@code CLUSTER NODES} into a {@link RedisClusterNode}. + * + * @param clusterNodesLine + * @return + * @since 1.7 + */ + protected static RedisClusterNode toClusterNode(String clusterNodesLine) { + return STRING_TO_CLUSTER_NODE_CONVERTER.convert(clusterNodesLine); + } + + /** + * Converts the result of {@code CLUSTER NODES} into {@link RedisClusterNode}s. + * + * @param clusterNodes + * @return + * @since 1.7 + */ + public static Set toSetOfRedisClusterNodes(Collection lines) { + + if (CollectionUtils.isEmpty(lines)) { + return Collections.emptySet(); + } + + Set nodes = new LinkedHashSet(lines.size()); + + for (String line : lines) { + nodes.add(toClusterNode(line)); + } + + return nodes; + } + public static List toObjects(Set tuples) { List tupleArgs = new ArrayList(tuples.size() * 2); for (Tuple tuple : tuples) { diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterConnection.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterConnection.java new file mode 100644 index 000000000..e7ac8740f --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterConnection.java @@ -0,0 +1,3864 @@ +/* + * Copyright 2015 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.jedis; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.Random; +import java.util.Set; + +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.redis.ClusterStateFailureExeption; +import org.springframework.data.redis.ExceptionTranslationStrategy; +import org.springframework.data.redis.PassThroughExceptionTranslationStrategy; +import org.springframework.data.redis.connection.ClusterCommandExecutor; +import org.springframework.data.redis.connection.ClusterCommandExecutor.ClusterCommandCallback; +import org.springframework.data.redis.connection.ClusterCommandExecutor.MultiKeyClusterCommandCallback; +import org.springframework.data.redis.connection.ClusterInfo; +import org.springframework.data.redis.connection.ClusterNodeResourceProvider; +import org.springframework.data.redis.connection.ClusterSlotHashUtil; +import org.springframework.data.redis.connection.ClusterTopology; +import org.springframework.data.redis.connection.ClusterTopologyProvider; +import org.springframework.data.redis.connection.DataType; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.connection.RedisClusterConnection; +import org.springframework.data.redis.connection.RedisClusterNode; +import org.springframework.data.redis.connection.RedisClusterNode.SlotRange; +import org.springframework.data.redis.connection.RedisNode; +import org.springframework.data.redis.connection.RedisPipelineException; +import org.springframework.data.redis.connection.RedisSentinelConnection; +import org.springframework.data.redis.connection.RedisSubscribedConnectionException; +import org.springframework.data.redis.connection.ReturnType; +import org.springframework.data.redis.connection.SortParameters; +import org.springframework.data.redis.connection.Subscription; +import org.springframework.data.redis.connection.convert.Converters; +import org.springframework.data.redis.connection.util.ByteArraySet; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.ScanCursor; +import org.springframework.data.redis.core.ScanIteration; +import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.core.types.RedisClientInfo; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +import redis.clients.jedis.BinaryJedisPubSub; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisCluster; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.ZParams; + +/** + * {@link RedisClusterConnection} implementation on top of {@link JedisCluster}.
+ * Uses the native {@link JedisCluster} api where possible and falls back to direct node communication using + * {@link Jedis} where needed. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class JedisClusterConnection implements RedisClusterConnection { + + private static final ExceptionTranslationStrategy EXCEPTION_TRANSLATION = new PassThroughExceptionTranslationStrategy( + JedisConverters.exceptionConverter()); + + private final JedisCluster cluster; + + private boolean closed; + + private final JedisClusterTopologyProvider topologyProvider; + private ClusterCommandExecutor clusterCommandExecutor; + + private volatile JedisSubscription subscription; + + /** + * Create new {@link JedisClusterConnection} utilizing native connections via {@link JedisCluster}. + * + * @param cluster must not be {@literal null}. + */ + public JedisClusterConnection(JedisCluster cluster) { + + Assert.notNull(cluster, "JedisCluster must not be null."); + + this.cluster = cluster; + + closed = false; + topologyProvider = new JedisClusterTopologyProvider(cluster); + clusterCommandExecutor = new ClusterCommandExecutor(topologyProvider, + new JedisClusterNodeResourceProvider(cluster), EXCEPTION_TRANSLATION); + + try { + DirectFieldAccessor dfa = new DirectFieldAccessor(cluster); + clusterCommandExecutor.setMaxRedirects((Integer) dfa.getPropertyValue("maxRedirections")); + } catch (Exception e) { + // ignore it and work with the executor default + } + + } + + /** + * Create new {@link JedisClusterConnection} utilizing native connections via {@link JedisCluster} running commands + * across the cluster via given {@link ClusterCommandExecutor}. + * + * @param cluster must not be {@literal null}. + * @param executor must not be {@literal null}. + */ + public JedisClusterConnection(JedisCluster cluster, ClusterCommandExecutor executor) { + + Assert.notNull(cluster, "JedisCluster must not be null."); + Assert.notNull(executor, "ClusterCommandExecutor must not be null."); + + this.closed = false; + this.cluster = cluster; + this.topologyProvider = new JedisClusterTopologyProvider(cluster); + this.clusterCommandExecutor = executor; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisCommands#execute(java.lang.String, byte[][]) + */ + @Override + public Object execute(String command, byte[]... args) { + + // TODO: execute command on all nodes? or throw exception requiring to execute command on a specific node + throw new UnsupportedOperationException("Execute is currently not supported in cluster mode."); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisKeyCommands#del(byte[][]) + */ + @Override + public Long del(byte[]... keys) { + + Assert.noNullElements(keys, "Keys must not be null or contain null key!"); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(keys)) { + try { + return cluster.del(keys); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + return Long.valueOf(this.clusterCommandExecutor.executeMuliKeyCommand( + new JedisMultiKeyClusterCommandCallback() { + + @Override + public Long doInCluster(Jedis client, byte[] key) { + return client.del(key); + } + }, Arrays.asList(keys)).size()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisKeyCommands#type(byte[]) + */ + @Override + public DataType type(byte[] key) { + + try { + return JedisConverters.toDataType(cluster.type(key)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisKeyCommands#keys(byte[]) + */ + @Override + public Set keys(final byte[] pattern) { + + Assert.notNull(pattern, "Pattern must not be null!"); + + Collection> keysPerNode = clusterCommandExecutor.executeCommandOnAllNodes( + new JedisClusterCommandCallback>() { + + @Override + public Set doInCluster(Jedis client) { + return client.keys(pattern); + } + }).values(); + + Set keys = new HashSet(); + for (Set keySet : keysPerNode) { + keys.addAll(keySet); + } + return keys; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#keys(org.springframework.data.redis.connection.RedisClusterNode, byte[]) + */ + @Override + public Set keys(RedisClusterNode node, final byte[] pattern) { + + Assert.notNull(pattern, "Pattern must not be null!"); + + return clusterCommandExecutor.executeCommandOnSingleNode(new JedisClusterCommandCallback>() { + + @Override + public Set doInCluster(Jedis client) { + return client.keys(pattern); + } + }, node); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisKeyCommands#scan(org.springframework.data.redis.core.ScanOptions) + */ + @Override + public Cursor scan(ScanOptions options) { + throw new InvalidDataAccessApiUsageException("Scan is not supported accros multiple nodes within a cluster"); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisKeyCommands#randomKey() + */ + @Override + public byte[] randomKey() { + + List nodes = new ArrayList(topologyProvider.getTopology().getActiveMasterNodes()); + Set inspectedNodes = new HashSet(nodes.size()); + + do { + + RedisClusterNode node = nodes.get(new Random().nextInt(nodes.size())); + + while (inspectedNodes.contains(node)) { + node = nodes.get(new Random().nextInt(nodes.size())); + } + inspectedNodes.add(node); + byte[] key = randomKey(node); + + if (key != null && key.length > 0) { + return key; + } + } while (nodes.size() != inspectedNodes.size()); + + return null; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#randomKey(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public byte[] randomKey(RedisClusterNode node) { + + return clusterCommandExecutor.executeCommandOnSingleNode(new JedisClusterCommandCallback() { + + @Override + public byte[] doInCluster(Jedis client) { + return client.randomBinaryKey(); + } + }, node); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisKeyCommands#rename(byte[], byte[]) + */ + @Override + public void rename(final byte[] sourceKey, final byte[] targetKey) { + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(sourceKey, targetKey)) { + + try { + cluster.rename(sourceKey, targetKey); + return; + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + byte[] value = dump(sourceKey); + + if (value != null && value.length > 0) { + + restore(targetKey, 0, value); + del(sourceKey); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisKeyCommands#renameNX(byte[], byte[]) + */ + @Override + public Boolean renameNX(final byte[] sourceKey, final byte[] targetKey) { + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(sourceKey, targetKey)) { + + try { + return JedisConverters.toBoolean(cluster.renamenx(sourceKey, targetKey)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + byte[] value = dump(sourceKey); + + if (value != null && value.length > 0 && !exists(targetKey)) { + + restore(targetKey, 0, value); + del(sourceKey); + return Boolean.TRUE; + } + return Boolean.FALSE; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisKeyCommands#expire(byte[], long) + */ + @Override + public Boolean expire(byte[] key, long seconds) { + + if (seconds > Integer.MAX_VALUE) { + throw new UnsupportedOperationException("Jedis does not support seconds exceeding Integer.MAX_VALUE."); + } + try { + return JedisConverters.toBoolean(cluster.expire(key, Long.valueOf(seconds).intValue())); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisKeyCommands#pExpire(byte[], long) + */ + @Override + public Boolean pExpire(final byte[] key, final long millis) { + + try { + return JedisConverters.toBoolean(cluster.pexpire(key, millis)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisKeyCommands#expireAt(byte[], long) + */ + @Override + public Boolean expireAt(byte[] key, long unixTime) { + + try { + return JedisConverters.toBoolean(cluster.expireAt(key, unixTime)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisKeyCommands#pExpireAt(byte[], long) + */ + @Override + public Boolean pExpireAt(byte[] key, long unixTimeInMillis) { + + try { + return JedisConverters.toBoolean(cluster.pexpireAt(key, unixTimeInMillis)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisKeyCommands#persist(byte[]) + */ + @Override + public Boolean persist(byte[] key) { + + try { + return JedisConverters.toBoolean(cluster.persist(key)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisKeyCommands#move(byte[], int) + */ + @Override + public Boolean move(byte[] key, int dbIndex) { + throw new UnsupportedOperationException("Cluster mode does not allow moving keys."); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisKeyCommands#ttl(byte[]) + */ + @Override + public Long ttl(byte[] key) { + + try { + return cluster.ttl(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisKeyCommands#pTtl(byte[]) + */ + @Override + public Long pTtl(final byte[] key) { + + return clusterCommandExecutor.executeCommandOnSingleNode(new JedisClusterCommandCallback() { + + @Override + public Long doInCluster(Jedis client) { + return client.pttl(key); + } + }, topologyProvider.getTopology().getKeyServingMasterNode(key)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisKeyCommands#sort(byte[], org.springframework.data.redis.connection.SortParameters) + */ + @Override + public List sort(byte[] key, SortParameters params) { + + try { + return cluster.sort(key, JedisConverters.toSortingParams(params)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisKeyCommands#sort(byte[], org.springframework.data.redis.connection.SortParameters, byte[]) + */ + @Override + public Long sort(byte[] key, SortParameters params, byte[] storeKey) { + + List sorted = sort(key, params); + if (!CollectionUtils.isEmpty(sorted)) { + + byte[][] arr = new byte[sorted.size()][]; + switch (type(key)) { + + case SET: + sAdd(storeKey, sorted.toArray(arr)); + return 1L; + case LIST: + lPush(storeKey, sorted.toArray(arr)); + return 1L; + default: + throw new IllegalArgumentException("sort and store is only supported for SET and LIST"); + } + } + return 0L; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisKeyCommands#dump(byte[]) + */ + @Override + public byte[] dump(final byte[] key) { + + return clusterCommandExecutor.executeCommandOnSingleNode(new JedisClusterCommandCallback() { + + @Override + public byte[] doInCluster(Jedis client) { + return client.dump(key); + } + }, topologyProvider.getTopology().getKeyServingMasterNode(key)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisKeyCommands#restore(byte[], long, byte[]) + */ + @Override + public void restore(final byte[] key, final long ttlInMillis, final byte[] serializedValue) { + + if (ttlInMillis > Integer.MAX_VALUE) { + throw new UnsupportedOperationException("Jedis does not support ttlInMillis exceeding Integer.MAX_VALUE."); + } + + this.clusterCommandExecutor.executeCommandOnSingleNode(new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + return client.restore(key, Long.valueOf(ttlInMillis).intValue(), serializedValue); + } + }, clusterGetNodeForKey(key)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#get(byte[]) + */ + @Override + public byte[] get(byte[] key) { + + try { + return cluster.get(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#getSet(byte[], byte[]) + */ + @Override + public byte[] getSet(byte[] key, byte[] value) { + + try { + return cluster.getSet(key, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#mGet(byte[][]) + */ + @Override + public List mGet(byte[]... keys) { + + Assert.noNullElements(keys); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(keys)) { + return cluster.mget(keys); + } + + Map nodeResult = this.clusterCommandExecutor.executeMuliKeyCommand( + new JedisMultiKeyClusterCommandCallback() { + + @Override + public byte[] doInCluster(Jedis client, byte[] key) { + return client.get(key); + } + }, Arrays.asList(keys)); + + return new ArrayList(nodeResult.values()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#set(byte[], byte[]) + */ + @Override + public void set(byte[] key, byte[] value) { + + try { + cluster.set(key, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#setNX(byte[], byte[]) + */ + @Override + public Boolean setNX(byte[] key, byte[] value) { + + try { + return JedisConverters.toBoolean(cluster.setnx(key, value)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#setEx(byte[], long, byte[]) + */ + @Override + public void setEx(byte[] key, long seconds, byte[] value) { + + if (seconds > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Seconds have cannot exceed Integer.MAX_VALUE!"); + } + + try { + cluster.setex(key, Long.valueOf(seconds).intValue(), value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#pSetEx(byte[], long, byte[]) + */ + @Override + public void pSetEx(final byte[] key, final long milliseconds, final byte[] value) { + + if (milliseconds > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Milliseconds have cannot exceed Integer.MAX_VALUE!"); + } + + this.clusterCommandExecutor.executeCommandOnSingleNode(new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + return client.psetex(key, milliseconds, value); + } + }, topologyProvider.getTopology().getKeyServingMasterNode(key)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#mSet(java.util.Map) + */ + @Override + public void mSet(Map tuples) { + + Assert.notNull(tuples, "Tuples must not be null!"); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(tuples.keySet().toArray(new byte[tuples.keySet().size()][]))) { + try { + cluster.mset(JedisConverters.toByteArrays(tuples)); + return; + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + for (Map.Entry entry : tuples.entrySet()) { + set(entry.getKey(), entry.getValue()); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#mSetNX(java.util.Map) + */ + @Override + public Boolean mSetNX(Map tuples) { + + Assert.notNull(tuples, "Tuple must not be null!"); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(tuples.keySet().toArray(new byte[tuples.keySet().size()][]))) { + try { + return JedisConverters.toBoolean(cluster.msetnx(JedisConverters.toByteArrays(tuples))); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + boolean result = true; + for (Map.Entry entry : tuples.entrySet()) { + if (!setNX(entry.getKey(), entry.getValue()) && result) { + result = false; + } + } + return result; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#incr(byte[]) + */ + @Override + public Long incr(byte[] key) { + + try { + return cluster.incr(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#incrBy(byte[], long) + */ + @Override + public Long incrBy(byte[] key, long value) { + + try { + return cluster.incrBy(key, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#incrBy(byte[], double) + */ + @Override + public Double incrBy(byte[] key, double value) { + + try { + return cluster.incrByFloat(key, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#decr(byte[]) + */ + @Override + public Long decr(byte[] key) { + + try { + return cluster.decr(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#decrBy(byte[], long) + */ + @Override + public Long decrBy(byte[] key, long value) { + + try { + return cluster.decrBy(key, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#append(byte[], byte[]) + */ + @Override + public Long append(byte[] key, byte[] value) { + + try { + return cluster.append(key, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#getRange(byte[], long, long) + */ + @Override + public byte[] getRange(byte[] key, long begin, long end) { + + try { + return cluster.getrange(key, begin, end); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#setRange(byte[], byte[], long) + */ + @Override + public void setRange(byte[] key, byte[] value, long offset) { + + try { + cluster.setrange(key, offset, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Boolean getBit(byte[] key, long offset) { + + try { + return cluster.getbit(key, offset); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Boolean setBit(byte[] key, long offset, boolean value) { + + try { + return cluster.setbit(key, offset, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long bitCount(byte[] key) { + + try { + return cluster.bitcount(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long bitCount(byte[] key, long begin, long end) { + + try { + return cluster.bitcount(key, begin, end); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long bitOp(BitOperation op, byte[] destination, byte[]... keys) { + + byte[][] allKeys = new byte[keys.length + 1][]; + allKeys[0] = destination; + System.arraycopy(keys, 0, allKeys, 1, keys.length); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { + try { + return cluster.bitop(JedisConverters.toBitOp(op), destination, keys); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + throw new InvalidDataAccessApiUsageException("BITOP is only supported for same slot keys in cluster mode."); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#strLen(byte[]) + */ + @Override + public Long strLen(byte[] key) { + + try { + return cluster.strlen(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisListCommands#rPush(byte[], byte[][]) + */ + @Override + public Long rPush(byte[] key, byte[]... values) { + + try { + return cluster.rpush(key, values); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisListCommands#lPush(byte[], byte[][]) + */ + @Override + public Long lPush(byte[] key, byte[]... values) { + + try { + return cluster.lpush(key, values); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisListCommands#rPushX(byte[], byte[]) + */ + @Override + public Long rPushX(byte[] key, byte[] value) { + + try { + return cluster.rpushx(key, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisListCommands#lPushX(byte[], byte[]) + */ + @Override + public Long lPushX(byte[] key, byte[] value) { + + try { + return cluster.lpushx(key, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisListCommands#lLen(byte[]) + */ + @Override + public Long lLen(byte[] key) { + + try { + return cluster.llen(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisListCommands#lRange(byte[], long, long) + */ + @Override + public List lRange(byte[] key, long begin, long end) { + + try { + return cluster.lrange(key, begin, end); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisListCommands#lTrim(byte[], long, long) + */ + @Override + public void lTrim(final byte[] key, final long begin, final long end) { + + try { + cluster.ltrim(key, begin, end); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisListCommands#lIndex(byte[], long) + */ + @Override + public byte[] lIndex(byte[] key, long index) { + + try { + return cluster.lindex(key, index); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisListCommands#lInsert(byte[], org.springframework.data.redis.connection.RedisListCommands.Position, byte[], byte[]) + */ + @Override + public Long lInsert(byte[] key, Position where, byte[] pivot, byte[] value) { + + try { + return cluster.linsert(key, JedisConverters.toListPosition(where), pivot, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisListCommands#lSet(byte[], long, byte[]) + */ + @Override + public void lSet(byte[] key, long index, byte[] value) { + + try { + cluster.lset(key, index, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisListCommands#lRem(byte[], long, byte[]) + */ + @Override + public Long lRem(byte[] key, long count, byte[] value) { + + try { + return cluster.lrem(key, count, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisListCommands#lPop(byte[]) + */ + @Override + public byte[] lPop(byte[] key) { + + try { + return cluster.lpop(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisListCommands#rPop(byte[]) + */ + @Override + public byte[] rPop(byte[] key) { + + try { + return cluster.rpop(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisListCommands#bLPop(int, byte[][]) + */ + @Override + public List bLPop(final int timeout, final byte[]... keys) { + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(keys)) { + try { + return cluster.blpop(timeout, keys); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + Map> nodeResult = this.clusterCommandExecutor.executeMuliKeyCommand( + new JedisMultiKeyClusterCommandCallback>() { + + @Override + public List doInCluster(Jedis client, byte[] key) { + return client.blpop(timeout, key); + } + }, Arrays.asList(keys)); + + for (List partial : nodeResult.values()) { + if (!partial.isEmpty()) { + return partial; + } + } + + return Collections.emptyList(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisListCommands#bRPop(int, byte[][]) + */ + @Override + public List bRPop(final int timeout, byte[]... keys) { + + Map> nodeResult = this.clusterCommandExecutor.executeMuliKeyCommand( + new JedisMultiKeyClusterCommandCallback>() { + + @Override + public List doInCluster(Jedis client, byte[] key) { + return client.brpop(timeout, key); + } + }, Arrays.asList(keys)); + + for (List partial : nodeResult.values()) { + if (!partial.isEmpty()) { + return partial; + } + } + + return Collections.emptyList(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisListCommands#rPopLPush(byte[], byte[]) + */ + @Override + public byte[] rPopLPush(byte[] srcKey, byte[] dstKey) { + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(srcKey, dstKey)) { + try { + return cluster.rpoplpush(srcKey, dstKey); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + byte[] val = rPop(srcKey); + lPush(dstKey, val); + return val; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisListCommands#bRPopLPush(int, byte[], byte[]) + */ + @Override + public byte[] bRPopLPush(int timeout, byte[] srcKey, byte[] dstKey) { + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(srcKey, dstKey)) { + try { + return cluster.brpoplpush(srcKey, dstKey, timeout); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + List val = bRPop(timeout, srcKey); + if (!CollectionUtils.isEmpty(val)) { + lPush(dstKey, val.get(1)); + return val.get(1); + } + + return null; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisSetCommands#sAdd(byte[], byte[][]) + */ + @Override + public Long sAdd(byte[] key, byte[]... values) { + + try { + return cluster.sadd(key, values); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisSetCommands#sRem(byte[], byte[][]) + */ + @Override + public Long sRem(byte[] key, byte[]... values) { + + try { + return cluster.srem(key, values); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisSetCommands#sPop(byte[]) + */ + @Override + public byte[] sPop(byte[] key) { + try { + return cluster.spop(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisSetCommands#sMove(byte[], byte[], byte[]) + */ + @Override + public Boolean sMove(byte[] srcKey, byte[] destKey, byte[] value) { + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(srcKey, destKey)) { + try { + return JedisConverters.toBoolean(cluster.smove(srcKey, destKey, value)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + if (exists(srcKey)) { + if (sRem(srcKey, value) > 0 && !sIsMember(destKey, value)) { + return JedisConverters.toBoolean(sAdd(destKey, value)); + } + } + return Boolean.FALSE; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisSetCommands#sCard(byte[]) + */ + @Override + public Long sCard(byte[] key) { + + try { + return cluster.scard(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisSetCommands#sIsMember(byte[], byte[]) + */ + @Override + public Boolean sIsMember(byte[] key, byte[] value) { + + try { + return cluster.sismember(key, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisSetCommands#sInter(byte[][]) + */ + @Override + public Set sInter(byte[]... keys) { + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(keys)) { + try { + return cluster.sinter(keys); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + Map> nodeResult = this.clusterCommandExecutor.executeMuliKeyCommand( + new JedisMultiKeyClusterCommandCallback>() { + + @Override + public Set doInCluster(Jedis client, byte[] key) { + return client.smembers(key); + } + }, Arrays.asList(keys)); + + ByteArraySet result = null; + for (Entry> entry : nodeResult.entrySet()) { + + ByteArraySet tmp = new ByteArraySet(entry.getValue()); + if (result == null) { + result = tmp; + } else { + result.retainAll(tmp); + if (result.isEmpty()) { + break; + } + } + } + + if (result.isEmpty()) { + return Collections.emptySet(); + } + + return result.asRawSet(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisSetCommands#sInterStore(byte[], byte[][]) + */ + @Override + public Long sInterStore(byte[] destKey, byte[]... keys) { + + byte[][] allKeys = new byte[keys.length + 1][]; + allKeys[0] = destKey; + System.arraycopy(keys, 0, allKeys, 1, keys.length); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { + try { + return cluster.sinterstore(destKey, keys); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + Set result = sInter(keys); + if (result.isEmpty()) { + return 0L; + } + return sAdd(destKey, result.toArray(new byte[result.size()][])); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisSetCommands#sUnion(byte[][]) + */ + @Override + public Set sUnion(byte[]... keys) { + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(keys)) { + try { + return cluster.sunion(keys); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + Map> nodeResult = this.clusterCommandExecutor.executeMuliKeyCommand( + new JedisMultiKeyClusterCommandCallback>() { + + @Override + public Set doInCluster(Jedis client, byte[] key) { + return client.smembers(key); + } + }, Arrays.asList(keys)); + + ByteArraySet result = new ByteArraySet(); + for (Entry> entry : nodeResult.entrySet()) { + result.addAll(entry.getValue()); + } + + if (result.isEmpty()) { + return Collections.emptySet(); + } + + return result.asRawSet(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisSetCommands#sUnionStore(byte[], byte[][]) + */ + @Override + public Long sUnionStore(byte[] destKey, byte[]... keys) { + + byte[][] allKeys = new byte[keys.length + 1][]; + allKeys[0] = destKey; + System.arraycopy(keys, 0, allKeys, 1, keys.length); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(keys)) { + try { + return cluster.sunionstore(destKey, keys); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + Set result = sUnion(keys); + if (result.isEmpty()) { + return 0L; + } + return sAdd(destKey, result.toArray(new byte[result.size()][])); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisSetCommands#sDiff(byte[][]) + */ + @Override + public Set sDiff(byte[]... keys) { + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(keys)) { + try { + return cluster.sdiff(keys); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + byte[] source = keys[0]; + byte[][] others = Arrays.copyOfRange(keys, 1, keys.length - 1); + + ByteArraySet values = new ByteArraySet(sMembers(source)); + Map> nodeResult = clusterCommandExecutor.executeMuliKeyCommand( + new JedisMultiKeyClusterCommandCallback>() { + + @Override + public Set doInCluster(Jedis client, byte[] key) { + return client.smembers(key); + } + }, Arrays.asList(others)); + + if (values.isEmpty()) { + return Collections.emptySet(); + } + + for (Set toSubstract : nodeResult.values()) { + values.removeAll(toSubstract); + } + + return values.asRawSet(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisSetCommands#sDiffStore(byte[], byte[][]) + */ + @Override + public Long sDiffStore(byte[] destKey, byte[]... keys) { + + byte[][] allKeys = new byte[keys.length + 1][]; + allKeys[0] = destKey; + System.arraycopy(keys, 0, allKeys, 1, keys.length); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { + try { + return cluster.sdiffstore(destKey, keys); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + Set diff = sDiff(keys); + if (diff.isEmpty()) { + return 0L; + } + + return sAdd(destKey, diff.toArray(new byte[diff.size()][])); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisSetCommands#sMembers(byte[]) + */ + @Override + public Set sMembers(byte[] key) { + + try { + return cluster.smembers(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisSetCommands#sRandMember(byte[]) + */ + @Override + public byte[] sRandMember(byte[] key) { + + try { + return cluster.srandmember(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisSetCommands#sRandMember(byte[], long) + */ + @Override + public List sRandMember(byte[] key, long count) { + + if (count > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Count have cannot exceed Integer.MAX_VALUE!"); + } + + try { + return cluster.srandmember(key, Long.valueOf(count).intValue()); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisSetCommands#sScan(byte[], org.springframework.data.redis.core.ScanOptions) + */ + @Override + public Cursor sScan(final byte[] key, ScanOptions options) { + + return new ScanCursor(options) { + + @Override + protected ScanIteration doScan(long cursorId, ScanOptions options) { + + redis.clients.jedis.ScanResult result = cluster.sscan(JedisConverters.toString(key), + Long.toString(cursorId)); + return new ScanIteration(Long.valueOf(result.getCursor()), JedisConverters.stringListToByteList() + .convert(result.getResult())); + } + }.open(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zAdd(byte[], double, byte[]) + */ + @Override + public Boolean zAdd(byte[] key, double score, byte[] value) { + + try { + return JedisConverters.toBoolean(cluster.zadd(key, score, value)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public Long zAdd(byte[] key, Set tuples) { + + // TODO: need to move the tuple conversion form jedisconnection. + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRem(byte[], byte[][]) + */ + @Override + public Long zRem(byte[] key, byte[]... values) { + + try { + return cluster.zrem(key, values); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zIncrBy(byte[], double, byte[]) + */ + @Override + public Double zIncrBy(byte[] key, double increment, byte[] value) { + try { + return cluster.zincrby(key, increment, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRank(byte[], byte[]) + */ + @Override + public Long zRank(byte[] key, byte[] value) { + + try { + return cluster.zrank(key, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRevRank(byte[], byte[]) + */ + @Override + public Long zRevRank(byte[] key, byte[] value) { + + try { + return cluster.zrevrank(key, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRange(byte[], long, long) + */ + @Override + public Set zRange(byte[] key, long begin, long end) { + + try { + return cluster.zrange(key, begin, end); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRangeByScoreWithScores(byte[], org.springframework.data.redis.connection.RedisZSetCommands.Range) + */ + @Override + public Set zRangeByScoreWithScores(byte[] key, Range range) { + return zRangeByScoreWithScores(key, range, null); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRangeByScoreWithScores(byte[], org.springframework.data.redis.connection.RedisZSetCommands.Range, org.springframework.data.redis.connection.RedisZSetCommands.Limit) + */ + @Override + public Set zRangeByScoreWithScores(byte[] key, Range range, Limit limit) { + + Assert.notNull(range, "Range cannot be null for ZRANGEBYSCOREWITHSCORES."); + + byte[] min = JedisConverters.boundaryToBytesForZRange(range.getMin(), JedisConverters.NEGATIVE_INFINITY_BYTES); + byte[] max = JedisConverters.boundaryToBytesForZRange(range.getMax(), JedisConverters.POSITIVE_INFINITY_BYTES); + + try { + if (limit != null) { + return JedisConverters.toTupleSet(cluster.zrangeByScoreWithScores(key, min, max, limit.getOffset(), + limit.getCount())); + } + return JedisConverters.toTupleSet(cluster.zrangeByScoreWithScores(key, min, max)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRevRangeByScore(byte[], org.springframework.data.redis.connection.RedisZSetCommands.Range) + */ + @Override + public Set zRevRangeByScore(byte[] key, Range range) { + return zRevRangeByScore(key, range, null); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRevRangeByScore(byte[], org.springframework.data.redis.connection.RedisZSetCommands.Range, org.springframework.data.redis.connection.RedisZSetCommands.Limit) + */ + @Override + public Set zRevRangeByScore(byte[] key, Range range, Limit limit) { + + Assert.notNull(range, "Range cannot be null for ZREVRANGEBYSCORE."); + byte[] min = JedisConverters.boundaryToBytesForZRange(range.getMin(), JedisConverters.NEGATIVE_INFINITY_BYTES); + byte[] max = JedisConverters.boundaryToBytesForZRange(range.getMax(), JedisConverters.POSITIVE_INFINITY_BYTES); + + try { + if (limit != null) { + return cluster.zrevrangeByScore(key, max, min, limit.getOffset(), limit.getCount()); + } + return cluster.zrevrangeByScore(key, max, min); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRevRangeByScoreWithScores(byte[], org.springframework.data.redis.connection.RedisZSetCommands.Range) + */ + @Override + public Set zRevRangeByScoreWithScores(byte[] key, Range range) { + return zRevRangeByScoreWithScores(key, range, null); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRevRangeByScoreWithScores(byte[], org.springframework.data.redis.connection.RedisZSetCommands.Range, org.springframework.data.redis.connection.RedisZSetCommands.Limit) + */ + @Override + public Set zRevRangeByScoreWithScores(byte[] key, Range range, Limit limit) { + + Assert.notNull(range, "Range cannot be null for ZREVRANGEBYSCOREWITHSCORES."); + + byte[] min = JedisConverters.boundaryToBytesForZRange(range.getMin(), JedisConverters.NEGATIVE_INFINITY_BYTES); + byte[] max = JedisConverters.boundaryToBytesForZRange(range.getMax(), JedisConverters.POSITIVE_INFINITY_BYTES); + + try { + if (limit != null) { + return JedisConverters.toTupleSet(cluster.zrevrangeByScoreWithScores(key, max, min, limit.getOffset(), + limit.getCount())); + } + return JedisConverters.toTupleSet(cluster.zrevrangeByScoreWithScores(key, max, min)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zCount(byte[], org.springframework.data.redis.connection.RedisZSetCommands.Range) + */ + @Override + public Long zCount(byte[] key, Range range) { + + Assert.notNull(range, "Range cannot be null for ZCOUNT."); + + byte[] min = JedisConverters.boundaryToBytesForZRange(range.getMin(), JedisConverters.NEGATIVE_INFINITY_BYTES); + byte[] max = JedisConverters.boundaryToBytesForZRange(range.getMax(), JedisConverters.POSITIVE_INFINITY_BYTES); + + try { + return cluster.zcount(key, min, max); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRemRangeByScore(byte[], org.springframework.data.redis.connection.RedisZSetCommands.Range) + */ + @Override + public Long zRemRangeByScore(byte[] key, Range range) { + + Assert.notNull(range, "Range cannot be null for ZREMRANGEBYSCORE."); + + byte[] min = JedisConverters.boundaryToBytesForZRange(range.getMin(), JedisConverters.NEGATIVE_INFINITY_BYTES); + byte[] max = JedisConverters.boundaryToBytesForZRange(range.getMax(), JedisConverters.POSITIVE_INFINITY_BYTES); + + try { + return cluster.zremrangeByScore(key, min, max); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRangeByScore(byte[], org.springframework.data.redis.connection.RedisZSetCommands.Range) + */ + @Override + public Set zRangeByScore(byte[] key, Range range) { + return zRangeByScore(key, range, null); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRangeByScore(byte[], org.springframework.data.redis.connection.RedisZSetCommands.Range, org.springframework.data.redis.connection.RedisZSetCommands.Limit) + */ + @Override + public Set zRangeByScore(byte[] key, Range range, Limit limit) { + + Assert.notNull(range, "Range cannot be null for ZRANGEBYSCORE."); + + byte[] min = JedisConverters.boundaryToBytesForZRange(range.getMin(), JedisConverters.NEGATIVE_INFINITY_BYTES); + byte[] max = JedisConverters.boundaryToBytesForZRange(range.getMax(), JedisConverters.POSITIVE_INFINITY_BYTES); + + try { + if (limit != null) { + return cluster.zrangeByScore(key, min, max, limit.getOffset(), limit.getCount()); + } + return cluster.zrangeByScore(key, min, max); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRangeByLex(byte[]) + */ + @Override + public Set zRangeByLex(byte[] key) { + return zRangeByLex(key, Range.unbounded()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRangeByLex(byte[], org.springframework.data.redis.connection.RedisZSetCommands.Range) + */ + @Override + public Set zRangeByLex(byte[] key, Range range) { + return zRangeByLex(key, range, null); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRangeByLex(byte[], org.springframework.data.redis.connection.RedisZSetCommands.Range, org.springframework.data.redis.connection.RedisZSetCommands.Limit) + */ + @Override + public Set zRangeByLex(byte[] key, Range range, Limit limit) { + + Assert.notNull(range, "Range cannot be null for ZRANGEBYLEX."); + + byte[] min = JedisConverters.boundaryToBytesForZRangeByLex(range.getMin(), JedisConverters.toBytes("-")); + byte[] max = JedisConverters.boundaryToBytesForZRangeByLex(range.getMax(), JedisConverters.toBytes("+")); + + try { + if (limit != null) { + return cluster.zrangeByLex(key, min, max, limit.getOffset(), limit.getCount()); + } + return cluster.zrangeByLex(key, min, max); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRangeWithScores(byte[], long, long) + */ + @Override + public Set zRangeWithScores(byte[] key, long begin, long end) { + + try { + return JedisConverters.toTupleSet(cluster.zrangeWithScores(key, begin, end)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRangeByScore(byte[], double, double) + */ + @Override + public Set zRangeByScore(byte[] key, double min, double max) { + + try { + return cluster.zrangeByScore(key, min, max); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRangeByScoreWithScores(byte[], double, double) + */ + @Override + public Set zRangeByScoreWithScores(byte[] key, double min, double max) { + + try { + return JedisConverters.toTupleSet(cluster.zrangeByScoreWithScores(key, min, max)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRangeByScore(byte[], double, double, long, long) + */ + @Override + public Set zRangeByScore(byte[] key, double min, double max, long offset, long count) { + + if (offset > Integer.MAX_VALUE || count > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Count/Offset cannot exceed Integer.MAX_VALUE!"); + } + + try { + return cluster.zrangeByScore(key, min, max, Long.valueOf(offset).intValue(), Long.valueOf(count).intValue()); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRangeByScoreWithScores(byte[], double, double, long, long) + */ + @Override + public Set zRangeByScoreWithScores(byte[] key, double min, double max, long offset, long count) { + + if (offset > Integer.MAX_VALUE || count > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Count/Offset cannot exceed Integer.MAX_VALUE!"); + } + + try { + return JedisConverters.toTupleSet(cluster.zrangeByScoreWithScores(key, min, max, Long.valueOf(offset).intValue(), + Long.valueOf(count).intValue())); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRevRange(byte[], long, long) + */ + @Override + public Set zRevRange(byte[] key, long begin, long end) { + + try { + return cluster.zrevrange(key, begin, end); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRevRangeWithScores(byte[], long, long) + */ + @Override + public Set zRevRangeWithScores(byte[] key, long begin, long end) { + + try { + return JedisConverters.toTupleSet(cluster.zrevrangeWithScores(key, begin, end)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRevRangeByScore(byte[], double, double) + */ + @Override + public Set zRevRangeByScore(byte[] key, double min, double max) { + + try { + return cluster.zrevrangeByScore(key, max, min); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRevRangeByScoreWithScores(byte[], double, double) + */ + @Override + public Set zRevRangeByScoreWithScores(byte[] key, double min, double max) { + + try { + return JedisConverters.toTupleSet(cluster.zrevrangeByScoreWithScores(key, max, min)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRevRangeByScore(byte[], double, double, long, long) + */ + @Override + public Set zRevRangeByScore(byte[] key, double min, double max, long offset, long count) { + + if (offset > Integer.MAX_VALUE || count > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Count/Offset cannot exceed Integer.MAX_VALUE!"); + } + + try { + return cluster.zrevrangeByScore(key, max, min, Long.valueOf(offset).intValue(), Long.valueOf(count).intValue()); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRevRangeByScoreWithScores(byte[], double, double, long, long) + */ + @Override + public Set zRevRangeByScoreWithScores(byte[] key, double min, double max, long offset, long count) { + + if (offset > Integer.MAX_VALUE || count > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Count/Offset cannot exceed Integer.MAX_VALUE!"); + } + + try { + return JedisConverters.toTupleSet(cluster.zrevrangeByScoreWithScores(key, max, min, Long.valueOf(offset) + .intValue(), Long.valueOf(count).intValue())); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zCount(byte[], double, double) + */ + @Override + public Long zCount(byte[] key, double min, double max) { + + try { + return cluster.zcount(key, min, max); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zCard(byte[]) + */ + @Override + public Long zCard(byte[] key) { + + try { + return cluster.zcard(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zScore(byte[], byte[]) + */ + @Override + public Double zScore(byte[] key, byte[] value) { + + try { + return cluster.zscore(key, value); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRemRange(byte[], long, long) + */ + @Override + public Long zRemRange(byte[] key, long begin, long end) { + + try { + return cluster.zremrangeByRank(key, begin, end); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRemRangeByScore(byte[], double, double) + */ + @Override + public Long zRemRangeByScore(byte[] key, double min, double max) { + + try { + return cluster.zremrangeByScore(key, min, max); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zUnionStore(byte[], byte[][]) + */ + @Override + public Long zUnionStore(byte[] destKey, byte[]... sets) { + + byte[][] allKeys = new byte[sets.length + 1][]; + allKeys[0] = destKey; + System.arraycopy(sets, 0, allKeys, 1, sets.length); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { + + try { + return cluster.zunionstore(destKey, sets); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + throw new InvalidDataAccessApiUsageException("ZUNIONSTORE can only be executed when all keys map to the same slot"); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zUnionStore(byte[], org.springframework.data.redis.connection.RedisZSetCommands.Aggregate, int[], byte[][]) + */ + @Override + public Long zUnionStore(byte[] destKey, Aggregate aggregate, int[] weights, byte[]... sets) { + + byte[][] allKeys = new byte[sets.length + 1][]; + allKeys[0] = destKey; + System.arraycopy(sets, 0, allKeys, 1, sets.length); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { + + ZParams zparams = new ZParams().weights(weights).aggregate(ZParams.Aggregate.valueOf(aggregate.name())); + + try { + return cluster.zunionstore(destKey, zparams, sets); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + throw new InvalidDataAccessApiUsageException("ZUNIONSTORE can only be executed when all keys map to the same slot"); + } + + @Override + public Long zInterStore(byte[] destKey, byte[]... sets) { + + byte[][] allKeys = new byte[sets.length + 1][]; + allKeys[0] = destKey; + System.arraycopy(sets, 0, allKeys, 1, sets.length); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { + + try { + return cluster.zinterstore(destKey, sets); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + throw new InvalidDataAccessApiUsageException("ZINTERSTORE can only be executed when all keys map to the same slot"); + } + + @Override + public Long zInterStore(byte[] destKey, Aggregate aggregate, int[] weights, byte[]... sets) { + + byte[][] allKeys = new byte[sets.length + 1][]; + allKeys[0] = destKey; + System.arraycopy(sets, 0, allKeys, 1, sets.length); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { + + ZParams zparams = new ZParams().weights(weights).aggregate(ZParams.Aggregate.valueOf(aggregate.name())); + + try { + return cluster.zinterstore(destKey, zparams, sets); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + throw new IllegalArgumentException("ZINTERSTORE can only be executed when all keys map to the same slot"); + } + + @Override + public Cursor zScan(byte[] key, ScanOptions options) { + throw new UnsupportedOperationException("Jedis does currently not support binary zscan command."); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRangeByScore(byte[], java.lang.String, java.lang.String) + */ + @Override + public Set zRangeByScore(byte[] key, String min, String max) { + + try { + return cluster.zrangeByScore(key, JedisConverters.toBytes(min), JedisConverters.toBytes(max)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRangeByScore(byte[], java.lang.String, java.lang.String, long, long) + */ + @Override + public Set zRangeByScore(byte[] key, String min, String max, long offset, long count) { + + if (offset > Integer.MAX_VALUE || count > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Count/Offset cannot exceed Integer.MAX_VALUE!"); + } + + try { + return cluster.zrangeByScore(key, JedisConverters.toBytes(min), JedisConverters.toBytes(max), Long + .valueOf(offset).intValue(), Long.valueOf(count).intValue()); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hSet(byte[], byte[], byte[]) + */ + @Override + public Boolean hSet(byte[] key, byte[] field, byte[] value) { + + try { + return JedisConverters.toBoolean(cluster.hset(key, field, value)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hSetNX(byte[], byte[], byte[]) + */ + @Override + public Boolean hSetNX(byte[] key, byte[] field, byte[] value) { + + try { + return JedisConverters.toBoolean(cluster.hsetnx(key, field, value)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hGet(byte[], byte[]) + */ + @Override + public byte[] hGet(byte[] key, byte[] field) { + + try { + return cluster.hget(key, field); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hMGet(byte[], byte[][]) + */ + @Override + public List hMGet(byte[] key, byte[]... fields) { + + try { + return cluster.hmget(key, fields); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hMSet(byte[], java.util.Map) + */ + @Override + public void hMSet(byte[] key, Map hashes) { + + try { + cluster.hmset(key, hashes); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hIncrBy(byte[], byte[], long) + */ + @Override + public Long hIncrBy(byte[] key, byte[] field, long delta) { + + try { + return cluster.hincrBy(key, field, delta); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hIncrBy(byte[], byte[], double) + */ + @Override + public Double hIncrBy(byte[] key, byte[] field, double delta) { + try { + return cluster.hincrByFloat(key, field, delta); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hExists(byte[], byte[]) + */ + @Override + public Boolean hExists(byte[] key, byte[] field) { + + try { + return cluster.hexists(key, field); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hDel(byte[], byte[][]) + */ + @Override + public Long hDel(byte[] key, byte[]... fields) { + + try { + return cluster.hdel(key, fields); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hLen(byte[]) + */ + @Override + public Long hLen(byte[] key) { + + try { + return cluster.hlen(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hKeys(byte[]) + */ + @Override + public Set hKeys(byte[] key) { + + try { + return cluster.hkeys(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hVals(byte[]) + */ + @Override + public List hVals(byte[] key) { + + try { + return new ArrayList(cluster.hvals(key)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hGetAll(byte[]) + */ + @Override + public Map hGetAll(byte[] key) { + + try { + return cluster.hgetAll(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hScan(byte[], org.springframework.data.redis.core.ScanOptions) + */ + @Override + public Cursor> hScan(final byte[] key, ScanOptions options) { + + return new ScanCursor>(options) { + + @Override + protected ScanIteration> doScan(long cursorId, ScanOptions options) { + throw new UnsupportedOperationException("Jedis does currently not support binary hscan"); + } + }.open(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisTxCommands#multi() + */ + @Override + public void multi() { + throw new InvalidDataAccessApiUsageException("MUTLI is currently not supported in cluster mode."); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisTxCommands#exec() + */ + @Override + public List exec() { + throw new InvalidDataAccessApiUsageException("EXEC is currently not supported in cluster mode."); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisTxCommands#discard() + */ + @Override + public void discard() { + throw new InvalidDataAccessApiUsageException("DISCARD is currently not supported in cluster mode."); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisTxCommands#watch(byte[][]) + */ + @Override + public void watch(byte[]... keys) { + throw new InvalidDataAccessApiUsageException("WATCH is currently not supported in cluster mode."); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisTxCommands#unwatch() + */ + @Override + public void unwatch() { + throw new InvalidDataAccessApiUsageException("UNWATCH is currently not supported in cluster mode."); + + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisPubSubCommands#isSubscribed() + */ + @Override + public boolean isSubscribed() { + return (subscription != null && subscription.isAlive()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisPubSubCommands#getSubscription() + */ + @Override + public Subscription getSubscription() { + return subscription; + } + + @Override + public Long publish(byte[] channel, byte[] message) { + try { + return cluster.publish(channel, message); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public void subscribe(MessageListener listener, byte[]... channels) { + + if (isSubscribed()) { + throw new RedisSubscribedConnectionException( + "Connection already subscribed; use the connection Subscription to cancel or add new channels"); + } + try { + BinaryJedisPubSub jedisPubSub = new JedisMessageListener(listener); + subscription = new JedisSubscription(listener, jedisPubSub, channels, null); + cluster.subscribe(jedisPubSub, channels); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public void pSubscribe(MessageListener listener, byte[]... patterns) { + + if (isSubscribed()) { + throw new RedisSubscribedConnectionException( + "Connection already subscribed; use the connection Subscription to cancel or add new channels"); + } + try { + BinaryJedisPubSub jedisPubSub = new JedisMessageListener(listener); + subscription = new JedisSubscription(listener, jedisPubSub, null, patterns); + cluster.psubscribe(jedisPubSub, patterns); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisConnectionCommands#select(int) + */ + @Override + public void select(final int dbIndex) { + + if (dbIndex != 0) { + throw new InvalidDataAccessApiUsageException("Cannot SELECT non zero index in cluster mode."); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisConnectionCommands#echo(byte[]) + */ + @Override + public byte[] echo(final byte[] message) { + + try { + return cluster.echo(message); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisConnectionCommands#ping() + */ + @Override + public String ping() { + + return !clusterCommandExecutor.executeCommandOnAllNodes(new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + return client.ping(); + } + }).isEmpty() ? "PONG" : null; + + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#ping(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public String ping(RedisClusterNode node) { + + return clusterCommandExecutor.executeCommandOnSingleNode(new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + return client.ping(); + } + }, node); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#bgWriteAof() + */ + @Override + public void bgWriteAof() { + + clusterCommandExecutor.executeCommandOnAllNodes(new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + return client.bgrewriteaof(); + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#bgReWriteAof(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public void bgReWriteAof(RedisClusterNode node) { + + clusterCommandExecutor.executeCommandOnSingleNode(new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + return client.bgrewriteaof(); + } + }, node); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#bgReWriteAof() + */ + @Override + public void bgReWriteAof() { + + clusterCommandExecutor.executeCommandOnAllNodes(new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + return client.bgrewriteaof(); + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#bgSave() + */ + @Override + public void bgSave() { + + clusterCommandExecutor.executeCommandOnAllNodes(new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + return client.bgsave(); + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#bgSave(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public void bgSave(RedisClusterNode node) { + + clusterCommandExecutor.executeCommandOnSingleNode(new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + return client.bgsave(); + } + }, node); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#lastSave() + */ + @Override + public Long lastSave() { + + List result = new ArrayList(clusterCommandExecutor.executeCommandOnAllNodes( + new JedisClusterCommandCallback() { + + @Override + public Long doInCluster(Jedis client) { + return client.lastsave(); + } + }).values()); + + if (CollectionUtils.isEmpty(result)) { + return null; + } + + Collections.sort(result, Collections.reverseOrder()); + return result.get(0); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#lastSave(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public Long lastSave(RedisClusterNode node) { + + return clusterCommandExecutor.executeCommandOnSingleNode(new JedisClusterCommandCallback() { + + @Override + public Long doInCluster(Jedis client) { + return client.lastsave(); + } + }, node); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#save() + */ + @Override + public void save() { + + clusterCommandExecutor.executeCommandOnAllNodes(new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + return client.save(); + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#save(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public void save(RedisClusterNode node) { + + clusterCommandExecutor.executeCommandOnSingleNode(new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + return client.save(); + } + }, node); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#dbSize() + */ + @Override + public Long dbSize() { + + Map dbSizes = clusterCommandExecutor + .executeCommandOnAllNodes(new JedisClusterCommandCallback() { + + @Override + public Long doInCluster(Jedis client) { + return client.dbSize(); + } + }); + + if (CollectionUtils.isEmpty(dbSizes)) { + return 0L; + } + + Long size = 0L; + for (Long value : dbSizes.values()) { + size += value; + } + return size; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#dbSize(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public Long dbSize(RedisClusterNode node) { + + return clusterCommandExecutor.executeCommandOnSingleNode(new JedisClusterCommandCallback() { + + @Override + public Long doInCluster(Jedis client) { + return client.dbSize(); + } + }, node); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#flushDb() + */ + @Override + public void flushDb() { + + clusterCommandExecutor.executeCommandOnAllNodes(new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + return client.flushDB(); + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#flushDb(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public void flushDb(RedisClusterNode node) { + + clusterCommandExecutor.executeCommandOnSingleNode(new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + return client.flushDB(); + } + }, node); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#flushAll() + */ + @Override + public void flushAll() { + + clusterCommandExecutor.executeCommandOnAllNodes(new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + return client.flushAll(); + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#flushAll(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public void flushAll(RedisClusterNode node) { + + clusterCommandExecutor.executeCommandOnSingleNode(new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + return client.flushAll(); + } + }, node); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#info() + */ + @Override + public Properties info() { + + Properties infos = new Properties(); + + infos.putAll(clusterCommandExecutor.executeCommandOnAllNodes(new JedisClusterCommandCallback() { + + @Override + public Properties doInCluster(Jedis client) { + return JedisConverters.toProperties(client.info()); + } + })); + + return infos; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#info(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public Properties info(RedisClusterNode node) { + + return JedisConverters.toProperties(clusterCommandExecutor.executeCommandOnSingleNode( + new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + return client.info(); + } + }, node)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#info(java.lang.String) + */ + @Override + public Properties info(final String section) { + + Properties infos = new Properties(); + + infos.putAll(clusterCommandExecutor.executeCommandOnAllNodes(new JedisClusterCommandCallback() { + + @Override + public Properties doInCluster(Jedis client) { + return JedisConverters.toProperties(client.info(section)); + } + })); + + return infos; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#info(org.springframework.data.redis.connection.RedisClusterNode, java.lang.String) + */ + @Override + public Properties info(RedisClusterNode node, final String section) { + + return JedisConverters.toProperties(clusterCommandExecutor.executeCommandOnSingleNode( + new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + return client.info(section); + } + }, node)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#shutdown() + */ + @Override + public void shutdown() { + + clusterCommandExecutor.executeCommandOnAllNodes(new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + return client.shutdown(); + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#shutdown(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public void shutdown(RedisClusterNode node) { + + clusterCommandExecutor.executeCommandOnSingleNode(new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + return client.shutdown(); + } + }, node); + + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#shutdown(org.springframework.data.redis.connection.RedisServerCommands.ShutdownOption) + */ + @Override + public void shutdown(ShutdownOption option) { + + if (option == null) { + shutdown(); + return; + } + + throw new IllegalArgumentException("Shutdown with options is not supported for jedis."); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#getConfig(java.lang.String) + */ + @Override + public List getConfig(final String pattern) { + + Map> mapResult = clusterCommandExecutor + .executeCommandOnAllNodes(new JedisClusterCommandCallback>() { + + @Override + public List doInCluster(Jedis client) { + return client.configGet(pattern); + } + }); + + List result = new ArrayList(); + for (Entry> entry : mapResult.entrySet()) { + + String prefix = entry.getKey().asString(); + int i = 0; + for (String value : entry.getValue()) { + result.add((i++ % 2 == 0 ? (prefix + ".") : "") + value); + } + } + + return result; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#getConfig(org.springframework.data.redis.connection.RedisClusterNode, java.lang.String) + */ + @Override + public List getConfig(RedisClusterNode node, final String pattern) { + + return clusterCommandExecutor.executeCommandOnSingleNode(new JedisClusterCommandCallback>() { + + @Override + public List doInCluster(Jedis client) { + return client.configGet(pattern); + } + }, node); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#setConfig(java.lang.String, java.lang.String) + */ + @Override + public void setConfig(final String param, final String value) { + + clusterCommandExecutor.executeCommandOnAllNodes(new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + return client.configSet(param, value); + } + }); + + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#setConfig(org.springframework.data.redis.connection.RedisClusterNode, java.lang.String, java.lang.String) + */ + @Override + public void setConfig(RedisClusterNode node, final String param, final String value) { + + clusterCommandExecutor.executeCommandOnSingleNode(new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + return client.configSet(param, value); + } + }, node); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#resetConfigStats() + */ + @Override + public void resetConfigStats() { + + clusterCommandExecutor.executeCommandOnAllNodes(new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + return client.configResetStat(); + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#resetConfigStats(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public void resetConfigStats(RedisClusterNode node) { + + clusterCommandExecutor.executeCommandOnSingleNode(new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + return client.configResetStat(); + } + }, node); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#time() + */ + @Override + public Long time() { + + return convertListOfStringToTime(clusterCommandExecutor + .executeCommandOnArbitraryNode(new JedisClusterCommandCallback>() { + + @Override + public List doInCluster(Jedis client) { + return client.time(); + } + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#time(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public Long time(RedisClusterNode node) { + + return convertListOfStringToTime(clusterCommandExecutor.executeCommandOnSingleNode( + new JedisClusterCommandCallback>() { + + @Override + public List doInCluster(Jedis client) { + return client.time(); + } + }, node)); + } + + private Long convertListOfStringToTime(List serverTimeInformation) { + + Assert.notEmpty(serverTimeInformation, "Received invalid result from server. Expected 2 items in collection."); + Assert.isTrue(serverTimeInformation.size() == 2, + "Received invalid nr of arguments from redis server. Expected 2 received " + serverTimeInformation.size()); + + return Converters.toTimeMillis(serverTimeInformation.get(0), serverTimeInformation.get(1)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#killClient(java.lang.String, int) + */ + @Override + public void killClient(String host, int port) { + + final String hostAndPort = String.format("%s:%s", host, port); + + clusterCommandExecutor.executeCommandOnAllNodes(new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + return client.clientKill(hostAndPort); + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#setClientName(byte[]) + */ + @Override + public void setClientName(byte[] name) { + throw new InvalidDataAccessApiUsageException("CLIENT SETNAME is not supported in cluster environment."); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#getClientName() + */ + @Override + public String getClientName() { + throw new InvalidDataAccessApiUsageException("CLIENT GETNAME is not supported in cluster environment."); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#getClientList() + */ + @Override + public List getClientList() { + + Map map = clusterCommandExecutor + .executeCommandOnAllNodes(new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + return client.clientList(); + } + }); + + ArrayList result = new ArrayList(); + for (String infos : map.values()) { + result.addAll(JedisConverters.toListOfRedisClientInformation(infos)); + } + return result; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#getClientList(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public List getClientList(RedisClusterNode node) { + + return JedisConverters.toListOfRedisClientInformation(clusterCommandExecutor.executeCommandOnSingleNode( + new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + return client.clientList(); + } + }, node)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#slaveOf(java.lang.String, int) + */ + @Override + public void slaveOf(String host, int port) { + throw new InvalidDataAccessApiUsageException( + "Slaveof is not supported in cluster environment. Please use CLUSTER REPLICATE."); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#slaveOfNoOne() + */ + @Override + public void slaveOfNoOne() { + throw new InvalidDataAccessApiUsageException( + "Slaveof is not supported in cluster environment. Please use CLUSTER REPLICATE."); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisScriptingCommands#scriptFlush() + */ + @Override + public void scriptFlush() { + throw new InvalidDataAccessApiUsageException("ScriptFlush is not supported in cluster environment."); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisScriptingCommands#scriptKill() + */ + @Override + public void scriptKill() { + throw new InvalidDataAccessApiUsageException("ScriptKill is not supported in cluster environment."); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisScriptingCommands#scriptLoad(byte[]) + */ + @Override + public String scriptLoad(byte[] script) { + throw new InvalidDataAccessApiUsageException("ScriptLoad is not supported in cluster environment."); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisScriptingCommands#scriptExists(java.lang.String[]) + */ + @Override + public List scriptExists(String... scriptShas) { + throw new InvalidDataAccessApiUsageException("ScriptExists is not supported in cluster environment."); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisScriptingCommands#eval(byte[], org.springframework.data.redis.connection.ReturnType, int, byte[][]) + */ + @Override + public T eval(byte[] script, ReturnType returnType, int numKeys, byte[]... keysAndArgs) { + throw new InvalidDataAccessApiUsageException("Eval is not supported in cluster environment."); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisScriptingCommands#evalSha(java.lang.String, org.springframework.data.redis.connection.ReturnType, int, byte[][]) + */ + @Override + public T evalSha(String scriptSha, ReturnType returnType, int numKeys, byte[]... keysAndArgs) { + throw new InvalidDataAccessApiUsageException("EvalSha is not supported in cluster environment."); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisScriptingCommands#evalSha(byte[], org.springframework.data.redis.connection.ReturnType, int, byte[][]) + */ + @Override + public T evalSha(byte[] scriptSha, ReturnType returnType, int numKeys, byte[]... keysAndArgs) { + throw new InvalidDataAccessApiUsageException("EvalSha is not supported in cluster environment."); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.HyperLogLogCommands#pfAdd(byte[], byte[][]) + */ + @Override + public Long pfAdd(byte[] key, byte[]... values) { + + try { + return cluster.pfadd(key, values); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.HyperLogLogCommands#pfCount(byte[][]) + */ + @Override + public Long pfCount(byte[]... keys) { + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(keys)) { + + try { + return cluster.pfcount(keys); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + + } + throw new InvalidDataAccessApiUsageException("All keys must map to same slot for pfcount in cluster mode."); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.HyperLogLogCommands#pfMerge(byte[], byte[][]) + */ + @Override + public void pfMerge(byte[] destinationKey, byte[]... sourceKeys) { + + byte[][] allKeys = new byte[sourceKeys.length + 1][]; + allKeys[0] = destinationKey; + System.arraycopy(sourceKeys, 0, allKeys, 1, sourceKeys.length); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { + try { + cluster.pfmerge(destinationKey, sourceKeys); + return; + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + throw new InvalidDataAccessApiUsageException("All keys must map to same slot for pfmerge in cluster mode."); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisKeyCommands#exists(byte[]) + */ + @Override + public Boolean exists(final byte[] key) { + + try { + return cluster.exists(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * --> Cluster Commands + */ + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#clusterSetSlot(org.springframework.data.redis.connection.RedisClusterNode, int, org.springframework.data.redis.connection.RedisClusterCommands.AddSlots) + */ + @Override + public void clusterSetSlot(final RedisClusterNode node, final int slot, final AddSlots mode) { + + Assert.notNull(node, "Node must not be null."); + Assert.notNull(mode, "AddSlots mode must not be null."); + + final String nodeId = StringUtils.hasText(node.getId()) ? node.getId() : topologyProvider.getTopology() + .lookup(node.getHost(), node.getPort()).getId(); + + clusterCommandExecutor.executeCommandOnSingleNode(new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + + switch (mode) { + case IMPORTING: + return client.clusterSetSlotImporting(slot, nodeId); + case MIGRATING: + return client.clusterSetSlotMigrating(slot, nodeId); + case STABLE: + return client.clusterSetSlotStable(slot); + case NODE: + return client.clusterSetSlotNode(slot, nodeId); + } + + throw new IllegalArgumentException(String.format("Unknown AddSlots mode '%s'.", mode)); + } + }, node); + + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#clusterGetKeysInSlot(int, java.lang.Integer) + */ + @Override + public List clusterGetKeysInSlot(final int slot, final Integer count) { + + RedisClusterNode node = clusterGetNodeForSlot(slot); + + clusterCommandExecutor.executeCommandOnSingleNode(new JedisClusterCommandCallback>() { + + @Override + public List doInCluster(Jedis client) { + return JedisConverters.stringListToByteList().convert( + client.clusterGetKeysInSlot(slot, count != null ? count.intValue() : Integer.MAX_VALUE)); + } + }, node); + return null; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#clusterAddSlots(org.springframework.data.redis.connection.RedisClusterNode, int[]) + */ + @Override + public void clusterAddSlots(RedisClusterNode node, final int... slots) { + + clusterCommandExecutor.executeCommandOnSingleNode(new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + + return client.clusterAddSlots(slots); + } + }, node); + + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#clusterAddSlots(org.springframework.data.redis.connection.RedisClusterNode, org.springframework.data.redis.connection.RedisClusterNode.SlotRange) + */ + @Override + public void clusterAddSlots(RedisClusterNode node, SlotRange range) { + + Assert.notNull(range, "Range must not be null."); + + clusterAddSlots(node, range.getSlotsArray()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#clusterCountKeysInSlot(int) + */ + @Override + public Long clusterCountKeysInSlot(final int slot) { + + RedisClusterNode node = clusterGetNodeForSlot(slot); + + return clusterCommandExecutor.executeCommandOnSingleNode(new JedisClusterCommandCallback() { + + @Override + public Long doInCluster(Jedis client) { + + return client.clusterCountKeysInSlot(slot); + } + }, node); + + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#clusterDeleteSlots(org.springframework.data.redis.connection.RedisClusterNode, int[]) + */ + @Override + public void clusterDeleteSlots(RedisClusterNode node, final int... slots) { + + clusterCommandExecutor.executeCommandOnSingleNode(new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + return client.clusterDelSlots(slots); + } + }, node); + + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#clusterDeleteSlotsInRange(org.springframework.data.redis.connection.RedisClusterNode, org.springframework.data.redis.connection.RedisClusterNode.SlotRange) + */ + @Override + public void clusterDeleteSlotsInRange(RedisClusterNode node, SlotRange range) { + + Assert.notNull(range, "Range must not be null."); + + clusterDeleteSlots(node, range.getSlotsArray()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#clusterForget(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public void clusterForget(final RedisClusterNode node) { + + Set nodes = new LinkedHashSet(topologyProvider.getTopology().getActiveMasterNodes()); + nodes.remove(node); + + clusterCommandExecutor.executeCommandAsyncOnNodes(new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + return client.clusterForget(node.getId()); + } + }, nodes); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#clusterMeet(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public void clusterMeet(final RedisClusterNode node) { + + Assert.notNull(node, "Node to meet cluster must not be null!"); + + clusterCommandExecutor.executeCommandOnAllNodes(new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + + return client.clusterMeet(node.getHost(), node.getPort()); + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#clusterReplicate(org.springframework.data.redis.connection.RedisClusterNode, org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public void clusterReplicate(final RedisClusterNode master, RedisClusterNode slave) { + + clusterCommandExecutor.executeCommandOnSingleNode(new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + + return client.clusterReplicate(master.getId()); + } + }, slave); + + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#getClusterSlotForKey(byte[]) + */ + @Override + public Integer clusterGetSlotForKey(final byte[] key) { + + return clusterCommandExecutor.executeCommandOnArbitraryNode(new JedisClusterCommandCallback() { + + @Override + public Integer doInCluster(Jedis client) { + return client.clusterKeySlot(JedisConverters.toString(key)).intValue(); + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#clusterGetNodeForSlot(int) + */ + @Override + public RedisClusterNode clusterGetNodeForSlot(int slot) { + + for (RedisClusterNode node : topologyProvider.getTopology().getSlotServingNodes(slot)) { + if (node.isMaster()) { + return node; + } + } + + return null; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#clusterGetClusterNodes() + */ + @Override + public Set clusterGetClusterNodes() { + return topologyProvider.getTopology().getNodes(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#clusterGetSlaves(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public Set clusterGetSlaves(final RedisClusterNode master) { + + Assert.notNull(master, "Master cannot be null!"); + + final RedisClusterNode nodeToUse = StringUtils.hasText(master.getId()) ? master : topologyProvider.getTopology() + .lookup(master.getHost(), master.getPort()); + + return JedisConverters.toSetOfRedisClusterNodes(clusterCommandExecutor.executeCommandOnSingleNode( + new JedisClusterCommandCallback>() { + + @Override + public List doInCluster(Jedis client) { + return client.clusterSlaves(nodeToUse.getId()); + } + }, master)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#clusterGetMasterSlaveMap() + */ + public Map> clusterGetMasterSlaveMap() { + + return clusterCommandExecutor.executeCommandAsyncOnNodes( + new JedisClusterCommandCallback>() { + + @Override + public Set doInCluster(Jedis client) { + + // TODO: remove client.eval as soon as Jedis offers support for myid + return JedisConverters.toSetOfRedisClusterNodes(client.clusterSlaves((String) client.eval( + "return redis.call('cluster', 'myid')", 0))); + } + }, topologyProvider.getTopology().getActiveMasterNodes()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#clusterGetNodeForKey(byte[]) + */ + @Override + public RedisClusterNode clusterGetNodeForKey(byte[] key) { + return clusterGetNodeForSlot(clusterGetSlotForKey(key)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#clusterGetClusterInfo() + */ + @Override + public ClusterInfo clusterGetClusterInfo() { + + return new ClusterInfo(JedisConverters.toProperties(clusterCommandExecutor + .executeCommandOnArbitraryNode(new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + return client.clusterInfo(); + } + }))); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#migrate(byte[], org.springframework.data.redis.connection.RedisNode, int, org.springframework.data.redis.connection.RedisServerCommands.MigrateOption) + */ + @Override + public void migrate(byte[] key, RedisNode target, int dbIndex, MigrateOption option) { + migrate(key, target, dbIndex, option, Long.MAX_VALUE); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#migrate(byte[], org.springframework.data.redis.connection.RedisNode, int, org.springframework.data.redis.connection.RedisServerCommands.MigrateOption, long) + */ + @Override + public void migrate(final byte[] key, final RedisNode target, final int dbIndex, final MigrateOption option, + final long timeout) { + + final int timeoutToUse = timeout <= Integer.MAX_VALUE ? (int) timeout : Integer.MAX_VALUE; + + RedisClusterNode node = topologyProvider.getTopology().lookup(target.getHost(), target.getPort()); + + clusterCommandExecutor.executeCommandOnSingleNode(new JedisClusterCommandCallback() { + + @Override + public String doInCluster(Jedis client) { + return client.migrate(JedisConverters.toBytes(target.getHost()), target.getPort(), key, dbIndex, timeoutToUse); + } + }, node); + } + + /* + * --> Little helpers to make it work + */ + + protected DataAccessException convertJedisAccessException(Exception ex) { + return EXCEPTION_TRANSLATION.translate(ex); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisConnection#close() + */ + @Override + public void close() throws DataAccessException { + closed = true; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisConnection#isClosed() + */ + @Override + public boolean isClosed() { + return closed; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisConnection#getNativeConnection() + */ + @Override + public JedisCluster getNativeConnection() { + return cluster; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisConnection#isQueueing() + */ + @Override + public boolean isQueueing() { + return false; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisConnection#isPipelined() + */ + @Override + public boolean isPipelined() { + return false; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisConnection#openPipeline() + */ + @Override + public void openPipeline() { + throw new UnsupportedOperationException("Pipeline is currently not supported for JedisClusterConnection."); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisConnection#closePipeline() + */ + @Override + public List closePipeline() throws RedisPipelineException { + throw new UnsupportedOperationException("Pipeline is currently not supported for JedisClusterConnection."); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisConnection#getSentinelConnection() + */ + @Override + public RedisSentinelConnection getSentinelConnection() { + throw new UnsupportedOperationException("Sentinel is currently not supported for JedisClusterConnection."); + } + + /** + * {@link Jedis} specific {@link ClusterCommandCallback}. + * + * @author Christoph Strobl + * @param + * @since 1.7 + */ + protected interface JedisClusterCommandCallback extends ClusterCommandCallback {} + + /** + * {@link Jedis} specific {@link MultiKeyClusterCommandCallback}. + * + * @author Christoph Strobl + * @param + * @since 1.7 + */ + protected interface JedisMultiKeyClusterCommandCallback extends MultiKeyClusterCommandCallback {} + + /** + * Jedis specific implementation of {@link ClusterNodeResourceProvider}. + * + * @author Christoph Strobl + * @since 1.7 + */ + static class JedisClusterNodeResourceProvider implements ClusterNodeResourceProvider { + + private final JedisCluster cluster; + + /** + * Creates new {@link JedisClusterNodeResourceProvider}. + * + * @param cluster must not be {@literal null}. + */ + public JedisClusterNodeResourceProvider(JedisCluster cluster) { + this.cluster = cluster; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ClusterNodeResourceProvider#getResourceForSpecificNode(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + @SuppressWarnings("unchecked") + public Jedis getResourceForSpecificNode(RedisClusterNode node) { + + JedisPool pool = getResourcePoolForSpecificNode(node); + if (pool != null) { + return pool.getResource(); + } + + throw new IllegalArgumentException(String.format("Node %s is unknown to cluster", node)); + } + + protected JedisPool getResourcePoolForSpecificNode(RedisNode node) { + + Assert.notNull(node, "Cannot get Pool for 'null' node!"); + + Map clusterNodes = cluster.getClusterNodes(); + if (clusterNodes.containsKey(node.asString())) { + return clusterNodes.get(node.asString()); + } + + return null; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ClusterNodeResourceProvider#returnResourceForSpecificNode(org.springframework.data.redis.connection.RedisClusterNode, java.lang.Object) + */ + @Override + public void returnResourceForSpecificNode(RedisClusterNode node, Object client) { + getResourcePoolForSpecificNode(node).returnResource((Jedis) client); + } + + } + + /** + * Jedis specific implementation of {@link ClusterTopologyProvider}. + * + * @author Christoph Strobl + * @since 1.7 + */ + static class JedisClusterTopologyProvider implements ClusterTopologyProvider { + + private Object lock = new Object(); + private final JedisCluster cluster; + private long time = 0; + private ClusterTopology cached; + + /** + * Create new {@link JedisClusterTopologyProvider}.s + * + * @param cluster + */ + public JedisClusterTopologyProvider(JedisCluster cluster) { + this.cluster = cluster; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ClusterTopologyProvider#getTopology() + */ + @Override + public ClusterTopology getTopology() { + + if (cached != null && time + 100 > System.currentTimeMillis()) { + return cached; + } + + Map errors = new LinkedHashMap(); + + for (Entry entry : cluster.getClusterNodes().entrySet()) { + + Jedis jedis = null; + + try { + jedis = entry.getValue().getResource(); + + time = System.currentTimeMillis(); + Set nodes = JedisConverters.toSetOfRedisClusterNodes(jedis.clusterNodes()); + + synchronized (lock) { + cached = new ClusterTopology(nodes); + } + return cached; + } catch (Exception ex) { + errors.put(entry.getKey(), ex); + } finally { + if (jedis != null) { + entry.getValue().returnResource(jedis); + } + } + } + + StringBuilder sb = new StringBuilder(); + for (Entry entry : errors.entrySet()) { + sb.append(String.format("\r\n\t- %s failed: %s", entry.getKey(), entry.getValue().getMessage())); + } + throw new ClusterStateFailureExeption( + "Could not retrieve cluster information. CLUSTER NODES returned with error." + sb.toString()); + } + } + +} diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnection.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnection.java index 48ea05327..c199294ae 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnection.java @@ -3538,4 +3538,41 @@ public class JedisConnection extends AbstractRedisConnection { } } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#migrate(byte[], org.springframework.data.redis.connection.RedisNode, int, org.springframework.data.redis.connection.RedisServerCommands.MigrateOption) + */ + @Override + public void migrate(byte[] key, RedisNode target, int dbIndex, MigrateOption option) { + migrate(key, target, dbIndex, option, Long.MAX_VALUE); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#migrate(byte[], org.springframework.data.redis.connection.RedisNode, int, org.springframework.data.redis.connection.RedisServerCommands.MigrateOption, long) + */ + @Override + public void migrate(byte[] key, RedisNode target, int dbIndex, MigrateOption option, long timeout) { + + final int timeoutToUse = timeout <= Integer.MAX_VALUE ? (int) timeout : Integer.MAX_VALUE; + + try { + if (isPipelined()) { + + pipeline(new JedisResult(pipeline.migrate(JedisConverters.toBytes(target.getHost()), target.getPort(), key, + dbIndex, timeoutToUse))); + return; + } + if (isQueueing()) { + transaction(new JedisResult(transaction.migrate(JedisConverters.toBytes(target.getHost()), target.getPort(), + key, dbIndex, timeoutToUse))); + return; + } + jedis.migrate(JedisConverters.toBytes(target.getHost()), target.getPort(), key, dbIndex, timeoutToUse); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + + } + } diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java index a58d63d28..7b44bce16 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java @@ -18,18 +18,24 @@ package org.springframework.data.redis.connection.jedis; import java.lang.reflect.Method; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.dao.DataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.InvalidDataAccessResourceUsageException; import org.springframework.data.redis.ExceptionTranslationStrategy; import org.springframework.data.redis.PassThroughExceptionTranslationStrategy; import org.springframework.data.redis.RedisConnectionFailureException; +import org.springframework.data.redis.connection.ClusterCommandExecutor; +import org.springframework.data.redis.connection.RedisClusterConfiguration; +import org.springframework.data.redis.connection.RedisClusterConnection; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisNode; @@ -40,7 +46,9 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; +import redis.clients.jedis.HostAndPort; import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisCluster; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; import redis.clients.jedis.JedisSentinelPool; @@ -93,6 +101,9 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, private int dbIndex = 0; private boolean convertPipelineAndTxResults = true; private RedisSentinelConfiguration sentinelConfig; + private RedisClusterConfiguration clusterConfig; + private JedisCluster cluster; + private ClusterCommandExecutor clusterCommandExecutor; /** * Constructs a new JedisConnectionFactory instance with default settings (default connection pooling, no @@ -116,7 +127,7 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, * @param poolConfig pool configuration */ public JedisConnectionFactory(JedisPoolConfig poolConfig) { - this(null, poolConfig); + this((RedisSentinelConfiguration) null, poolConfig); } /** @@ -143,6 +154,29 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, this.poolConfig = poolConfig != null ? poolConfig : new JedisPoolConfig(); } + /** + * Constructs a new {@link JedisConnectionFactory} instance using the given {@link RedisClusterConfiguration} applied + * to create a {@link JedisCluster}. + * + * @param clusterConfig + * @since 1.7 + */ + public JedisConnectionFactory(RedisClusterConfiguration clusterConfig) { + this.clusterConfig = clusterConfig; + } + + /** + * Constructs a new {@link JedisConnectionFactory} instance using the given {@link RedisClusterConfiguration} applied + * to create a {@link JedisCluster}. + * + * @param clusterConfig + * @since 1.7 + */ + public JedisConnectionFactory(RedisClusterConfiguration clusterConfig, JedisPoolConfig poolConfig) { + this.clusterConfig = clusterConfig; + this.poolConfig = poolConfig; + } + /** * Returns a Jedis instance to be used as a Redis connection. The instance can be newly created or retrieved from a * pool. @@ -151,6 +185,7 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, */ protected Jedis fetchJedisConnector() { try { + if (usePool && pool != null) { return pool.getResource(); } @@ -191,9 +226,13 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, } } - if (usePool) { + if (usePool && clusterConfig == null) { this.pool = createPool(); } + + if (clusterConfig != null) { + this.cluster = createCluster(); + } } private Pool createPool() { @@ -228,6 +267,40 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, getTimeoutFrom(getShardInfo()), getShardInfo().getPassword()); } + private JedisCluster createCluster() { + + JedisCluster cluster = createCluster(this.clusterConfig, this.poolConfig); + this.clusterCommandExecutor = new ClusterCommandExecutor(new JedisClusterConnection.JedisClusterTopologyProvider( + cluster), new JedisClusterConnection.JedisClusterNodeResourceProvider(cluster), EXCEPTION_TRANSLATION); + return cluster; + } + + /** + * Creates {@link JedisCluster} for given {@link RedisClusterConfiguration} and {@link GenericObjectPoolConfig}. + * + * @param clusterConfig must not be {@literal null}. + * @param poolConfig can be {@literal null}. + * @return + * @since 1.7 + */ + protected JedisCluster createCluster(RedisClusterConfiguration clusterConfig, GenericObjectPoolConfig poolConfig) { + + Assert.notNull("Cluster configuration must not be null!"); + + Set hostAndPort = new HashSet(); + for (RedisNode node : clusterConfig.getClusterNodes()) { + hostAndPort.add(new HostAndPort(node.getHost(), node.getPort())); + } + + int timeout = clusterConfig.getClusterTimeout() != null ? clusterConfig.getClusterTimeout().intValue() : 1; + int redirects = clusterConfig.getMaxRedirects() != null ? clusterConfig.getMaxRedirects().intValue() : 5; + + if (poolConfig != null) { + return new JedisCluster(hostAndPort, timeout, redirects, poolConfig); + } + return new JedisCluster(hostAndPort, timeout, redirects, poolConfig); + } + /* * (non-Javadoc) * @see org.springframework.beans.factory.DisposableBean#destroy() @@ -241,13 +314,30 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, } pool = null; } + if (cluster != null) { + try { + cluster.close(); + } catch (Exception ex) { + log.warn("Cannot properly close Jedis cluster", ex); + } + try { + clusterCommandExecutor.destroy(); + } catch (Exception ex) { + log.warn("Cannot properly close cluster command executor", ex); + } + } } /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisConnectionFactory#getConnection() */ - public JedisConnection getConnection() { + public RedisConnection getConnection() { + + if (cluster != null) { + return getClusterConnection(); + } + Jedis jedis = fetchJedisConnector(); JedisConnection connection = (usePool ? new JedisConnection(jedis, pool, dbIndex) : new JedisConnection(jedis, null, dbIndex)); @@ -255,6 +345,19 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, return postProcessConnection(connection); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisConnectionFactory#getClusterConnection() + */ + @Override + public RedisClusterConnection getClusterConnection() { + + if (cluster == null) { + throw new InvalidDataAccessApiUsageException("Cluster is not configured!"); + } + return new JedisClusterConnection(cluster, clusterCommandExecutor); + } + /* * (non-Javadoc) * @see org.springframework.dao.support.PersistenceExceptionTranslator#translateExceptionIfPossible(java.lang.RuntimeException) diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java index 9c0404fee..4efd694f0 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java @@ -17,6 +17,7 @@ package org.springframework.data.redis.connection.jedis; import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; @@ -25,6 +26,7 @@ import java.util.Set; import org.springframework.core.convert.converter.Converter; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.connection.DefaultTuple; +import org.springframework.data.redis.connection.RedisClusterNode; import org.springframework.data.redis.connection.RedisListCommands.Position; import org.springframework.data.redis.connection.RedisServer; import org.springframework.data.redis.connection.RedisStringCommands.BitOperation; @@ -67,14 +69,15 @@ abstract public class JedisConverters extends Converters { private static final Converter> STRING_TO_CLIENT_INFO_CONVERTER = new StringToRedisClientInfoConverter(); private static final Converter TUPLE_CONVERTER; private static final ListConverter TUPLE_LIST_TO_TUPLE_LIST_CONVERTER; + private static final Converter OBJECT_TO_CLUSTER_NODE_CONVERTER; public static final byte[] PLUS_BYTES; public static final byte[] MINUS_BYTES; public static final byte[] POSITIVE_INFINITY_BYTES; public static final byte[] NEGATIVE_INFINITY_BYTES; - + static { - + STRING_TO_BYTES = new Converter() { public byte[] convert(String source) { return source == null ? null : SafeEncoder.encode(source); @@ -91,11 +94,24 @@ abstract public class JedisConverters extends Converters { }; TUPLE_SET_TO_TUPLE_SET = new SetConverter(TUPLE_CONVERTER); TUPLE_LIST_TO_TUPLE_LIST_CONVERTER = new ListConverter(TUPLE_CONVERTER); - PLUS_BYTES = toBytes("+"); MINUS_BYTES = toBytes("-"); POSITIVE_INFINITY_BYTES = toBytes("+inf"); NEGATIVE_INFINITY_BYTES = toBytes("-inf"); + + OBJECT_TO_CLUSTER_NODE_CONVERTER = new Converter() { + + @Override + public RedisClusterNode convert(Object infos) { + + List values = (List) infos; + RedisClusterNode.SlotRange range = new RedisClusterNode.SlotRange(((Number) values.get(0)).intValue(), + ((Number) values.get(1)).intValue()); + List nodeInfo = (List) values.get(2); + return new RedisClusterNode(JedisConverters.toString((byte[]) nodeInfo.get(0)), + ((Number) nodeInfo.get(1)).intValue(), range); + } + }; } public static Converter stringToBytes() { @@ -169,6 +185,15 @@ abstract public class JedisConverters extends Converters { return source == null ? null : SafeEncoder.encode(source); } + /** + * @param source + * @return + * @since 1.7 + */ + public static RedisClusterNode toNode(Object source) { + return OBJECT_TO_CLUSTER_NODE_CONVERTER.convert(source); + } + /** * @param source * @return @@ -200,6 +225,21 @@ abstract public class JedisConverters extends Converters { return sentinels; } + /** + * @param clusterNodes + * @return + * @since 1.7 + */ + public static Set toSetOfRedisClusterNodes(String clusterNodes) { + + if (StringUtils.isEmpty(clusterNodes)) { + return Collections.emptySet(); + } + + String[] lines = clusterNodes.split(System.getProperty("line.separator")); + return toSetOfRedisClusterNodes(Arrays.asList(lines)); + } + public static DataAccessException toDataAccessException(Exception ex) { return EXCEPTION_CONVERTER.convert(ex); } diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisExceptionConverter.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisExceptionConverter.java index 27444a2ce..5229d09ce 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisExceptionConverter.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisExceptionConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * Copyright 2013-2015 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. @@ -21,26 +21,46 @@ import java.net.UnknownHostException; import org.springframework.core.convert.converter.Converter; import org.springframework.dao.DataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.redis.ClusterRedirectException; import org.springframework.data.redis.RedisConnectionFailureException; +import org.springframework.data.redis.TooManyClusterRedirectionsException; +import redis.clients.jedis.exceptions.JedisClusterMaxRedirectionsException; import redis.clients.jedis.exceptions.JedisConnectionException; import redis.clients.jedis.exceptions.JedisDataException; import redis.clients.jedis.exceptions.JedisException; +import redis.clients.jedis.exceptions.JedisRedirectionException; /** * Converts Exceptions thrown from Jedis to {@link DataAccessException}s * * @author Jennifer Hickey * @author Thomas Darimont + * @author Christoph Strobl */ public class JedisExceptionConverter implements Converter { + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ public DataAccessException convert(Exception ex) { if (ex instanceof DataAccessException) { return (DataAccessException) ex; } if (ex instanceof JedisDataException) { + + if (ex instanceof JedisRedirectionException) { + JedisRedirectionException re = (JedisRedirectionException) ex; + return new ClusterRedirectException(re.getSlot(), re.getTargetNode().getHost(), re.getTargetNode().getPort(), + ex); + } + + if (ex instanceof JedisClusterMaxRedirectionsException) { + return new TooManyClusterRedirectionsException(ex.getMessage(), ex); + } + return new InvalidDataAccessApiUsageException(ex.getMessage(), ex); } if (ex instanceof JedisConnectionException) { diff --git a/src/main/java/org/springframework/data/redis/connection/jredis/JredisConnection.java b/src/main/java/org/springframework/data/redis/connection/jredis/JredisConnection.java index 21f96b1dd..39ade9b97 100644 --- a/src/main/java/org/springframework/data/redis/connection/jredis/JredisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/jredis/JredisConnection.java @@ -41,6 +41,7 @@ import org.springframework.data.redis.connection.AbstractRedisConnection; import org.springframework.data.redis.connection.DataType; import org.springframework.data.redis.connection.MessageListener; import org.springframework.data.redis.connection.Pool; +import org.springframework.data.redis.connection.RedisNode; import org.springframework.data.redis.connection.ReturnType; import org.springframework.data.redis.connection.SortParameters; import org.springframework.data.redis.connection.Subscription; @@ -1370,38 +1371,84 @@ public class JredisConnection extends AbstractRedisConnection { throw new UnsupportedOperationException("ZRANGEBYLEX is no supported for jredis."); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRevRangeByScore(byte[], org.springframework.data.redis.connection.RedisZSetCommands.Range) + */ @Override public Set zRevRangeByScore(byte[] key, Range range) { throw new UnsupportedOperationException(); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRevRangeByScore(byte[], org.springframework.data.redis.connection.RedisZSetCommands.Range, org.springframework.data.redis.connection.RedisZSetCommands.Limit) + */ @Override public Set zRevRangeByScore(byte[] key, Range range, Limit limit) { throw new UnsupportedOperationException(); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRevRangeByScoreWithScores(byte[], org.springframework.data.redis.connection.RedisZSetCommands.Range, org.springframework.data.redis.connection.RedisZSetCommands.Limit) + */ @Override public Set zRevRangeByScoreWithScores(byte[] key, Range range, Limit limit) { throw new UnsupportedOperationException(); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRemRangeByScore(byte[], org.springframework.data.redis.connection.RedisZSetCommands.Range) + */ @Override public Long zRemRangeByScore(byte[] key, Range range) { throw new UnsupportedOperationException(); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRangeByScore(byte[], org.springframework.data.redis.connection.RedisZSetCommands.Range) + */ @Override public Set zRangeByScore(byte[] key, Range range) { throw new UnsupportedOperationException(); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRangeByScore(byte[], org.springframework.data.redis.connection.RedisZSetCommands.Range, org.springframework.data.redis.connection.RedisZSetCommands.Limit) + */ @Override public Set zRangeByScore(byte[] key, Range range, Limit limit) { throw new UnsupportedOperationException(); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRevRangeByScoreWithScores(byte[], org.springframework.data.redis.connection.RedisZSetCommands.Range) + */ @Override public Set zRevRangeByScoreWithScores(byte[] key, Range range) { throw new UnsupportedOperationException(); } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#migrate(byte[], org.springframework.data.redis.connection.RedisNode, int, org.springframework.data.redis.connection.RedisServerCommands.MigrateOption) + */ + @Override + public void migrate(byte[] key, RedisNode target, int dbIndex, MigrateOption option) { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#migrate(byte[], org.springframework.data.redis.connection.RedisNode, int, org.springframework.data.redis.connection.RedisServerCommands.MigrateOption, long) + */ + @Override + public void migrate(byte[] key, RedisNode target, int dbIndex, MigrateOption option, long timeout) { + throw new UnsupportedOperationException(); + } } diff --git a/src/main/java/org/springframework/data/redis/connection/jredis/JredisConnectionFactory.java b/src/main/java/org/springframework/data/redis/connection/jredis/JredisConnectionFactory.java index 0682a5108..2ed153a6f 100644 --- a/src/main/java/org/springframework/data/redis/connection/jredis/JredisConnectionFactory.java +++ b/src/main/java/org/springframework/data/redis/connection/jredis/JredisConnectionFactory.java @@ -26,6 +26,7 @@ import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.connection.Pool; +import org.springframework.data.redis.connection.RedisClusterConnection; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisSentinelConnection; @@ -107,6 +108,15 @@ public class JredisConnectionFactory implements InitializingBean, DisposableBean return postProcessConnection(connection); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisConnectionFactory#getClusterConnection() + */ + @Override + public RedisClusterConnection getClusterConnection() { + throw new UnsupportedOperationException("Jredis does not support Redis Cluster."); + } + /** * Post process a newly retrieved connection. Useful for decorating or executing initialization commands on a new * connection. This implementation simply returns the connection. diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnection.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnection.java new file mode 100644 index 000000000..acd47943e --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnection.java @@ -0,0 +1,1618 @@ +/* + * Copyright 2015 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.lettuce; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.Random; +import java.util.Set; + +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.redis.ExceptionTranslationStrategy; +import org.springframework.data.redis.PassThroughExceptionTranslationStrategy; +import org.springframework.data.redis.connection.ClusterCommandExecutor; +import org.springframework.data.redis.connection.ClusterCommandExecutor.ClusterCommandCallback; +import org.springframework.data.redis.connection.ClusterCommandExecutor.MultiKeyClusterCommandCallback; +import org.springframework.data.redis.connection.ClusterInfo; +import org.springframework.data.redis.connection.ClusterNodeResourceProvider; +import org.springframework.data.redis.connection.ClusterSlotHashUtil; +import org.springframework.data.redis.connection.ClusterTopology; +import org.springframework.data.redis.connection.ClusterTopologyProvider; +import org.springframework.data.redis.connection.RedisClusterNode; +import org.springframework.data.redis.connection.RedisClusterNode.SlotRange; +import org.springframework.data.redis.connection.SortParameters; +import org.springframework.data.redis.connection.convert.Converters; +import org.springframework.data.redis.connection.jedis.JedisConverters; +import org.springframework.data.redis.connection.util.ByteArraySet; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.core.types.RedisClientInfo; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +import com.lambdaworks.redis.KeyValue; +import com.lambdaworks.redis.RedisAsyncConnection; +import com.lambdaworks.redis.RedisAsyncConnectionImpl; +import com.lambdaworks.redis.RedisClusterConnection; +import com.lambdaworks.redis.RedisConnection; +import com.lambdaworks.redis.RedisException; +import com.lambdaworks.redis.cluster.RedisClusterClient; +import com.lambdaworks.redis.cluster.SlotHash; +import com.lambdaworks.redis.cluster.models.partitions.Partitions; +import com.lambdaworks.redis.codec.RedisCodec; + +/** + * @author Christoph Strobl + * @author Mark Paluch + * @since 1.7 + */ +public class LettuceClusterConnection extends LettuceConnection implements + org.springframework.data.redis.connection.RedisClusterConnection { + + static final ExceptionTranslationStrategy exceptionConverter = new PassThroughExceptionTranslationStrategy( + new LettuceExceptionConverter()); + static final RedisCodec CODEC = new BytesRedisCodec(); + + private final RedisClusterClient clusterClient; + private ClusterCommandExecutor clusterCommandExecutor; + private ClusterTopologyProvider topologyProvider; + + /** + * Creates new {@link LettuceClusterConnection} using {@link RedisClusterClient}. + * + * @param clusterClient must not be {@literal null}. + */ + public LettuceClusterConnection(RedisClusterClient clusterClient) { + + super(null, 100, clusterClient, null, 0); + + Assert.notNull(clusterClient, "RedisClusterClient must not be null."); + + this.clusterClient = clusterClient; + topologyProvider = new LettuceClusterTopologyProvider(clusterClient); + clusterCommandExecutor = new ClusterCommandExecutor(topologyProvider, new LettuceClusterNodeResourceProvider( + clusterClient), exceptionConverter); + } + + /** + * Creates new {@link LettuceClusterConnection} using {@link RedisClusterClient} running commands across the cluster + * via given {@link ClusterCommandExecutor}. + * + * @param clusterClient must not be {@literal null}. + * @param executor must not be {@literal null}. + */ + public LettuceClusterConnection(RedisClusterClient clusterClient, ClusterCommandExecutor executor) { + + super(null, 100, clusterClient, null, 0); + + Assert.notNull(clusterClient, "RedisClusterClient must not be null."); + Assert.notNull(executor, "ClusterCommandExecutor must not be null."); + + this.clusterClient = clusterClient; + topologyProvider = new LettuceClusterTopologyProvider(clusterClient); + clusterCommandExecutor = executor; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#scan(long, org.springframework.data.redis.core.ScanOptions) + */ + @Override + public Cursor scan(long cursorId, ScanOptions options) { + throw new InvalidDataAccessApiUsageException("Scan is not supported accros multiple nodes within a cluster."); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#keys(byte[]) + */ + public Set keys(final byte[] pattern) { + + Assert.notNull(pattern, "Pattern must not be null!"); + + Collection> keysPerNode = clusterCommandExecutor.executeCommandOnAllNodes( + new LettuceClusterCommandCallback>() { + + @Override + public List doInCluster(RedisClusterConnection connection) { + return connection.keys(pattern); + } + }).values(); + + Set keys = new HashSet(); + + for (List keySet : keysPerNode) { + keys.addAll(keySet); + } + return keys; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#flushAll() + */ + public void flushAll() { + + clusterCommandExecutor.executeCommandOnAllNodes(new LettuceClusterCommandCallback() { + + @Override + public String doInCluster(RedisClusterConnection client) { + return client.flushall(); + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#flushDb() + */ + @Override + public void flushDb() { + + clusterCommandExecutor.executeCommandOnAllNodes(new LettuceClusterCommandCallback() { + + @Override + public String doInCluster(RedisClusterConnection client) { + return client.flushdb(); + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#dbSize() + */ + @Override + public Long dbSize() { + + Map dbSizes = clusterCommandExecutor + .executeCommandOnAllNodes(new LettuceClusterCommandCallback() { + + @Override + public Long doInCluster(RedisClusterConnection client) { + return client.dbsize(); + } + + }); + + if (CollectionUtils.isEmpty(dbSizes)) { + return 0L; + } + + Long size = 0L; + for (Long value : dbSizes.values()) { + size += value; + } + return size; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#info() + */ + @Override + public Properties info() { + + Properties infos = new Properties(); + + infos.putAll(clusterCommandExecutor.executeCommandOnAllNodes(new LettuceClusterCommandCallback() { + + @Override + public Properties doInCluster(RedisClusterConnection client) { + return LettuceConverters.toProperties(client.info()); + } + })); + + return infos; + } + + @Override + public Properties info(final String section) { + + Properties infos = new Properties(); + + infos.putAll(clusterCommandExecutor.executeCommandOnAllNodes(new LettuceClusterCommandCallback() { + + @Override + public Properties doInCluster(RedisClusterConnection client) { + return LettuceConverters.toProperties(client.info(section)); + } + })); + + return infos; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#info(org.springframework.data.redis.connection.RedisClusterNode, java.lang.String) + */ + @Override + public Properties info(RedisClusterNode node, final String section) { + + return LettuceConverters.toProperties(clusterCommandExecutor.executeCommandOnSingleNode( + new LettuceClusterCommandCallback() { + + @Override + public String doInCluster(RedisClusterConnection client) { + return client.info(section); + } + }, node)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#move(byte[], int) + */ + @Override + public Boolean move(byte[] key, int dbIndex) { + throw new UnsupportedOperationException("MOVE not supported in CLUSTER mode!"); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#del(byte[][]) + */ + @Override + public Long del(byte[]... keys) { + + Assert.noNullElements(keys, "Keys must not be null or contain null key!"); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(keys)) { + return super.del(keys); + } + + long total = 0; + for (byte[] key : keys) { + Long delted = super.del(key); + total += (delted != null ? delted.longValue() : 0); + } + return Long.valueOf(total); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#getClusterSlaves(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public Set clusterGetSlaves(final RedisClusterNode master) { + + Assert.notNull(master, "Master must not be null!"); + + final RedisClusterNode nodeToUse = StringUtils.hasText(master.getId()) ? master : topologyProvider.getTopology() + .lookup(master.getHost(), master.getPort()); + + return clusterCommandExecutor.executeCommandOnSingleNode( + new LettuceClusterCommandCallback>() { + + @Override + public Set doInCluster(RedisClusterConnection client) { + return LettuceConverters.toSetOfRedisClusterNodes(client.clusterSlaves(nodeToUse.getId())); + } + }, master); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#getClusterSlotForKey(byte[]) + */ + @Override + public Integer clusterGetSlotForKey(byte[] key) { + return SlotHash.getSlot(key); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#getClusterNodeForSlot(int) + */ + @Override + public RedisClusterNode clusterGetNodeForSlot(int slot) { + + DirectFieldAccessor accessor = new DirectFieldAccessor(clusterClient); + return LettuceConverters.toRedisClusterNode(((Partitions) accessor.getPropertyValue("partitions")) + .getPartitionBySlot(slot)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#getClusterNodeForKey(byte[]) + */ + @Override + public RedisClusterNode clusterGetNodeForKey(byte[] key) { + return clusterGetNodeForSlot(clusterGetSlotForKey(key)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#getClusterInfo() + */ + @Override + public ClusterInfo clusterGetClusterInfo() { + + return clusterCommandExecutor.executeCommandOnArbitraryNode(new LettuceClusterCommandCallback() { + + @Override + public ClusterInfo doInCluster(RedisClusterConnection client) { + return new ClusterInfo(LettuceConverters.toProperties(client.clusterInfo())); + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#addSlots(org.springframework.data.redis.connection.RedisClusterNode, int[]) + */ + @Override + public void clusterAddSlots(RedisClusterNode node, final int... slots) { + + clusterCommandExecutor.executeCommandOnSingleNode(new LettuceClusterCommandCallback() { + + @Override + public String doInCluster(RedisClusterConnection client) { + return client.clusterAddSlots(slots); + } + }, node); + + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#clusterAddSlots(org.springframework.data.redis.connection.RedisClusterNode, org.springframework.data.redis.connection.RedisClusterNode.SlotRange) + */ + @Override + public void clusterAddSlots(RedisClusterNode node, SlotRange range) { + + Assert.notNull(range, "Range must not be null."); + + clusterAddSlots(node, range.getSlotsArray()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#deleteSlots(org.springframework.data.redis.connection.RedisClusterNode, int[]) + */ + @Override + public void clusterDeleteSlots(RedisClusterNode node, final int... slots) { + + clusterCommandExecutor.executeCommandOnSingleNode(new LettuceClusterCommandCallback() { + + @Override + public String doInCluster(RedisClusterConnection client) { + return client.clusterDelSlots(slots); + } + }, node); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#clusterDeleteSlotsInRange(org.springframework.data.redis.connection.RedisClusterNode, org.springframework.data.redis.connection.RedisClusterNode.SlotRange) + */ + @Override + public void clusterDeleteSlotsInRange(RedisClusterNode node, SlotRange range) { + + Assert.notNull(range, "Range must not be null."); + + clusterDeleteSlots(node, range.getSlotsArray()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#clusterForget(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public void clusterForget(final RedisClusterNode node) { + + List nodes = new ArrayList(clusterGetClusterNodes()); + nodes.remove(node); + + this.clusterCommandExecutor.executeCommandAsyncOnNodes(new LettuceClusterCommandCallback() { + + @Override + public String doInCluster(RedisClusterConnection client) { + return client.clusterForget(node.getId()); + } + + }, nodes); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#clusterMeet(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public void clusterMeet(final RedisClusterNode node) { + + Assert.notNull(node, "Cluster node must not be null for CLUSTER MEET command!"); + + this.clusterCommandExecutor.executeCommandOnAllNodes(new LettuceClusterCommandCallback() { + + @Override + public String doInCluster(RedisClusterConnection client) { + return client.clusterMeet(node.getHost(), node.getPort()); + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#clusterSetSlot(org.springframework.data.redis.connection.RedisClusterNode, int, org.springframework.data.redis.connection.RedisClusterCommands.AddSlots) + */ + @Override + public void clusterSetSlot(final RedisClusterNode node, final int slot, final AddSlots mode) { + + Assert.notNull(node, "Node must not be null."); + Assert.notNull(mode, "AddSlots mode must not be null."); + + final String nodeId = StringUtils.hasText(node.getId()) ? node.getId() : topologyProvider.getTopology() + .lookup(node.getHost(), node.getPort()).getId(); + + clusterCommandExecutor.executeCommandOnSingleNode(new LettuceClusterCommandCallback() { + + @Override + public String doInCluster(RedisClusterConnection client) { + switch (mode) { + case MIGRATING: + return client.clusterSetSlotMigrating(slot, nodeId); + case IMPORTING: + return client.clusterSetSlotImporting(slot, nodeId); + case NODE: + return client.clusterSetSlotNode(slot, nodeId); + case STABLE: + throw new IllegalArgumentException("STABLE is not valid when using lettuce."); + default: + throw new InvalidDataAccessApiUsageException("Invlid import mode for cluster slot: " + slot); + } + } + }, node); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#getKeysInSlot(int, java.lang.Integer) + */ + @Override + public List clusterGetKeysInSlot(int slot, Integer count) { + + try { + return getConnection().clusterGetKeysInSlot(slot, count); + } catch (Exception ex) { + throw exceptionConverter.translate(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#countKeys(int) + */ + @Override + public Long clusterCountKeysInSlot(int slot) { + + try { + return getConnection().clusterCountKeysInSlot(slot); + } catch (Exception ex) { + throw exceptionConverter.translate(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#clusterReplicate(org.springframework.data.redis.connection.RedisClusterNode, org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public void clusterReplicate(final RedisClusterNode master, RedisClusterNode slave) { + + clusterCommandExecutor.executeCommandOnSingleNode(new LettuceClusterCommandCallback() { + + @Override + public String doInCluster(RedisClusterConnection client) { + return client.clusterReplicate(master.getId()); + } + }, slave); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#ping() + */ + @Override + public String ping() { + Collection ping = clusterCommandExecutor.executeCommandOnAllNodes( + new LettuceClusterCommandCallback() { + + @Override + public String doInCluster(RedisClusterConnection connection) { + return doPing(connection); + } + }).values(); + + for (String result : ping) { + if (!ObjectUtils.nullSafeEquals("PONG", result)) { + return ""; + } + } + + return "PONG"; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#ping(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public String ping(RedisClusterNode node) { + + return clusterCommandExecutor.executeCommandOnSingleNode(new LettuceClusterCommandCallback() { + + @Override + public String doInCluster(RedisClusterConnection client) { + return doPing(client); + } + }, node); + } + + protected String doPing(RedisClusterConnection client) { + + if (client instanceof RedisConnection) { + return ((RedisConnection) client).ping(); + } + + if (client instanceof RedisAsyncConnectionImpl) { + try { + return (String) ((RedisAsyncConnectionImpl) client).ping().get(); + } catch (Exception e) { + throw exceptionConverter.translate(e); + } + } + + throw new DataAccessResourceFailureException("Cannot execute ping using " + client); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#bgReWriteAof(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public void bgReWriteAof(RedisClusterNode node) { + + clusterCommandExecutor.executeCommandOnSingleNode(new LettuceClusterCommandCallback() { + + @Override + public String doInCluster(RedisClusterConnection client) { + return client.bgrewriteaof(); + } + }, node); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#bgSave(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public void bgSave(RedisClusterNode node) { + + clusterCommandExecutor.executeCommandOnSingleNode(new LettuceClusterCommandCallback() { + + @Override + public String doInCluster(RedisClusterConnection client) { + return client.bgsave(); + } + }, node); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#lastSave(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public Long lastSave(RedisClusterNode node) { + + return clusterCommandExecutor.executeCommandOnSingleNode(new LettuceClusterCommandCallback() { + + @Override + public Long doInCluster(RedisClusterConnection client) { + return client.lastsave().getTime(); + } + }, node); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#save(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public void save(RedisClusterNode node) { + + clusterCommandExecutor.executeCommandOnSingleNode(new LettuceClusterCommandCallback() { + + @Override + public String doInCluster(RedisClusterConnection client) { + return client.save(); + } + }, node); + + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#dbSize(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public Long dbSize(RedisClusterNode node) { + + return clusterCommandExecutor.executeCommandOnSingleNode(new LettuceClusterCommandCallback() { + + @Override + public Long doInCluster(RedisClusterConnection client) { + return client.dbsize(); + } + }, node); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#flushDb(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public void flushDb(RedisClusterNode node) { + + clusterCommandExecutor.executeCommandOnSingleNode(new LettuceClusterCommandCallback() { + + @Override + public String doInCluster(RedisClusterConnection client) { + return client.flushdb(); + } + }, node); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#flushAll(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public void flushAll(RedisClusterNode node) { + + clusterCommandExecutor.executeCommandOnSingleNode(new LettuceClusterCommandCallback() { + + @Override + public String doInCluster(RedisClusterConnection client) { + return client.flushall(); + } + }, node); + + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#info(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public Properties info(RedisClusterNode node) { + + return LettuceConverters.toProperties(clusterCommandExecutor.executeCommandOnSingleNode( + new LettuceClusterCommandCallback() { + + @Override + public String doInCluster(RedisClusterConnection client) { + return client.info(); + } + }, node)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#keys(org.springframework.data.redis.connection.RedisClusterNode, byte[]) + */ + @Override + public Set keys(RedisClusterNode node, final byte[] pattern) { + + return LettuceConverters.toBytesSet(clusterCommandExecutor.executeCommandOnSingleNode( + new LettuceClusterCommandCallback>() { + + @Override + public List doInCluster(RedisClusterConnection client) { + return client.keys(pattern); + } + }, node)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#randomKey(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public byte[] randomKey(RedisClusterNode node) { + + return clusterCommandExecutor.executeCommandOnSingleNode(new LettuceClusterCommandCallback() { + + @Override + public byte[] doInCluster(RedisClusterConnection client) { + return client.randomkey(); + } + }, node); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#randomKey() + */ + @Override + public byte[] randomKey() { + + List nodes = clusterGetClusterNodes(); + Set inspectedNodes = new HashSet(nodes.size()); + + do { + + RedisClusterNode node = nodes.get(new Random().nextInt(nodes.size())); + + while (inspectedNodes.contains(node)) { + node = nodes.get(new Random().nextInt(nodes.size())); + } + inspectedNodes.add(node); + byte[] key = randomKey(node); + + if (key != null && key.length > 0) { + return key; + } + } while (nodes.size() != inspectedNodes.size()); + + return null; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#rename(byte[], byte[]) + */ + @Override + public void rename(byte[] oldName, byte[] newName) { + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(oldName, newName)) { + super.rename(oldName, newName); + return; + } + + byte[] value = dump(oldName); + + if (value != null && value.length > 0) { + + restore(newName, 0, value); + del(oldName); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#renameNX(byte[], byte[]) + */ + @Override + public Boolean renameNX(byte[] oldName, byte[] newName) { + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(oldName, newName)) { + return super.renameNX(oldName, newName); + } + + byte[] value = dump(oldName); + + if (value != null && value.length > 0 && !exists(newName)) { + + restore(newName, 0, value); + del(oldName); + return Boolean.TRUE; + } + return Boolean.FALSE; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#shutdown(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public void shutdown(RedisClusterNode node) { + + clusterCommandExecutor.executeCommandOnSingleNode(new LettuceClusterCommandCallback() { + + @Override + public Void doInCluster(RedisClusterConnection client) { + client.shutdown(true); + return null; + } + }, node); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#sort(byte[], org.springframework.data.redis.connection.SortParameters, byte[]) + */ + @Override + public Long sort(byte[] key, SortParameters params, byte[] storeKey) { + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(key, storeKey)) { + return super.sort(key, params, storeKey); + } + + List sorted = sort(key, params); + if (!CollectionUtils.isEmpty(sorted)) { + + byte[][] arr = new byte[sorted.size()][]; + switch (type(key)) { + + case SET: + sAdd(storeKey, sorted.toArray(arr)); + return 1L; + case LIST: + lPush(storeKey, sorted.toArray(arr)); + return 1L; + default: + throw new IllegalArgumentException("sort and store is only supported for SET and LIST"); + } + } + return 0L; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#mGet(byte[][]) + */ + @Override + public List mGet(byte[]... keys) { + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(keys)) { + return super.mGet(keys); + } + + Map nodeResult = this.clusterCommandExecutor.executeMuliKeyCommand( + new LettuceMultiKeyClusterCommandCallback() { + + @Override + public byte[] doInCluster(RedisClusterConnection client, byte[] key) { + return client.get(key); + } + }, Arrays.asList(keys)); + + return new ArrayList(nodeResult.values()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#mSet(java.util.Map) + */ + @Override + public void mSet(Map tuples) { + + Assert.notNull(tuples, "Tuple must not be null!"); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(tuples.keySet().toArray(new byte[tuples.keySet().size()][]))) { + super.mSet(tuples); + return; + } + + for (Map.Entry entry : tuples.entrySet()) { + set(entry.getKey(), entry.getValue()); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#mSetNX(java.util.Map) + */ + @Override + public Boolean mSetNX(Map tuples) { + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(tuples.keySet().toArray(new byte[tuples.keySet().size()][]))) { + return super.mSetNX(tuples); + } + + boolean result = true; + for (Map.Entry entry : tuples.entrySet()) { + if (!setNX(entry.getKey(), entry.getValue()) && result) { + result = false; + } + } + return result; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#bLPop(int, byte[][]) + */ + @Override + public List bLPop(final int timeout, byte[]... keys) { + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(keys)) { + return super.bLPop(timeout, keys); + } + + Map> nodeResult = this.clusterCommandExecutor.executeMuliKeyCommand( + new LettuceMultiKeyClusterCommandCallback>() { + + @Override + public KeyValue doInCluster(RedisClusterConnection client, byte[] key) { + return client.blpop(timeout, key); + } + }, Arrays.asList(keys)); + + for (KeyValue partial : nodeResult.values()) { + return LettuceConverters.toBytesList(partial); + } + + return Collections.emptyList(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#bRPop(int, byte[][]) + */ + @Override + public List bRPop(final int timeout, byte[]... keys) { + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(keys)) { + return super.bRPop(timeout, keys); + } + + Map> nodeResult = this.clusterCommandExecutor.executeMuliKeyCommand( + new LettuceMultiKeyClusterCommandCallback>() { + + @Override + public KeyValue doInCluster(RedisClusterConnection client, byte[] key) { + return client.brpop(timeout, key); + } + }, Arrays.asList(keys)); + + for (KeyValue partial : nodeResult.values()) { + return LettuceConverters.toBytesList(partial); + } + + return Collections.emptyList(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#rPopLPush(byte[], byte[]) + */ + @Override + public byte[] rPopLPush(byte[] srcKey, byte[] dstKey) { + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(srcKey, dstKey)) { + return super.rPopLPush(srcKey, dstKey); + } + + byte[] val = rPop(srcKey); + lPush(dstKey, val); + return val; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#bRPopLPush(int, byte[], byte[]) + */ + @Override + public byte[] bRPopLPush(int timeout, byte[] srcKey, byte[] dstKey) { + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(srcKey, dstKey)) { + return super.bRPopLPush(timeout, srcKey, dstKey); + } + + List val = bRPop(timeout, srcKey); + if (!CollectionUtils.isEmpty(val)) { + lPush(dstKey, val.get(1)); + return val.get(1); + } + + return null; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#sMove(byte[], byte[], byte[]) + */ + @Override + public Boolean sMove(byte[] srcKey, byte[] destKey, byte[] value) { + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(srcKey, destKey)) { + return super.sMove(srcKey, destKey, value); + } + + if (exists(srcKey)) { + if (sRem(srcKey, value) > 0 && !sIsMember(destKey, value)) { + return LettuceConverters.toBoolean(sAdd(destKey, value)); + } + } + return Boolean.FALSE; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#sInter(byte[][]) + */ + @Override + public Set sInter(byte[]... keys) { + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(keys)) { + return super.sInter(keys); + } + + Map> nodeResult = this.clusterCommandExecutor.executeMuliKeyCommand( + new LettuceMultiKeyClusterCommandCallback>() { + + @Override + public Set doInCluster(RedisClusterConnection client, byte[] key) { + return client.smembers(key); + } + }, Arrays.asList(keys)); + + ByteArraySet result = null; + for (Entry> entry : nodeResult.entrySet()) { + + ByteArraySet tmp = new ByteArraySet(entry.getValue()); + if (result == null) { + result = tmp; + } else { + result.retainAll(tmp); + if (result.isEmpty()) { + break; + } + } + } + + if (result.isEmpty()) { + return Collections.emptySet(); + } + + return result.asRawSet(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#sInterStore(byte[], byte[][]) + */ + @Override + public Long sInterStore(byte[] destKey, byte[]... keys) { + + byte[][] allKeys = new byte[keys.length + 1][]; + allKeys[0] = destKey; + System.arraycopy(keys, 0, allKeys, 1, keys.length); + if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { + return super.sInterStore(destKey, keys); + } + + Set result = sInter(keys); + if (result.isEmpty()) { + return 0L; + } + return sAdd(destKey, result.toArray(new byte[result.size()][])); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#sUnion(byte[][]) + */ + @Override + public Set sUnion(byte[]... keys) { + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(keys)) { + return super.sUnion(keys); + } + + Map> nodeResult = this.clusterCommandExecutor.executeMuliKeyCommand( + new LettuceMultiKeyClusterCommandCallback>() { + + @Override + public Set doInCluster(RedisClusterConnection client, byte[] key) { + return client.smembers(key); + } + }, Arrays.asList(keys)); + + ByteArraySet result = new ByteArraySet(); + for (Entry> entry : nodeResult.entrySet()) { + result.addAll(entry.getValue()); + } + + if (result.isEmpty()) { + return Collections.emptySet(); + } + + return result.asRawSet(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#sUnionStore(byte[], byte[][]) + */ + @Override + public Long sUnionStore(byte[] destKey, byte[]... keys) { + + byte[][] allKeys = new byte[keys.length + 1][]; + allKeys[0] = destKey; + System.arraycopy(keys, 0, allKeys, 1, keys.length); + if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { + return super.sUnionStore(destKey, keys); + } + + Set result = sUnion(keys); + if (result.isEmpty()) { + return 0L; + } + return sAdd(destKey, result.toArray(new byte[result.size()][])); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#sDiff(byte[][]) + */ + @Override + public Set sDiff(byte[]... keys) { + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(keys)) { + return super.sDiff(keys); + } + + byte[] source = keys[0]; + byte[][] others = Arrays.copyOfRange(keys, 1, keys.length - 1); + + ByteArraySet values = new ByteArraySet(sMembers(source)); + Map> nodeResult = clusterCommandExecutor.executeMuliKeyCommand( + new LettuceMultiKeyClusterCommandCallback>() { + + @Override + public Set doInCluster(RedisClusterConnection client, byte[] key) { + return client.smembers(key); + } + }, Arrays.asList(others)); + + if (values.isEmpty()) { + return Collections.emptySet(); + } + + for (Set toSubstract : nodeResult.values()) { + values.removeAll(toSubstract); + } + + return values.asRawSet(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#sDiffStore(byte[], byte[][]) + */ + @Override + public Long sDiffStore(byte[] destKey, byte[]... keys) { + + byte[][] allKeys = new byte[keys.length + 1][]; + allKeys[0] = destKey; + System.arraycopy(keys, 0, allKeys, 1, keys.length); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { + return super.sDiffStore(destKey, keys); + } + + Set diff = sDiff(keys); + if (diff.isEmpty()) { + return 0L; + } + + return sAdd(destKey, diff.toArray(new byte[diff.size()][])); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#getAsyncDedicatedConnection() + */ + @Override + protected RedisAsyncConnection doGetAsyncDedicatedConnection() { + return (RedisAsyncConnection) clusterClient.connectClusterAsync(CODEC); + } + + // --> cluster node stuff + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#getClusterNodes() + */ + @Override + public List clusterGetClusterNodes() { + return LettuceConverters.partitionsToClusterNodes(clusterClient.getPartitions()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#pfCount(byte[][]) + */ + @Override + public Long pfCount(byte[]... keys) { + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(keys)) { + + try { + return super.pfCount(keys); + } catch (Exception ex) { + throw convertLettuceAccessException(ex); + } + + } + throw new InvalidDataAccessApiUsageException("All keys must map to same slot for pfcount in cluster mode."); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#pfMerge(byte[], byte[][]) + */ + @Override + public void pfMerge(byte[] destinationKey, byte[]... sourceKeys) { + + byte[][] allKeys = new byte[sourceKeys.length + 1][]; + allKeys[0] = destinationKey; + System.arraycopy(sourceKeys, 0, allKeys, 1, sourceKeys.length); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { + try { + super.pfMerge(destinationKey, sourceKeys); + return; + } catch (Exception ex) { + throw convertLettuceAccessException(ex); + } + + } + throw new InvalidDataAccessApiUsageException("All keys must map to same slot for pfmerge in cluster mode."); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#watch(byte[][]) + */ + @Override + public void watch(byte[]... keys) { + throw new InvalidDataAccessApiUsageException("WATCH is currently not supported in cluster mode."); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#unwatch() + */ + @Override + public void unwatch() { + throw new InvalidDataAccessApiUsageException("UNWATCH is currently not supported in cluster mode."); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#multi() + */ + @Override + public void multi() { + throw new InvalidDataAccessApiUsageException("MULTI is currently not supported in cluster mode."); + } + + /* + * + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#getConfig(java.lang.String) + */ + @Override + public List getConfig(final String pattern) { + + Map> mapResult = clusterCommandExecutor + .executeCommandOnAllNodes(new LettuceClusterCommandCallback>() { + + @Override + public List doInCluster(RedisClusterConnection client) { + return client.configGet(pattern); + } + }); + + List result = new ArrayList(); + for (Entry> entry : mapResult.entrySet()) { + + String prefix = entry.getKey().asString(); + int i = 0; + for (String value : entry.getValue()) { + result.add((i++ % 2 == 0 ? (prefix + ".") : "") + value); + } + } + + return result; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#getConfig(org.springframework.data.redis.connection.RedisClusterNode, java.lang.String) + */ + @Override + public List getConfig(RedisClusterNode node, final String pattern) { + + return clusterCommandExecutor.executeCommandOnSingleNode(new LettuceClusterCommandCallback>() { + + @Override + public List doInCluster(RedisClusterConnection client) { + return client.configGet(pattern); + } + }, node); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#setConfig(java.lang.String, java.lang.String) + */ + @Override + public void setConfig(final String param, final String value) { + + clusterCommandExecutor.executeCommandOnAllNodes(new LettuceClusterCommandCallback() { + + @Override + public String doInCluster(RedisClusterConnection client) { + return client.configSet(param, value); + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#setConfig(org.springframework.data.redis.connection.RedisClusterNode, java.lang.String, java.lang.String) + */ + @Override + public void setConfig(RedisClusterNode node, final String param, final String value) { + + clusterCommandExecutor.executeCommandOnSingleNode(new LettuceClusterCommandCallback() { + + @Override + public String doInCluster(RedisClusterConnection client) { + return client.configSet(param, value); + } + }, node); + + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#resetConfigStats() + */ + @Override + public void resetConfigStats() { + + clusterCommandExecutor.executeCommandOnAllNodes(new LettuceClusterCommandCallback() { + + @Override + public String doInCluster(RedisClusterConnection client) { + return client.configResetstat(); + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#resetConfigStats(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public void resetConfigStats(RedisClusterNode node) { + + clusterCommandExecutor.executeCommandOnSingleNode(new LettuceClusterCommandCallback() { + + @Override + public String doInCluster(RedisClusterConnection client) { + return client.configResetstat(); + } + }, node); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#time() + */ + @Override + public Long time() { + + return convertListOfStringToTime(clusterCommandExecutor + .executeCommandOnArbitraryNode(new LettuceClusterCommandCallback>() { + + @Override + public List doInCluster(RedisClusterConnection client) { + return client.time(); + } + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#time(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public Long time(RedisClusterNode node) { + + return convertListOfStringToTime(clusterCommandExecutor.executeCommandOnSingleNode( + new LettuceClusterCommandCallback>() { + + @Override + public List doInCluster(RedisClusterConnection client) { + return client.time(); + } + }, node)); + } + + private Long convertListOfStringToTime(List serverTimeInformation) { + + Assert.notEmpty(serverTimeInformation, "Received invalid result from server. Expected 2 items in collection."); + Assert.isTrue(serverTimeInformation.size() == 2, + "Received invalid nr of arguments from redis server. Expected 2 received " + serverTimeInformation.size()); + + return Converters.toTimeMillis(LettuceConverters.toString(serverTimeInformation.get(0)), + LettuceConverters.toString(serverTimeInformation.get(1))); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#getClientList() + */ + @Override + public List getClientList() { + + Map map = clusterCommandExecutor + .executeCommandOnAllNodes(new LettuceClusterCommandCallback() { + + @Override + public String doInCluster(RedisClusterConnection client) { + return client.clientList(); + } + }); + + ArrayList result = new ArrayList(); + for (String infos : map.values()) { + result.addAll(LettuceConverters.toListOfRedisClientInformation(infos)); + } + return result; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#getClientList(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public List getClientList(RedisClusterNode node) { + + return LettuceConverters.toListOfRedisClientInformation(clusterCommandExecutor.executeCommandOnSingleNode( + new LettuceClusterCommandCallback() { + + @Override + public String doInCluster(RedisClusterConnection client) { + return client.clientList(); + } + }, node)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterCommands#clusterGetMasterSlaveMap() + */ + @Override + public Map> clusterGetMasterSlaveMap() { + + return clusterCommandExecutor.executeCommandAsyncOnNodes( + new LettuceClusterCommandCallback>() { + + @Override + public Set doInCluster(RedisClusterConnection client) { + return JedisConverters.toSetOfRedisClusterNodes(client.clusterSlaves((String) client.clusterMyId())); + } + }, topologyProvider.getTopology().getActiveMasterNodes()); + } + + /** + * Lettuce specific implementation of {@link ClusterCommandCallback}. + * + * @author Christoph Strobl + * @param + * @since 1.7 + */ + protected interface LettuceClusterCommandCallback extends + ClusterCommandCallback, T> {} + + /** + * Lettuce specific implementation of {@link MultiKeyClusterCommandCallback}. + * + * @author Christoph Strobl + * @param + * @since 1.7 + */ + protected interface LettuceMultiKeyClusterCommandCallback extends + MultiKeyClusterCommandCallback, T> { + + } + + /** + * Lettuce specific implementation of {@link ClusterNodeResourceProvider}. + * + * @author Christoph Strobl + * @since 1.7 + */ + static class LettuceClusterNodeResourceProvider implements ClusterNodeResourceProvider { + + private final RedisClusterClient client; + + public LettuceClusterNodeResourceProvider(RedisClusterClient client) { + + this.client = client; + } + + @Override + @SuppressWarnings("unchecked") + public RedisClusterConnection getResourceForSpecificNode(RedisClusterNode node) { + + Assert.notNull(node, "Node must not be null!"); + + try { + RedisClusterConnection connection = client.connectCluster(CODEC).getConnection(node.getHost(), + node.getPort()); + return connection; + } catch (RedisException e) { + + // unwrap cause when cluster node not known in cluster + if (e.getCause() instanceof IllegalArgumentException) { + throw (IllegalArgumentException) e.getCause(); + } + throw e; + } + } + + @Override + @SuppressWarnings("unchecked") + public void returnResourceForSpecificNode(RedisClusterNode node, Object resource) { + + RedisClusterConnection connection = (RedisClusterConnection) resource; + connection.close(); + } + + } + + /** + * Lettuce specific implementation of {@link ClusterTopologyProvider}. + * + * @author Christoph Strobl + * @since 1.7 + */ + static class LettuceClusterTopologyProvider implements ClusterTopologyProvider { + + private final RedisClusterClient client; + + /** + * @param client must not be {@literal null}. + */ + public LettuceClusterTopologyProvider(RedisClusterClient client) { + + Assert.notNull(client, "RedisClusteClient must not be null."); + this.client = client; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ClusterTopologyProvider#getTopology() + */ + @Override + public ClusterTopology getTopology() { + return new ClusterTopology(new LinkedHashSet(LettuceConverters.partitionsToClusterNodes(client + .getPartitions()))); + } + } + +} diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java index a00497b3d..d3547b04a 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java @@ -149,7 +149,7 @@ public class LettuceConnection extends AbstractRedisConnection { private boolean isPipelined = false; private List ppline; private Queue> txResults = new LinkedList>(); - private RedisClient client; + private AbstractRedisClient client; private volatile LettuceSubscription subscription; private LettucePool pool; /** flag indicating whether the connection needs to be dropped or not */ @@ -310,7 +310,7 @@ public class LettuceConnection extends AbstractRedisConnection { * @since 1.7 */ public LettuceConnection(com.lambdaworks.redis.RedisAsyncConnection sharedConnection, long timeout, - RedisClient client, LettucePool pool, int defaultDbIndex) { + AbstractRedisClient client, LettucePool pool, int defaultDbIndex) { this.asyncSharedConn = sharedConnection; this.timeout = timeout; @@ -3398,7 +3398,7 @@ public class LettuceConnection extends AbstractRedisConnection { private RedisPubSubConnection switchToPubSub() { close(); // open a pubsub one - return client.connectPubSub(CODEC); + return ((RedisClient) client).connectPubSub(CODEC); } private void pipeline(LettuceResult result) { @@ -3423,7 +3423,7 @@ public class LettuceConnection extends AbstractRedisConnection { return getAsyncDedicatedConnection(); } - private com.lambdaworks.redis.RedisConnection getConnection() { + protected com.lambdaworks.redis.RedisConnection getConnection() { if (isQueueing()) { return getDedicatedConnection(); } @@ -3433,19 +3433,28 @@ public class LettuceConnection extends AbstractRedisConnection { return getDedicatedConnection(); } - private RedisAsyncConnection getAsyncDedicatedConnection() { + protected RedisAsyncConnection getAsyncDedicatedConnection() { if (asyncDedicatedConn == null) { - if (this.pool != null) { - this.asyncDedicatedConn = pool.getResource(); - } else { - this.asyncDedicatedConn = client.connectAsync(CODEC); + asyncDedicatedConn = doGetAsyncDedicatedConnection(); + + if (this.pool == null) { this.asyncDedicatedConn.select(dbIndex); } + } return asyncDedicatedConn; } + protected RedisAsyncConnection doGetAsyncDedicatedConnection() { + + if (this.pool != null) { + return pool.getResource(); + } else { + return ((RedisClient) client).connectAsync(CODEC); + } + } + private com.lambdaworks.redis.RedisConnection getDedicatedConnection() { if (dedicatedConn == null) { this.dedicatedConn = syncConnection(getAsyncDedicatedConnection()); @@ -3581,7 +3590,7 @@ public class LettuceConnection extends AbstractRedisConnection { RedisConnection connection = null; try { - connection = client.connect(getRedisURI(node)); + connection = ((RedisClient) client).connect(getRedisURI(node)); return connection.ping().equalsIgnoreCase("pong"); } catch (Exception e) { return false; @@ -3602,7 +3611,8 @@ public class LettuceConnection extends AbstractRedisConnection { */ @Override protected RedisSentinelConnection getSentinelConnection(RedisNode sentinel) { - RedisSentinelAsyncConnection connection = client.connectSentinelAsync(getRedisURI(sentinel)); + RedisSentinelAsyncConnection connection = ((RedisClient) client) + .connectSentinelAsync(getRedisURI(sentinel)); return new LettuceSentinelConnection(connection); } @@ -4025,4 +4035,37 @@ public class LettuceConnection extends AbstractRedisConnection { } } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#migrate(byte[], org.springframework.data.redis.connection.RedisNode, int, org.springframework.data.redis.connection.RedisServerCommands.MigrateOption) + */ + @Override + public void migrate(byte[] key, RedisNode target, int dbIndex, MigrateOption option) { + migrate(key, target, dbIndex, option, Long.MAX_VALUE); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#migrate(byte[], org.springframework.data.redis.connection.RedisNode, int, org.springframework.data.redis.connection.RedisServerCommands.MigrateOption, long) + */ + @Override + public void migrate(byte[] key, RedisNode target, int dbIndex, MigrateOption option, long timeout) { + + try { + if (isPipelined()) { + pipeline(new LettuceResult(getAsyncConnection().migrate(target.getHost(), target.getPort(), key, dbIndex, + timeout))); + return; + } + if (isQueueing()) { + transaction(new LettuceTxResult(getConnection().migrate(target.getHost(), target.getPort(), key, dbIndex, + timeout))); + return; + } + getConnection().migrate(target.getHost(), target.getPort(), key, dbIndex, timeout); + } catch (Exception ex) { + throw convertLettuceAccessException(ex); + } + } + } diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java index 48640226b..c2f6ab72f 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java @@ -16,6 +16,8 @@ package org.springframework.data.redis.connection.lettuce; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.TimeUnit; import org.apache.commons.logging.Log; @@ -23,22 +25,30 @@ import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.dao.DataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.dao.InvalidDataAccessResourceUsageException; import org.springframework.data.redis.ExceptionTranslationStrategy; import org.springframework.data.redis.PassThroughExceptionTranslationStrategy; import org.springframework.data.redis.RedisConnectionFailureException; +import org.springframework.data.redis.connection.ClusterCommandExecutor; import org.springframework.data.redis.connection.Pool; +import org.springframework.data.redis.connection.RedisClusterConfiguration; +import org.springframework.data.redis.connection.RedisClusterConnection; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisNode; import org.springframework.data.redis.connection.RedisSentinelConfiguration; import org.springframework.data.redis.connection.RedisSentinelConnection; import org.springframework.util.Assert; +import com.lambdaworks.redis.AbstractRedisClient; import com.lambdaworks.redis.LettuceFutures; import com.lambdaworks.redis.RedisAsyncConnection; import com.lambdaworks.redis.RedisClient; import com.lambdaworks.redis.RedisException; import com.lambdaworks.redis.RedisFuture; import com.lambdaworks.redis.RedisURI; +import com.lambdaworks.redis.cluster.RedisClusterClient; /** * Connection factory creating Lettuce-based connections. @@ -68,7 +78,7 @@ public class LettuceConnectionFactory implements InitializingBean, DisposableBea private String hostName = "localhost"; private int port = 6379; - private RedisClient client; + private AbstractRedisClient client; private long timeout = TimeUnit.MILLISECONDS.convert(60, TimeUnit.SECONDS); private long shutdownTimeout = TimeUnit.MILLISECONDS.convert(2, TimeUnit.SECONDS); private boolean validateConnection = false; @@ -81,6 +91,8 @@ public class LettuceConnectionFactory implements InitializingBean, DisposableBea private String password; private boolean convertPipelineAndTxResults = true; private RedisSentinelConfiguration sentinelConfiguration; + private RedisClusterConfiguration clusterConfiguration; + private ClusterCommandExecutor clusterCommandExecutor; /** * Constructs a new LettuceConnectionFactory instance with default settings. @@ -105,17 +117,46 @@ public class LettuceConnectionFactory implements InitializingBean, DisposableBea this.sentinelConfiguration = sentinelConfiguration; } + /** + * Constructs a new {@link LettuceConnectionFactory} instance using the given {@link RedisClusterConfiguration} + * applied to create a {@link RedisClusterClient}. + * + * @param clusterConfig + * @since 1.7 + */ + public LettuceConnectionFactory(RedisClusterConfiguration clusterConfig) { + this.clusterConfiguration = clusterConfig; + } + public LettuceConnectionFactory(LettucePool pool) { this.pool = pool; } + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ public void afterPropertiesSet() { this.client = createRedisClient(); + } + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.DisposableBean#destroy() + */ public void destroy() { resetConnection(); client.shutdown(shutdownTimeout, shutdownTimeout, TimeUnit.MILLISECONDS); + + if (clusterCommandExecutor != null) { + + try { + clusterCommandExecutor.destroy(); + } catch (Exception ex) { + log.warn("Cannot properly close cluster command executor", ex); + } + } } /* @@ -124,11 +165,29 @@ public class LettuceConnectionFactory implements InitializingBean, DisposableBea */ public RedisConnection getConnection() { + if (isClusterAware()) { + return getClusterConnection(); + } + LettuceConnection connection = new LettuceConnection(getSharedConnection(), timeout, client, pool, dbIndex); connection.setConvertPipelineAndTxResults(convertPipelineAndTxResults); return connection; } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisConnectionFactory#getClusterConnection() + */ + @Override + public RedisClusterConnection getClusterConnection() { + + if (!isClusterAware()) { + throw new InvalidDataAccessApiUsageException("Cluster is not configured!"); + } + + return new LettuceClusterConnection((RedisClusterClient) client, clusterCommandExecutor); + } + public void initConnection() { synchronized (this.connectionMonitor) { if (this.connection != null) { @@ -377,9 +436,16 @@ public class LettuceConnectionFactory implements InitializingBean, DisposableBea protected RedisAsyncConnection createLettuceConnector() { try { - RedisAsyncConnection connection = client.connectAsync(LettuceConnection.CODEC); - if (dbIndex > 0) { - connection.select(dbIndex); + + RedisAsyncConnection connection = null; + if (client instanceof RedisClient) { + connection = ((RedisClient) client).connectAsync(LettuceConnection.CODEC); + if (dbIndex > 0) { + connection.select(dbIndex); + } + } else { + connection = (RedisAsyncConnection) ((RedisClusterClient) client) + .connectClusterAsync(LettuceConnection.CODEC); } return connection; } catch (RedisException e) { @@ -387,13 +453,29 @@ public class LettuceConnectionFactory implements InitializingBean, DisposableBea } } - private RedisClient createRedisClient() { + private AbstractRedisClient createRedisClient() { if (isRedisSentinelAware()) { RedisURI redisURI = getSentinelRedisURI(); return new RedisClient(redisURI); } + if (isClusterAware()) { + + List initialUris = new ArrayList(); + for (RedisNode node : this.clusterConfiguration.getClusterNodes()) { + initialUris.add(new RedisURI(node.getHost(), node.getPort(), this.timeout, TimeUnit.MILLISECONDS)); + } + + RedisClusterClient clusterClient = new RedisClusterClient(initialUris); + + this.clusterCommandExecutor = new ClusterCommandExecutor( + new LettuceClusterConnection.LettuceClusterTopologyProvider(clusterClient), + new LettuceClusterConnection.LettuceClusterNodeResourceProvider(clusterClient), EXCEPTION_TRANSLATION); + + return clusterClient; + } + if (pool != null) { return pool.getClient(); } @@ -419,8 +501,18 @@ public class LettuceConnectionFactory implements InitializingBean, DisposableBea return sentinelConfiguration != null; } + /** + * @return since 1.7 + */ + public boolean isClusterAware() { + return clusterConfiguration != null; + } + @Override public RedisSentinelConnection getSentinelConnection() { - return new LettuceSentinelConnection(client.connectSentinelAsync()); + if (!(client instanceof RedisClient)) { + throw new InvalidDataAccessResourceUsageException("Unable to connect to senitels using " + client.getClass()); + } + return new LettuceSentinelConnection(((RedisClient) client).connectSentinelAsync()); } } diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java index 4649bd41d..06d641e17 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java @@ -30,8 +30,13 @@ import java.util.Set; import org.springframework.core.convert.converter.Converter; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.connection.DefaultTuple; +import org.springframework.data.redis.connection.RedisClusterNode; +import org.springframework.data.redis.connection.RedisClusterNode.Flag; +import org.springframework.data.redis.connection.RedisClusterNode.LinkState; +import org.springframework.data.redis.connection.RedisClusterNode.SlotRange; import org.springframework.data.redis.connection.RedisListCommands.Position; import org.springframework.data.redis.connection.RedisNode; +import org.springframework.data.redis.connection.RedisNode.NodeType; import org.springframework.data.redis.connection.RedisSentinelConfiguration; import org.springframework.data.redis.connection.RedisServer; import org.springframework.data.redis.connection.RedisZSetCommands.Range.Boundary; @@ -52,6 +57,8 @@ import com.lambdaworks.redis.RedisURI; import com.lambdaworks.redis.ScoredValue; import com.lambdaworks.redis.ScriptOutputType; import com.lambdaworks.redis.SortArgs; +import com.lambdaworks.redis.cluster.models.partitions.Partitions; +import com.lambdaworks.redis.cluster.models.partitions.RedisClusterNode.NodeFlag; import com.lambdaworks.redis.protocol.LettuceCharsets; /** @@ -78,6 +85,8 @@ abstract public class LettuceConverters extends Converters { private static final Converter, Map> BYTES_LIST_TO_MAP; private static final Converter, List> BYTES_LIST_TO_TUPLE_LIST_CONVERTER; private static final Converter> STRING_TO_LIST_OF_CLIENT_INFO = new StringToRedisClientInfoConverter(); + private static final Converter> PARTITIONS_TO_CLUSTER_NODES; + private static Converter CLUSTER_NODE_TO_CLUSTER_NODE_CONVERTER; public static final byte[] PLUS_BYTES; public static final byte[] MINUS_BYTES; @@ -199,6 +208,74 @@ abstract public class LettuceConverters extends Converters { } }; + PARTITIONS_TO_CLUSTER_NODES = new Converter>() { + + @Override + public List convert(Partitions source) { + + if (source == null) { + return Collections.emptyList(); + } + List nodes = new ArrayList(); + for (com.lambdaworks.redis.cluster.models.partitions.RedisClusterNode node : source.getPartitions()) { + nodes.add(CLUSTER_NODE_TO_CLUSTER_NODE_CONVERTER.convert(node)); + } + + return nodes; + }; + + }; + + CLUSTER_NODE_TO_CLUSTER_NODE_CONVERTER = new Converter() { + + @Override + public RedisClusterNode convert(com.lambdaworks.redis.cluster.models.partitions.RedisClusterNode source) { + + Set flags = parseFlags(source.getFlags()); + + return RedisClusterNode.newRedisClusterNode().listeningAt(source.getUri().getHost(), source.getUri().getPort()) + .withId(source.getNodeId()).promotedAs(flags.contains(Flag.MASTER) ? NodeType.MASTER : NodeType.SLAVE) + .serving(new SlotRange(source.getSlots())).withFlags(flags) + .linkState(source.isConnected() ? LinkState.CONNECTED : LinkState.DISCONNECTED) + .slaveOf(source.getSlaveOf()).build(); + } + + private Set parseFlags(Set source) { + + Set flags = new LinkedHashSet(source != null ? source.size() : 8, 1); + for (NodeFlag flag : source) { + switch (flag) { + case NOFLAGS: + flags.add(Flag.NOFLAGS); + break; + case EVENTUAL_FAIL: + flags.add(Flag.PFAIL); + break; + case FAIL: + flags.add(Flag.FAIL); + break; + case HANDSHAKE: + flags.add(Flag.HANDSHAKE); + break; + case MASTER: + flags.add(Flag.MASTER); + break; + case MYSELF: + flags.add(Flag.MYSELF); + break; + case NOADDR: + flags.add(Flag.NOADDR); + break; + case SLAVE: + flags.add(Flag.SLAVE); + break; + } + } + return flags; + } + + }; + PLUS_BYTES = toBytes("+"); MINUS_BYTES = toBytes("-"); POSITIVE_INFINITY_BYTES = toBytes("+inf"); @@ -523,4 +600,18 @@ abstract public class LettuceConverters extends Converters { return toString(buffer.array()); } + public static List partitionsToClusterNodes(Partitions partitions) { + return PARTITIONS_TO_CLUSTER_NODES.convert(partitions); + } + + /** + * @param source + * @return + * @since 1.7 + */ + public static RedisClusterNode toRedisClusterNode( + com.lambdaworks.redis.cluster.models.partitions.RedisClusterNode source) { + return CLUSTER_NODE_TO_CLUSTER_NODE_CONVERTER.convert(source); + } + } diff --git a/src/main/java/org/springframework/data/redis/connection/srp/SrpConnection.java b/src/main/java/org/springframework/data/redis/connection/srp/SrpConnection.java index e689b8fbf..8c592bf8c 100644 --- a/src/main/java/org/springframework/data/redis/connection/srp/SrpConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/srp/SrpConnection.java @@ -38,6 +38,7 @@ import org.springframework.data.redis.connection.AbstractRedisConnection; import org.springframework.data.redis.connection.DataType; import org.springframework.data.redis.connection.FutureResult; import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.connection.RedisNode; import org.springframework.data.redis.connection.RedisPipelineException; import org.springframework.data.redis.connection.RedisSubscribedConnectionException; import org.springframework.data.redis.connection.ReturnType; @@ -2623,4 +2624,22 @@ public class SrpConnection extends AbstractRedisConnection { throw new UnsupportedOperationException("ZRANGEBYLEX is no supported for srp."); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#migrate(byte[], org.springframework.data.redis.connection.RedisNode, int, org.springframework.data.redis.connection.RedisServerCommands.MigrateOption) + */ + @Override + public void migrate(byte[] key, RedisNode target, int dbIndex, MigrateOption option) { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisServerCommands#migrate(byte[], org.springframework.data.redis.connection.RedisNode, int, org.springframework.data.redis.connection.RedisServerCommands.MigrateOption, long) + */ + @Override + public void migrate(byte[] key, RedisNode target, int dbIndex, MigrateOption option, long timeout) { + throw new UnsupportedOperationException(); + } + } diff --git a/src/main/java/org/springframework/data/redis/connection/srp/SrpConnectionFactory.java b/src/main/java/org/springframework/data/redis/connection/srp/SrpConnectionFactory.java index 0841da240..f0926c620 100644 --- a/src/main/java/org/springframework/data/redis/connection/srp/SrpConnectionFactory.java +++ b/src/main/java/org/springframework/data/redis/connection/srp/SrpConnectionFactory.java @@ -24,6 +24,7 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.ExceptionTranslationStrategy; import org.springframework.data.redis.PassThroughExceptionTranslationStrategy; +import org.springframework.data.redis.connection.RedisClusterConnection; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisSentinelConnection; @@ -83,6 +84,15 @@ public class SrpConnectionFactory implements InitializingBean, DisposableBean, R return connection; } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisConnectionFactory#getClusterConnection() + */ + @Override + public RedisClusterConnection getClusterConnection() { + throw new UnsupportedOperationException("Srp does not support Redis Cluster."); + } + public DataAccessException translateExceptionIfPossible(RuntimeException ex) { return EXCEPTION_TRANSLATION.translate(ex); } diff --git a/src/main/java/org/springframework/data/redis/connection/util/ByteArraySet.java b/src/main/java/org/springframework/data/redis/connection/util/ByteArraySet.java new file mode 100644 index 000000000..d17103621 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/util/ByteArraySet.java @@ -0,0 +1,141 @@ +/* + * Copyright 2015 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.util; + +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Set; + +public class ByteArraySet implements Set { + + LinkedHashSet delegate; + + public ByteArraySet() { + this.delegate = new LinkedHashSet(); + } + + public ByteArraySet(Collection values) { + this(); + addAll(values); + } + + public int size() { + return delegate.size(); + } + + public boolean contains(Object o) { + + if (o instanceof byte[]) { + return delegate.contains(new ByteArrayWrapper((byte[]) o)); + } + return delegate.contains(o); + } + + public boolean add(ByteArrayWrapper e) { + return delegate.add(e); + } + + public boolean add(byte[] e) { + return delegate.add(new ByteArrayWrapper(e)); + } + + public boolean containsAll(Collection c) { + + for (Object o : c) { + if (o instanceof byte[]) { + if (!contains(new ByteArrayWrapper((byte[]) o))) { + return false; + } + } else { + if (!contains(o)) { + return false; + } + } + } + return true; + } + + public boolean addAll(Collection c) { + return delegate.addAll(c); + } + + public boolean addAll(Iterable c) { + + for (byte[] o : c) { + add(o); + } + return true; + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public Iterator iterator() { + return delegate.iterator(); + } + + @Override + public Object[] toArray() { + return delegate.toArray(); + } + + @Override + public T[] toArray(T[] a) { + return delegate.toArray(a); + } + + @Override + public boolean remove(Object o) { + + if (o instanceof byte[]) { + delegate.remove(new ByteArrayWrapper((byte[]) o)); + } + return delegate.remove(o); + } + + @Override + public boolean retainAll(Collection c) { + return delegate.retainAll(c); + } + + @Override + public boolean removeAll(Collection c) { + + for (Object o : c) { + remove(o); + } + return true; + } + + @Override + public void clear() { + delegate.clear(); + } + + public Set asRawSet() { + + Set result = new LinkedHashSet(); + for (ByteArrayWrapper wrapper : delegate) { + result.add(wrapper.getArray()); + } + return result; + } + +} diff --git a/src/main/java/org/springframework/data/redis/core/AbstractOperations.java b/src/main/java/org/springframework/data/redis/core/AbstractOperations.java index e2dc42b99..5611da96b 100644 --- a/src/main/java/org/springframework/data/redis/core/AbstractOperations.java +++ b/src/main/java/org/springframework/data/redis/core/AbstractOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2014 the original author or authors. + * Copyright 2011-2015 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. @@ -16,6 +16,7 @@ package org.springframework.data.redis.core; import java.util.Collection; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -29,6 +30,7 @@ import org.springframework.data.redis.core.ZSetOperations.TypedTuple; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.SerializationUtils; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; /** * Internal base class used by various RedisTemplate XXXOperations implementations. @@ -288,6 +290,23 @@ abstract class AbstractOperations { return (K) keySerializer().deserialize(value); } + /** + * @param keys + * @return + * @since 1.7 + */ + Set deserializeKeys(Set keys) { + + if (CollectionUtils.isEmpty(keys)) { + return Collections.emptySet(); + } + Set result = new LinkedHashSet(keys.size()); + for (byte[] key : keys) { + result.add(deserializeKey(key)); + } + return result; + } + @SuppressWarnings("unchecked") V deserializeValue(byte[] value) { if (valueSerializer() == null) { diff --git a/src/main/java/org/springframework/data/redis/core/ClusterOperations.java b/src/main/java/org/springframework/data/redis/core/ClusterOperations.java new file mode 100644 index 000000000..3337bfd3e --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/ClusterOperations.java @@ -0,0 +1,145 @@ +/* + * Copyright 2015 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.core; + +import java.util.Collection; +import java.util.Set; + +import org.springframework.data.redis.connection.RedisClusterNode; +import org.springframework.data.redis.connection.RedisClusterNode.SlotRange; +import org.springframework.data.redis.connection.RedisConnection; + +/** + * Redis operations for cluster specific operations. + * + * @author Christoph Strobl + * @since 1.7 + */ +public interface ClusterOperations { + + /** + * Get all keys located at given node. + * + * @param node must not be {@literal null}. + * @param pattern + * @return never {@literal null}. + * @see RedisConnection#keys(byte[]) + */ + Set keys(RedisClusterNode node, K pattern); + + /** + * Ping the given node; + * + * @param node must not be {@literal null}. + * @return + * @see RedisConnection#ping() + */ + String ping(RedisClusterNode node); + + /** + * Get a random key from the range served by the given node. + * + * @param node must not be {@literal null}. + * @return + * @see RedisConnection#randomKey() + */ + K randomKey(RedisClusterNode node); + + /** + * Add slots to given node; + * + * @param node must not be {@literal null}. + * @param slots must not be {@literal null}. + */ + void addSlots(RedisClusterNode node, int... slots); + + /** + * Add slots in {@link SlotRange} to given node. + * + * @param node must not be {@literal null}. + * @param range must not be {@literal null}. + */ + void addSlots(RedisClusterNode node, SlotRange range); + + /** + * tart an {@literal Append Only File} rewrite process on given node. + * + * @param node must not be {@literal null}. + * @see RedisConnection#bgReWriteAof() + */ + void bgReWriteAof(RedisClusterNode node); + + /** + * Start background saving of db on given node. + * + * @param node must not be {@literal null}. + * @see RedisConnection#bgSave() + */ + void bgSave(RedisClusterNode node); + + /** + * Add the node to cluster. + * + * @param node must not be {@literal null}. + */ + void meet(RedisClusterNode node); + + /** + * Remove the node from the cluster. + * + * @param node must not be {@literal null}. + */ + void forget(RedisClusterNode node); + + /** + * Flush db on node. + * + * @param node must not be {@literal null}. + * @see RedisConnection#flushDb() + */ + void flushDb(RedisClusterNode node); + + /** + * @param node must not be {@literal null}. + * @return + */ + Collection getSlaves(RedisClusterNode node); + + /** + * Synchronous save current db snapshot on server. + * + * @param node must not be {@literal null}. + * @see RedisConnection#save() + */ + void save(RedisClusterNode node); + + /** + * Shutdown given node. + * + * @param node must not be {@literal null}. + * @see RedisConnection#shutdown() + */ + void shutdown(RedisClusterNode node); + + /** + * Move slot assignment from one source to target node and copy keys associated with the slot. + * + * @param source must not be {@literal null}. + * @param slot + * @param target must not be {@literal null}. + */ + void reshard(RedisClusterNode source, int slot, RedisClusterNode target); +} diff --git a/src/main/java/org/springframework/data/redis/core/DefaultClusterOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultClusterOperations.java new file mode 100644 index 000000000..755a94bbc --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/DefaultClusterOperations.java @@ -0,0 +1,336 @@ +/* + * Copyright 2015 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.core; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import org.springframework.dao.DataAccessException; +import org.springframework.data.redis.connection.RedisClusterCommands.AddSlots; +import org.springframework.data.redis.connection.RedisClusterConnection; +import org.springframework.data.redis.connection.RedisClusterNode; +import org.springframework.data.redis.connection.RedisClusterNode.SlotRange; +import org.springframework.data.redis.connection.RedisServerCommands.MigrateOption; +import org.springframework.util.Assert; + +/** + * Default {@link ClusterOperations} implementation. + * + * @author Christoph Strobl + * @since 1.7 + * @param + * @param + */ +public class DefaultClusterOperations extends AbstractOperations implements ClusterOperations { + + private final RedisTemplate template; + + /** + * Creates new {@link DefaultClusterOperations} delegating to the given {@link RedisTemplate}. + * + * @param template must not be {@literal null}. + */ + public DefaultClusterOperations(RedisTemplate template) { + + super(template); + this.template = template; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.RedisClusterOperations#keys(org.springframework.data.redis.connection.RedisNode, byte[]) + */ + @Override + public Set keys(final RedisClusterNode node, final K pattern) { + + Assert.notNull(node, "ClusterNode must not be null."); + + return execute(new RedisClusterCallback>() { + + @Override + public Set doInRedis(RedisClusterConnection connection) throws DataAccessException { + return deserializeKeys(connection.keys(node, rawKey(pattern))); + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.RedisClusterOperations#randomKey(org.springframework.data.redis.connection.RedisNode) + */ + @Override + public K randomKey(final RedisClusterNode node) { + + Assert.notNull(node, "ClusterNode must not be null."); + + return execute(new RedisClusterCallback() { + + @Override + public K doInRedis(RedisClusterConnection connection) throws DataAccessException { + return deserializeKey(connection.randomKey(node)); + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.RedisClusterOperations#ping(org.springframework.data.redis.connection.RedisNode) + */ + @Override + public String ping(final RedisClusterNode node) { + + Assert.notNull(node, "ClusterNode must not be null."); + + return execute(new RedisClusterCallback() { + + @Override + public String doInRedis(RedisClusterConnection connection) throws DataAccessException { + return connection.ping(node); + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.RedisClusterOperations#addSlots(org.springframework.data.redis.connection.RedisClusterNode, int[]) + */ + @Override + public void addSlots(final RedisClusterNode node, final int... slots) { + + Assert.notNull(node, "ClusterNode must not be null."); + + execute(new RedisClusterCallback() { + + @Override + public Void doInRedis(RedisClusterConnection connection) throws DataAccessException { + connection.clusterAddSlots(node, slots); + return null; + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.RedisClusterOperations#addSlots(org.springframework.data.redis.connection.RedisClusterNode, org.springframework.data.redis.connection.RedisClusterNode.SlotRange) + */ + @Override + public void addSlots(RedisClusterNode node, SlotRange range) { + + Assert.notNull(node, "ClusterNode must not be null."); + Assert.notNull(range, "Range must not be null."); + + addSlots(node, range.getSlotsArray()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.RedisClusterOperations#bgReWriteAof(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public void bgReWriteAof(final RedisClusterNode node) { + + Assert.notNull(node, "ClusterNode must not be null."); + + execute(new RedisClusterCallback() { + + @Override + public Void doInRedis(RedisClusterConnection connection) throws DataAccessException { + connection.bgReWriteAof(node); + return null; + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.RedisClusterOperations#bgSave(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public void bgSave(final RedisClusterNode node) { + + Assert.notNull(node, "ClusterNode must not be null."); + + execute(new RedisClusterCallback() { + + @Override + public Void doInRedis(RedisClusterConnection connection) throws DataAccessException { + connection.bgSave(node); + return null; + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.RedisClusterOperations#meet(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public void meet(final RedisClusterNode node) { + + Assert.notNull(node, "ClusterNode must not be null."); + + execute(new RedisClusterCallback() { + + @Override + public Void doInRedis(RedisClusterConnection connection) throws DataAccessException { + connection.clusterMeet(node); + return null; + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.RedisClusterOperations#forget(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public void forget(final RedisClusterNode node) { + + Assert.notNull(node, "ClusterNode must not be null."); + + execute(new RedisClusterCallback() { + + @Override + public Void doInRedis(RedisClusterConnection connection) throws DataAccessException { + connection.clusterForget(node); + return null; + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.RedisClusterOperations#flushDb(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public void flushDb(final RedisClusterNode node) { + + Assert.notNull(node, "ClusterNode must not be null."); + + execute(new RedisClusterCallback() { + + @Override + public Void doInRedis(RedisClusterConnection connection) throws DataAccessException { + connection.flushDb(node); + return null; + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.RedisClusterOperations#getSlaves(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public Collection getSlaves(final RedisClusterNode node) { + + Assert.notNull(node, "ClusterNode must not be null."); + + return execute(new RedisClusterCallback>() { + + @Override + public Collection doInRedis(RedisClusterConnection connection) throws DataAccessException { + return connection.clusterGetSlaves(node); + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.RedisClusterOperations#save(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public void save(final RedisClusterNode node) { + + Assert.notNull(node, "ClusterNode must not be null."); + + execute(new RedisClusterCallback() { + + @Override + public Void doInRedis(RedisClusterConnection connection) throws DataAccessException { + connection.save(node); + return null; + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.RedisClusterOperations#shutdown(org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public void shutdown(final RedisClusterNode node) { + + Assert.notNull(node, "ClusterNode must not be null."); + + execute(new RedisClusterCallback() { + + @Override + public Void doInRedis(RedisClusterConnection connection) throws DataAccessException { + connection.shutdown(node); + return null; + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ClusterOperations#reshard(org.springframework.data.redis.connection.RedisClusterNode, int, org.springframework.data.redis.connection.RedisClusterNode) + */ + @Override + public void reshard(final RedisClusterNode source, final int slot, final RedisClusterNode target) { + + Assert.notNull(source, "Source node must not be null."); + Assert.notNull(target, "Target node must not be null."); + + execute(new RedisClusterCallback() { + + @Override + public Void doInRedis(RedisClusterConnection connection) throws DataAccessException { + + connection.clusterSetSlot(target, slot, AddSlots.IMPORTING); + connection.clusterSetSlot(source, slot, AddSlots.MIGRATING); + List keys = connection.clusterGetKeysInSlot(slot, Integer.MAX_VALUE); + + for (byte[] key : keys) { + connection.migrate(key, source, 0, MigrateOption.COPY); + } + connection.clusterSetSlot(target, slot, AddSlots.NODE); + return null; + } + }); + } + + /** + * Executed wrapped command upon {@link RedisClusterConnection}. + * + * @param callback + * @return + */ + public T execute(RedisClusterCallback callback) { + + Assert.notNull(callback, "ClusterCallback must not be null!"); + + RedisClusterConnection connection = template.getConnectionFactory().getClusterConnection(); + + try { + return callback.doInRedis(connection); + } finally { + connection.close(); + } + } +} diff --git a/src/main/java/org/springframework/data/redis/core/RedisClusterCallback.java b/src/main/java/org/springframework/data/redis/core/RedisClusterCallback.java new file mode 100644 index 000000000..327913446 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/RedisClusterCallback.java @@ -0,0 +1,39 @@ +/* + * Copyright 2015 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.core; + +import org.springframework.dao.DataAccessException; +import org.springframework.data.redis.connection.RedisClusterConnection; + +/** + * Callback interface for low level operations executed against a clustered Redis environment. + * + * @author Christoph Strobl + * @since 1.7 + * @param + */ +public interface RedisClusterCallback { + + /** + * Gets called by {@link RedisClusterTemplate} with an active Redis connection. Does not need to care about activating + * or closing the connection or handling exceptions. + * + * @param connection never {@literal null}. + * @return + * @throws DataAccessException + */ + T doInRedis(RedisClusterConnection connection) throws DataAccessException; +} diff --git a/src/main/java/org/springframework/data/redis/core/RedisOperations.java b/src/main/java/org/springframework/data/redis/core/RedisOperations.java index 8cd506d87..446df60da 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisOperations.java +++ b/src/main/java/org/springframework/data/redis/core/RedisOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2014 the original author or authors. + * Copyright 2011-2015 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. @@ -283,6 +283,14 @@ public interface RedisOperations { */ BoundHashOperations boundHashOps(K key); + /** + * Returns the cluster specific operations interface. + * + * @return never {@literal null}. + * @since 1.7 + */ + ClusterOperations opsForCluster(); + List sort(SortQuery query); List sort(SortQuery query, RedisSerializer resultSerializer); diff --git a/src/main/java/org/springframework/data/redis/core/RedisTemplate.java b/src/main/java/org/springframework/data/redis/core/RedisTemplate.java index 613aad619..933343ca5 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisTemplate.java +++ b/src/main/java/org/springframework/data/redis/core/RedisTemplate.java @@ -1016,6 +1016,14 @@ public class RedisTemplate extends RedisAccessor implements RedisOperation return new DefaultHashOperations(this); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.RedisOperations#opsForCluster() + */ + public ClusterOperations opsForCluster() { + return new DefaultClusterOperations(this); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.core.RedisOperations#killClient(java.lang.Object) diff --git a/src/test/java/org/springframework/data/redis/RedisTestProfileValueSource.java b/src/test/java/org/springframework/data/redis/RedisTestProfileValueSource.java index 0e1a6bb12..9168bee93 100644 --- a/src/test/java/org/springframework/data/redis/RedisTestProfileValueSource.java +++ b/src/test/java/org/springframework/data/redis/RedisTestProfileValueSource.java @@ -52,30 +52,34 @@ public class RedisTestProfileValueSource implements ProfileValueSource { Version version = fallbackVersion; - Jedis jedis = new Jedis(SettingsUtils.getHost(), SettingsUtils.getPort(), 100); + Jedis jedis = null; try { - jedis.connect(); + jedis = new Jedis(SettingsUtils.getHost(), SettingsUtils.getPort(), 100); + // jedis.connect(); String info = jedis.info(); String versionString = (String) JedisConverters.stringToProps().convert(info).get("redis_version"); version = RedisVersionUtils.parseVersion(versionString); } finally { - try { - // force socket to be closed - jedis.getClient().quit(); - jedis.getClient().getSocket().close(); + if (jedis != null) { try { - // need to wait a bit - Thread.sleep(100); - } catch (InterruptedException e) { - // just ignore it + // force socket to be closed + jedis.getClient().quit(); + jedis.getClient().getSocket().close(); + try { + // need to wait a bit + Thread.sleep(5); + } catch (InterruptedException e) { + // just ignore it + Thread.interrupted(); + } + } catch (IOException e1) { + // ignore as well } - } catch (IOException e1) { - // ignore as well + jedis.close(); } - jedis.close(); } return version; diff --git a/src/test/java/org/springframework/data/redis/connection/ClusterCommandExecutorUnitTests.java b/src/test/java/org/springframework/data/redis/connection/ClusterCommandExecutorUnitTests.java new file mode 100644 index 000000000..95358dd88 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/ClusterCommandExecutorUnitTests.java @@ -0,0 +1,374 @@ +/* + * Copyright 2015 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.Matchers.*; +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 java.util.Map; + +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.runners.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.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 + */ +@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 UNKNOWN_CLUSTER_NODE = new RedisClusterNode("8.8.8.8", 7379, null); + + private ClusterCommandExecutor executor; + + private static final ConnectionCommandCallback COMMAND_CALLBACK = new ConnectionCommandCallback() { + + @Override + public String doInCluster(Connection connection) { + return connection.theWheelWeavesAsTheWheelWills(); + } + }; + + private static final Converter exceptionConverter = new Converter() { + + @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 MULTIKEY_CALLBACK = new MultiKeyConnectionCommandCallback() { + + @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(); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void executeCommandOnSingleNodeShouldBeExecutedCorrectly() { + + executor.executeCommandOnSingleNode(COMMAND_CALLBACK, CLUSTER_NODE_2); + + verify(con2, times(1)).theWheelWeavesAsTheWheelWills(); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = IllegalArgumentException.class) + public void executeCommandOnSingleNodeShouldThrowExceptionWhenNodeIsNull() { + executor.executeCommandOnSingleNode(COMMAND_CALLBACK, null); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = IllegalArgumentException.class) + public void executeCommandOnSingleNodeShouldThrowExceptionWhenCommandCallbackIsNull() { + executor.executeCommandOnSingleNode(null, CLUSTER_NODE_1); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = IllegalArgumentException.class) + public void executeCommandOnSingleNodeShouldThrowExceptionWhenNodeIsUnknown() { + executor.executeCommandOnSingleNode(COMMAND_CALLBACK, UNKNOWN_CLUSTER_NODE); + } + + /** + * @see DATAREDIS-315 + */ + @Test + 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(); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void executeCommandOnAllNodesShouldExecuteCommandOnEveryKnwonClusterNode() { + + 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(); + } + + /** + * @see DATAREDIS-315 + */ + @Test + 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(); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void executeCommandAsyncOnNodesShouldCollectResultsCorrectly() { + + when(con1.theWheelWeavesAsTheWheelWills()).thenReturn("rand"); + when(con2.theWheelWeavesAsTheWheelWills()).thenReturn("mat"); + when(con3.theWheelWeavesAsTheWheelWills()).thenReturn("perrin"); + + Map result = executor.executeCommandOnAllNodes(COMMAND_CALLBACK); + + assertThat(result.keySet(), hasItems(CLUSTER_NODE_1, CLUSTER_NODE_2, CLUSTER_NODE_3)); + assertThat(result.values(), hasItems("rand", "mat", "perrin")); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void executeMultikeyCommandShouldRunCommandAcrossCluster() { + + // key-1 and key-9 map both to node1 + ArgumentCaptor captor = ArgumentCaptor.forClass(byte[].class); + when(con1.bloodAndAshes(captor.capture())).thenReturn("rand"); + + when(con2.bloodAndAshes(any(byte[].class))).thenReturn("mat"); + when(con3.bloodAndAshes(any(byte[].class))).thenReturn("perrin"); + + Map result = executor.executeMuliKeyCommand( + MULTIKEY_CALLBACK, + new HashSet(Arrays.asList("key-1".getBytes(), "key-2".getBytes(), "key-3".getBytes(), + "key-9".getBytes()))); + + assertThat(result.keySet(), hasItems(CLUSTER_NODE_1, CLUSTER_NODE_2, CLUSTER_NODE_3)); + assertThat(result.values(), hasItems("rand", "mat", "perrin")); + + // check that 2 keys have been routed to node1 + assertThat(captor.getAllValues().size(), is(2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + 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(); + } + + /** + * @see DATAREDIS-315 + */ + @Test + 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(); + } + + /** + * @see DATAREDIS-315 + */ + @Test + 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(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 extends ClusterCommandCallback { + + } + + static interface MultiKeyConnectionCommandCallback extends MultiKeyClusterCommandCallback { + + } + + 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; + } + + } + +} diff --git a/src/test/java/org/springframework/data/redis/connection/ClusterConnectionTests.java b/src/test/java/org/springframework/data/redis/connection/ClusterConnectionTests.java new file mode 100644 index 000000000..fef7bfb58 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/ClusterConnectionTests.java @@ -0,0 +1,896 @@ +/* + * Copyright 2015 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; + +public interface ClusterConnectionTests { + + /** + * @see DATAREDIS-315 + */ + void shouldAllowSettingAndGettingValues(); + + /** + * @see DATAREDIS-315 + */ + void delShouldRemoveSingleKeyCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void delShouldRemoveMultipleKeysCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void delShouldRemoveMultipleKeysOnSameSlotCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void typeShouldReadKeyTypeCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void keysShouldReturnAllKeys(); + + /** + * @see DATAREDIS-315 + */ + void keysShouldReturnAllKeysForSpecificNode(); + + /** + * @see DATAREDIS-315 + */ + void randomKeyShouldReturnCorrectlyWhenKeysAvailable(); + + /** + * @see DATAREDIS-315 + */ + void randomKeyShouldReturnNullWhenNoKeysAvailable(); + + /** + * @see DATAREDIS-315 + */ + void rename(); + + /** + * @see DATAREDIS-315 + */ + void renameSameKeysOnSameSlot(); + + /** + * @see DATAREDIS-315 + */ + void renameNXWhenTargetKeyDoesNotExist(); + + /** + * @see DATAREDIS-315 + */ + void renameNXWhenTargetKeyDoesExist(); + + /** + * @see DATAREDIS-315 + */ + void renameNXWhenOnSameSlot(); + + /** + * @see DATAREDIS-315 + */ + void expireShouldBeSetCorreclty(); + + /** + * @see DATAREDIS-315 + */ + void pExpireShouldBeSetCorreclty(); + + /** + * @see DATAREDIS-315 + */ + void expireAtShouldBeSetCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void pExpireAtShouldBeSetCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void persistShoudRemoveTTL(); + + /** + * @see DATAREDIS-315 + */ + void moveShouldNotBeSupported(); + + /** + * @see DATAREDIS-315 + */ + void dbSizeShouldReturnCummulatedDbSize(); + + /** + * @see DATAREDIS-315 + */ + void dbSizeForSpecificNodeShouldGetNodeDbSize(); + + /** + * @see DATAREDIS-315 + */ + void ttlShouldReturnMinusTwoWhenKeyDoesNotExist(); + + /** + * @see DATAREDIS-315 + */ + void ttlShouldReturnMinusOneWhenKeyDoesNotHaveExpirationSet(); + + /** + * @see DATAREDIS-315 + */ + void ttlShouldReturnValueCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void pTtlShouldReturnMinusTwoWhenKeyDoesNotExist(); + + /** + * @see DATAREDIS-315 + */ + void pTtlShouldReturnMinusOneWhenKeyDoesNotHaveExpirationSet(); + + /** + * @see DATAREDIS-315 + */ + void pTtlShouldReturValueCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void sortShouldReturnValuesCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void sortAndStoreShouldAddSortedValuesValuesCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void sortAndStoreShouldReturnZeroWhenListDoesNotExist(); + + /** + * @see DATAREDIS-315 + */ + void dumpAndRestoreShouldWorkCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void getShouldReturnValueCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void getSetShouldWorkCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void mGetShouldReturnCorrectlyWhenKeysMapToSameSlot(); + + /** + * @see DATAREDIS-315 + */ + void mGetShouldReturnCorrectlyWhenKeysDoNotMapToSameSlot(); + + /** + * @see DATAREDIS-315 + */ + void setShouldSetValueCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void setNxShouldSetValueCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void setNxShouldNotSetValueWhenAlreadyExistsInDBCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void setExShouldSetValueCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void pSetExShouldSetValueCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void mSetShouldWorkWhenKeysMapToSameSlot(); + + /** + * @see DATAREDIS-315 + */ + void mSetShouldWorkWhenKeysDoNotMapToSameSlot(); + + /** + * @see DATAREDIS-315 + */ + void mSetNXShouldReturnTrueIfAllKeysSet(); + + /** + * @see DATAREDIS-315 + */ + void mSetNXShouldReturnFalseIfNotAllKeysSet(); + + /** + * @see DATAREDIS-315 + */ + void mSetNXShouldWorkForOnSameSlotKeys(); + + /** + * @see DATAREDIS-315 + */ + void incrShouldIncreaseValueCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void incrByShouldIncreaseValueCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void incrByFloatShouldIncreaseValueCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void decrShouldDecreaseValueCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void decrByShouldDecreaseValueCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void appendShouldAddValueCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void getRangeShouldReturnValueCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void setRangeShouldWorkCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void getBitShouldWorkCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void setBitShouldWorkCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void bitCountShouldWorkCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void bitCountWithRangeShouldWorkCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void bitOpShouldThrowExceptionWhenKeysDoNotMapToSameSlot(); + + /** + * @see DATAREDIS-315 + */ + void strLenShouldWorkCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void rPushShoultAddValuesCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void lPushShoultAddValuesCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void rPushNXShoultNotAddValuesWhenKeyDoesNotExist(); + + /** + * @see DATAREDIS-315 + */ + void lPushNXShoultNotAddValuesWhenKeyDoesNotExist(); + + /** + * @see DATAREDIS-315 + */ + void lLenShouldCountValuesCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void lRangeShouldGetValuesCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void lTrimShouldTrimListCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void lIndexShouldGetElementAtIndexCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void lInsertShouldAddElementAtPositionCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void lSetShouldSetElementAtPositionCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void lRemShouldRemoveElementAtPositionCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void lPopShouldReturnElementCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void rPopShouldReturnElementCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void blPopShouldPopElementCorectly(); + + /** + * @see DATAREDIS-315 + */ + void blPopShouldPopElementCorectlyWhenKeyOnSameSlot(); + + /** + * @see DATAREDIS-315 + */ + void brPopShouldPopElementCorectly(); + + /** + * @see DATAREDIS-315 + */ + void brPopShouldPopElementCorectlyWhenKeyOnSameSlot(); + + /** + * @see DATAREDIS-315 + */ + void rPopLPushShouldWorkWhenDoNotMapToSameSlot(); + + /** + * @see DATAREDIS-315 + */ + public void rPopLPushShouldWorkWhenKeysOnSameSlot(); + + /** + * @see DATAREDIS-315 + */ + void bRPopLPushShouldWork(); + + /** + * @see DATAREDIS-315 + */ + void bRPopLPushShouldWorkOnSameSlotKeys(); + + /** + * @see DATAREDIS-315 + */ + void sAddShouldAddValueToSetCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void sRemShouldRemoveValueFromSetCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void sPopShouldPopValueFromSetCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void sMoveShouldWorkWhenKeysMapToSameSlot(); + + /** + * @see DATAREDIS-315 + */ + void sMoveShouldWorkWhenKeysDoNotMapToSameSlot(); + + /** + * @see DATAREDIS-315 + */ + void sCardShouldCountValuesInSetCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void sIsMemberShouldReturnTrueIfValueIsMemberOfSet(); + + /** + * @see DATAREDIS-315 + */ + void sIsMemberShouldReturnFalseIfValueIsMemberOfSet(); + + /** + * @see DATAREDIS-315 + */ + void sInterShouldWorkForKeysMappingToSameSlot(); + + /** + * @see DATAREDIS-315 + */ + void sInterShouldWorkForKeysNotMappingToSameSlot(); + + /** + * @see DATAREDIS-315 + */ + void sInterStoreShouldWorkForKeysMappingToSameSlot(); + + /** + * @see DATAREDIS-315 + */ + void sInterStoreShouldWorkForKeysNotMappingToSameSlot(); + + /** + * @see DATAREDIS-315 + */ + void sUnionShouldWorkForKeysMappingToSameSlot(); + + /** + * @see DATAREDIS-315 + */ + void sUnionShouldWorkForKeysNotMappingToSameSlot(); + + /** + * @see DATAREDIS-315 + */ + void sUnionStoreShouldWorkForKeysMappingToSameSlot(); + + /** + * @see DATAREDIS-315 + */ + void sUnionStoreShouldWorkForKeysNotMappingToSameSlot(); + + /** + * @see DATAREDIS-315 + */ + void sDiffShouldWorkWhenKeysMapToSameSlot(); + + /** + * @see DATAREDIS-315 + */ + void sDiffShouldWorkWhenKeysNotMapToSameSlot(); + + /** + * @see DATAREDIS-315 + */ + void sDiffStoreShouldWorkWhenKeysMapToSameSlot(); + + /** + * @see DATAREDIS-315 + */ + void sDiffStoreShouldWorkWhenKeysNotMapToSameSlot(); + + /** + * @see DATAREDIS-315 + */ + void sMembersShouldReturnValuesContainedInSetCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void sRandMamberShouldReturnValueCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void sRandMamberWithCountShouldReturnValueCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void sscanShouldRetrieveAllValuesInSetCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void zAddShouldAddValueWithScoreCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void zRemShouldRemoveValueWithScoreCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void zIncrByShouldIncScoreForValueCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void zRankShouldReturnPositionForValueCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void zRankShouldReturnReversePositionForValueCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void zRangeShouldReturnValuesCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void zRangeWithScoresShouldReturnValuesAndScoreCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void zRangeByScoreShouldReturnValuesCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void zRangeByScoreWithScoresShouldReturnValuesAndScoreCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void zRangeByScoreShouldReturnValuesCorrectlyWhenGivenOffsetAndScore(); + + /** + * @see DATAREDIS-315 + */ + void zRangeByScoreWithScoresShouldReturnValuesCorrectlyWhenGivenOffsetAndScore(); + + /** + * @see DATAREDIS-315 + */ + void zRevRangeShouldReturnValuesCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void zRevRangeWithScoresShouldReturnValuesAndScoreCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void zRevRangeByScoreShouldReturnValuesCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void zRevRangeByScoreWithScoresShouldReturnValuesAndScoreCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void zRevRangeByScoreShouldReturnValuesCorrectlyWhenGivenOffsetAndScore(); + + /** + * @see DATAREDIS-315 + */ + void zRevRangeByScoreWithScoresShouldReturnValuesCorrectlyWhenGivenOffsetAndScore(); + + /** + * @see DATAREDIS-315 + */ + void zCountShouldCountValuesInRange(); + + /** + * @see DATAREDIS-315 + */ + void zCardShouldReturnTotalNumberOfValues(); + + /** + * @see DATAREDIS-315 + */ + void zScoreShouldRetrieveScoreForValue(); + + /** + * @see DATAREDIS-315 + */ + void zRemRangeShouldRemoveValues(); + + /** + * @see DATAREDIS-315 + */ + void zRemRangeByScoreShouldRemoveValues(); + + /** + * @see DATAREDIS-315 + */ + void zUnionStoreShouldWorkForSameSlotKeys(); + + /** + * @see DATAREDIS-315 + */ + void zUnionStoreShouldThrowExceptionWhenKeysDoNotMapToSameSlots(); + + /** + * @see DATAREDIS-315 + */ + void zInterStoreShouldWorkForSameSlotKeys(); + + /** + * @see DATAREDIS-315 + */ + void zInterStoreShouldThrowExceptionWhenKeysDoNotMapToSameSlots(); + + /** + * @see DATAREDIS-315 + */ + void hSetShouldSetValueCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void hSetNXShouldSetValueCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void hSetNXShouldNotSetValueWhenAlreadyExists(); + + /** + * @see DATAREDIS-315 + */ + void hGetShouldRetrieveValueCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void hMGetShouldRetrieveValueCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void hMSetShouldAddValuesCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void hIncrByShouldIncreaseFieldCorretly(); + + /** + * @see DATAREDIS-315 + */ + void hIncrByFloatShouldIncreaseFieldCorretly(); + + /** + * @see DATAREDIS-315 + */ + void hExistsShouldReturnPresenceOfFieldCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void hDelShouldRemoveFieldsCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void hLenShouldRetrieveSizeCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void hKeysShouldRetrieveKeysCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void hValsShouldRetrieveValuesCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void hGetAllShouldRetrieveEntriesCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void multiShouldThrowException(); + + /** + * @see DATAREDIS-315 + */ + void execShouldThrowException(); + + /** + * @see DATAREDIS-315 + */ + void discardShouldThrowException(); + + /** + * @see DATAREDIS-315 + */ + void watchShouldThrowException(); + + /** + * @see DATAREDIS-315 + */ + void unwatchShouldThrowException(); + + /** + * @see DATAREDIS-315 + */ + void selectShouldAllowSelectionOfDBIndexZero(); + + /** + * @see DATAREDIS-315 + */ + void selectShouldThrowExceptionWhenSelectingNonZeroDbIndex(); + + /** + * @see DATAREDIS-315 + */ + void echoShouldReturnInputCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void pingShouldRetrunPongForExistingNode(); + + /** + * @see DATAREDIS-315 + */ + void pingShouldRetrunPong(); + + /** + * @see DATAREDIS-315 + */ + void pingShouldThrowExceptionWhenNodeNotKnownToCluster(); + + /** + * @see DATAREDIS-315 + */ + void flushDbShouldFlushAllClusterNodes(); + + /** + * @see DATAREDIS-315 + */ + void flushDbOnSingleNodeShouldFlushOnlyGivenNodesDb(); + + /** + * @see DATAREDIS-315 + */ + void zRangeByLexShouldReturnResultCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void infoShouldCollectionInfoFromAllClusterNodes(); + + /** + * @see DATAREDIS-315 + */ + void clientListShouldGetInfosForAllClients(); + + /** + * @see DATAREDIS-315 + */ + void getClusterNodeForKeyShouldReturnNodeCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void countKeysShouldReturnNumberOfKeysInSlot(); + + /** + * @see DATAREDIS-315 + */ + + void pfAddShouldAddValuesCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void pfCountShouldAllowCountingOnSingleKey(); + + /** + * @see DATAREDIS-315 + */ + void pfCountShouldAllowCountingOnSameSlotKeys(); + + /** + * @see DATAREDIS-315 + */ + void pfCountShouldThrowErrorCountingOnDifferentSlotKeys(); + + /** + * @see DATAREDIS-315 + */ + void pfMergeShouldWorkWhenAllKeysMapToSameSlot(); + + /** + * @see DATAREDIS-315 + */ + public void pfMergeShouldThrowErrorOnDifferentSlotKeys(); + + /** + * @see DATAREDIS-315 + */ + void infoShouldCollectInfoForSpecificNode(); + + /** + * @see DATAREDIS-315 + */ + void infoShouldCollectInfoForSpecificNodeAndSection(); + + /** + * @see DATAREDIS-315 + */ + void getConfigShouldLoadCumulatedConfiguration(); + + /** + * @see DATAREDIS-315 + */ + void getConfigShouldLoadConfigurationOfSpecificNode(); + + /** + * @see DATAREDIS-315 + */ + void clusterGetSlavesShouldReturnSlaveCorrectly(); + + /** + * @see DATAREDIS-315 + */ + void clusterGetMasterSlaveMapShouldListMastersAndSlavesCorrectly(); + +} diff --git a/src/test/java/org/springframework/data/redis/connection/ClusterSlotHashUtilsTests.java b/src/test/java/org/springframework/data/redis/connection/ClusterSlotHashUtilsTests.java new file mode 100644 index 000000000..b0322d005 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/ClusterSlotHashUtilsTests.java @@ -0,0 +1,137 @@ +/* + * Copyright 2015 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.junit.Assert.*; + +import java.io.IOException; +import java.util.Collections; +import java.util.Random; + +import org.junit.ClassRule; +import org.junit.Test; +import org.springframework.data.redis.test.util.RedisClusterRule; +import org.springframework.util.StringUtils; + +import redis.clients.jedis.HostAndPort; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisCluster; +import redis.clients.jedis.JedisPool; + +/** + * @author Christoph Strobl + */ +public class ClusterSlotHashUtilsTests { + + public static @ClassRule RedisClusterRule requiresCluster = new RedisClusterRule(); + + @Test + public void localCalculationShoudMatchServers() throws IOException { + + JedisCluster cluster = null; + try { + cluster = new JedisCluster(Collections.singleton(new HostAndPort("127.0.0.1", 7379))); + + JedisPool pool = cluster.getClusterNodes().values().iterator().next(); + Jedis jedis = pool.getResource(); + for (int i = 0; i < 10000; i++) { + + String key = randomString(); + int slot = ClusterSlotHashUtil.calculateSlot(key); + Long serverSlot = jedis.clusterKeySlot(key); + + assertEquals( + String.format("Expected slot for key '%s' to be %s but server calculated %s.", key, slot, serverSlot), + serverSlot.intValue(), slot); + + } + pool.returnResource(jedis); + } finally { + if (cluster != null) { + cluster.close(); + } + } + + } + + @Test + public void localCalculationShoudMatchServersForPrefixedKeys() throws IOException { + + JedisCluster cluster = null; + try { + cluster = new JedisCluster(Collections.singleton(new HostAndPort("127.0.0.1", 7379))); + + JedisPool pool = cluster.getClusterNodes().values().iterator().next(); + Jedis jedis = pool.getResource(); + for (int i = 0; i < 10000; i++) { + + String slotPrefix = "{" + randomString() + "}"; + + String key1 = slotPrefix + "." + randomString(); + String key2 = slotPrefix + "." + randomString(); + + int slot1 = ClusterSlotHashUtil.calculateSlot(key1); + int slot2 = ClusterSlotHashUtil.calculateSlot(key2); + + assertEquals(String.format("Expected slot for prefixed keys '%s' and '%s' to be %s but was %s.", key1, key2, + slot1, slot2), slot1, slot2); + + Long serverSlot1 = jedis.clusterKeySlot(key1); + Long serverSlot2 = jedis.clusterKeySlot(key2); + + assertEquals( + String.format("Expected slot for key '%s' to be %s but server calculated %s.", key1, slot1, serverSlot1), + serverSlot1.intValue(), slot1); + assertEquals( + String.format("Expected slot for key '%s' to be %s but server calculated %s.", key2, slot2, serverSlot2), + serverSlot1.intValue(), slot1); + + } + pool.returnResource(jedis); + } finally { + if (cluster != null) { + cluster.close(); + } + } + } + + /** + * Generate random string using ascii chars {@code ' ' (space)} to {@code 'z'}. Explicitly skipping { and }. + * + * @return + */ + private String randomString() { + + int leftLimit = 32; // letter ' ' (space) + int rightLimit = 122; // letter 'z' (tilde) + int targetStringLength = 0; + while (targetStringLength == 0) { + targetStringLength = new Random().nextInt(100); + } + + StringBuilder buffer = new StringBuilder(targetStringLength); + for (int i = 0; i < targetStringLength; i++) { + int randomLimitedInt = leftLimit + (int) (new Random().nextFloat() * (rightLimit - leftLimit)); + buffer.append((char) randomLimitedInt); + } + + String result = buffer.toString(); + if (StringUtils.hasText(result)) { + return result; + } + return randomString(); + } +} diff --git a/src/test/java/org/springframework/data/redis/connection/ClusterTestVariables.java b/src/test/java/org/springframework/data/redis/connection/ClusterTestVariables.java new file mode 100644 index 000000000..4a61ad870 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/ClusterTestVariables.java @@ -0,0 +1,61 @@ +/* + * Copyright 2015 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 org.springframework.data.redis.connection.RedisNode.NodeType; + +/** + * @author Christoph Strobl + */ +public abstract class ClusterTestVariables { + + public static final String KEY_1 = "key1"; + public static final String KEY_2 = "key2"; + public static final String KEY_3 = "key3"; + + public static final String VALUE_1 = "value1"; + public static final String VALUE_2 = "value2"; + public static final String VALUE_3 = "value3"; + + public static final String SAME_SLOT_KEY_1 = "key2660"; + public static final String SAME_SLOT_KEY_2 = "key7112"; + public static final String SAME_SLOT_KEY_3 = "key8885"; + + public static final String CLUSTER_HOST = "127.0.0.1"; + public static final int MASTER_NODE_1_PORT = 7379; + public static final int MASTER_NODE_2_PORT = 7380; + public static final int MASTER_NODE_3_PORT = 7381; + public static final int SLAVEOF_NODE_1_PORT = 7382; + + public static final String MASTER_NODE_1_ID = "ef570f86c7b1a953846668debc177a3a16733420"; + public static final String MASTER_NODE_2_ID = "0f2ee5df45d18c50aca07228cc18b1da96fd5e84"; + public static final String MASTER_NODE_3_ID = "3b9b8192a874fa8f1f09dbc0ee20afab5738eee7"; + public static final String SLAVEOF_NODE_1_ID = "b8b5ee73b1d1997abff694b3fe8b2397d2138b6d"; + + public static final RedisClusterNode CLUSTER_NODE_1 = RedisClusterNode.newRedisClusterNode() + .listeningAt(CLUSTER_HOST, MASTER_NODE_1_PORT).withId(MASTER_NODE_1_ID).promotedAs(NodeType.MASTER).build(); + public static final RedisClusterNode CLUSTER_NODE_2 = RedisClusterNode.newRedisClusterNode() + .listeningAt(CLUSTER_HOST, MASTER_NODE_2_PORT).withId(MASTER_NODE_2_ID).promotedAs(NodeType.MASTER).build(); + public static final RedisClusterNode CLUSTER_NODE_3 = RedisClusterNode.newRedisClusterNode() + .listeningAt(CLUSTER_HOST, MASTER_NODE_3_PORT).withId(MASTER_NODE_3_ID).promotedAs(NodeType.MASTER).build(); + public static final RedisClusterNode SLAVE_OF_NODE_1 = RedisClusterNode.newRedisClusterNode() + .listeningAt(CLUSTER_HOST, SLAVEOF_NODE_1_PORT).withId(SLAVEOF_NODE_1_ID).promotedAs(NodeType.SLAVE).build(); + + public static final RedisClusterNode UNKNOWN_CLUSTER_NODE = new RedisClusterNode("8.8.8.8", 6379); + + private ClusterTestVariables() {} + +} diff --git a/src/test/java/org/springframework/data/redis/connection/RedisClusterConfigurationUnitTests.java b/src/test/java/org/springframework/data/redis/connection/RedisClusterConfigurationUnitTests.java new file mode 100644 index 000000000..37df50ec3 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/RedisClusterConfigurationUnitTests.java @@ -0,0 +1,156 @@ +/* + * Copyright 2015 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.hamcrest.core.IsNull.*; +import static org.junit.Assert.*; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; + +import org.junit.Test; +import org.springframework.core.env.PropertySource; +import org.springframework.mock.env.MockPropertySource; +import org.springframework.util.StringUtils; + +/** + * @author Christoph Strobl + */ +public class RedisClusterConfigurationUnitTests { + + static final String HOST_AND_PORT_1 = "127.0.0.1:123"; + static final String HOST_AND_PORT_2 = "localhost:456"; + static final String HOST_AND_PORT_3 = "localhost:789"; + static final String HOST_AND_NO_PORT = "localhost"; + + /** + * @see DATAREDIS-315 + */ + @Test + public void shouldCreateRedisClusterConfigurationCorrectly() { + + RedisClusterConfiguration config = new RedisClusterConfiguration(Collections.singleton(HOST_AND_PORT_1)); + + assertThat(config.getClusterNodes().size(), is(1)); + assertThat(config.getClusterNodes(), hasItems(new RedisNode("127.0.0.1", 123))); + assertThat(config.getClusterTimeout(), nullValue()); + assertThat(config.getMaxRedirects(), nullValue()); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void shouldCreateRedisClusterConfigurationCorrectlyGivenMultipleHostAndPortStrings() { + + RedisClusterConfiguration config = new RedisClusterConfiguration(new HashSet(Arrays.asList(HOST_AND_PORT_1, + HOST_AND_PORT_2, HOST_AND_PORT_3))); + + assertThat(config.getClusterNodes().size(), is(3)); + assertThat(config.getClusterNodes(), + hasItems(new RedisNode("127.0.0.1", 123), new RedisNode("localhost", 456), new RedisNode("localhost", 789))); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = IllegalArgumentException.class) + public void shouldThrowExecptionOnInvalidHostAndPortString() { + new RedisClusterConfiguration(Collections.singleton(HOST_AND_NO_PORT)); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = IllegalArgumentException.class) + public void shouldThrowExceptionWhenListOfHostAndPortIsNull() { + new RedisClusterConfiguration(Collections. singleton(null)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void shouldNotFailWhenListOfHostAndPortIsEmpty() { + + RedisClusterConfiguration config = new RedisClusterConfiguration(Collections. emptySet()); + + assertThat(config.getClusterNodes().size(), is(0)); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = IllegalArgumentException.class) + public void shouldThrowExceptionGivenNullPropertySource() { + new RedisClusterConfiguration((PropertySource) null); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void shouldNotFailWhenGivenPropertySourceNotContainingRelevantProperties() { + + RedisClusterConfiguration config = new RedisClusterConfiguration(new MockPropertySource()); + + assertThat(config.getMaxRedirects(), nullValue()); + assertThat(config.getClusterTimeout(), nullValue()); + assertThat(config.getClusterNodes().size(), is(0)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void shouldBeCreatedCorrecltyGivenValidPropertySourceWithSingleHostPort() { + + MockPropertySource propertySource = new MockPropertySource(); + propertySource.setProperty("spring.redis.cluster.timeout", "10"); + propertySource.setProperty("spring.redis.cluster.nodes", HOST_AND_PORT_1); + propertySource.setProperty("spring.redis.cluster.max-redirects", "5"); + + RedisClusterConfiguration config = new RedisClusterConfiguration(propertySource); + + assertThat(config.getMaxRedirects(), is(5)); + assertThat(config.getClusterTimeout(), is(10L)); + assertThat(config.getClusterNodes(), hasItems(new RedisNode("127.0.0.1", 123))); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void shouldBeCreatedCorrecltyGivenValidPropertySourceWithMultipleHostPort() { + + MockPropertySource propertySource = new MockPropertySource(); + propertySource.setProperty("spring.redis.cluster.timeout", "10"); + propertySource.setProperty("spring.redis.cluster.nodes", + StringUtils.collectionToCommaDelimitedString(Arrays.asList(HOST_AND_PORT_1, HOST_AND_PORT_2, HOST_AND_PORT_3))); + propertySource.setProperty("spring.redis.cluster.max-redirects", "5"); + + RedisClusterConfiguration config = new RedisClusterConfiguration(propertySource); + + assertThat(config.getMaxRedirects(), is(5)); + assertThat(config.getClusterTimeout(), is(10L)); + assertThat(config.getClusterNodes(), + hasItems(new RedisNode("127.0.0.1", 123), new RedisNode("localhost", 456), new RedisNode("localhost", 789))); + } + +} diff --git a/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java b/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java index a032f6ef8..7442f3628 100644 --- a/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java @@ -881,5 +881,15 @@ public class RedisConnectionUnitTests { public Set zRevRangeByScoreWithScores(byte[] key, Range range) { return delegate.zRevRangeByScoreWithScores(key, range); } + + @Override + public void migrate(byte[] key, RedisNode target, int dbIndex, MigrateOption option) { + delegate.migrate(key, target, dbIndex, option); + } + + @Override + public void migrate(byte[] key, RedisNode target, int dbIndex, MigrateOption option, long timeout) { + delegate.migrate(key, target, dbIndex, option, timeout); + } } } diff --git a/src/test/java/org/springframework/data/redis/connection/convert/ConvertersUnitTests.java b/src/test/java/org/springframework/data/redis/connection/convert/ConvertersUnitTests.java new file mode 100644 index 000000000..9bee1a6eb --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/convert/ConvertersUnitTests.java @@ -0,0 +1,155 @@ +/* + * Copyright 2015 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.convert; + +import static org.hamcrest.core.Is.*; +import static org.hamcrest.core.IsCollectionContaining.*; +import static org.hamcrest.core.IsNull.*; +import static org.junit.Assert.*; + +import java.util.Iterator; + +import org.junit.Test; +import org.springframework.data.redis.connection.RedisClusterNode; +import org.springframework.data.redis.connection.RedisClusterNode.Flag; +import org.springframework.data.redis.connection.RedisClusterNode.LinkState; +import org.springframework.data.redis.connection.RedisNode.NodeType; +import org.springframework.data.redis.connection.jedis.JedisConverters; + +/** + * @author Christoph Strobl + */ +public class ConvertersUnitTests { + + private static final String CLUSTER_NODES_RESPONSE = "" // + + "ef570f86c7b1a953846668debc177a3a16733420 127.0.0.1:6379 myself,master - 0 0 1 connected 0-5460" + + System.getProperty("line.separator") + + "0f2ee5df45d18c50aca07228cc18b1da96fd5e84 127.0.0.1:6380 master - 0 1427718161587 2 connected 5461-10922" + + System.getProperty("line.separator") + + "3b9b8192a874fa8f1f09dbc0ee20afab5738eee7 127.0.0.1:6381 master - 0 1427718161587 3 connected 10923-16383" + + System.getProperty("line.separator") + + "8cad73f63eb996fedba89f041636f17d88cda075 127.0.0.1:7369 slave ef570f86c7b1a953846668debc177a3a16733420 0 1427718161587 1 connected"; + + private static final String CLUSTER_NODE_WITH_SINGLE_SLOT_RESPONSE = "ef570f86c7b1a953846668debc177a3a16733420 127.0.0.1:6379 myself,master - 0 0 1 connected 3456"; + + private static final String CLUSTER_NODE_WITH_FAIL_FLAG_AND_DISCONNECTED_LINK_STATE = "b8b5ee73b1d1997abff694b3fe8b2397d2138b6d 127.0.0.1:7382 master,fail - 1450160048933 1450160048832 38 disconnected"; + + private static final String CLUSTER_NODE_IMPORTING_SLOT = "ef570f86c7b1a953846668debc177a3a16733420 127.0.0.1:6379 myself,master - 0 0 1 connected [5461-<-0f2ee5df45d18c50aca07228cc18b1da96fd5e84]"; + + /** + * @see DATAREDIS-315 + */ + @Test + public void toSetOfRedisClusterNodesShouldConvertSingleStringNodesResponseCorrectly() { + + Iterator nodes = JedisConverters.toSetOfRedisClusterNodes(CLUSTER_NODES_RESPONSE).iterator(); + + RedisClusterNode node = nodes.next(); // 127.0.0.1:6379 + assertThat(node.getId(), is("ef570f86c7b1a953846668debc177a3a16733420")); + assertThat(node.getHost(), is("127.0.0.1")); + assertThat(node.getPort(), is(6379)); + assertThat(node.getType(), is(NodeType.MASTER)); + assertThat(node.getSlotRange().contains(0), is(true)); + assertThat(node.getSlotRange().contains(5460), is(true)); + assertThat(node.getFlags(), hasItems(Flag.MASTER, Flag.MYSELF)); + assertThat(node.getLinkState(), is(LinkState.CONNECTED)); + + node = nodes.next(); // 127.0.0.1:6380 + assertThat(node.getId(), is("0f2ee5df45d18c50aca07228cc18b1da96fd5e84")); + assertThat(node.getHost(), is("127.0.0.1")); + assertThat(node.getPort(), is(6380)); + assertThat(node.getType(), is(NodeType.MASTER)); + assertThat(node.getSlotRange().contains(5461), is(true)); + assertThat(node.getSlotRange().contains(10922), is(true)); + assertThat(node.getFlags(), hasItems(Flag.MASTER)); + assertThat(node.getLinkState(), is(LinkState.CONNECTED)); + + node = nodes.next(); // 127.0.0.1:638 + assertThat(node.getId(), is("3b9b8192a874fa8f1f09dbc0ee20afab5738eee7")); + assertThat(node.getHost(), is("127.0.0.1")); + assertThat(node.getPort(), is(6381)); + assertThat(node.getType(), is(NodeType.MASTER)); + assertThat(node.getSlotRange().contains(10923), is(true)); + assertThat(node.getSlotRange().contains(16383), is(true)); + assertThat(node.getFlags(), hasItems(Flag.MASTER)); + assertThat(node.getLinkState(), is(LinkState.CONNECTED)); + + node = nodes.next(); // 127.0.0.1:7369 + assertThat(node.getId(), is("8cad73f63eb996fedba89f041636f17d88cda075")); + assertThat(node.getHost(), is("127.0.0.1")); + assertThat(node.getPort(), is(7369)); + assertThat(node.getType(), is(NodeType.SLAVE)); + assertThat(node.getMasterId(), is("ef570f86c7b1a953846668debc177a3a16733420")); + assertThat(node.getSlotRange(), notNullValue()); + assertThat(node.getFlags(), hasItems(Flag.SLAVE)); + assertThat(node.getLinkState(), is(LinkState.CONNECTED)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void toSetOfRedisClusterNodesShouldConvertNodesWihtSingleSlotCorrectly() { + + Iterator nodes = JedisConverters.toSetOfRedisClusterNodes(CLUSTER_NODE_WITH_SINGLE_SLOT_RESPONSE) + .iterator(); + + RedisClusterNode node = nodes.next(); // 127.0.0.1:6379 + assertThat(node.getId(), is("ef570f86c7b1a953846668debc177a3a16733420")); + assertThat(node.getHost(), is("127.0.0.1")); + assertThat(node.getPort(), is(6379)); + assertThat(node.getType(), is(NodeType.MASTER)); + assertThat(node.getSlotRange().contains(3456), is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void toSetOfRedisClusterNodesShouldParseLinkStateAndDisconnectedCorrectly() { + + Iterator nodes = JedisConverters.toSetOfRedisClusterNodes( + CLUSTER_NODE_WITH_FAIL_FLAG_AND_DISCONNECTED_LINK_STATE).iterator(); + + RedisClusterNode node = nodes.next(); + assertThat(node.getId(), is("b8b5ee73b1d1997abff694b3fe8b2397d2138b6d")); + assertThat(node.getHost(), is("127.0.0.1")); + assertThat(node.getPort(), is(7382)); + assertThat(node.getType(), is(NodeType.MASTER)); + assertThat(node.getFlags(), hasItems(Flag.MASTER, Flag.FAIL)); + assertThat(node.getLinkState(), is(LinkState.DISCONNECTED)); + assertThat(node.getSlotRange().getSlots().size(), is(0)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void toSetOfRedisClusterNodesShouldIgnoreImportingSlot() { + + Iterator nodes = JedisConverters.toSetOfRedisClusterNodes(CLUSTER_NODE_IMPORTING_SLOT).iterator(); + + RedisClusterNode node = nodes.next(); + assertThat(node.getId(), is("ef570f86c7b1a953846668debc177a3a16733420")); + assertThat(node.getHost(), is("127.0.0.1")); + assertThat(node.getPort(), is(6379)); + assertThat(node.getType(), is(NodeType.MASTER)); + assertThat(node.getFlags(), hasItem(Flag.MASTER)); + assertThat(node.getLinkState(), is(LinkState.CONNECTED)); + assertThat(node.getSlotRange().getSlots().size(), is(0)); + } + +} diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionTests.java new file mode 100644 index 000000000..6689e8dc7 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionTests.java @@ -0,0 +1,2310 @@ +/* + * Copyright 2015 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.jedis; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.springframework.data.redis.connection.ClusterTestVariables.*; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.springframework.dao.DataAccessException; +import org.springframework.data.redis.connection.ClusterConnectionTests; +import org.springframework.data.redis.connection.ClusterSlotHashUtil; +import org.springframework.data.redis.connection.DataType; +import org.springframework.data.redis.connection.DefaultSortParameters; +import org.springframework.data.redis.connection.DefaultTuple; +import org.springframework.data.redis.connection.RedisClusterNode; +import org.springframework.data.redis.connection.RedisListCommands.Position; +import org.springframework.data.redis.connection.RedisNode; +import org.springframework.data.redis.connection.RedisStringCommands.BitOperation; +import org.springframework.data.redis.connection.RedisZSetCommands.Range; +import org.springframework.data.redis.connection.RedisZSetCommands.Tuple; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.test.util.RedisClusterRule; + +import redis.clients.jedis.HostAndPort; +import redis.clients.jedis.JedisCluster; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.exceptions.JedisDataException; + +/** + * @author Christoph Strobl + */ +public class JedisClusterConnectionTests implements ClusterConnectionTests { + + static final List CLUSTER_NODES = Arrays.asList(new HostAndPort(CLUSTER_HOST, MASTER_NODE_1_PORT), + new HostAndPort(CLUSTER_HOST, MASTER_NODE_2_PORT), new HostAndPort(CLUSTER_HOST, MASTER_NODE_3_PORT)); + + static final byte[] KEY_1_BYTES = JedisConverters.toBytes(KEY_1); + static final byte[] KEY_2_BYTES = JedisConverters.toBytes(KEY_2); + static final byte[] KEY_3_BYTES = JedisConverters.toBytes(KEY_3); + + static final byte[] SAME_SLOT_KEY_1_BYTES = JedisConverters.toBytes(SAME_SLOT_KEY_1); + static final byte[] SAME_SLOT_KEY_2_BYTES = JedisConverters.toBytes(SAME_SLOT_KEY_2); + static final byte[] SAME_SLOT_KEY_3_BYTES = JedisConverters.toBytes(SAME_SLOT_KEY_3); + + static final byte[] VALUE_1_BYTES = JedisConverters.toBytes(VALUE_1); + static final byte[] VALUE_2_BYTES = JedisConverters.toBytes(VALUE_2); + static final byte[] VALUE_3_BYTES = JedisConverters.toBytes(VALUE_3); + + JedisCluster nativeConnection; + JedisClusterConnection clusterConnection; + + /** + * ONLY RUN WHEN CLUSTER AVAILABLE + */ + public static @ClassRule RedisClusterRule clusterRule = new RedisClusterRule(); + + @Before + public void setUp() throws IOException { + + nativeConnection = new JedisCluster(new HashSet(CLUSTER_NODES)); + clusterConnection = new JedisClusterConnection(this.nativeConnection); + } + + @After + public void tearDown() throws IOException { + + for (JedisPool pool : nativeConnection.getClusterNodes().values()) { + try { + pool.getResource().flushDB(); + } catch (JedisDataException e) { + // ignore this one since we cannot remove data from slaves + } + } + nativeConnection.close(); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void shouldAllowSettingAndGettingValues() { + + clusterConnection.set(KEY_1_BYTES, VALUE_1_BYTES); + assertThat(clusterConnection.get(KEY_1_BYTES), is(VALUE_1_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void delShouldRemoveSingleKeyCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + clusterConnection.del(KEY_1_BYTES); + + assertThat(nativeConnection.get(KEY_1), nullValue()); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void delShouldRemoveMultipleKeysCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + nativeConnection.set(KEY_2, VALUE_2); + + clusterConnection.del(KEY_1_BYTES, KEY_2_BYTES); + + assertThat(nativeConnection.get(KEY_1), nullValue()); + assertThat(nativeConnection.get(KEY_2), nullValue()); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void delShouldRemoveMultipleKeysOnSameSlotCorrectly() { + + nativeConnection.set(SAME_SLOT_KEY_1, VALUE_1); + nativeConnection.set(SAME_SLOT_KEY_2, VALUE_2); + + clusterConnection.del(SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES); + + assertThat(nativeConnection.get(SAME_SLOT_KEY_1), nullValue()); + assertThat(nativeConnection.get(SAME_SLOT_KEY_2), nullValue()); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void typeShouldReadKeyTypeCorrectly() { + + nativeConnection.sadd(KEY_1_BYTES, VALUE_1_BYTES); + nativeConnection.set(KEY_2_BYTES, VALUE_2_BYTES); + nativeConnection.hmset(KEY_3_BYTES, Collections.singletonMap(KEY_1_BYTES, VALUE_1_BYTES)); + + assertThat(clusterConnection.type(KEY_1_BYTES), is(DataType.SET)); + assertThat(clusterConnection.type(KEY_2_BYTES), is(DataType.STRING)); + assertThat(clusterConnection.type(KEY_3_BYTES), is(DataType.HASH)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void keysShouldReturnAllKeys() { + + nativeConnection.set(KEY_1, VALUE_1); + nativeConnection.set(KEY_2, VALUE_2); + + assertThat(clusterConnection.keys(JedisConverters.toBytes("*")), hasItems(KEY_1_BYTES, KEY_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void keysShouldReturnAllKeysForSpecificNode() { + + nativeConnection.set(KEY_1, VALUE_1); + nativeConnection.set(KEY_2, VALUE_2); + + Set keysOnNode = clusterConnection.keys(new RedisClusterNode("127.0.0.1", 7379, null), + JedisConverters.toBytes("*")); + + assertThat(keysOnNode, hasItems(KEY_2_BYTES)); + assertThat(keysOnNode, not(hasItems(KEY_1_BYTES))); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void randomKeyShouldReturnCorrectlyWhenKeysAvailable() { + + nativeConnection.set(KEY_1, VALUE_1); + nativeConnection.set(KEY_2, VALUE_2); + + assertThat(clusterConnection.randomKey(), notNullValue()); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void randomKeyShouldReturnNullWhenNoKeysAvailable() { + assertThat(clusterConnection.randomKey(), nullValue()); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void rename() { + + nativeConnection.set(KEY_1_BYTES, VALUE_1_BYTES); + + clusterConnection.rename(KEY_1_BYTES, KEY_2_BYTES); + + assertThat(nativeConnection.exists(KEY_1_BYTES), is(false)); + assertThat(nativeConnection.get(KEY_2_BYTES), is(VALUE_1_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void renameSameKeysOnSameSlot() { + + nativeConnection.set(SAME_SLOT_KEY_1_BYTES, VALUE_1_BYTES); + + clusterConnection.rename(SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES); + + assertThat(nativeConnection.exists(SAME_SLOT_KEY_1_BYTES), is(false)); + assertThat(nativeConnection.get(SAME_SLOT_KEY_2_BYTES), is(VALUE_1_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void renameNXWhenTargetKeyDoesNotExist() { + + nativeConnection.set(KEY_1_BYTES, VALUE_1_BYTES); + + assertThat(clusterConnection.renameNX(KEY_1_BYTES, KEY_2_BYTES), is(Boolean.TRUE)); + + assertThat(nativeConnection.exists(KEY_1_BYTES), is(false)); + assertThat(nativeConnection.get(KEY_2_BYTES), is(VALUE_1_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void renameNXWhenTargetKeyDoesExist() { + + nativeConnection.set(KEY_1_BYTES, VALUE_1_BYTES); + nativeConnection.set(KEY_2_BYTES, VALUE_2_BYTES); + + assertThat(clusterConnection.renameNX(KEY_1_BYTES, KEY_2_BYTES), is(Boolean.FALSE)); + + assertThat(nativeConnection.get(KEY_1_BYTES), is(VALUE_1_BYTES)); + assertThat(nativeConnection.get(KEY_2_BYTES), is(VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void renameNXWhenOnSameSlot() { + + nativeConnection.set(SAME_SLOT_KEY_1_BYTES, VALUE_1_BYTES); + + assertThat(clusterConnection.renameNX(SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES), is(Boolean.TRUE)); + + assertThat(nativeConnection.exists(SAME_SLOT_KEY_1_BYTES), is(false)); + assertThat(nativeConnection.get(SAME_SLOT_KEY_2_BYTES), is(VALUE_1_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void expireShouldBeSetCorreclty() { + + nativeConnection.set(KEY_1, VALUE_1); + + clusterConnection.expire(KEY_1_BYTES, 5); + + assertThat(nativeConnection.ttl(JedisConverters.toString(KEY_1_BYTES)) > 1, is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void pExpireShouldBeSetCorreclty() { + + nativeConnection.set(KEY_1, VALUE_1); + + clusterConnection.pExpire(KEY_1_BYTES, 5000); + + assertThat(nativeConnection.ttl(JedisConverters.toString(KEY_1_BYTES)) > 1, is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void expireAtShouldBeSetCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + clusterConnection.expireAt(KEY_1_BYTES, System.currentTimeMillis() / 1000 + 5000); + + assertThat(nativeConnection.ttl(JedisConverters.toString(KEY_1_BYTES)) > 1, is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void pExpireAtShouldBeSetCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + clusterConnection.pExpireAt(KEY_1_BYTES, System.currentTimeMillis() + 5000); + + assertThat(nativeConnection.ttl(JedisConverters.toString(KEY_1_BYTES)) > 1, is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void persistShoudRemoveTTL() { + + nativeConnection.setex(KEY_1_BYTES, 10, VALUE_1_BYTES); + + assertThat(clusterConnection.persist(KEY_1_BYTES), is(Boolean.TRUE)); + assertThat(nativeConnection.ttl(KEY_1_BYTES), is(-1L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = UnsupportedOperationException.class) + public void moveShouldNotBeSupported() { + clusterConnection.move(KEY_1_BYTES, 3); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void dbSizeShouldReturnCummulatedDbSize() { + + nativeConnection.set(KEY_1, VALUE_1); + nativeConnection.set(KEY_2, VALUE_2); + + assertThat(clusterConnection.dbSize(), is(2L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void dbSizeForSpecificNodeShouldGetNodeDbSize() { + + nativeConnection.set(KEY_1, VALUE_1); + nativeConnection.set(KEY_2, VALUE_2); + + assertThat(clusterConnection.dbSize(new RedisClusterNode("127.0.0.1", 7379, null)), is(1L)); + assertThat(clusterConnection.dbSize(new RedisClusterNode("127.0.0.1", 7380, null)), is(1L)); + assertThat(clusterConnection.dbSize(new RedisClusterNode("127.0.0.1", 7381, null)), is(0L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void ttlShouldReturnMinusTwoWhenKeyDoesNotExist() { + assertThat(clusterConnection.ttl(KEY_1_BYTES), is(-2L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void ttlShouldReturnMinusOneWhenKeyDoesNotHaveExpirationSet() { + + nativeConnection.set(KEY_1, VALUE_1); + + assertThat(clusterConnection.ttl(KEY_1_BYTES), is(-1L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void ttlShouldReturnValueCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + nativeConnection.expire(KEY_1, 5); + + assertThat(clusterConnection.ttl(KEY_1_BYTES) > 1, is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void pTtlShouldReturnMinusTwoWhenKeyDoesNotExist() { + assertThat(clusterConnection.pTtl(KEY_1_BYTES), is(-2L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void pTtlShouldReturnMinusOneWhenKeyDoesNotHaveExpirationSet() { + + nativeConnection.set(KEY_1, VALUE_1); + + assertThat(clusterConnection.pTtl(KEY_1_BYTES), is(-1L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void pTtlShouldReturValueCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + nativeConnection.expire(KEY_1, 5); + + assertThat(clusterConnection.pTtl(KEY_1_BYTES) > 1, is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sortShouldReturnValuesCorrectly() { + + nativeConnection.lpush(KEY_1, VALUE_2, VALUE_1); + + assertThat(clusterConnection.sort(KEY_1_BYTES, new DefaultSortParameters().alpha()), + hasItems(VALUE_1_BYTES, VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sortAndStoreShouldAddSortedValuesValuesCorrectly() { + + nativeConnection.lpush(KEY_1, VALUE_2, VALUE_1); + + assertThat(clusterConnection.sort(KEY_1_BYTES, new DefaultSortParameters().alpha(), KEY_2_BYTES), is(1L)); + assertThat(nativeConnection.exists(KEY_2_BYTES), is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sortAndStoreShouldReturnZeroWhenListDoesNotExist() { + assertThat(clusterConnection.sort(KEY_1_BYTES, new DefaultSortParameters().alpha(), KEY_2_BYTES), is(0L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void dumpAndRestoreShouldWorkCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + byte[] dumpedValue = clusterConnection.dump(KEY_1_BYTES); + clusterConnection.restore(KEY_2_BYTES, 0, dumpedValue); + + assertThat(nativeConnection.get(KEY_2), is(VALUE_1)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void getShouldReturnValueCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + assertThat(clusterConnection.get(KEY_1_BYTES), is(VALUE_1_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void getSetShouldWorkCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + byte[] valueBeforeSet = clusterConnection.getSet(KEY_1_BYTES, VALUE_2_BYTES); + + assertThat(valueBeforeSet, is(VALUE_1_BYTES)); + assertThat(nativeConnection.get(KEY_1), is(VALUE_2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void mGetShouldReturnCorrectlyWhenKeysMapToSameSlot() { + + nativeConnection.set(SAME_SLOT_KEY_1_BYTES, VALUE_1_BYTES); + nativeConnection.set(SAME_SLOT_KEY_2_BYTES, VALUE_2_BYTES); + + assertThat(clusterConnection.mGet(SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES), + hasItems(VALUE_1_BYTES, VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void mGetShouldReturnCorrectlyWhenKeysDoNotMapToSameSlot() { + + nativeConnection.set(KEY_1_BYTES, VALUE_1_BYTES); + nativeConnection.set(KEY_2_BYTES, VALUE_2_BYTES); + + assertThat(clusterConnection.mGet(KEY_1_BYTES, KEY_2_BYTES), hasItems(VALUE_1_BYTES, VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void setShouldSetValueCorrectly() { + + clusterConnection.set(KEY_1_BYTES, VALUE_1_BYTES); + + assertThat(nativeConnection.get(KEY_1), is(VALUE_1)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void setNxShouldSetValueCorrectly() { + + clusterConnection.setNX(KEY_1_BYTES, VALUE_1_BYTES); + + assertThat(nativeConnection.get(KEY_1), is(VALUE_1)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void setNxShouldNotSetValueWhenAlreadyExistsInDBCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + clusterConnection.setNX(KEY_1_BYTES, VALUE_2_BYTES); + + assertThat(nativeConnection.get(KEY_1), is(VALUE_1)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void setExShouldSetValueCorrectly() { + + clusterConnection.setEx(KEY_1_BYTES, 5, VALUE_1_BYTES); + + assertThat(nativeConnection.get(KEY_1), is(VALUE_1)); + assertThat(nativeConnection.ttl(KEY_1) > 1, is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void pSetExShouldSetValueCorrectly() { + + clusterConnection.pSetEx(KEY_1_BYTES, 5000, VALUE_1_BYTES); + + assertThat(nativeConnection.get(KEY_1), is(VALUE_1)); + assertThat(nativeConnection.ttl(KEY_1) > 1, is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void mSetShouldWorkWhenKeysMapToSameSlot() { + + Map map = new LinkedHashMap(); + map.put(SAME_SLOT_KEY_1_BYTES, VALUE_1_BYTES); + map.put(SAME_SLOT_KEY_2_BYTES, VALUE_2_BYTES); + + clusterConnection.mSet(map); + + assertThat(nativeConnection.get(SAME_SLOT_KEY_1), is(VALUE_1)); + assertThat(nativeConnection.get(SAME_SLOT_KEY_2), is(VALUE_2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void mSetShouldWorkWhenKeysDoNotMapToSameSlot() { + + Map map = new LinkedHashMap(); + map.put(KEY_1_BYTES, VALUE_1_BYTES); + map.put(KEY_2_BYTES, VALUE_2_BYTES); + + clusterConnection.mSet(map); + + assertThat(nativeConnection.get(KEY_1), is(VALUE_1)); + assertThat(nativeConnection.get(KEY_2), is(VALUE_2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void mSetNXShouldReturnTrueIfAllKeysSet() { + + Map map = new LinkedHashMap(); + map.put(KEY_1_BYTES, VALUE_1_BYTES); + map.put(KEY_2_BYTES, VALUE_2_BYTES); + + assertThat(clusterConnection.mSetNX(map), is(true)); + + assertThat(nativeConnection.get(KEY_1), is(VALUE_1)); + assertThat(nativeConnection.get(KEY_2), is(VALUE_2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void mSetNXShouldReturnFalseIfNotAllKeysSet() { + + nativeConnection.set(KEY_2_BYTES, VALUE_3_BYTES); + Map map = new LinkedHashMap(); + map.put(KEY_1_BYTES, VALUE_1_BYTES); + map.put(KEY_2_BYTES, VALUE_2_BYTES); + + assertThat(clusterConnection.mSetNX(map), is(false)); + + assertThat(nativeConnection.get(KEY_1), is(VALUE_1)); + assertThat(nativeConnection.get(KEY_2), is(VALUE_3)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void mSetNXShouldWorkForOnSameSlotKeys() { + + Map map = new LinkedHashMap(); + map.put(SAME_SLOT_KEY_1_BYTES, VALUE_1_BYTES); + map.put(SAME_SLOT_KEY_2_BYTES, VALUE_2_BYTES); + + assertThat(clusterConnection.mSetNX(map), is(true)); + + assertThat(nativeConnection.get(SAME_SLOT_KEY_1), is(VALUE_1)); + assertThat(nativeConnection.get(SAME_SLOT_KEY_2), is(VALUE_2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void incrShouldIncreaseValueCorrectly() { + + nativeConnection.set(KEY_1, "1"); + + assertThat(clusterConnection.incr(KEY_1_BYTES), is(2L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void incrByShouldIncreaseValueCorrectly() { + + nativeConnection.set(KEY_1, "1"); + + assertThat(clusterConnection.incrBy(KEY_1_BYTES, 5), is(6L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void incrByFloatShouldIncreaseValueCorrectly() { + + nativeConnection.set(KEY_1, "1"); + + assertThat(clusterConnection.incrBy(KEY_1_BYTES, 5.5D), is(6.5D)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void decrShouldDecreaseValueCorrectly() { + + nativeConnection.set(KEY_1, "5"); + + assertThat(clusterConnection.decr(KEY_1_BYTES), is(4L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void decrByShouldDecreaseValueCorrectly() { + + nativeConnection.set(KEY_1, "5"); + + assertThat(clusterConnection.decrBy(KEY_1_BYTES, 4), is(1L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void appendShouldAddValueCorrectly() { + + clusterConnection.append(KEY_1_BYTES, VALUE_1_BYTES); + clusterConnection.append(KEY_1_BYTES, VALUE_2_BYTES); + + assertThat(nativeConnection.get(KEY_1), is(VALUE_1.concat(VALUE_2))); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void getRangeShouldReturnValueCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + assertThat(clusterConnection.getRange(KEY_1_BYTES, 0, 2), is(JedisConverters.toBytes("val"))); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void setRangeShouldWorkCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + clusterConnection.setRange(KEY_1_BYTES, JedisConverters.toBytes("UE"), 3); + + assertThat(nativeConnection.get(KEY_1), is("valUE1")); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void getBitShouldWorkCorrectly() { + + nativeConnection.setbit(KEY_1, 0, true); + nativeConnection.setbit(KEY_1, 1, false); + + assertThat(clusterConnection.getBit(KEY_1_BYTES, 0), is(true)); + assertThat(clusterConnection.getBit(KEY_1_BYTES, 1), is(false)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void setBitShouldWorkCorrectly() { + + clusterConnection.setBit(KEY_1_BYTES, 0, true); + clusterConnection.setBit(KEY_1_BYTES, 1, false); + + assertThat(nativeConnection.getbit(KEY_1, 0), is(true)); + assertThat(nativeConnection.getbit(KEY_1, 1), is(false)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void bitCountShouldWorkCorrectly() { + + nativeConnection.setbit(KEY_1, 0, true); + nativeConnection.setbit(KEY_1, 1, false); + + assertThat(clusterConnection.bitCount(KEY_1_BYTES), is(1L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void bitCountWithRangeShouldWorkCorrectly() { + + nativeConnection.setbit(KEY_1, 0, true); + nativeConnection.setbit(KEY_1, 1, false); + nativeConnection.setbit(KEY_1, 2, true); + nativeConnection.setbit(KEY_1, 3, false); + nativeConnection.setbit(KEY_1, 4, true); + + assertThat(clusterConnection.bitCount(KEY_1_BYTES, 0, 3), is(3L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void bitOpShouldWorkCorrectly() { + + nativeConnection.set(SAME_SLOT_KEY_1, "foo"); + nativeConnection.set(SAME_SLOT_KEY_2, "bar"); + + clusterConnection.bitOp(BitOperation.AND, SAME_SLOT_KEY_3_BYTES, SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES); + + assertThat(nativeConnection.get(SAME_SLOT_KEY_3), is("bab")); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = DataAccessException.class) + public void bitOpShouldThrowExceptionWhenKeysDoNotMapToSameSlot() { + clusterConnection.bitOp(BitOperation.AND, KEY_1_BYTES, KEY_2_BYTES, KEY_3_BYTES); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void strLenShouldWorkCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + assertThat(clusterConnection.strLen(KEY_1_BYTES), is(6L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void rPushShoultAddValuesCorrectly() { + + clusterConnection.rPush(KEY_1_BYTES, VALUE_1_BYTES, VALUE_2_BYTES); + + assertThat(nativeConnection.lrange(KEY_1, 0, -1), hasItems(VALUE_1, VALUE_2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void lPushShoultAddValuesCorrectly() { + + clusterConnection.lPush(KEY_1_BYTES, VALUE_1_BYTES, VALUE_2_BYTES); + + assertThat(nativeConnection.lrange(KEY_1, 0, -1), hasItems(VALUE_1, VALUE_2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void rPushNXShoultNotAddValuesWhenKeyDoesNotExist() { + + clusterConnection.rPushX(KEY_1_BYTES, VALUE_1_BYTES); + + assertThat(nativeConnection.exists(KEY_1), is(false)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void lPushNXShoultNotAddValuesWhenKeyDoesNotExist() { + + clusterConnection.lPushX(KEY_1_BYTES, VALUE_1_BYTES); + + assertThat(nativeConnection.exists(KEY_1), is(false)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void lLenShouldCountValuesCorrectly() { + + nativeConnection.lpush(KEY_1, VALUE_1, VALUE_2); + + assertThat(clusterConnection.lLen(KEY_1_BYTES), is(2L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void lRangeShouldGetValuesCorrectly() { + + nativeConnection.lpush(KEY_1, VALUE_1, VALUE_2); + + assertThat(clusterConnection.lRange(KEY_1_BYTES, 0L, -1L), hasItems(VALUE_1_BYTES, VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void lTrimShouldTrimListCorrectly() { + + nativeConnection.lpush(KEY_1, VALUE_1, VALUE_2, "foo", "bar"); + + clusterConnection.lTrim(KEY_1_BYTES, 2, 3); + + assertThat(nativeConnection.lrange(KEY_1, 0, -1), hasItems(VALUE_1, VALUE_2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void lIndexShouldGetElementAtIndexCorrectly() { + + nativeConnection.rpush(KEY_1, VALUE_1, VALUE_2, "foo", "bar"); + + assertThat(clusterConnection.lIndex(KEY_1_BYTES, 1), is(VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void lInsertShouldAddElementAtPositionCorrectly() { + + nativeConnection.rpush(KEY_1, VALUE_1, VALUE_2, "foo", "bar"); + + clusterConnection.lInsert(KEY_1_BYTES, Position.AFTER, VALUE_2_BYTES, JedisConverters.toBytes("booh!")); + + assertThat(nativeConnection.lrange(KEY_1, 0, -1).get(2), is("booh!")); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void lSetShouldSetElementAtPositionCorrectly() { + + nativeConnection.rpush(KEY_1, VALUE_1, VALUE_2, "foo", "bar"); + + clusterConnection.lSet(KEY_1_BYTES, 1L, VALUE_1_BYTES); + + assertThat(nativeConnection.lrange(KEY_1, 0, -1).get(1), is(VALUE_1)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void lRemShouldRemoveElementAtPositionCorrectly() { + + nativeConnection.rpush(KEY_1, VALUE_1, VALUE_2, "foo", "bar"); + + clusterConnection.lRem(KEY_1_BYTES, 1L, VALUE_1_BYTES); + + assertThat(nativeConnection.llen(KEY_1), is(3L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void lPopShouldReturnElementCorrectly() { + + nativeConnection.rpush(KEY_1, VALUE_1, VALUE_2); + + assertThat(clusterConnection.lPop(KEY_1_BYTES), is(VALUE_1_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void rPopShouldReturnElementCorrectly() { + + nativeConnection.rpush(KEY_1, VALUE_1, VALUE_2); + + assertThat(clusterConnection.rPop(KEY_1_BYTES), is(VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void blPopShouldPopElementCorectly() { + + nativeConnection.lpush(KEY_1_BYTES, VALUE_1_BYTES, VALUE_2_BYTES); + nativeConnection.lpush(KEY_2_BYTES, VALUE_3_BYTES); + + assertThat(clusterConnection.bLPop(100, KEY_1_BYTES, KEY_2_BYTES).size(), is(2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void blPopShouldPopElementCorectlyWhenKeyOnSameSlot() { + + nativeConnection.lpush(SAME_SLOT_KEY_1_BYTES, VALUE_1_BYTES, VALUE_2_BYTES); + nativeConnection.lpush(SAME_SLOT_KEY_2_BYTES, VALUE_3_BYTES); + + assertThat(clusterConnection.bLPop(100, SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES).size(), is(2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void brPopShouldPopElementCorectly() { + + nativeConnection.lpush(KEY_1_BYTES, VALUE_1_BYTES, VALUE_2_BYTES); + nativeConnection.lpush(KEY_2_BYTES, VALUE_3_BYTES); + + assertThat(clusterConnection.bRPop(100, KEY_1_BYTES, KEY_2_BYTES).size(), is(2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void brPopShouldPopElementCorectlyWhenKeyOnSameSlot() { + + nativeConnection.lpush(SAME_SLOT_KEY_1_BYTES, VALUE_1_BYTES, VALUE_2_BYTES); + nativeConnection.lpush(SAME_SLOT_KEY_2_BYTES, VALUE_3_BYTES); + + assertThat(clusterConnection.bRPop(100, SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES).size(), is(2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void rPopLPushShouldWorkWhenDoNotMapToSameSlot() { + + nativeConnection.lpush(KEY_1_BYTES, VALUE_1_BYTES, VALUE_2_BYTES); + + assertThat(clusterConnection.rPopLPush(KEY_1_BYTES, KEY_2_BYTES), is(VALUE_1_BYTES)); + assertThat(nativeConnection.exists(KEY_2_BYTES), is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void rPopLPushShouldWorkWhenKeysOnSameSlot() { + + nativeConnection.lpush(SAME_SLOT_KEY_1_BYTES, VALUE_1_BYTES, VALUE_2_BYTES); + + assertThat(clusterConnection.rPopLPush(SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES), is(VALUE_1_BYTES)); + assertThat(nativeConnection.exists(SAME_SLOT_KEY_2_BYTES), is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void bRPopLPushShouldWork() { + + nativeConnection.lpush(KEY_1_BYTES, VALUE_1_BYTES, VALUE_2_BYTES); + + assertThat(clusterConnection.bRPopLPush(0, KEY_1_BYTES, KEY_2_BYTES), is(VALUE_1_BYTES)); + assertThat(nativeConnection.exists(KEY_2_BYTES), is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void bRPopLPushShouldWorkOnSameSlotKeys() { + + nativeConnection.lpush(SAME_SLOT_KEY_1_BYTES, VALUE_1_BYTES, VALUE_2_BYTES); + + assertThat(clusterConnection.bRPopLPush(0, SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES), is(VALUE_1_BYTES)); + assertThat(nativeConnection.exists(SAME_SLOT_KEY_2_BYTES), is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sAddShouldAddValueToSetCorrectly() { + + clusterConnection.sAdd(KEY_1_BYTES, VALUE_1_BYTES, VALUE_2_BYTES); + + assertThat(nativeConnection.smembers(KEY_1), hasItems(VALUE_1, VALUE_2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sRemShouldRemoveValueFromSetCorrectly() { + + nativeConnection.sadd(KEY_1, VALUE_1, VALUE_2); + + clusterConnection.sRem(KEY_1_BYTES, VALUE_2_BYTES); + + assertThat(nativeConnection.smembers(KEY_1), hasItems(VALUE_1)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sPopShouldPopValueFromSetCorrectly() { + + nativeConnection.sadd(KEY_1, VALUE_1, VALUE_2); + + assertThat(clusterConnection.sPop(KEY_1_BYTES), notNullValue()); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sMoveShouldWorkWhenKeysMapToSameSlot() { + + nativeConnection.sadd(SAME_SLOT_KEY_1_BYTES, VALUE_1_BYTES, VALUE_2_BYTES); + nativeConnection.sadd(SAME_SLOT_KEY_2_BYTES, VALUE_3_BYTES); + + clusterConnection.sMove(SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES, VALUE_2_BYTES); + + assertThat(nativeConnection.sismember(SAME_SLOT_KEY_1_BYTES, VALUE_2_BYTES), is(false)); + assertThat(nativeConnection.sismember(SAME_SLOT_KEY_2_BYTES, VALUE_2_BYTES), is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sMoveShouldWorkWhenKeysDoNotMapToSameSlot() { + + nativeConnection.sadd(KEY_1_BYTES, VALUE_1_BYTES, VALUE_2_BYTES); + nativeConnection.sadd(KEY_2_BYTES, VALUE_3_BYTES); + + clusterConnection.sMove(KEY_1_BYTES, KEY_2_BYTES, VALUE_2_BYTES); + + assertThat(nativeConnection.sismember(KEY_1_BYTES, VALUE_2_BYTES), is(false)); + assertThat(nativeConnection.sismember(KEY_2_BYTES, VALUE_2_BYTES), is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sCardShouldCountValuesInSetCorrectly() { + + nativeConnection.sadd(KEY_1, VALUE_1, VALUE_2); + + assertThat(clusterConnection.sCard(KEY_1_BYTES), is(2L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sIsMemberShouldReturnTrueIfValueIsMemberOfSet() { + + nativeConnection.sadd(KEY_1, VALUE_1, VALUE_2); + + assertThat(clusterConnection.sIsMember(KEY_1_BYTES, VALUE_1_BYTES), is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sIsMemberShouldReturnFalseIfValueIsMemberOfSet() { + + nativeConnection.sadd(KEY_1, VALUE_1, VALUE_2); + + assertThat(clusterConnection.sIsMember(KEY_1_BYTES, JedisConverters.toBytes("foo")), is(false)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sInterShouldWorkForKeysMappingToSameSlot() { + + nativeConnection.sadd(SAME_SLOT_KEY_1_BYTES, VALUE_1_BYTES, VALUE_2_BYTES); + nativeConnection.sadd(SAME_SLOT_KEY_2_BYTES, VALUE_2_BYTES, VALUE_3_BYTES); + + assertThat(clusterConnection.sInter(SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES), hasItem(VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sInterShouldWorkForKeysNotMappingToSameSlot() { + + nativeConnection.sadd(KEY_1_BYTES, VALUE_1_BYTES, VALUE_2_BYTES); + nativeConnection.sadd(KEY_2_BYTES, VALUE_2_BYTES, VALUE_3_BYTES); + + assertThat(clusterConnection.sInter(KEY_1_BYTES, KEY_2_BYTES), hasItem(VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sInterStoreShouldWorkForKeysMappingToSameSlot() { + + nativeConnection.sadd(SAME_SLOT_KEY_1_BYTES, VALUE_1_BYTES, VALUE_2_BYTES); + nativeConnection.sadd(SAME_SLOT_KEY_2_BYTES, VALUE_2_BYTES, VALUE_3_BYTES); + + clusterConnection.sInterStore(SAME_SLOT_KEY_3_BYTES, SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES); + + assertThat(nativeConnection.smembers(SAME_SLOT_KEY_3_BYTES), hasItem(VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sInterStoreShouldWorkForKeysNotMappingToSameSlot() { + + nativeConnection.sadd(KEY_1_BYTES, VALUE_1_BYTES, VALUE_2_BYTES); + nativeConnection.sadd(KEY_2_BYTES, VALUE_2_BYTES, VALUE_3_BYTES); + + clusterConnection.sInterStore(KEY_3_BYTES, KEY_1_BYTES, KEY_2_BYTES); + + assertThat(nativeConnection.smembers(KEY_3_BYTES), hasItem(VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sUnionShouldWorkForKeysMappingToSameSlot() { + + nativeConnection.sadd(SAME_SLOT_KEY_1_BYTES, VALUE_1_BYTES, VALUE_2_BYTES); + nativeConnection.sadd(SAME_SLOT_KEY_2_BYTES, VALUE_2_BYTES, VALUE_3_BYTES); + + assertThat(clusterConnection.sUnion(SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES), + hasItems(VALUE_1_BYTES, VALUE_2_BYTES, VALUE_3_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sUnionShouldWorkForKeysNotMappingToSameSlot() { + + nativeConnection.sadd(KEY_1_BYTES, VALUE_1_BYTES, VALUE_2_BYTES); + nativeConnection.sadd(KEY_2_BYTES, VALUE_2_BYTES, VALUE_3_BYTES); + + assertThat(clusterConnection.sUnion(KEY_1_BYTES, KEY_2_BYTES), + hasItems(VALUE_1_BYTES, VALUE_2_BYTES, VALUE_3_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sUnionStoreShouldWorkForKeysMappingToSameSlot() { + + nativeConnection.sadd(SAME_SLOT_KEY_1_BYTES, VALUE_1_BYTES, VALUE_2_BYTES); + nativeConnection.sadd(SAME_SLOT_KEY_2_BYTES, VALUE_2_BYTES, VALUE_3_BYTES); + + clusterConnection.sUnionStore(SAME_SLOT_KEY_3_BYTES, SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES); + + assertThat(nativeConnection.smembers(SAME_SLOT_KEY_3_BYTES), hasItems(VALUE_1_BYTES, VALUE_2_BYTES, VALUE_3_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sUnionStoreShouldWorkForKeysNotMappingToSameSlot() { + + nativeConnection.sadd(KEY_1_BYTES, VALUE_1_BYTES, VALUE_2_BYTES); + nativeConnection.sadd(KEY_2_BYTES, VALUE_2_BYTES, VALUE_3_BYTES); + + clusterConnection.sUnionStore(KEY_3_BYTES, KEY_1_BYTES, KEY_2_BYTES); + + assertThat(nativeConnection.smembers(KEY_3_BYTES), hasItems(VALUE_1_BYTES, VALUE_2_BYTES, VALUE_3_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sDiffShouldWorkWhenKeysMapToSameSlot() { + + nativeConnection.sadd(SAME_SLOT_KEY_1_BYTES, VALUE_1_BYTES, VALUE_2_BYTES); + nativeConnection.sadd(SAME_SLOT_KEY_2_BYTES, VALUE_2_BYTES, VALUE_3_BYTES); + + assertThat(clusterConnection.sDiff(SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES), hasItems(VALUE_1_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sDiffShouldWorkWhenKeysNotMapToSameSlot() { + + nativeConnection.sadd(KEY_1_BYTES, VALUE_1_BYTES, VALUE_2_BYTES); + nativeConnection.sadd(KEY_2_BYTES, VALUE_2_BYTES, VALUE_3_BYTES); + + assertThat(clusterConnection.sDiff(KEY_1_BYTES, KEY_2_BYTES), hasItems(VALUE_1_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sDiffStoreShouldWorkWhenKeysMapToSameSlot() { + + nativeConnection.sadd(SAME_SLOT_KEY_1_BYTES, VALUE_1_BYTES, VALUE_2_BYTES); + nativeConnection.sadd(SAME_SLOT_KEY_2_BYTES, VALUE_2_BYTES, VALUE_3_BYTES); + + clusterConnection.sDiffStore(SAME_SLOT_KEY_3_BYTES, SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES); + + assertThat(nativeConnection.smembers(SAME_SLOT_KEY_3_BYTES), hasItems(VALUE_1_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sDiffStoreShouldWorkWhenKeysNotMapToSameSlot() { + + nativeConnection.sadd(KEY_1_BYTES, VALUE_1_BYTES, VALUE_2_BYTES); + nativeConnection.sadd(KEY_2_BYTES, VALUE_2_BYTES, VALUE_3_BYTES); + + clusterConnection.sDiffStore(KEY_3_BYTES, KEY_1_BYTES, KEY_2_BYTES); + + assertThat(nativeConnection.smembers(KEY_3_BYTES), hasItems(VALUE_1_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sMembersShouldReturnValuesContainedInSetCorrectly() { + + nativeConnection.sadd(KEY_1, VALUE_1, VALUE_2); + + assertThat(clusterConnection.sMembers(KEY_1_BYTES), hasItems(VALUE_1_BYTES, VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sRandMamberShouldReturnValueCorrectly() { + + nativeConnection.sadd(KEY_1, VALUE_1, VALUE_2); + + assertThat(clusterConnection.sRandMember(KEY_1_BYTES), notNullValue()); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sRandMamberWithCountShouldReturnValueCorrectly() { + + nativeConnection.sadd(KEY_1, VALUE_1, VALUE_2); + + assertThat(clusterConnection.sRandMember(KEY_1_BYTES, 3), notNullValue()); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sscanShouldRetrieveAllValuesInSetCorrectly() { + + for (int i = 0; i < 30; i++) { + nativeConnection.sadd(KEY_1_BYTES, JedisConverters.toBytes(Integer.valueOf(i))); + } + + int count = 0; + Cursor cursor = clusterConnection.sScan(KEY_1_BYTES, ScanOptions.NONE); + while (cursor.hasNext()) { + count++; + cursor.next(); + } + + assertThat(count, is(30)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zAddShouldAddValueWithScoreCorrectly() { + + clusterConnection.zAdd(KEY_1_BYTES, 10D, VALUE_1_BYTES); + clusterConnection.zAdd(KEY_1_BYTES, 20D, VALUE_2_BYTES); + + assertThat(nativeConnection.zcard(KEY_1_BYTES), is(2L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRemShouldRemoveValueWithScoreCorrectly() { + + nativeConnection.zadd(KEY_1_BYTES, 10D, VALUE_1_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 20D, VALUE_2_BYTES); + + clusterConnection.zRem(KEY_1_BYTES, VALUE_1_BYTES); + + assertThat(nativeConnection.zcard(KEY_1_BYTES), is(1L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zIncrByShouldIncScoreForValueCorrectly() { + + nativeConnection.zadd(KEY_1_BYTES, 10D, VALUE_1_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 20D, VALUE_2_BYTES); + + clusterConnection.zIncrBy(KEY_1_BYTES, 100D, VALUE_1_BYTES); + + assertThat(nativeConnection.zrank(KEY_1_BYTES, VALUE_1_BYTES), is(1L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRankShouldReturnPositionForValueCorrectly() { + + nativeConnection.zadd(KEY_1_BYTES, 10D, VALUE_1_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 20D, VALUE_2_BYTES); + + assertThat(clusterConnection.zRank(KEY_1_BYTES, VALUE_2_BYTES), is(1L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRankShouldReturnReversePositionForValueCorrectly() { + + nativeConnection.zadd(KEY_1_BYTES, 10D, VALUE_1_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 20D, VALUE_2_BYTES); + + assertThat(clusterConnection.zRevRank(KEY_1_BYTES, VALUE_2_BYTES), is(0L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRangeShouldReturnValuesCorrectly() { + + nativeConnection.zadd(KEY_1_BYTES, 10D, VALUE_1_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 20D, VALUE_2_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 5D, VALUE_3_BYTES); + + assertThat(clusterConnection.zRange(KEY_1_BYTES, 1, 2), hasItems(VALUE_1_BYTES, VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRangeWithScoresShouldReturnValuesAndScoreCorrectly() { + + nativeConnection.zadd(KEY_1_BYTES, 10D, VALUE_1_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 20D, VALUE_2_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 5D, VALUE_3_BYTES); + + assertThat(clusterConnection.zRangeWithScores(KEY_1_BYTES, 1, 2), + hasItems((Tuple) new DefaultTuple(VALUE_1_BYTES, 10D), (Tuple) new DefaultTuple(VALUE_2_BYTES, 20D))); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRangeByScoreShouldReturnValuesCorrectly() { + + nativeConnection.zadd(KEY_1_BYTES, 10D, VALUE_1_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 20D, VALUE_2_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 5D, VALUE_3_BYTES); + + assertThat(clusterConnection.zRangeByScore(KEY_1_BYTES, 10, 20), hasItems(VALUE_1_BYTES, VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRangeByScoreWithScoresShouldReturnValuesAndScoreCorrectly() { + + nativeConnection.zadd(KEY_1_BYTES, 10D, VALUE_1_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 20D, VALUE_2_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 5D, VALUE_3_BYTES); + + assertThat(clusterConnection.zRangeByScoreWithScores(KEY_1_BYTES, 10, 20), + hasItems((Tuple) new DefaultTuple(VALUE_1_BYTES, 10D), (Tuple) new DefaultTuple(VALUE_2_BYTES, 20D))); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRangeByScoreShouldReturnValuesCorrectlyWhenGivenOffsetAndScore() { + + nativeConnection.zadd(KEY_1_BYTES, 10D, VALUE_1_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 20D, VALUE_2_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 5D, VALUE_3_BYTES); + + assertThat(clusterConnection.zRangeByScore(KEY_1_BYTES, 10D, 20D, 0L, 1L), hasItems(VALUE_1_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRangeByScoreWithScoresShouldReturnValuesCorrectlyWhenGivenOffsetAndScore() { + + nativeConnection.zadd(KEY_1_BYTES, 10D, VALUE_1_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 20D, VALUE_2_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 5D, VALUE_3_BYTES); + + assertThat(clusterConnection.zRangeByScoreWithScores(KEY_1_BYTES, 10D, 20D, 0L, 1L), + hasItems((Tuple) new DefaultTuple(VALUE_1_BYTES, 10D))); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRevRangeShouldReturnValuesCorrectly() { + + nativeConnection.zadd(KEY_1_BYTES, 10D, VALUE_1_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 20D, VALUE_2_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 5D, VALUE_3_BYTES); + + assertThat(clusterConnection.zRevRange(KEY_1_BYTES, 1, 2), hasItems(VALUE_3_BYTES, VALUE_1_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRevRangeWithScoresShouldReturnValuesAndScoreCorrectly() { + + nativeConnection.zadd(KEY_1_BYTES, 10D, VALUE_1_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 20D, VALUE_2_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 5D, VALUE_3_BYTES); + + assertThat(clusterConnection.zRevRangeWithScores(KEY_1_BYTES, 1, 2), + hasItems((Tuple) new DefaultTuple(VALUE_3_BYTES, 5D), (Tuple) new DefaultTuple(VALUE_1_BYTES, 10D))); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRevRangeByScoreShouldReturnValuesCorrectly() { + + nativeConnection.zadd(KEY_1_BYTES, 10D, VALUE_1_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 20D, VALUE_2_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 5D, VALUE_3_BYTES); + + assertThat(clusterConnection.zRevRangeByScore(KEY_1_BYTES, 10D, 20D), hasItems(VALUE_2_BYTES, VALUE_1_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRevRangeByScoreWithScoresShouldReturnValuesAndScoreCorrectly() { + + nativeConnection.zadd(KEY_1_BYTES, 10D, VALUE_1_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 20D, VALUE_2_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 5D, VALUE_3_BYTES); + + assertThat(clusterConnection.zRevRangeByScoreWithScores(KEY_1_BYTES, 10D, 20D), + hasItems((Tuple) new DefaultTuple(VALUE_2_BYTES, 20D), (Tuple) new DefaultTuple(VALUE_1_BYTES, 10D))); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRevRangeByScoreShouldReturnValuesCorrectlyWhenGivenOffsetAndScore() { + + nativeConnection.zadd(KEY_1_BYTES, 10D, VALUE_1_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 20D, VALUE_2_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 5D, VALUE_3_BYTES); + + assertThat(clusterConnection.zRevRangeByScore(KEY_1_BYTES, 10D, 20D, 0L, 1L), hasItems(VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRevRangeByScoreWithScoresShouldReturnValuesCorrectlyWhenGivenOffsetAndScore() { + + nativeConnection.zadd(KEY_1_BYTES, 10D, VALUE_1_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 20D, VALUE_2_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 5D, VALUE_3_BYTES); + + assertThat(clusterConnection.zRevRangeByScoreWithScores(KEY_1_BYTES, 10D, 20D, 0L, 1L), + hasItems((Tuple) new DefaultTuple(VALUE_2_BYTES, 20D))); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zCountShouldCountValuesInRange() { + + nativeConnection.zadd(KEY_1_BYTES, 10D, VALUE_1_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 20D, VALUE_2_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 5D, VALUE_3_BYTES); + + assertThat(clusterConnection.zCount(KEY_1_BYTES, 10, 20), is(2L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zCardShouldReturnTotalNumberOfValues() { + + nativeConnection.zadd(KEY_1_BYTES, 10D, VALUE_1_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 20D, VALUE_2_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 5D, VALUE_3_BYTES); + + assertThat(clusterConnection.zCard(KEY_1_BYTES), is(3L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zScoreShouldRetrieveScoreForValue() { + + nativeConnection.zadd(KEY_1_BYTES, 10D, VALUE_1_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 20D, VALUE_2_BYTES); + + assertThat(clusterConnection.zScore(KEY_1_BYTES, VALUE_2_BYTES), is(20D)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRemRangeShouldRemoveValues() { + + nativeConnection.zadd(KEY_1_BYTES, 10D, VALUE_1_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 20D, VALUE_2_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 30D, VALUE_3_BYTES); + + clusterConnection.zRemRange(KEY_1_BYTES, 1, 2); + + assertThat(nativeConnection.zcard(KEY_1_BYTES), is(1L)); + assertThat(nativeConnection.zrange(KEY_1_BYTES, 0, -1), hasItem(VALUE_1_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRemRangeByScoreShouldRemoveValues() { + + nativeConnection.zadd(KEY_1_BYTES, 10D, VALUE_1_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 20D, VALUE_2_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 30D, VALUE_3_BYTES); + + clusterConnection.zRemRangeByScore(KEY_1_BYTES, 15D, 25D); + + assertThat(nativeConnection.zcard(KEY_1_BYTES), is(2L)); + assertThat(nativeConnection.zrange(KEY_1_BYTES, 0, -1), hasItems(VALUE_1_BYTES, VALUE_3_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zUnionStoreShouldWorkForSameSlotKeys() { + + nativeConnection.zadd(SAME_SLOT_KEY_1_BYTES, 10D, VALUE_1_BYTES); + nativeConnection.zadd(SAME_SLOT_KEY_1_BYTES, 30D, VALUE_3_BYTES); + nativeConnection.zadd(SAME_SLOT_KEY_2_BYTES, 20D, VALUE_2_BYTES); + + clusterConnection.zUnionStore(SAME_SLOT_KEY_3_BYTES, SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES); + + assertThat(nativeConnection.zrange(SAME_SLOT_KEY_3_BYTES, 0, -1), + hasItems(VALUE_1_BYTES, VALUE_2_BYTES, VALUE_3_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = DataAccessException.class) + public void zUnionStoreShouldThrowExceptionWhenKeysDoNotMapToSameSlots() { + clusterConnection.zUnionStore(KEY_3_BYTES, KEY_1_BYTES, KEY_2_BYTES); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zInterStoreShouldWorkForSameSlotKeys() { + + nativeConnection.zadd(SAME_SLOT_KEY_1_BYTES, 10D, VALUE_1_BYTES); + nativeConnection.zadd(SAME_SLOT_KEY_1_BYTES, 20D, VALUE_2_BYTES); + + nativeConnection.zadd(SAME_SLOT_KEY_2_BYTES, 20D, VALUE_2_BYTES); + nativeConnection.zadd(SAME_SLOT_KEY_2_BYTES, 30D, VALUE_3_BYTES); + + clusterConnection.zInterStore(SAME_SLOT_KEY_3_BYTES, SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES); + + assertThat(nativeConnection.zrange(SAME_SLOT_KEY_3_BYTES, 0, -1), hasItems(VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = DataAccessException.class) + public void zInterStoreShouldThrowExceptionWhenKeysDoNotMapToSameSlots() { + clusterConnection.zInterStore(KEY_3_BYTES, KEY_1_BYTES, KEY_2_BYTES); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void hSetShouldSetValueCorrectly() { + + clusterConnection.hSet(KEY_1_BYTES, KEY_2_BYTES, VALUE_1_BYTES); + + assertThat(nativeConnection.hget(KEY_1_BYTES, KEY_2_BYTES), is(VALUE_1_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void hSetNXShouldSetValueCorrectly() { + + clusterConnection.hSetNX(KEY_1_BYTES, KEY_2_BYTES, VALUE_1_BYTES); + + assertThat(nativeConnection.hget(KEY_1_BYTES, KEY_2_BYTES), is(VALUE_1_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void hSetNXShouldNotSetValueWhenAlreadyExists() { + + nativeConnection.hset(KEY_1_BYTES, KEY_2_BYTES, VALUE_1_BYTES); + + clusterConnection.hSetNX(KEY_1_BYTES, KEY_2_BYTES, VALUE_2_BYTES); + + assertThat(nativeConnection.hget(KEY_1_BYTES, KEY_2_BYTES), is(VALUE_1_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void hGetShouldRetrieveValueCorrectly() { + + nativeConnection.hset(KEY_1_BYTES, KEY_2_BYTES, VALUE_1_BYTES); + + assertThat(clusterConnection.hGet(KEY_1_BYTES, KEY_2_BYTES), is(VALUE_1_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void hMGetShouldRetrieveValueCorrectly() { + + nativeConnection.hset(KEY_1_BYTES, KEY_2_BYTES, VALUE_1_BYTES); + nativeConnection.hset(KEY_1_BYTES, KEY_3_BYTES, VALUE_2_BYTES); + + assertThat(clusterConnection.hMGet(KEY_1_BYTES, KEY_2_BYTES, KEY_3_BYTES), hasItems(VALUE_1_BYTES, VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void hMSetShouldAddValuesCorrectly() { + + Map hashes = new HashMap(); + hashes.put(KEY_2_BYTES, VALUE_1_BYTES); + hashes.put(KEY_3_BYTES, VALUE_2_BYTES); + + clusterConnection.hMSet(KEY_1_BYTES, hashes); + + assertThat(nativeConnection.hmget(KEY_1_BYTES, KEY_2_BYTES, KEY_3_BYTES), hasItems(VALUE_1_BYTES, VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void hIncrByShouldIncreaseFieldCorretly() { + + nativeConnection.hset(KEY_1_BYTES, KEY_2_BYTES, JedisConverters.toBytes(1L)); + nativeConnection.hset(KEY_1_BYTES, KEY_3_BYTES, JedisConverters.toBytes(2L)); + + clusterConnection.hIncrBy(KEY_1_BYTES, KEY_3_BYTES, 3); + + assertThat(nativeConnection.hget(KEY_1_BYTES, KEY_3_BYTES), is(JedisConverters.toBytes(5L))); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void hIncrByFloatShouldIncreaseFieldCorretly() { + + nativeConnection.hset(KEY_1_BYTES, KEY_2_BYTES, JedisConverters.toBytes(1L)); + nativeConnection.hset(KEY_1_BYTES, KEY_3_BYTES, JedisConverters.toBytes(2L)); + + clusterConnection.hIncrBy(KEY_1_BYTES, KEY_3_BYTES, 3.5D); + + assertThat(nativeConnection.hget(KEY_1_BYTES, KEY_3_BYTES), is(JedisConverters.toBytes(5.5D))); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void hExistsShouldReturnPresenceOfFieldCorrectly() { + + nativeConnection.hset(KEY_1_BYTES, KEY_2_BYTES, VALUE_1_BYTES); + + assertThat(clusterConnection.hExists(KEY_1_BYTES, KEY_2_BYTES), is(true)); + assertThat(clusterConnection.hExists(KEY_1_BYTES, KEY_3_BYTES), is(false)); + assertThat(clusterConnection.hExists(JedisConverters.toBytes("foo"), KEY_2_BYTES), is(false)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void hDelShouldRemoveFieldsCorrectly() { + + nativeConnection.hset(KEY_1_BYTES, KEY_2_BYTES, VALUE_1_BYTES); + nativeConnection.hset(KEY_1_BYTES, KEY_3_BYTES, VALUE_2_BYTES); + + clusterConnection.hDel(KEY_1_BYTES, KEY_2_BYTES); + + assertThat(nativeConnection.hexists(KEY_1_BYTES, KEY_2_BYTES), is(false)); + assertThat(nativeConnection.hexists(KEY_1_BYTES, KEY_3_BYTES), is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void hLenShouldRetrieveSizeCorrectly() { + + nativeConnection.hset(KEY_1_BYTES, KEY_2_BYTES, VALUE_1_BYTES); + nativeConnection.hset(KEY_1_BYTES, KEY_3_BYTES, VALUE_2_BYTES); + + assertThat(clusterConnection.hLen(KEY_1_BYTES), is(2L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void hKeysShouldRetrieveKeysCorrectly() { + + nativeConnection.hset(KEY_1_BYTES, KEY_2_BYTES, VALUE_1_BYTES); + nativeConnection.hset(KEY_1_BYTES, KEY_3_BYTES, VALUE_2_BYTES); + + assertThat(clusterConnection.hKeys(KEY_1_BYTES), hasItems(KEY_2_BYTES, KEY_3_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void hValsShouldRetrieveValuesCorrectly() { + + nativeConnection.hset(KEY_1_BYTES, KEY_2_BYTES, VALUE_1_BYTES); + nativeConnection.hset(KEY_1_BYTES, KEY_3_BYTES, VALUE_2_BYTES); + + assertThat(clusterConnection.hVals(KEY_1_BYTES), hasItems(VALUE_1_BYTES, VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void hGetAllShouldRetrieveEntriesCorrectly() { + + Map hashes = new HashMap(); + hashes.put(KEY_2_BYTES, VALUE_1_BYTES); + hashes.put(KEY_3_BYTES, VALUE_2_BYTES); + + nativeConnection.hmset(KEY_1_BYTES, hashes); + + Map hGetAll = clusterConnection.hGetAll(KEY_1_BYTES); + + assertThat(hGetAll.containsKey(KEY_2_BYTES), is(true)); + assertThat(hGetAll.containsKey(KEY_3_BYTES), is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = DataAccessException.class) + public void multiShouldThrowException() { + clusterConnection.multi(); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = DataAccessException.class) + public void execShouldThrowException() { + clusterConnection.exec(); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = DataAccessException.class) + public void discardShouldThrowException() { + clusterConnection.discard(); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = DataAccessException.class) + public void watchShouldThrowException() { + clusterConnection.watch(); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = DataAccessException.class) + public void unwatchShouldThrowException() { + clusterConnection.unwatch(); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void selectShouldAllowSelectionOfDBIndexZero() { + clusterConnection.select(0); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = DataAccessException.class) + public void selectShouldThrowExceptionWhenSelectingNonZeroDbIndex() { + clusterConnection.select(1); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void echoShouldReturnInputCorrectly() { + assertThat(clusterConnection.echo(VALUE_1_BYTES), is(VALUE_1_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void pingShouldRetrunPongForExistingNode() { + assertThat(clusterConnection.ping(new RedisClusterNode("127.0.0.1", 7379, null)), is("PONG")); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void pingShouldRetrunPong() { + assertThat(clusterConnection.ping(), is("PONG")); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = IllegalArgumentException.class) + public void pingShouldThrowExceptionWhenNodeNotKnownToCluster() { + clusterConnection.ping(new RedisClusterNode("127.0.0.1", 1234, null)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void flushDbShouldFlushAllClusterNodes() { + + nativeConnection.set(KEY_1, VALUE_1); + nativeConnection.set(KEY_2, VALUE_2); + + clusterConnection.flushDb(); + + assertThat(nativeConnection.get(KEY_1), nullValue()); + assertThat(nativeConnection.get(KEY_2), nullValue()); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void flushDbOnSingleNodeShouldFlushOnlyGivenNodesDb() { + + nativeConnection.set(KEY_1, VALUE_1); + nativeConnection.set(KEY_2, VALUE_2); + + clusterConnection.flushDb(new RedisClusterNode("127.0.0.1", 7379, null)); + + assertThat(nativeConnection.get(KEY_1), notNullValue()); + assertThat(nativeConnection.get(KEY_2), nullValue()); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRangeByLexShouldReturnResultCorrectly() { + + nativeConnection.zadd(KEY_1, 0, "a"); + nativeConnection.zadd(KEY_1, 0, "b"); + nativeConnection.zadd(KEY_1, 0, "c"); + nativeConnection.zadd(KEY_1, 0, "d"); + nativeConnection.zadd(KEY_1, 0, "e"); + nativeConnection.zadd(KEY_1, 0, "f"); + nativeConnection.zadd(KEY_1, 0, "g"); + + Set values = clusterConnection.zRangeByLex(KEY_1_BYTES, Range.range().lte("c")); + + assertThat(values, + hasItems(JedisConverters.toBytes("a"), JedisConverters.toBytes("b"), JedisConverters.toBytes("c"))); + assertThat( + values, + not(hasItems(JedisConverters.toBytes("d"), JedisConverters.toBytes("e"), JedisConverters.toBytes("f"), + JedisConverters.toBytes("g")))); + + values = clusterConnection.zRangeByLex(KEY_1_BYTES, Range.range().lt("c")); + assertThat(values, hasItems(JedisConverters.toBytes("a"), JedisConverters.toBytes("b"))); + assertThat(values, not(hasItem(JedisConverters.toBytes("c")))); + + values = clusterConnection.zRangeByLex(KEY_1_BYTES, Range.range().gte("aaa").lt("g")); + assertThat( + values, + hasItems(JedisConverters.toBytes("b"), JedisConverters.toBytes("c"), JedisConverters.toBytes("d"), + JedisConverters.toBytes("e"), JedisConverters.toBytes("f"))); + assertThat(values, not(hasItems(JedisConverters.toBytes("a"), JedisConverters.toBytes("g")))); + + values = clusterConnection.zRangeByLex(KEY_1_BYTES, Range.range().gte("e")); + assertThat(values, + hasItems(JedisConverters.toBytes("e"), JedisConverters.toBytes("f"), JedisConverters.toBytes("g"))); + assertThat( + values, + not(hasItems(JedisConverters.toBytes("a"), JedisConverters.toBytes("b"), JedisConverters.toBytes("c"), + JedisConverters.toBytes("d")))); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void infoShouldCollectionInfoFromAllClusterNodes() { + assertThat(clusterConnection.info().size(), is(3)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void clientListShouldGetInfosForAllClients() { + assertThat(clusterConnection.getClientList().isEmpty(), is(false)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void getClusterNodeForKeyShouldReturnNodeCorrectly() { + assertThat((RedisNode) clusterConnection.clusterGetNodeForKey(KEY_1_BYTES), is(new RedisNode("127.0.0.1", 7380))); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void countKeysShouldReturnNumberOfKeysInSlot() { + + nativeConnection.set(SAME_SLOT_KEY_1, VALUE_1); + nativeConnection.set(SAME_SLOT_KEY_2, VALUE_2); + + assertThat(clusterConnection.clusterCountKeysInSlot(ClusterSlotHashUtil.calculateSlot(SAME_SLOT_KEY_1)), is(2L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void pfAddShouldAddValuesCorrectly() { + + clusterConnection.pfAdd(KEY_1_BYTES, VALUE_1_BYTES, VALUE_2_BYTES, VALUE_3_BYTES); + + assertThat(nativeConnection.pfcount(KEY_1_BYTES), is(3L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void pfCountShouldAllowCountingOnSingleKey() { + + nativeConnection.pfadd(KEY_1, VALUE_1, VALUE_2, VALUE_3); + + assertThat(clusterConnection.pfCount(KEY_1_BYTES), is(3L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void pfCountShouldAllowCountingOnSameSlotKeys() { + + nativeConnection.pfadd(SAME_SLOT_KEY_1, VALUE_1, VALUE_2); + nativeConnection.pfadd(SAME_SLOT_KEY_2, VALUE_2, VALUE_3); + + assertThat(clusterConnection.pfCount(SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES), is(3L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = DataAccessException.class) + public void pfCountShouldThrowErrorCountingOnDifferentSlotKeys() { + + nativeConnection.pfadd(KEY_1, VALUE_1, VALUE_2); + nativeConnection.pfadd(KEY_2, VALUE_2, VALUE_3); + + clusterConnection.pfCount(KEY_1_BYTES, KEY_2_BYTES); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void pfMergeShouldWorkWhenAllKeysMapToSameSlot() { + + nativeConnection.pfadd(SAME_SLOT_KEY_1, VALUE_1, VALUE_2); + nativeConnection.pfadd(SAME_SLOT_KEY_2, VALUE_2, VALUE_3); + + clusterConnection.pfMerge(SAME_SLOT_KEY_3_BYTES, SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES); + + assertThat(nativeConnection.pfcount(SAME_SLOT_KEY_3), is(3L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = DataAccessException.class) + public void pfMergeShouldThrowErrorOnDifferentSlotKeys() { + clusterConnection.pfMerge(KEY_3_BYTES, KEY_1_BYTES, KEY_2_BYTES); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void infoShouldCollectInfoForSpecificNode() { + + Properties properties = clusterConnection.info(new RedisClusterNode(CLUSTER_HOST, MASTER_NODE_2_PORT)); + + assertThat(properties.getProperty("tcp_port"), is(Integer.toString(MASTER_NODE_2_PORT))); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void infoShouldCollectInfoForSpecificNodeAndSection() { + + Properties properties = clusterConnection.info(new RedisClusterNode(CLUSTER_HOST, MASTER_NODE_2_PORT), "server"); + + assertThat(properties.getProperty("tcp_port"), is(Integer.toString(MASTER_NODE_2_PORT))); + assertThat(properties.getProperty("used_memory"), nullValue()); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void getConfigShouldLoadCumulatedConfiguration() { + + List result = clusterConnection.getConfig("*max-*-entries*"); + + assertThat(result.size(), is(24)); + for (int i = 0; i < result.size(); i++) { + + if (i % 2 == 0) { + assertThat(result.get(i), startsWith(CLUSTER_HOST)); + } else { + assertThat(result.get(i), not(startsWith(CLUSTER_HOST))); + } + } + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void getConfigShouldLoadConfigurationOfSpecificNode() { + + List result = clusterConnection.getConfig(new RedisClusterNode(CLUSTER_HOST, SLAVEOF_NODE_1_PORT), "*"); + + ListIterator it = result.listIterator(); + Integer valueIndex = null; + while (it.hasNext()) { + + String cur = it.next(); + if (cur.equals("slaveof")) { + valueIndex = it.nextIndex(); + break; + } + } + + assertThat(valueIndex, notNullValue()); + assertThat(result.get(valueIndex), endsWith("7379")); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void clusterGetSlavesShouldReturnSlaveCorrectly() { + + Set slaves = clusterConnection.clusterGetSlaves(new RedisClusterNode(CLUSTER_HOST, + MASTER_NODE_1_PORT)); + + assertThat(slaves.size(), is(1)); + assertThat(slaves, hasItem(new RedisClusterNode(CLUSTER_HOST, SLAVEOF_NODE_1_PORT))); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void clusterGetMasterSlaveMapShouldListMastersAndSlavesCorrectly() { + + Map> masterSlaveMap = clusterConnection.clusterGetMasterSlaveMap(); + + assertThat(masterSlaveMap, notNullValue()); + assertThat(masterSlaveMap.size(), is(3)); + assertThat(masterSlaveMap.get(new RedisClusterNode(CLUSTER_HOST, MASTER_NODE_1_PORT)), + hasItem(new RedisClusterNode(CLUSTER_HOST, SLAVEOF_NODE_1_PORT))); + assertThat(masterSlaveMap.get(new RedisClusterNode(CLUSTER_HOST, MASTER_NODE_2_PORT)).isEmpty(), is(true)); + assertThat(masterSlaveMap.get(new RedisClusterNode(CLUSTER_HOST, MASTER_NODE_3_PORT)).isEmpty(), is(true)); + } +} diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionUnitTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionUnitTests.java new file mode 100644 index 000000000..16941f849 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionUnitTests.java @@ -0,0 +1,361 @@ +/* + * Copyright 2015 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.jedis; + +import static org.hamcrest.core.Is.*; +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; +import static org.springframework.data.redis.connection.ClusterTestVariables.*; +import static org.springframework.data.redis.test.util.MockitoUtils.*; + +import java.io.IOException; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.redis.ClusterStateFailureExeption; +import org.springframework.data.redis.connection.ClusterInfo; +import org.springframework.data.redis.connection.RedisClusterCommands.AddSlots; +import org.springframework.data.redis.connection.RedisClusterNode; +import org.springframework.data.redis.connection.jedis.JedisClusterConnection.JedisClusterTopologyProvider; + +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisCluster; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.exceptions.JedisConnectionException; + +/** + * @author Christoph Strobl + */ +@RunWith(MockitoJUnitRunner.class) +public class JedisClusterConnectionUnitTests { + + private static final String CLUSTER_NODES_RESPONSE = "" // + + MASTER_NODE_1_ID + " " + CLUSTER_HOST + ":" + MASTER_NODE_1_PORT + + " myself,master - 0 0 1 connected 0-5460" + + System.getProperty("line.separator") + MASTER_NODE_2_ID + " " + CLUSTER_HOST + ":" + + MASTER_NODE_2_PORT + + " master - 0 1427718161587 2 connected 5461-10922" + System.getProperty("line.separator") + + MASTER_NODE_2_ID + + " " + CLUSTER_HOST + ":" + MASTER_NODE_3_PORT + " master - 0 1427718161587 3 connected 10923-16383"; + + static final String CLUSTER_INFO_RESPONSE = "cluster_state:ok" + System.getProperty("line.separator") + + "cluster_slots_assigned:16384" + System.getProperty("line.separator") + "cluster_slots_ok:16384" + + System.getProperty("line.separator") + "cluster_slots_pfail:0" + System.getProperty("line.separator") + + "cluster_slots_fail:0" + System.getProperty("line.separator") + "cluster_known_nodes:4" + + System.getProperty("line.separator") + "cluster_size:3" + System.getProperty("line.separator") + + "cluster_current_epoch:30" + System.getProperty("line.separator") + "cluster_my_epoch:2" + + System.getProperty("line.separator") + "cluster_stats_messages_sent:2560260" + + System.getProperty("line.separator") + "cluster_stats_messages_received:2560086"; + + JedisClusterConnection connection; + + @Mock JedisCluster clusterMock; + + @Mock JedisPool node1PoolMock; + @Mock JedisPool node2PoolMock; + @Mock JedisPool node3PoolMock; + + @Mock Jedis con1Mock; + @Mock Jedis con2Mock; + @Mock Jedis con3Mock; + + public @Rule ExpectedException expectedException = ExpectedException.none(); + + @Before + public void setUp() { + + Map nodes = new LinkedHashMap(3); + nodes.put(CLUSTER_HOST + ":" + MASTER_NODE_1_PORT, node1PoolMock); + nodes.put(CLUSTER_HOST + ":" + MASTER_NODE_2_PORT, node2PoolMock); + nodes.put(CLUSTER_HOST + ":" + MASTER_NODE_3_PORT, node3PoolMock); + + when(clusterMock.getClusterNodes()).thenReturn(nodes); + when(node1PoolMock.getResource()).thenReturn(con1Mock); + when(node2PoolMock.getResource()).thenReturn(con2Mock); + when(node3PoolMock.getResource()).thenReturn(con3Mock); + + when(con1Mock.clusterNodes()).thenReturn(CLUSTER_NODES_RESPONSE); + + connection = new JedisClusterConnection(clusterMock); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void thowsExceptionWhenClusterCommandExecturorIsNull() { + + expectedException.expect(IllegalArgumentException.class); + + new JedisClusterConnection(clusterMock, null); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void clusterMeetShouldSendCommandsToExistingNodesCorrectly() { + + connection.clusterMeet(UNKNOWN_CLUSTER_NODE); + + verify(con1Mock, times(1)).clusterMeet(UNKNOWN_CLUSTER_NODE.getHost(), UNKNOWN_CLUSTER_NODE.getPort()); + verify(con2Mock, times(1)).clusterMeet(UNKNOWN_CLUSTER_NODE.getHost(), UNKNOWN_CLUSTER_NODE.getPort()); + verify(con2Mock, times(1)).clusterMeet(UNKNOWN_CLUSTER_NODE.getHost(), UNKNOWN_CLUSTER_NODE.getPort()); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void clusterMeetShouldThrowExceptionWhenNodeIsNull() { + + expectedException.expect(IllegalArgumentException.class); + + connection.clusterMeet(null); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void clusterForgetShouldSendCommandsToRemainingNodesCorrectly() { + + connection.clusterForget(CLUSTER_NODE_2); + + verify(con1Mock, times(1)).clusterForget(CLUSTER_NODE_2.getId()); + verifyZeroInteractions(con2Mock); + verify(con3Mock, times(1)).clusterForget(CLUSTER_NODE_2.getId()); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void clusterReplicateShouldSendCommandsCorrectly() { + + connection.clusterReplicate(CLUSTER_NODE_1, CLUSTER_NODE_2); + + verify(con2Mock, times(1)).clusterReplicate(CLUSTER_NODE_1.getId()); + verifyZeroInteractions(con1Mock); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void closeShouldNotCloseUnderlyingClusterPool() throws IOException { + + connection.close(); + + verify(clusterMock, never()).close(); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void isClosedShouldReturnConnectionStateCorrectly() { + + assertThat(connection.isClosed(), is(false)); + + connection.close(); + + assertThat(connection.isClosed(), is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void clusterInfoShouldBeReturnedCorrectly() { + + when(con1Mock.clusterInfo()).thenReturn(CLUSTER_INFO_RESPONSE); + when(con2Mock.clusterInfo()).thenReturn(CLUSTER_INFO_RESPONSE); + when(con3Mock.clusterInfo()).thenReturn(CLUSTER_INFO_RESPONSE); + + ClusterInfo p = connection.clusterGetClusterInfo(); + assertThat(p.getSlotsAssigned(), is(16384L)); + + verifyInvocationsAcross("clusterInfo", times(1), con1Mock, con2Mock, con3Mock); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void clusterSetSlotImportingShouldBeExecutedCorrectly() { + + connection.clusterSetSlot(CLUSTER_NODE_1, 100, AddSlots.IMPORTING); + + verify(con1Mock, times(1)).clusterSetSlotImporting(eq(100), eq(CLUSTER_NODE_1.getId())); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void clusterSetSlotMigratingShouldBeExecutedCorrectly() { + + connection.clusterSetSlot(CLUSTER_NODE_1, 100, AddSlots.MIGRATING); + + verify(con1Mock, times(1)).clusterSetSlotMigrating(eq(100), eq(CLUSTER_NODE_1.getId())); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void clusterSetSlotStableShouldBeExecutedCorrectly() { + + connection.clusterSetSlot(CLUSTER_NODE_1, 100, AddSlots.STABLE); + + verify(con1Mock, times(1)).clusterSetSlotStable(eq(100)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void clusterSetSlotNodeShouldBeExecutedCorrectly() { + + connection.clusterSetSlot(CLUSTER_NODE_1, 100, AddSlots.NODE); + + verify(con1Mock, times(1)).clusterSetSlotNode(eq(100), eq(CLUSTER_NODE_1.getId())); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void clusterSetSlotShouldBeExecutedOnTargetNodeWhenNodeIdNotSet() { + + connection.clusterSetSlot(new RedisClusterNode(CLUSTER_HOST, MASTER_NODE_2_PORT), 100, AddSlots.IMPORTING); + + verify(con2Mock, times(1)).clusterSetSlotImporting(eq(100), eq(CLUSTER_NODE_2.getId())); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = IllegalArgumentException.class) + public void clusterSetSlotShouldThrowExceptionWhenModeIsNull() { + connection.clusterSetSlot(CLUSTER_NODE_1, 100, null); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void clusterDeleteSlotsShouldBeExecutedCorrectly() { + + int[] slots = new int[] { 9000, 10000 }; + connection.clusterDeleteSlots(CLUSTER_NODE_2, slots); + + verify(con2Mock, times(1)).clusterDelSlots((int[]) anyVararg()); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = IllegalArgumentException.class) + public void clusterDeleteSlotShouldThrowExceptionWhenNodeIsNull() { + connection.clusterDeleteSlots(null, new int[] { 1 }); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void timeShouldBeExecutedOnArbitraryNode() { + + List values = Arrays.asList("1449655759", "92217"); + when(con1Mock.time()).thenReturn(values); + when(con2Mock.time()).thenReturn(values); + when(con3Mock.time()).thenReturn(values); + + connection.time(); + + verifyInvocationsAcross("time", times(1), con1Mock, con2Mock, con3Mock); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void timeShouldBeExecutedOnSingleNode() { + + when(con2Mock.time()).thenReturn(Arrays.asList("1449655759", "92217")); + + connection.time(CLUSTER_NODE_2); + + verify(con2Mock, times(1)).time(); + verifyZeroInteractions(con1Mock, con3Mock); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void resetConfigStatsShouldBeExecutedOnAllNodes() { + + connection.resetConfigStats(); + + verify(con1Mock, times(1)).configResetStat(); + verify(con2Mock, times(1)).configResetStat(); + verify(con3Mock, times(1)).configResetStat(); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void resetConfigStatsShouldBeExecutedOnSingleNodeCorrectly() { + + connection.resetConfigStats(CLUSTER_NODE_2); + + verify(con2Mock, times(1)).configResetStat(); + verify(con1Mock, never()).configResetStat(); + verify(con3Mock, never()).configResetStat(); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void clusterTopologyProviderShouldCollectErrorsWhenLoadingNodes() { + + expectedException.expect(ClusterStateFailureExeption.class); + expectedException.expectMessage("127.0.0.1:7379 failed: o.O"); + expectedException.expectMessage("127.0.0.1:7380 failed: o.1"); + expectedException.expectMessage("127.0.0.1:7381 failed: o.2"); + + when(con1Mock.clusterNodes()).thenThrow(new JedisConnectionException("o.O")); + when(con2Mock.clusterNodes()).thenThrow(new JedisConnectionException("o.1")); + when(con3Mock.clusterNodes()).thenThrow(new JedisConnectionException("o.2")); + + new JedisClusterTopologyProvider(clusterMock).getTopology(); + } +} diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryUnitTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryUnitTests.java index 50aa8bcb8..60b538593 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2015 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. @@ -17,10 +17,16 @@ package org.springframework.data.redis.connection.jedis; import static org.mockito.Mockito.*; +import java.io.IOException; + +import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.junit.Test; import org.mockito.Matchers; +import org.springframework.data.redis.connection.RedisClusterConfiguration; import org.springframework.data.redis.connection.RedisSentinelConfiguration; +import org.springframework.test.util.ReflectionTestUtils; +import redis.clients.jedis.JedisCluster; import redis.clients.jedis.JedisPoolConfig; /** @@ -33,6 +39,9 @@ public class JedisConnectionFactoryUnitTests { private static final RedisSentinelConfiguration SINGLE_SENTINEL_CONFIG = new RedisSentinelConfiguration().master( "mymaster").sentinel("127.0.0.1", 26379); + private static final RedisClusterConfiguration CLUSTER_CONFIG = new RedisClusterConfiguration().clusterNode( + "127.0.0.1", 6379).clusterNode("127.0.0.1", 6380); + /** * @see DATAREDIS-324 */ @@ -52,13 +61,43 @@ public class JedisConnectionFactoryUnitTests { @Test public void shouldInitJedisPoolWhenNoSentinelConfigPresent() { - connectionFactory = initSpyedConnectionFactory(null, new JedisPoolConfig()); + connectionFactory = initSpyedConnectionFactory((RedisSentinelConfiguration) null, new JedisPoolConfig()); connectionFactory.afterPropertiesSet(); verify(connectionFactory, times(1)).createRedisPool(); verify(connectionFactory, never()).createRedisSentinelPool(Matchers.any(RedisSentinelConfiguration.class)); } + /** + * @see DATAREDIS-315 + */ + @Test + public void shouldInitConnectionCorrectlyWhenClusterConfigPresent() { + + connectionFactory = initSpyedConnectionFactory(CLUSTER_CONFIG, new JedisPoolConfig()); + connectionFactory.afterPropertiesSet(); + + verify(connectionFactory, times(1)).createCluster(Matchers.eq(CLUSTER_CONFIG), + Matchers.any(GenericObjectPoolConfig.class)); + verify(connectionFactory, never()).createRedisPool(); + } + + /** + * @throws IOException + * @see DATAREDIS-315 + */ + @Test + public void shouldClostClusterCorrectlyOnFactoryDestruction() throws IOException { + + JedisCluster clusterMock = mock(JedisCluster.class); + JedisConnectionFactory factory = new JedisConnectionFactory(); + ReflectionTestUtils.setField(factory, "cluster", clusterMock); + + factory.destroy(); + + verify(clusterMock, times(1)).close(); + } + private JedisConnectionFactory initSpyedConnectionFactory(RedisSentinelConfiguration sentinelConfig, JedisPoolConfig poolConfig) { @@ -68,4 +107,14 @@ public class JedisConnectionFactoryUnitTests { doReturn(null).when(factorySpy).createRedisPool(); return factorySpy; } + + private JedisConnectionFactory initSpyedConnectionFactory(RedisClusterConfiguration clusterConfig, + JedisPoolConfig poolConfig) { + + JedisConnectionFactory factorySpy = spy(new JedisConnectionFactory(clusterConfig)); + doReturn(null).when(factorySpy).createCluster(Matchers.any(RedisClusterConfiguration.class), + Matchers.any(GenericObjectPoolConfig.class)); + doReturn(null).when(factorySpy).createRedisPool(); + return factorySpy; + } } diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java index 4757185b1..9680a302c 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java @@ -115,7 +115,6 @@ public class JedisConvertersUnitTests { } /** - * @see DATAREDIS-378 */ @Test public void boundaryToBytesForZRangeByLexShouldReturnDefaultValueWhenBoundaryIsNull() { diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisExceptionConverterUnitTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisExceptionConverterUnitTests.java new file mode 100644 index 000000000..1a47d6617 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisExceptionConverterUnitTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2015 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.jedis; + +import static org.hamcrest.core.Is.*; +import static org.hamcrest.core.IsInstanceOf.*; +import static org.junit.Assert.*; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.dao.DataAccessException; +import org.springframework.data.redis.ClusterRedirectException; +import org.springframework.data.redis.TooManyClusterRedirectionsException; + +import redis.clients.jedis.HostAndPort; +import redis.clients.jedis.exceptions.JedisAskDataException; +import redis.clients.jedis.exceptions.JedisClusterMaxRedirectionsException; +import redis.clients.jedis.exceptions.JedisMovedDataException; + +/** + * @author Christoph Strobl + */ +public class JedisExceptionConverterUnitTests { + + JedisExceptionConverter converter; + + @Before + public void setUp() { + converter = new JedisExceptionConverter(); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void shouldConvertMovedDataException() { + + DataAccessException converted = converter.convert(new JedisMovedDataException("MOVED 3999 127.0.0.1:6381", + new HostAndPort("127.0.0.1", 6381), 3999)); + + assertThat(converted, instanceOf(ClusterRedirectException.class)); + assertThat(((ClusterRedirectException) converted).getSlot(), is(3999)); + assertThat(((ClusterRedirectException) converted).getTargetHost(), is("127.0.0.1")); + assertThat(((ClusterRedirectException) converted).getTargetPort(), is(6381)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void shouldConvertAskDataException() { + + DataAccessException converted = converter.convert(new JedisAskDataException("ASK 3999 127.0.0.1:6381", + new HostAndPort("127.0.0.1", 6381), 3999)); + + assertThat(converted, instanceOf(ClusterRedirectException.class)); + assertThat(((ClusterRedirectException) converted).getSlot(), is(3999)); + assertThat(((ClusterRedirectException) converted).getTargetHost(), is("127.0.0.1")); + assertThat(((ClusterRedirectException) converted).getTargetPort(), is(6381)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void shouldConvertMaxRedirectException() { + + DataAccessException converted = converter + .convert(new JedisClusterMaxRedirectionsException("Too many redirections?")); + + assertThat(converted, instanceOf(TooManyClusterRedirectionsException.class)); + } +} diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionTests.java new file mode 100644 index 000000000..8157c6773 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionTests.java @@ -0,0 +1,2302 @@ +/* + * Copyright 2015 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.lettuce; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.springframework.data.redis.connection.ClusterTestVariables.*; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Ignore; +import org.junit.Test; +import org.springframework.dao.DataAccessException; +import org.springframework.data.redis.connection.ClusterConnectionTests; +import org.springframework.data.redis.connection.ClusterSlotHashUtil; +import org.springframework.data.redis.connection.DataType; +import org.springframework.data.redis.connection.DefaultSortParameters; +import org.springframework.data.redis.connection.DefaultTuple; +import org.springframework.data.redis.connection.RedisClusterNode; +import org.springframework.data.redis.connection.RedisListCommands.Position; +import org.springframework.data.redis.connection.RedisNode; +import org.springframework.data.redis.connection.RedisStringCommands.BitOperation; +import org.springframework.data.redis.connection.RedisZSetCommands.Range; +import org.springframework.data.redis.connection.RedisZSetCommands.Tuple; +import org.springframework.data.redis.connection.jedis.JedisConverters; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.test.util.RedisClusterRule; + +import com.lambdaworks.redis.RedisURI.Builder; +import com.lambdaworks.redis.cluster.RedisAdvancedClusterConnection; +import com.lambdaworks.redis.cluster.RedisClusterClient; + +/** + * @author Christoph Strobl + */ +public class LettuceClusterConnectionTests implements ClusterConnectionTests { + + static final byte[] KEY_1_BYTES = LettuceConverters.toBytes(KEY_1); + static final byte[] KEY_2_BYTES = LettuceConverters.toBytes(KEY_2); + static final byte[] KEY_3_BYTES = LettuceConverters.toBytes(KEY_3); + + static final byte[] SAME_SLOT_KEY_1_BYTES = LettuceConverters.toBytes(SAME_SLOT_KEY_1); + static final byte[] SAME_SLOT_KEY_2_BYTES = LettuceConverters.toBytes(SAME_SLOT_KEY_2); + static final byte[] SAME_SLOT_KEY_3_BYTES = LettuceConverters.toBytes(SAME_SLOT_KEY_3); + + static final byte[] VALUE_1_BYTES = LettuceConverters.toBytes(VALUE_1); + static final byte[] VALUE_2_BYTES = LettuceConverters.toBytes(VALUE_2); + static final byte[] VALUE_3_BYTES = LettuceConverters.toBytes(VALUE_3); + + RedisClusterClient client; + RedisAdvancedClusterConnection nativeConnection; + LettuceClusterConnection clusterConnection; + + public static @ClassRule RedisClusterRule clusterAvailable = new RedisClusterRule(); + + @Before + public void setUp() { + + client = new RedisClusterClient(Builder.redis(CLUSTER_HOST, MASTER_NODE_1_PORT) + .withTimeout(100, TimeUnit.MILLISECONDS).build()); + nativeConnection = client.connectCluster(); + clusterConnection = new LettuceClusterConnection(client); + } + + @After + public void tearDown() throws InterruptedException { + + clusterConnection.flushDb(); + nativeConnection.close(); + clusterConnection.close(); + client.shutdown(0, 0, TimeUnit.MILLISECONDS); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void shouldAllowSettingAndGettingValues() { + + clusterConnection.set(KEY_1_BYTES, VALUE_1_BYTES); + assertThat(clusterConnection.get(KEY_1_BYTES), is(VALUE_1_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void delShouldRemoveSingleKeyCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + clusterConnection.del(KEY_1_BYTES); + + assertThat(nativeConnection.get(KEY_1), nullValue()); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void delShouldRemoveMultipleKeysCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + nativeConnection.set(KEY_2, VALUE_2); + + clusterConnection.del(KEY_1_BYTES); + clusterConnection.del(KEY_2_BYTES); + + assertThat(nativeConnection.get(KEY_1), nullValue()); + assertThat(nativeConnection.get(KEY_2), nullValue()); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void delShouldRemoveMultipleKeysOnSameSlotCorrectly() { + + nativeConnection.set(SAME_SLOT_KEY_1, VALUE_1); + nativeConnection.set(SAME_SLOT_KEY_2, VALUE_2); + + clusterConnection.del(SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES); + + assertThat(nativeConnection.get(SAME_SLOT_KEY_1), nullValue()); + assertThat(nativeConnection.get(SAME_SLOT_KEY_2), nullValue()); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void typeShouldReadKeyTypeCorrectly() { + + nativeConnection.sadd(KEY_1, VALUE_1); + nativeConnection.set(KEY_2, VALUE_2); + nativeConnection.hmset(KEY_3, Collections.singletonMap(KEY_1, VALUE_1)); + + assertThat(clusterConnection.type(KEY_1_BYTES), is(DataType.SET)); + assertThat(clusterConnection.type(KEY_2_BYTES), is(DataType.STRING)); + assertThat(clusterConnection.type(KEY_3_BYTES), is(DataType.HASH)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void keysShouldReturnAllKeys() { + + nativeConnection.set(KEY_1, VALUE_1); + nativeConnection.set(KEY_2, VALUE_2); + + assertThat(clusterConnection.keys(LettuceConverters.toBytes("*")), hasItems(KEY_1_BYTES, KEY_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void keysShouldReturnAllKeysForSpecificNode() { + + nativeConnection.set(KEY_1, VALUE_1); + nativeConnection.set(KEY_2, VALUE_2); + + Set keysOnNode = clusterConnection.keys(new RedisClusterNode("127.0.0.1", 7379, null), + JedisConverters.toBytes("*")); + + assertThat(keysOnNode, hasItems(KEY_2_BYTES)); + assertThat(keysOnNode, not(hasItems(KEY_1_BYTES))); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void randomKeyShouldReturnCorrectlyWhenKeysAvailable() { + + nativeConnection.set(KEY_1, VALUE_1); + nativeConnection.set(KEY_2, VALUE_2); + + assertThat(clusterConnection.randomKey(), notNullValue()); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void randomKeyShouldReturnNullWhenNoKeysAvailable() { + assertThat(clusterConnection.randomKey(), nullValue()); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void rename() { + + nativeConnection.set(KEY_1, VALUE_1); + + clusterConnection.rename(KEY_1_BYTES, KEY_2_BYTES); + + assertThat(nativeConnection.exists(KEY_1), is(false)); + assertThat(nativeConnection.get(KEY_2), is(VALUE_1)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void renameSameKeysOnSameSlot() { + + nativeConnection.set(SAME_SLOT_KEY_1, VALUE_1); + + clusterConnection.rename(SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES); + + assertThat(nativeConnection.exists(SAME_SLOT_KEY_1), is(false)); + assertThat(nativeConnection.get(SAME_SLOT_KEY_2), is(VALUE_1)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void renameNXWhenTargetKeyDoesNotExist() { + + nativeConnection.set(KEY_1, VALUE_1); + + assertThat(clusterConnection.renameNX(KEY_1_BYTES, KEY_2_BYTES), is(Boolean.TRUE)); + + assertThat(nativeConnection.exists(KEY_1), is(false)); + assertThat(nativeConnection.get(KEY_2), is(VALUE_1)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void renameNXWhenTargetKeyDoesExist() { + + nativeConnection.set(KEY_1, VALUE_1); + nativeConnection.set(KEY_2, VALUE_2); + + assertThat(clusterConnection.renameNX(KEY_1_BYTES, KEY_2_BYTES), is(Boolean.FALSE)); + + assertThat(nativeConnection.get(KEY_1), is(VALUE_1)); + assertThat(nativeConnection.get(KEY_2), is(VALUE_2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void renameNXWhenOnSameSlot() { + + nativeConnection.set(SAME_SLOT_KEY_1, VALUE_1); + + assertThat(clusterConnection.renameNX(SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES), is(Boolean.TRUE)); + + assertThat(nativeConnection.exists(SAME_SLOT_KEY_1), is(false)); + assertThat(nativeConnection.get(SAME_SLOT_KEY_2), is(VALUE_1)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void expireShouldBeSetCorreclty() { + + nativeConnection.set(KEY_1, VALUE_1); + + clusterConnection.expire(KEY_1_BYTES, 5); + + assertThat(nativeConnection.ttl(LettuceConverters.toString(KEY_1_BYTES)) > 1, is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void pExpireShouldBeSetCorreclty() { + + nativeConnection.set(KEY_1, VALUE_1); + + clusterConnection.pExpire(KEY_1_BYTES, 5000); + + assertThat(nativeConnection.ttl(LettuceConverters.toString(KEY_1_BYTES)) > 1, is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void expireAtShouldBeSetCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + clusterConnection.expireAt(KEY_1_BYTES, System.currentTimeMillis() / 1000 + 5000); + + assertThat(nativeConnection.ttl(LettuceConverters.toString(KEY_1_BYTES)) > 1, is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void pExpireAtShouldBeSetCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + clusterConnection.pExpireAt(KEY_1_BYTES, System.currentTimeMillis() + 5000); + + assertThat(nativeConnection.ttl(LettuceConverters.toString(KEY_1_BYTES)) > 1, is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void persistShoudRemoveTTL() { + + nativeConnection.setex(KEY_1, 10, VALUE_1); + + assertThat(clusterConnection.persist(KEY_1_BYTES), is(Boolean.TRUE)); + assertThat(nativeConnection.ttl(KEY_1), is(-1L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = UnsupportedOperationException.class) + public void moveShouldNotBeSupported() { + clusterConnection.move(KEY_1_BYTES, 3); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void dbSizeShouldReturnCummulatedDbSize() { + + nativeConnection.set(KEY_1, VALUE_1); + nativeConnection.set(KEY_2, VALUE_2); + + assertThat(clusterConnection.dbSize(), is(2L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void dbSizeForSpecificNodeShouldGetNodeDbSize() { + + nativeConnection.set(KEY_1, VALUE_1); + nativeConnection.set(KEY_2, VALUE_2); + + assertThat(clusterConnection.dbSize(new RedisClusterNode("127.0.0.1", 7379, null)), is(1L)); + assertThat(clusterConnection.dbSize(new RedisClusterNode("127.0.0.1", 7380, null)), is(1L)); + assertThat(clusterConnection.dbSize(new RedisClusterNode("127.0.0.1", 7381, null)), is(0L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void ttlShouldReturnMinusTwoWhenKeyDoesNotExist() { + assertThat(clusterConnection.ttl(KEY_1_BYTES), is(-2L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void ttlShouldReturnMinusOneWhenKeyDoesNotHaveExpirationSet() { + + nativeConnection.set(KEY_1, VALUE_1); + + assertThat(clusterConnection.ttl(KEY_1_BYTES), is(-1L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void ttlShouldReturnValueCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + nativeConnection.expire(KEY_1, 5); + + assertThat(clusterConnection.ttl(KEY_1_BYTES) > 1, is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void pTtlShouldReturnMinusTwoWhenKeyDoesNotExist() { + assertThat(clusterConnection.pTtl(KEY_1_BYTES), is(-2L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void pTtlShouldReturnMinusOneWhenKeyDoesNotHaveExpirationSet() { + + nativeConnection.set(KEY_1, VALUE_1); + + assertThat(clusterConnection.pTtl(KEY_1_BYTES), is(-1L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void pTtlShouldReturValueCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + nativeConnection.expire(KEY_1, 5); + + assertThat(clusterConnection.pTtl(KEY_1_BYTES) > 1, is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sortShouldReturnValuesCorrectly() { + + nativeConnection.lpush(KEY_1, VALUE_2, VALUE_1); + + assertThat(clusterConnection.sort(KEY_1_BYTES, new DefaultSortParameters().alpha()), + hasItems(VALUE_1_BYTES, VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sortAndStoreShouldAddSortedValuesValuesCorrectly() { + + nativeConnection.lpush(KEY_1, VALUE_2, VALUE_1); + + assertThat(clusterConnection.sort(KEY_1_BYTES, new DefaultSortParameters().alpha(), KEY_2_BYTES), is(1L)); + assertThat(nativeConnection.exists(KEY_2), is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sortAndStoreShouldReturnZeroWhenListDoesNotExist() { + assertThat(clusterConnection.sort(KEY_1_BYTES, new DefaultSortParameters().alpha(), KEY_2_BYTES), is(0L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void dumpAndRestoreShouldWorkCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + byte[] dumpedValue = clusterConnection.dump(KEY_1_BYTES); + clusterConnection.restore(KEY_2_BYTES, 0, dumpedValue); + + assertThat(nativeConnection.get(KEY_2), is(VALUE_1)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void getShouldReturnValueCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + assertThat(clusterConnection.get(KEY_1_BYTES), is(VALUE_1_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void getSetShouldWorkCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + byte[] valueBeforeSet = clusterConnection.getSet(KEY_1_BYTES, VALUE_2_BYTES); + + assertThat(valueBeforeSet, is(VALUE_1_BYTES)); + assertThat(nativeConnection.get(KEY_1), is(VALUE_2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void mGetShouldReturnCorrectlyWhenKeysMapToSameSlot() { + + nativeConnection.set(SAME_SLOT_KEY_1, VALUE_1); + nativeConnection.set(SAME_SLOT_KEY_2, VALUE_2); + + assertThat(clusterConnection.mGet(SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES), + hasItems(VALUE_1_BYTES, VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void mGetShouldReturnCorrectlyWhenKeysDoNotMapToSameSlot() { + + nativeConnection.set(KEY_1, VALUE_1); + nativeConnection.set(KEY_2, VALUE_2); + + assertThat(clusterConnection.mGet(KEY_1_BYTES, KEY_2_BYTES), hasItems(VALUE_1_BYTES, VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void setShouldSetValueCorrectly() { + + clusterConnection.set(KEY_1_BYTES, VALUE_1_BYTES); + + assertThat(nativeConnection.get(KEY_1), is(VALUE_1)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void setNxShouldSetValueCorrectly() { + + clusterConnection.setNX(KEY_1_BYTES, VALUE_1_BYTES); + + assertThat(nativeConnection.get(KEY_1), is(VALUE_1)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void setNxShouldNotSetValueWhenAlreadyExistsInDBCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + clusterConnection.setNX(KEY_1_BYTES, VALUE_2_BYTES); + + assertThat(nativeConnection.get(KEY_1), is(VALUE_1)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void setExShouldSetValueCorrectly() { + + clusterConnection.setEx(KEY_1_BYTES, 5, VALUE_1_BYTES); + + assertThat(nativeConnection.get(KEY_1), is(VALUE_1)); + assertThat(nativeConnection.ttl(KEY_1) > 1, is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void pSetExShouldSetValueCorrectly() { + + clusterConnection.pSetEx(KEY_1_BYTES, 5000, VALUE_1_BYTES); + + assertThat(nativeConnection.get(KEY_1), is(VALUE_1)); + assertThat(nativeConnection.ttl(KEY_1) > 1, is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void mSetShouldWorkWhenKeysMapToSameSlot() { + + Map map = new LinkedHashMap(); + map.put(SAME_SLOT_KEY_1_BYTES, VALUE_1_BYTES); + map.put(SAME_SLOT_KEY_2_BYTES, VALUE_2_BYTES); + + clusterConnection.mSet(map); + + assertThat(nativeConnection.get(SAME_SLOT_KEY_1), is(VALUE_1)); + assertThat(nativeConnection.get(SAME_SLOT_KEY_2), is(VALUE_2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void mSetShouldWorkWhenKeysDoNotMapToSameSlot() { + + Map map = new LinkedHashMap(); + map.put(KEY_1_BYTES, VALUE_1_BYTES); + map.put(KEY_2_BYTES, VALUE_2_BYTES); + + clusterConnection.mSet(map); + + assertThat(nativeConnection.get(KEY_1), is(VALUE_1)); + assertThat(nativeConnection.get(KEY_2), is(VALUE_2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void mSetNXShouldReturnTrueIfAllKeysSet() { + + Map map = new LinkedHashMap(); + map.put(KEY_1_BYTES, VALUE_1_BYTES); + map.put(KEY_2_BYTES, VALUE_2_BYTES); + + assertThat(clusterConnection.mSetNX(map), is(true)); + + assertThat(nativeConnection.get(KEY_1), is(VALUE_1)); + assertThat(nativeConnection.get(KEY_2), is(VALUE_2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void mSetNXShouldReturnFalseIfNotAllKeysSet() { + + nativeConnection.set(KEY_2, VALUE_3); + Map map = new LinkedHashMap(); + map.put(KEY_1_BYTES, VALUE_1_BYTES); + map.put(KEY_2_BYTES, VALUE_2_BYTES); + + assertThat(clusterConnection.mSetNX(map), is(false)); + + assertThat(nativeConnection.get(KEY_1), is(VALUE_1)); + assertThat(nativeConnection.get(KEY_2), is(VALUE_3)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void mSetNXShouldWorkForOnSameSlotKeys() { + + Map map = new LinkedHashMap(); + map.put(SAME_SLOT_KEY_1_BYTES, VALUE_1_BYTES); + map.put(SAME_SLOT_KEY_2_BYTES, VALUE_2_BYTES); + + assertThat(clusterConnection.mSetNX(map), is(true)); + + assertThat(nativeConnection.get(SAME_SLOT_KEY_1), is(VALUE_1)); + assertThat(nativeConnection.get(SAME_SLOT_KEY_2), is(VALUE_2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void incrShouldIncreaseValueCorrectly() { + + nativeConnection.set(KEY_1, "1"); + + assertThat(clusterConnection.incr(KEY_1_BYTES), is(2L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void incrByFloatShouldIncreaseValueCorrectly() { + + nativeConnection.set(KEY_1, "1"); + + assertThat(clusterConnection.incrBy(KEY_1_BYTES, 5.5D), is(6.5D)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void incrByShouldIncreaseValueCorrectly() { + + nativeConnection.set(KEY_1, "1"); + + assertThat(clusterConnection.incrBy(KEY_1_BYTES, 5), is(6L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void decrShouldDecreaseValueCorrectly() { + + nativeConnection.set(KEY_1, "5"); + + assertThat(clusterConnection.decr(KEY_1_BYTES), is(4L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void decrByShouldDecreaseValueCorrectly() { + + nativeConnection.set(KEY_1, "5"); + + assertThat(clusterConnection.decrBy(KEY_1_BYTES, 4), is(1L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void appendShouldAddValueCorrectly() { + + clusterConnection.append(KEY_1_BYTES, VALUE_1_BYTES); + clusterConnection.append(KEY_1_BYTES, VALUE_2_BYTES); + + assertThat(nativeConnection.get(KEY_1), is(VALUE_1.concat(VALUE_2))); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void getRangeShouldReturnValueCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + assertThat(clusterConnection.getRange(KEY_1_BYTES, 0, 2), is(LettuceConverters.toBytes("val"))); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void setRangeShouldWorkCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + clusterConnection.setRange(KEY_1_BYTES, LettuceConverters.toBytes("UE"), 3); + + assertThat(nativeConnection.get(KEY_1), is("valUE1")); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void getBitShouldWorkCorrectly() { + + nativeConnection.setbit(KEY_1, 0, 1); + nativeConnection.setbit(KEY_1, 1, 0); + + assertThat(clusterConnection.getBit(KEY_1_BYTES, 0), is(true)); + assertThat(clusterConnection.getBit(KEY_1_BYTES, 1), is(false)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void setBitShouldWorkCorrectly() { + + clusterConnection.setBit(KEY_1_BYTES, 0, true); + clusterConnection.setBit(KEY_1_BYTES, 1, false); + + assertThat(nativeConnection.getbit(KEY_1, 0), is(1L)); + assertThat(nativeConnection.getbit(KEY_1, 1), is(0L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void bitCountShouldWorkCorrectly() { + + nativeConnection.setbit(KEY_1, 0, 1); + nativeConnection.setbit(KEY_1, 1, 0); + + assertThat(clusterConnection.bitCount(KEY_1_BYTES), is(1L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void bitCountWithRangeShouldWorkCorrectly() { + + nativeConnection.setbit(KEY_1, 0, 1); + nativeConnection.setbit(KEY_1, 1, 0); + nativeConnection.setbit(KEY_1, 2, 1); + nativeConnection.setbit(KEY_1, 3, 0); + nativeConnection.setbit(KEY_1, 4, 1); + + assertThat(clusterConnection.bitCount(KEY_1_BYTES, 0, 3), is(3L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void bitOpShouldWorkCorrectly() { + + nativeConnection.set(SAME_SLOT_KEY_1, "foo"); + nativeConnection.set(SAME_SLOT_KEY_2, "bar"); + + clusterConnection.bitOp(BitOperation.AND, SAME_SLOT_KEY_3_BYTES, SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES); + + assertThat(nativeConnection.get(SAME_SLOT_KEY_3), is("bab")); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = DataAccessException.class) + public void bitOpShouldThrowExceptionWhenKeysDoNotMapToSameSlot() { + clusterConnection.bitOp(BitOperation.AND, KEY_1_BYTES, KEY_2_BYTES, KEY_3_BYTES); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void strLenShouldWorkCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + assertThat(clusterConnection.strLen(KEY_1_BYTES), is(6L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void rPushShoultAddValuesCorrectly() { + + clusterConnection.rPush(KEY_1_BYTES, VALUE_1_BYTES, VALUE_2_BYTES); + + assertThat(nativeConnection.lrange(KEY_1, 0, -1), hasItems(VALUE_1, VALUE_2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void lPushShoultAddValuesCorrectly() { + + clusterConnection.lPush(KEY_1_BYTES, VALUE_1_BYTES, VALUE_2_BYTES); + + assertThat(nativeConnection.lrange(KEY_1, 0, -1), hasItems(VALUE_1, VALUE_2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void rPushNXShoultNotAddValuesWhenKeyDoesNotExist() { + + clusterConnection.rPushX(KEY_1_BYTES, VALUE_1_BYTES); + + assertThat(nativeConnection.exists(KEY_1), is(false)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void lPushNXShoultNotAddValuesWhenKeyDoesNotExist() { + + clusterConnection.lPushX(KEY_1_BYTES, VALUE_1_BYTES); + + assertThat(nativeConnection.exists(KEY_1), is(false)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void lLenShouldCountValuesCorrectly() { + + nativeConnection.lpush(KEY_1, VALUE_1, VALUE_2); + + assertThat(clusterConnection.lLen(KEY_1_BYTES), is(2L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void lRangeShouldGetValuesCorrectly() { + + nativeConnection.lpush(KEY_1, VALUE_1, VALUE_2); + + assertThat(clusterConnection.lRange(KEY_1_BYTES, 0L, -1L), hasItems(VALUE_1_BYTES, VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void lTrimShouldTrimListCorrectly() { + + nativeConnection.lpush(KEY_1, VALUE_1, VALUE_2, "foo", "bar"); + + clusterConnection.lTrim(KEY_1_BYTES, 2, 3); + + assertThat(nativeConnection.lrange(KEY_1, 0, -1), hasItems(VALUE_1, VALUE_2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void lIndexShouldGetElementAtIndexCorrectly() { + + nativeConnection.rpush(KEY_1, VALUE_1, VALUE_2, "foo", "bar"); + + assertThat(clusterConnection.lIndex(KEY_1_BYTES, 1), is(VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void lInsertShouldAddElementAtPositionCorrectly() { + + nativeConnection.rpush(KEY_1, VALUE_1, VALUE_2, "foo", "bar"); + + clusterConnection.lInsert(KEY_1_BYTES, Position.AFTER, VALUE_2_BYTES, LettuceConverters.toBytes("booh!")); + + assertThat(nativeConnection.lrange(KEY_1, 0, -1).get(2), is("booh!")); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void lSetShouldSetElementAtPositionCorrectly() { + + nativeConnection.rpush(KEY_1, VALUE_1, VALUE_2, "foo", "bar"); + + clusterConnection.lSet(KEY_1_BYTES, 1L, VALUE_1_BYTES); + + assertThat(nativeConnection.lrange(KEY_1, 0, -1).get(1), is(VALUE_1)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void lRemShouldRemoveElementAtPositionCorrectly() { + + nativeConnection.rpush(KEY_1, VALUE_1, VALUE_2, "foo", "bar"); + + clusterConnection.lRem(KEY_1_BYTES, 1L, VALUE_1_BYTES); + + assertThat(nativeConnection.llen(KEY_1), is(3L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void lPopShouldReturnElementCorrectly() { + + nativeConnection.rpush(KEY_1, VALUE_1, VALUE_2); + + assertThat(clusterConnection.lPop(KEY_1_BYTES), is(VALUE_1_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void rPopShouldReturnElementCorrectly() { + + nativeConnection.rpush(KEY_1, VALUE_1, VALUE_2); + + assertThat(clusterConnection.rPop(KEY_1_BYTES), is(VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void blPopShouldPopElementCorectly() { + + nativeConnection.lpush(KEY_1, VALUE_1, VALUE_2); + nativeConnection.lpush(KEY_2, VALUE_3); + + assertThat(clusterConnection.bLPop(100, KEY_1_BYTES, KEY_2_BYTES).size(), is(2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void blPopShouldPopElementCorectlyWhenKeyOnSameSlot() { + + nativeConnection.lpush(SAME_SLOT_KEY_1, VALUE_1, VALUE_2); + nativeConnection.lpush(SAME_SLOT_KEY_2, VALUE_3); + + assertThat(clusterConnection.bLPop(100, SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES).size(), is(2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void brPopShouldPopElementCorectly() { + + nativeConnection.lpush(KEY_1, VALUE_1, VALUE_2); + nativeConnection.lpush(KEY_2, VALUE_3); + + assertThat(clusterConnection.bRPop(100, KEY_1_BYTES, KEY_2_BYTES).size(), is(2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void brPopShouldPopElementCorectlyWhenKeyOnSameSlot() { + + nativeConnection.lpush(SAME_SLOT_KEY_1, VALUE_1, VALUE_2); + nativeConnection.lpush(SAME_SLOT_KEY_2, VALUE_3); + + assertThat(clusterConnection.bRPop(100, SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES).size(), is(2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void rPopLPushShouldWorkWhenDoNotMapToSameSlot() { + + nativeConnection.lpush(KEY_1, VALUE_1, VALUE_2); + nativeConnection.lpush(KEY_2, VALUE_3); + + assertThat(clusterConnection.bLPop(100, KEY_1_BYTES, KEY_2_BYTES).size(), is(2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void bRPopLPushShouldWork() { + + nativeConnection.lpush(KEY_1, VALUE_1, VALUE_2); + + assertThat(clusterConnection.bRPopLPush(0, KEY_1_BYTES, KEY_2_BYTES), is(VALUE_1_BYTES)); + assertThat(nativeConnection.exists(KEY_2), is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void bRPopLPushShouldWorkOnSameSlotKeys() { + + nativeConnection.lpush(SAME_SLOT_KEY_1, VALUE_1, VALUE_2); + + assertThat(clusterConnection.bRPopLPush(0, SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES), is(VALUE_1_BYTES)); + assertThat(nativeConnection.exists(SAME_SLOT_KEY_2), is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void rPopLPushShouldWorkWhenKeysOnSameSlot() { + + nativeConnection.lpush(SAME_SLOT_KEY_1, VALUE_1, VALUE_2); + + assertThat(clusterConnection.rPopLPush(SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES), is(VALUE_1_BYTES)); + assertThat(nativeConnection.exists(SAME_SLOT_KEY_2), is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sAddShouldAddValueToSetCorrectly() { + + clusterConnection.sAdd(KEY_1_BYTES, VALUE_1_BYTES, VALUE_2_BYTES); + + assertThat(nativeConnection.smembers(KEY_1), hasItems(VALUE_1, VALUE_2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sRemShouldRemoveValueFromSetCorrectly() { + + nativeConnection.sadd(KEY_1, VALUE_1, VALUE_2); + + clusterConnection.sRem(KEY_1_BYTES, VALUE_2_BYTES); + + assertThat(nativeConnection.smembers(KEY_1), hasItems(VALUE_1)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sPopShouldPopValueFromSetCorrectly() { + + nativeConnection.sadd(KEY_1, VALUE_1, VALUE_2); + + assertThat(clusterConnection.sPop(KEY_1_BYTES), notNullValue()); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sMoveShouldWorkWhenKeysMapToSameSlot() { + + nativeConnection.sadd(SAME_SLOT_KEY_1, VALUE_1, VALUE_2); + nativeConnection.sadd(SAME_SLOT_KEY_2, VALUE_3); + + clusterConnection.sMove(SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES, VALUE_2_BYTES); + + assertThat(nativeConnection.sismember(SAME_SLOT_KEY_1, VALUE_2), is(false)); + assertThat(nativeConnection.sismember(SAME_SLOT_KEY_2, VALUE_2), is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sMoveShouldWorkWhenKeysDoNotMapToSameSlot() { + + nativeConnection.sadd(KEY_1, VALUE_1, VALUE_2); + nativeConnection.sadd(KEY_2, VALUE_3); + + clusterConnection.sMove(KEY_1_BYTES, KEY_2_BYTES, VALUE_2_BYTES); + + assertThat(nativeConnection.sismember(KEY_1, VALUE_2), is(false)); + assertThat(nativeConnection.sismember(KEY_2, VALUE_2), is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sCardShouldCountValuesInSetCorrectly() { + + nativeConnection.sadd(KEY_1, VALUE_1, VALUE_2); + + assertThat(clusterConnection.sCard(KEY_1_BYTES), is(2L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sIsMemberShouldReturnTrueIfValueIsMemberOfSet() { + + nativeConnection.sadd(KEY_1, VALUE_1, VALUE_2); + + assertThat(clusterConnection.sIsMember(KEY_1_BYTES, VALUE_1_BYTES), is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sIsMemberShouldReturnFalseIfValueIsMemberOfSet() { + + nativeConnection.sadd(KEY_1, VALUE_1, VALUE_2); + + assertThat(clusterConnection.sIsMember(KEY_1_BYTES, LettuceConverters.toBytes("foo")), is(false)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sInterShouldWorkForKeysMappingToSameSlot() { + + nativeConnection.sadd(SAME_SLOT_KEY_1, VALUE_1, VALUE_2); + nativeConnection.sadd(SAME_SLOT_KEY_2, VALUE_2, VALUE_3); + + assertThat(clusterConnection.sInter(SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES), hasItem(VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sInterShouldWorkForKeysNotMappingToSameSlot() { + + nativeConnection.sadd(KEY_1, VALUE_1, VALUE_2); + nativeConnection.sadd(KEY_2, VALUE_2, VALUE_3); + + assertThat(clusterConnection.sInter(KEY_1_BYTES, KEY_2_BYTES), hasItem(VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sInterStoreShouldWorkForKeysMappingToSameSlot() { + + nativeConnection.sadd(SAME_SLOT_KEY_1, VALUE_1, VALUE_2); + nativeConnection.sadd(SAME_SLOT_KEY_2, VALUE_2, VALUE_3); + + clusterConnection.sInterStore(SAME_SLOT_KEY_3_BYTES, SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES); + + assertThat(nativeConnection.smembers(SAME_SLOT_KEY_3), hasItem(VALUE_2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sInterStoreShouldWorkForKeysNotMappingToSameSlot() { + + nativeConnection.sadd(KEY_1, VALUE_1, VALUE_2); + nativeConnection.sadd(KEY_2, VALUE_2, VALUE_3); + + clusterConnection.sInterStore(KEY_3_BYTES, KEY_1_BYTES, KEY_2_BYTES); + + assertThat(nativeConnection.smembers(KEY_3), hasItem(VALUE_2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sUnionShouldWorkForKeysMappingToSameSlot() { + + nativeConnection.sadd(SAME_SLOT_KEY_1, VALUE_1, VALUE_2); + nativeConnection.sadd(SAME_SLOT_KEY_2, VALUE_2, VALUE_3); + + assertThat(clusterConnection.sUnion(SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES), + hasItems(VALUE_1_BYTES, VALUE_2_BYTES, VALUE_3_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sUnionShouldWorkForKeysNotMappingToSameSlot() { + + nativeConnection.sadd(KEY_1, VALUE_1, VALUE_2); + nativeConnection.sadd(KEY_2, VALUE_2, VALUE_3); + + assertThat(clusterConnection.sUnion(KEY_1_BYTES, KEY_2_BYTES), + hasItems(VALUE_1_BYTES, VALUE_2_BYTES, VALUE_3_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sUnionStoreShouldWorkForKeysMappingToSameSlot() { + + nativeConnection.sadd(SAME_SLOT_KEY_1, VALUE_1, VALUE_2); + nativeConnection.sadd(SAME_SLOT_KEY_2, VALUE_2, VALUE_3); + + clusterConnection.sUnionStore(SAME_SLOT_KEY_3_BYTES, SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES); + + assertThat(nativeConnection.smembers(SAME_SLOT_KEY_3), hasItems(VALUE_1, VALUE_2, VALUE_3)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sUnionStoreShouldWorkForKeysNotMappingToSameSlot() { + + nativeConnection.sadd(KEY_1, VALUE_1, VALUE_2); + nativeConnection.sadd(KEY_2, VALUE_2, VALUE_3); + + clusterConnection.sUnionStore(KEY_3_BYTES, KEY_1_BYTES, KEY_2_BYTES); + + assertThat(nativeConnection.smembers(KEY_3), hasItems(VALUE_1, VALUE_2, VALUE_3)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sDiffShouldWorkWhenKeysMapToSameSlot() { + + nativeConnection.sadd(SAME_SLOT_KEY_1, VALUE_1, VALUE_2); + nativeConnection.sadd(SAME_SLOT_KEY_2, VALUE_2, VALUE_3); + + assertThat(clusterConnection.sDiff(SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES), hasItems(VALUE_1_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sDiffShouldWorkWhenKeysNotMapToSameSlot() { + + nativeConnection.sadd(KEY_1, VALUE_1, VALUE_2); + nativeConnection.sadd(KEY_2, VALUE_2, VALUE_3); + + assertThat(clusterConnection.sDiff(KEY_1_BYTES, KEY_2_BYTES), hasItems(VALUE_1_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sDiffStoreShouldWorkWhenKeysMapToSameSlot() { + + nativeConnection.sadd(SAME_SLOT_KEY_1, VALUE_1, VALUE_2); + nativeConnection.sadd(SAME_SLOT_KEY_2, VALUE_2, VALUE_3); + + clusterConnection.sDiffStore(SAME_SLOT_KEY_3_BYTES, SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES); + + assertThat(nativeConnection.smembers(SAME_SLOT_KEY_3), hasItems(VALUE_1)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sDiffStoreShouldWorkWhenKeysNotMapToSameSlot() { + + nativeConnection.sadd(KEY_1, VALUE_1, VALUE_2); + nativeConnection.sadd(KEY_2, VALUE_2, VALUE_3); + + clusterConnection.sDiffStore(KEY_3_BYTES, KEY_1_BYTES, KEY_2_BYTES); + + assertThat(nativeConnection.smembers(KEY_3), hasItems(VALUE_1)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sMembersShouldReturnValuesContainedInSetCorrectly() { + + nativeConnection.sadd(KEY_1, VALUE_1, VALUE_2); + + assertThat(clusterConnection.sMembers(KEY_1_BYTES), hasItems(VALUE_1_BYTES, VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sRandMamberShouldReturnValueCorrectly() { + + nativeConnection.sadd(KEY_1, VALUE_1, VALUE_2); + + assertThat(clusterConnection.sRandMember(KEY_1_BYTES), notNullValue()); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sRandMamberWithCountShouldReturnValueCorrectly() { + + nativeConnection.sadd(KEY_1, VALUE_1, VALUE_2); + + assertThat(clusterConnection.sRandMember(KEY_1_BYTES, 3), notNullValue()); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void sscanShouldRetrieveAllValuesInSetCorrectly() { + + for (int i = 0; i < 30; i++) { + nativeConnection.sadd(KEY_1, Integer.valueOf(i).toString()); + } + + int count = 0; + Cursor cursor = clusterConnection.sScan(KEY_1_BYTES, ScanOptions.NONE); + while (cursor.hasNext()) { + count++; + cursor.next(); + } + + assertThat(count, is(30)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zAddShouldAddValueWithScoreCorrectly() { + + clusterConnection.zAdd(KEY_1_BYTES, 10D, VALUE_1_BYTES); + clusterConnection.zAdd(KEY_1_BYTES, 20D, VALUE_2_BYTES); + + assertThat(nativeConnection.zcard(KEY_1), is(2L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRemShouldRemoveValueWithScoreCorrectly() { + + nativeConnection.zadd(KEY_1, 10D, VALUE_1); + nativeConnection.zadd(KEY_1, 20D, VALUE_2); + + clusterConnection.zRem(KEY_1_BYTES, VALUE_1_BYTES); + + assertThat(nativeConnection.zcard(KEY_1), is(1L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zIncrByShouldIncScoreForValueCorrectly() { + + nativeConnection.zadd(KEY_1, 10D, VALUE_1); + nativeConnection.zadd(KEY_1, 20D, VALUE_2); + + clusterConnection.zIncrBy(KEY_1_BYTES, 100D, VALUE_1_BYTES); + + assertThat(nativeConnection.zrank(KEY_1, VALUE_1), is(1L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRankShouldReturnPositionForValueCorrectly() { + + nativeConnection.zadd(KEY_1, 10D, VALUE_1); + nativeConnection.zadd(KEY_1, 20D, VALUE_2); + + assertThat(clusterConnection.zRank(KEY_1_BYTES, VALUE_2_BYTES), is(1L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRankShouldReturnReversePositionForValueCorrectly() { + + nativeConnection.zadd(KEY_1, 10D, VALUE_1); + nativeConnection.zadd(KEY_1, 20D, VALUE_2); + + assertThat(clusterConnection.zRevRank(KEY_1_BYTES, VALUE_2_BYTES), is(0L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRangeShouldReturnValuesCorrectly() { + + nativeConnection.zadd(KEY_1, 10D, VALUE_1); + nativeConnection.zadd(KEY_1, 20D, VALUE_2); + nativeConnection.zadd(KEY_1, 5D, VALUE_3); + + assertThat(clusterConnection.zRange(KEY_1_BYTES, 1, 2), hasItems(VALUE_1_BYTES, VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRangeWithScoresShouldReturnValuesAndScoreCorrectly() { + + nativeConnection.zadd(KEY_1, 10D, VALUE_1); + nativeConnection.zadd(KEY_1, 20D, VALUE_2); + nativeConnection.zadd(KEY_1, 5D, VALUE_3); + + assertThat(clusterConnection.zRangeWithScores(KEY_1_BYTES, 1, 2), + hasItems((Tuple) new DefaultTuple(VALUE_1_BYTES, 10D), (Tuple) new DefaultTuple(VALUE_2_BYTES, 20D))); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRangeByScoreShouldReturnValuesCorrectly() { + + nativeConnection.zadd(KEY_1, 10D, VALUE_1); + nativeConnection.zadd(KEY_1, 20D, VALUE_2); + nativeConnection.zadd(KEY_1, 5D, VALUE_3); + + assertThat(clusterConnection.zRangeByScore(KEY_1_BYTES, 10, 20), hasItems(VALUE_1_BYTES, VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRangeByScoreWithScoresShouldReturnValuesAndScoreCorrectly() { + + nativeConnection.zadd(KEY_1, 10D, VALUE_1); + nativeConnection.zadd(KEY_1, 20D, VALUE_2); + nativeConnection.zadd(KEY_1, 5D, VALUE_3); + + assertThat(clusterConnection.zRangeByScoreWithScores(KEY_1_BYTES, 10, 20), + hasItems((Tuple) new DefaultTuple(VALUE_1_BYTES, 10D), (Tuple) new DefaultTuple(VALUE_2_BYTES, 20D))); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRangeByScoreShouldReturnValuesCorrectlyWhenGivenOffsetAndScore() { + + nativeConnection.zadd(KEY_1, 10D, VALUE_1); + nativeConnection.zadd(KEY_1, 20D, VALUE_2); + nativeConnection.zadd(KEY_1, 5D, VALUE_3); + + assertThat(clusterConnection.zRangeByScore(KEY_1_BYTES, 10D, 20D, 0L, 1L), hasItems(VALUE_1_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRangeByScoreWithScoresShouldReturnValuesCorrectlyWhenGivenOffsetAndScore() { + + nativeConnection.zadd(KEY_1, 10D, VALUE_1); + nativeConnection.zadd(KEY_1, 20D, VALUE_2); + nativeConnection.zadd(KEY_1, 5D, VALUE_3); + + assertThat(clusterConnection.zRangeByScoreWithScores(KEY_1_BYTES, 10D, 20D, 0L, 1L), + hasItems((Tuple) new DefaultTuple(VALUE_1_BYTES, 10D))); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRevRangeShouldReturnValuesCorrectly() { + + nativeConnection.zadd(KEY_1, 10D, VALUE_1); + nativeConnection.zadd(KEY_1, 20D, VALUE_2); + nativeConnection.zadd(KEY_1, 5D, VALUE_3); + + assertThat(clusterConnection.zRevRange(KEY_1_BYTES, 1, 2), hasItems(VALUE_3_BYTES, VALUE_1_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRevRangeWithScoresShouldReturnValuesAndScoreCorrectly() { + + nativeConnection.zadd(KEY_1, 10D, VALUE_1); + nativeConnection.zadd(KEY_1, 20D, VALUE_2); + nativeConnection.zadd(KEY_1, 5D, VALUE_3); + + assertThat(clusterConnection.zRevRangeWithScores(KEY_1_BYTES, 1, 2), + hasItems((Tuple) new DefaultTuple(VALUE_3_BYTES, 5D), (Tuple) new DefaultTuple(VALUE_1_BYTES, 10D))); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRevRangeByScoreShouldReturnValuesCorrectly() { + + nativeConnection.zadd(KEY_1, 10D, VALUE_1); + nativeConnection.zadd(KEY_1, 20D, VALUE_2); + nativeConnection.zadd(KEY_1, 5D, VALUE_3); + + assertThat(clusterConnection.zRevRangeByScore(KEY_1_BYTES, 10D, 20D), hasItems(VALUE_2_BYTES, VALUE_1_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRevRangeByScoreWithScoresShouldReturnValuesAndScoreCorrectly() { + + nativeConnection.zadd(KEY_1, 10D, VALUE_1); + nativeConnection.zadd(KEY_1, 20D, VALUE_2); + nativeConnection.zadd(KEY_1, 5D, VALUE_3); + + assertThat(clusterConnection.zRevRangeByScoreWithScores(KEY_1_BYTES, 10D, 20D), + hasItems((Tuple) new DefaultTuple(VALUE_2_BYTES, 20D), (Tuple) new DefaultTuple(VALUE_1_BYTES, 10D))); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRevRangeByScoreShouldReturnValuesCorrectlyWhenGivenOffsetAndScore() { + + nativeConnection.zadd(KEY_1, 10D, VALUE_1); + nativeConnection.zadd(KEY_1, 20D, VALUE_2); + nativeConnection.zadd(KEY_1, 5D, VALUE_3); + + assertThat(clusterConnection.zRevRangeByScore(KEY_1_BYTES, 10D, 20D, 0L, 1L), hasItems(VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRevRangeByScoreWithScoresShouldReturnValuesCorrectlyWhenGivenOffsetAndScore() { + + nativeConnection.zadd(KEY_1, 10D, VALUE_1); + nativeConnection.zadd(KEY_1, 20D, VALUE_2); + nativeConnection.zadd(KEY_1, 5D, VALUE_3); + + assertThat(clusterConnection.zRevRangeByScoreWithScores(KEY_1_BYTES, 10D, 20D, 0L, 1L), + hasItems((Tuple) new DefaultTuple(VALUE_2_BYTES, 20D))); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zCountShouldCountValuesInRange() { + + nativeConnection.zadd(KEY_1, 10D, VALUE_1); + nativeConnection.zadd(KEY_1, 20D, VALUE_2); + nativeConnection.zadd(KEY_1, 5D, VALUE_3); + + assertThat(clusterConnection.zCount(KEY_1_BYTES, 10, 20), is(2L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zCardShouldReturnTotalNumberOfValues() { + + nativeConnection.zadd(KEY_1, 10D, VALUE_1); + nativeConnection.zadd(KEY_1, 20D, VALUE_2); + nativeConnection.zadd(KEY_1, 5D, VALUE_3); + + assertThat(clusterConnection.zCard(KEY_1_BYTES), is(3L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zScoreShouldRetrieveScoreForValue() { + + nativeConnection.zadd(KEY_1, 10D, VALUE_1); + nativeConnection.zadd(KEY_1, 20D, VALUE_2); + + assertThat(clusterConnection.zScore(KEY_1_BYTES, VALUE_2_BYTES), is(20D)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRemRangeShouldRemoveValues() { + + nativeConnection.zadd(KEY_1, 10D, VALUE_1); + nativeConnection.zadd(KEY_1, 20D, VALUE_2); + nativeConnection.zadd(KEY_1, 30D, VALUE_3); + + clusterConnection.zRemRange(KEY_1_BYTES, 1, 2); + + assertThat(nativeConnection.zcard(KEY_1), is(1L)); + assertThat(nativeConnection.zrange(KEY_1, 0, -1), hasItem(VALUE_1)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRemRangeByScoreShouldRemoveValues() { + + nativeConnection.zadd(KEY_1, 10D, VALUE_1); + nativeConnection.zadd(KEY_1, 20D, VALUE_2); + nativeConnection.zadd(KEY_1, 30D, VALUE_3); + + clusterConnection.zRemRangeByScore(KEY_1_BYTES, 15D, 25D); + + assertThat(nativeConnection.zcard(KEY_1), is(2L)); + assertThat(nativeConnection.zrange(KEY_1, 0, -1), hasItems(VALUE_1, VALUE_3)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zUnionStoreShouldWorkForSameSlotKeys() { + + nativeConnection.zadd(SAME_SLOT_KEY_1, 10D, VALUE_1); + nativeConnection.zadd(SAME_SLOT_KEY_1, 30D, VALUE_3); + nativeConnection.zadd(SAME_SLOT_KEY_2, 20D, VALUE_2); + + clusterConnection.zUnionStore(SAME_SLOT_KEY_3_BYTES, SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES); + + assertThat(nativeConnection.zrange(SAME_SLOT_KEY_3, 0, -1), hasItems(VALUE_1, VALUE_2, VALUE_3)); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = DataAccessException.class) + public void zUnionStoreShouldThrowExceptionWhenKeysDoNotMapToSameSlots() { + clusterConnection.zUnionStore(KEY_3_BYTES, KEY_1_BYTES, KEY_2_BYTES); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zInterStoreShouldWorkForSameSlotKeys() { + + nativeConnection.zadd(SAME_SLOT_KEY_1, 10D, VALUE_1); + nativeConnection.zadd(SAME_SLOT_KEY_1, 20D, VALUE_2); + + nativeConnection.zadd(SAME_SLOT_KEY_2, 20D, VALUE_2); + nativeConnection.zadd(SAME_SLOT_KEY_2, 30D, VALUE_3); + + clusterConnection.zInterStore(SAME_SLOT_KEY_3_BYTES, SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES); + + assertThat(nativeConnection.zrange(SAME_SLOT_KEY_3, 0, -1), hasItems(VALUE_2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = DataAccessException.class) + public void zInterStoreShouldThrowExceptionWhenKeysDoNotMapToSameSlots() { + clusterConnection.zInterStore(KEY_3_BYTES, KEY_1_BYTES, KEY_2_BYTES); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void hSetShouldSetValueCorrectly() { + + clusterConnection.hSet(KEY_1_BYTES, KEY_2_BYTES, VALUE_1_BYTES); + + assertThat(nativeConnection.hget(KEY_1, KEY_2), is(VALUE_1)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void hSetNXShouldSetValueCorrectly() { + + clusterConnection.hSetNX(KEY_1_BYTES, KEY_2_BYTES, VALUE_1_BYTES); + + assertThat(nativeConnection.hget(KEY_1, KEY_2), is(VALUE_1)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void hSetNXShouldNotSetValueWhenAlreadyExists() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_1); + + clusterConnection.hSetNX(KEY_1_BYTES, KEY_2_BYTES, VALUE_2_BYTES); + + assertThat(nativeConnection.hget(KEY_1, KEY_2), is(VALUE_1)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void hGetShouldRetrieveValueCorrectly() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_1); + + assertThat(clusterConnection.hGet(KEY_1_BYTES, KEY_2_BYTES), is(VALUE_1_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void hMGetShouldRetrieveValueCorrectly() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_1); + nativeConnection.hset(KEY_1, KEY_3, VALUE_2); + + assertThat(clusterConnection.hMGet(KEY_1_BYTES, KEY_2_BYTES, KEY_3_BYTES), hasItems(VALUE_1_BYTES, VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void hMSetShouldAddValuesCorrectly() { + + Map hashes = new HashMap(); + hashes.put(KEY_2_BYTES, VALUE_1_BYTES); + hashes.put(KEY_3_BYTES, VALUE_2_BYTES); + + clusterConnection.hMSet(KEY_1_BYTES, hashes); + + assertThat(nativeConnection.hmget(KEY_1, KEY_2, KEY_3), hasItems(VALUE_1, VALUE_2)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void hIncrByShouldIncreaseFieldCorretly() { + + nativeConnection.hset(KEY_1, KEY_2, "1"); + nativeConnection.hset(KEY_1, KEY_3, "2"); + + clusterConnection.hIncrBy(KEY_1_BYTES, KEY_3_BYTES, 3); + + assertThat(nativeConnection.hget(KEY_1, KEY_3), is("5")); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void hIncrByFloatShouldIncreaseFieldCorretly() { + + nativeConnection.hset(KEY_1, KEY_2, "1"); + nativeConnection.hset(KEY_1, KEY_3, "2"); + + clusterConnection.hIncrBy(KEY_1_BYTES, KEY_3_BYTES, 3.5D); + + assertThat(nativeConnection.hget(KEY_1, KEY_3), is("5.5")); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void hExistsShouldReturnPresenceOfFieldCorrectly() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_1); + + assertThat(clusterConnection.hExists(KEY_1_BYTES, KEY_2_BYTES), is(true)); + assertThat(clusterConnection.hExists(KEY_1_BYTES, KEY_3_BYTES), is(false)); + assertThat(clusterConnection.hExists(LettuceConverters.toBytes("foo"), KEY_2_BYTES), is(false)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void hDelShouldRemoveFieldsCorrectly() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_1); + nativeConnection.hset(KEY_1, KEY_3, VALUE_2); + + clusterConnection.hDel(KEY_1_BYTES, KEY_2_BYTES); + + assertThat(nativeConnection.hexists(KEY_1, KEY_2), is(false)); + assertThat(nativeConnection.hexists(KEY_1, KEY_3), is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void hLenShouldRetrieveSizeCorrectly() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_1); + nativeConnection.hset(KEY_1, KEY_3, VALUE_2); + + assertThat(clusterConnection.hLen(KEY_1_BYTES), is(2L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void hKeysShouldRetrieveKeysCorrectly() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_1); + nativeConnection.hset(KEY_1, KEY_3, VALUE_2); + + assertThat(clusterConnection.hKeys(KEY_1_BYTES), hasItems(KEY_2_BYTES, KEY_3_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void hValsShouldRetrieveValuesCorrectly() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_1); + nativeConnection.hset(KEY_1, KEY_3, VALUE_2); + + assertThat(clusterConnection.hVals(KEY_1_BYTES), hasItems(VALUE_1_BYTES, VALUE_2_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void hGetAllShouldRetrieveEntriesCorrectly() { + + Map hashes = new HashMap(); + hashes.put(KEY_2, VALUE_1); + hashes.put(KEY_3, VALUE_2); + + nativeConnection.hmset(KEY_1, hashes); + + Map hGetAll = clusterConnection.hGetAll(KEY_1_BYTES); + + assertThat(hGetAll.keySet(), hasItems(KEY_2_BYTES, KEY_3_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = DataAccessException.class) + public void multiShouldThrowException() { + clusterConnection.multi(); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = DataAccessException.class) + public void execShouldThrowException() { + clusterConnection.exec(); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = DataAccessException.class) + public void discardShouldThrowException() { + clusterConnection.discard(); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = DataAccessException.class) + public void watchShouldThrowException() { + clusterConnection.watch(); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = DataAccessException.class) + public void unwatchShouldThrowException() { + clusterConnection.unwatch(); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void selectShouldAllowSelectionOfDBIndexZero() { + clusterConnection.select(0); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = DataAccessException.class) + public void selectShouldThrowExceptionWhenSelectingNonZeroDbIndex() { + clusterConnection.select(1); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void echoShouldReturnInputCorrectly() { + assertThat(clusterConnection.echo(VALUE_1_BYTES), is(VALUE_1_BYTES)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void pingShouldRetrunPongForExistingNode() { + assertThat(clusterConnection.ping(new RedisClusterNode("127.0.0.1", 7379, null)), is("PONG")); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void pingShouldRetrunPong() { + assertThat(clusterConnection.ping(), is("PONG")); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = IllegalArgumentException.class) + public void pingShouldThrowExceptionWhenNodeNotKnownToCluster() { + clusterConnection.ping(new RedisClusterNode("127.0.0.1", 1234, null)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void flushDbShouldFlushAllClusterNodes() { + + nativeConnection.set(KEY_1, VALUE_1); + nativeConnection.set(KEY_2, VALUE_2); + + clusterConnection.flushDb(); + + assertThat(nativeConnection.get(KEY_1), nullValue()); + assertThat(nativeConnection.get(KEY_2), nullValue()); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void flushDbOnSingleNodeShouldFlushOnlyGivenNodesDb() { + + nativeConnection.set(KEY_1, VALUE_1); + nativeConnection.set(KEY_2, VALUE_2); + + clusterConnection.flushDb(new RedisClusterNode("127.0.0.1", 7379, null)); + + assertThat(nativeConnection.get(KEY_1), notNullValue()); + assertThat(nativeConnection.get(KEY_2), nullValue()); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void zRangeByLexShouldReturnResultCorrectly() { + + nativeConnection.zadd(KEY_1, 0, "a"); + nativeConnection.zadd(KEY_1, 0, "b"); + nativeConnection.zadd(KEY_1, 0, "c"); + nativeConnection.zadd(KEY_1, 0, "d"); + nativeConnection.zadd(KEY_1, 0, "e"); + nativeConnection.zadd(KEY_1, 0, "f"); + nativeConnection.zadd(KEY_1, 0, "g"); + + Set values = clusterConnection.zRangeByLex(KEY_1_BYTES, Range.range().lte("c")); + + assertThat(values, + hasItems(LettuceConverters.toBytes("a"), LettuceConverters.toBytes("b"), LettuceConverters.toBytes("c"))); + assertThat( + values, + not(hasItems(LettuceConverters.toBytes("d"), LettuceConverters.toBytes("e"), LettuceConverters.toBytes("f"), + LettuceConverters.toBytes("g")))); + + values = clusterConnection.zRangeByLex(KEY_1_BYTES, Range.range().lt("c")); + assertThat(values, hasItems(LettuceConverters.toBytes("a"), LettuceConverters.toBytes("b"))); + assertThat(values, not(hasItem(LettuceConverters.toBytes("c")))); + + values = clusterConnection.zRangeByLex(KEY_1_BYTES, Range.range().gte("aaa").lt("g")); + assertThat( + values, + hasItems(LettuceConverters.toBytes("b"), LettuceConverters.toBytes("c"), LettuceConverters.toBytes("d"), + LettuceConverters.toBytes("e"), LettuceConverters.toBytes("f"))); + assertThat(values, not(hasItems(LettuceConverters.toBytes("a"), LettuceConverters.toBytes("g")))); + + values = clusterConnection.zRangeByLex(KEY_1_BYTES, Range.range().gte("e")); + assertThat(values, + hasItems(LettuceConverters.toBytes("e"), LettuceConverters.toBytes("f"), LettuceConverters.toBytes("g"))); + assertThat( + values, + not(hasItems(LettuceConverters.toBytes("a"), LettuceConverters.toBytes("b"), LettuceConverters.toBytes("c"), + LettuceConverters.toBytes("d")))); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void infoShouldCollectionInfoFromAllClusterNodes() { + assertThat(clusterConnection.info().size(), is(3)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void clientListShouldGetInfosForAllClients() { + assertThat(clusterConnection.getClientList().isEmpty(), is(false)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void getClusterNodeForKeyShouldReturnNodeCorrectly() { + assertThat((RedisNode) clusterConnection.clusterGetNodeForKey(KEY_1_BYTES), is(new RedisNode("127.0.0.1", 7380))); + } + + /** + * @see DATAREDIS-315 + */ + @Test + @Ignore("Should work in 3.4 but does not work in 3.3.2") + public void countKeysShouldReturnNumberOfKeysInSlot() { + + nativeConnection.set(SAME_SLOT_KEY_1, VALUE_1); + nativeConnection.set(SAME_SLOT_KEY_2, VALUE_2); + + assertThat(clusterConnection.clusterCountKeysInSlot(ClusterSlotHashUtil.calculateSlot(SAME_SLOT_KEY_1)), is(2L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void pfAddShouldAddValuesCorrectly() { + + clusterConnection.pfAdd(KEY_1_BYTES, VALUE_1_BYTES, VALUE_2_BYTES, VALUE_3_BYTES); + + assertThat(nativeConnection.pfcount(KEY_1), is(3L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void pfCountShouldAllowCountingOnSingleKey() { + + nativeConnection.pfadd(KEY_1, VALUE_1, VALUE_2, VALUE_3); + + assertThat(clusterConnection.pfCount(KEY_1_BYTES), is(3L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void pfCountShouldAllowCountingOnSameSlotKeys() { + + nativeConnection.pfadd(SAME_SLOT_KEY_1, VALUE_1, VALUE_2); + nativeConnection.pfadd(SAME_SLOT_KEY_2, VALUE_2, VALUE_3); + + assertThat(clusterConnection.pfCount(SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES), is(3L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = DataAccessException.class) + public void pfCountShouldThrowErrorCountingOnDifferentSlotKeys() { + + nativeConnection.pfadd(KEY_1, VALUE_1, VALUE_2); + nativeConnection.pfadd(KEY_2, VALUE_2, VALUE_3); + + clusterConnection.pfCount(KEY_1_BYTES, KEY_2_BYTES); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void pfMergeShouldWorkWhenAllKeysMapToSameSlot() { + + nativeConnection.pfadd(SAME_SLOT_KEY_1, VALUE_1, VALUE_2); + nativeConnection.pfadd(SAME_SLOT_KEY_2, VALUE_2, VALUE_3); + + nativeConnection.pfmerge(SAME_SLOT_KEY_3, SAME_SLOT_KEY_1, SAME_SLOT_KEY_2); + + assertThat(nativeConnection.pfcount(SAME_SLOT_KEY_3), is(3L)); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = DataAccessException.class) + public void pfMergeShouldThrowErrorOnDifferentSlotKeys() { + clusterConnection.pfMerge(KEY_3_BYTES, KEY_1_BYTES, KEY_2_BYTES); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void infoShouldCollectInfoForSpecificNode() { + + Properties properties = clusterConnection.info(new RedisClusterNode(CLUSTER_HOST, MASTER_NODE_2_PORT)); + + assertThat(properties.getProperty("tcp_port"), is(Integer.toString(MASTER_NODE_2_PORT))); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void infoShouldCollectInfoForSpecificNodeAndSection() { + + Properties properties = clusterConnection.info(new RedisClusterNode(CLUSTER_HOST, MASTER_NODE_2_PORT), "server"); + + assertThat(properties.getProperty("tcp_port"), is(Integer.toString(MASTER_NODE_2_PORT))); + assertThat(properties.getProperty("used_memory"), nullValue()); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void getConfigShouldLoadCumulatedConfiguration() { + + List result = clusterConnection.getConfig("*max-*-entries*"); + + assertThat(result.size(), is(24)); + for (int i = 0; i < result.size(); i++) { + + if (i % 2 == 0) { + assertThat(result.get(i), startsWith(CLUSTER_HOST)); + } else { + assertThat(result.get(i), not(startsWith(CLUSTER_HOST))); + } + } + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void getConfigShouldLoadConfigurationOfSpecificNode() { + + List result = clusterConnection.getConfig(new RedisClusterNode(CLUSTER_HOST, SLAVEOF_NODE_1_PORT), "*"); + + ListIterator it = result.listIterator(); + Integer valueIndex = null; + while (it.hasNext()) { + + String cur = it.next(); + if (cur.equals("slaveof")) { + valueIndex = it.nextIndex(); + break; + } + } + + assertThat(valueIndex, notNullValue()); + assertThat(result.get(valueIndex), endsWith("7379")); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void clusterGetSlavesShouldReturnSlaveCorrectly() { + + Set slaves = clusterConnection.clusterGetSlaves(new RedisClusterNode(CLUSTER_HOST, + MASTER_NODE_1_PORT)); + + assertThat(slaves.size(), is(1)); + assertThat(slaves, hasItem(new RedisClusterNode(CLUSTER_HOST, SLAVEOF_NODE_1_PORT))); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void clusterGetMasterSlaveMapShouldListMastersAndSlavesCorrectly() { + + Map> masterSlaveMap = clusterConnection.clusterGetMasterSlaveMap(); + + assertThat(masterSlaveMap, notNullValue()); + assertThat(masterSlaveMap.size(), is(3)); + assertThat(masterSlaveMap.get(new RedisClusterNode(CLUSTER_HOST, MASTER_NODE_1_PORT)), + hasItem(new RedisClusterNode(CLUSTER_HOST, SLAVEOF_NODE_1_PORT))); + assertThat(masterSlaveMap.get(new RedisClusterNode(CLUSTER_HOST, MASTER_NODE_2_PORT)).isEmpty(), is(true)); + assertThat(masterSlaveMap.get(new RedisClusterNode(CLUSTER_HOST, MASTER_NODE_3_PORT)).isEmpty(), is(true)); + } +} diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionUnitTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionUnitTests.java new file mode 100644 index 000000000..1059059a8 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionUnitTests.java @@ -0,0 +1,423 @@ +/* + * Copyright 2015 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.lettuce; + +import static org.hamcrest.core.AnyOf.*; +import static org.hamcrest.core.Is.*; +import static org.hamcrest.core.IsNull.*; +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; +import static org.springframework.data.redis.connection.ClusterTestVariables.*; +import static org.springframework.data.redis.test.util.MockitoUtils.*; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.redis.connection.ClusterCommandExecutor; +import org.springframework.data.redis.connection.ClusterNodeResourceProvider; +import org.springframework.data.redis.connection.RedisClusterCommands.AddSlots; +import org.springframework.data.redis.connection.RedisClusterNode; + +import com.lambdaworks.redis.RedisAsyncConnection; +import com.lambdaworks.redis.RedisConnection; +import com.lambdaworks.redis.RedisURI; +import com.lambdaworks.redis.cluster.RedisClusterClient; +import com.lambdaworks.redis.cluster.models.partitions.Partitions; +import com.lambdaworks.redis.cluster.models.partitions.RedisClusterNode.NodeFlag; + +/** + * @author Christoph Strobl + */ +@RunWith(MockitoJUnitRunner.class) +public class LettuceClusterConnectionUnitTests { + + static final byte[] KEY_1_BYTES = KEY_1.getBytes(); + + static final byte[] VALUE_1_BYTES = VALUE_1.getBytes(); + + static final byte[] KEY_2_BYTES = KEY_2.getBytes(); + + static final byte[] KEY_3_BYTES = KEY_3.getBytes(); + + @Mock RedisClusterClient clusterMock; + + @Mock ClusterNodeResourceProvider resourceProvider; + @Mock RedisAsyncConnection dedicatedConnectionMock; + @Mock RedisConnection clusterConnection1Mock; + @Mock RedisConnection clusterConnection2Mock; + @Mock RedisConnection clusterConnection3Mock; + + LettuceClusterConnection connection; + + @Before + public void setUp() { + + Partitions partitions = new Partitions(); + + com.lambdaworks.redis.cluster.models.partitions.RedisClusterNode partition1 = new com.lambdaworks.redis.cluster.models.partitions.RedisClusterNode(); + partition1.setNodeId(CLUSTER_NODE_1.getId()); + partition1.setConnected(true); + partition1.setFlags(Collections.singleton(NodeFlag.MASTER)); + partition1.setUri(RedisURI.create("redis://" + CLUSTER_HOST + ":" + MASTER_NODE_1_PORT)); + + com.lambdaworks.redis.cluster.models.partitions.RedisClusterNode partition2 = new com.lambdaworks.redis.cluster.models.partitions.RedisClusterNode(); + partition2.setNodeId(CLUSTER_NODE_2.getId()); + partition2.setConnected(true); + partition2.setFlags(Collections.singleton(NodeFlag.MASTER)); + partition2.setUri(RedisURI.create("redis://" + CLUSTER_HOST + ":" + MASTER_NODE_2_PORT)); + + com.lambdaworks.redis.cluster.models.partitions.RedisClusterNode partition3 = new com.lambdaworks.redis.cluster.models.partitions.RedisClusterNode(); + partition3.setNodeId(CLUSTER_NODE_3.getId()); + partition3.setConnected(true); + partition3.setFlags(Collections.singleton(NodeFlag.MASTER)); + partition3.setUri(RedisURI.create("redis://" + CLUSTER_HOST + ":" + MASTER_NODE_3_PORT)); + + partitions.addPartition(partition1); + partitions.addPartition(partition2); + partitions.addPartition(partition3); + + when(resourceProvider.getResourceForSpecificNode(CLUSTER_NODE_1)).thenReturn(clusterConnection1Mock); + when(resourceProvider.getResourceForSpecificNode(CLUSTER_NODE_2)).thenReturn(clusterConnection2Mock); + when(resourceProvider.getResourceForSpecificNode(CLUSTER_NODE_3)).thenReturn(clusterConnection3Mock); + + when(clusterMock.getPartitions()).thenReturn(partitions); + + ClusterCommandExecutor executor = new ClusterCommandExecutor( + new LettuceClusterConnection.LettuceClusterTopologyProvider(clusterMock), resourceProvider, + LettuceClusterConnection.exceptionConverter); + + connection = new LettuceClusterConnection(clusterMock, executor) { + + @Override + protected RedisAsyncConnection getAsyncDedicatedConnection() { + return dedicatedConnectionMock; + } + + @Override + public List clusterGetClusterNodes() { + return Arrays.asList(CLUSTER_NODE_1, CLUSTER_NODE_2, CLUSTER_NODE_3); + } + }; + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = IllegalArgumentException.class) + public void thowsExceptionWhenClusterCommandExecturorIsNull() { + new LettuceClusterConnection(clusterMock, null); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void clusterMeetShouldSendCommandsToExistingNodesCorrectly() { + + connection.clusterMeet(UNKNOWN_CLUSTER_NODE); + + verify(clusterConnection1Mock, times(1)) + .clusterMeet(UNKNOWN_CLUSTER_NODE.getHost(), UNKNOWN_CLUSTER_NODE.getPort()); + verify(clusterConnection2Mock, times(1)) + .clusterMeet(UNKNOWN_CLUSTER_NODE.getHost(), UNKNOWN_CLUSTER_NODE.getPort()); + verify(clusterConnection3Mock, times(1)) + .clusterMeet(UNKNOWN_CLUSTER_NODE.getHost(), UNKNOWN_CLUSTER_NODE.getPort()); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = IllegalArgumentException.class) + public void clusterMeetShouldThrowExceptionWhenNodeIsNull() { + connection.clusterMeet(null); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void clusterForgetShouldSendCommandsToRemainingNodesCorrectly() { + + connection.clusterForget(CLUSTER_NODE_2); + + verify(clusterConnection1Mock, times(1)).clusterForget(CLUSTER_NODE_2.getId()); + verifyZeroInteractions(clusterConnection2Mock); + verify(clusterConnection3Mock, times(1)).clusterForget(CLUSTER_NODE_2.getId()); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void clusterReplicateShouldSendCommandsCorrectly() { + + connection.clusterReplicate(CLUSTER_NODE_1, CLUSTER_NODE_2); + + verify(clusterConnection2Mock, times(1)).clusterReplicate(CLUSTER_NODE_1.getId()); + verifyZeroInteractions(clusterConnection1Mock); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void closeShouldNotCloseUnderlyingClusterPool() throws IOException { + + connection.close(); + + verify(clusterMock, never()).shutdown(); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void isClosedShouldReturnConnectionStateCorrectly() { + + assertThat(connection.isClosed(), is(false)); + + connection.close(); + + assertThat(connection.isClosed(), is(true)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void keysShouldBeRunOnAllClusterNodes() { + + when(clusterConnection1Mock.keys(any(byte[].class))).thenReturn(Collections. emptyList()); + when(clusterConnection2Mock.keys(any(byte[].class))).thenReturn(Collections. emptyList()); + when(clusterConnection3Mock.keys(any(byte[].class))).thenReturn(Collections. emptyList()); + + byte[] pattern = LettuceConverters.toBytes("*"); + + connection.keys(pattern); + + verify(clusterConnection1Mock, times(1)).keys(pattern); + verify(clusterConnection2Mock, times(1)).keys(pattern); + verify(clusterConnection3Mock, times(1)).keys(pattern); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void keysShouldOnlyBeRunOnDedicatedNodeWhenPinned() { + + when(clusterConnection2Mock.keys(any(byte[].class))).thenReturn(Collections. emptyList()); + + byte[] pattern = LettuceConverters.toBytes("*"); + + connection.keys(CLUSTER_NODE_2, pattern); + + verify(clusterConnection1Mock, never()).keys(pattern); + verify(clusterConnection2Mock, times(1)).keys(pattern); + verify(clusterConnection3Mock, never()).keys(pattern); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void randomKeyShouldReturnAnyKeyFromRandomNode() { + + when(clusterConnection1Mock.randomkey()).thenReturn(KEY_1_BYTES); + when(clusterConnection2Mock.randomkey()).thenReturn(KEY_2_BYTES); + when(clusterConnection3Mock.randomkey()).thenReturn(KEY_3_BYTES); + + assertThat(connection.randomKey(), anyOf(is(KEY_1_BYTES), is(KEY_2_BYTES), is(KEY_3_BYTES))); + verifyInvocationsAcross("randomkey", times(1), clusterConnection1Mock, clusterConnection2Mock, + clusterConnection3Mock); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void randomKeyShouldReturnKeyWhenAvailableOnAnyNode() { + + when(clusterConnection3Mock.randomkey()).thenReturn(KEY_3_BYTES); + + for (int i = 0; i < 100; i++) { + assertThat(connection.randomKey(), is(KEY_3_BYTES)); + } + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void randomKeyShouldReturnNullWhenNoKeysPresentOnAllNodes() { + + when(clusterConnection1Mock.randomkey()).thenReturn(null); + when(clusterConnection2Mock.randomkey()).thenReturn(null); + when(clusterConnection3Mock.randomkey()).thenReturn(null); + + assertThat(connection.randomKey(), nullValue()); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void clusterSetSlotImportingShouldBeExecutedCorrectly() { + + connection.clusterSetSlot(CLUSTER_NODE_1, 100, AddSlots.IMPORTING); + + verify(clusterConnection1Mock, times(1)).clusterSetSlotImporting(eq(100), eq(CLUSTER_NODE_1.getId())); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void clusterSetSlotMigratingShouldBeExecutedCorrectly() { + + connection.clusterSetSlot(CLUSTER_NODE_1, 100, AddSlots.MIGRATING); + + verify(clusterConnection1Mock, times(1)).clusterSetSlotMigrating(eq(100), eq(CLUSTER_NODE_1.getId())); + } + + /** + * @see DATAREDIS-315 + */ + @Test + @Ignore("Stable not available for lettuce") + public void clusterSetSlotStableShouldBeExecutedCorrectly() { + + connection.clusterSetSlot(CLUSTER_NODE_1, 100, AddSlots.STABLE); + + // verify(clusterConnection1Mock, times(1)).clusterSetSlotStable(eq(100)); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void clusterSetSlotNodeShouldBeExecutedCorrectly() { + + connection.clusterSetSlot(CLUSTER_NODE_1, 100, AddSlots.NODE); + + verify(clusterConnection1Mock, times(1)).clusterSetSlotNode(eq(100), eq(CLUSTER_NODE_1.getId())); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void clusterSetSlotShouldBeExecutedOnTargetNodeWhenNodeIdNotSet() { + + connection.clusterSetSlot(new RedisClusterNode(CLUSTER_HOST, MASTER_NODE_2_PORT), 100, AddSlots.IMPORTING); + + verify(clusterConnection2Mock, times(1)).clusterSetSlotImporting(eq(100), eq(CLUSTER_NODE_2.getId())); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = IllegalArgumentException.class) + public void clusterSetSlotShouldThrowExceptionWhenModeIsNull() { + connection.clusterSetSlot(CLUSTER_NODE_1, 100, null); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void clusterDeleteSlotsShouldBeExecutedCorrectly() { + + int[] slots = new int[] { 9000, 10000 }; + connection.clusterDeleteSlots(CLUSTER_NODE_2, slots); + + verify(clusterConnection2Mock, times(1)).clusterDelSlots((int[]) anyVararg()); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = IllegalArgumentException.class) + public void clusterDeleteSlotShouldThrowExceptionWhenNodeIsNull() { + connection.clusterDeleteSlots(null, new int[] { 1 }); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void timeShouldBeExecutedOnArbitraryNode() { + + List values = Arrays.asList("1449655759".getBytes(), "92217".getBytes()); + when(clusterConnection1Mock.time()).thenReturn(values); + when(clusterConnection2Mock.time()).thenReturn(values); + when(clusterConnection3Mock.time()).thenReturn(values); + + connection.time(); + + verifyInvocationsAcross("time", times(1), clusterConnection1Mock, clusterConnection2Mock, clusterConnection3Mock); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void timeShouldBeExecutedOnSingleNode() { + + when(clusterConnection2Mock.time()).thenReturn(Arrays.asList("1449655759".getBytes(), "92217".getBytes())); + + connection.time(CLUSTER_NODE_2); + + verify(clusterConnection2Mock, times(1)).time(); + verifyZeroInteractions(clusterConnection1Mock, clusterConnection3Mock); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void resetConfigStatsShouldBeExecutedOnAllNodes() { + + connection.resetConfigStats(); + + verify(clusterConnection1Mock, times(1)).configResetstat(); + verify(clusterConnection2Mock, times(1)).configResetstat(); + verify(clusterConnection3Mock, times(1)).configResetstat(); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void resetConfigStatsShouldBeExecutedOnSingleNodeCorrectly() { + + connection.resetConfigStats(CLUSTER_NODE_2); + + verify(clusterConnection2Mock, times(1)).configResetstat(); + verify(clusterConnection1Mock, never()).configResetstat(); + verify(clusterConnection1Mock, never()).configResetstat(); + } + +} diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryUnitTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryUnitTests.java new file mode 100644 index 000000000..87f238520 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryUnitTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2015 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.lettuce; + +import static org.hamcrest.core.IsInstanceOf.*; +import static org.junit.Assert.*; +import static org.springframework.test.util.ReflectionTestUtils.*; + +import org.junit.Test; +import org.springframework.data.redis.connection.RedisClusterConfiguration; + +import com.lambdaworks.redis.cluster.RedisClusterClient; + +/** + * @author Christoph Strobl + */ +public class LettuceConnectionFactoryUnitTests { + + private static final RedisClusterConfiguration CLUSTER_CONFIG = new RedisClusterConfiguration().clusterNode( + "127.0.0.1", 6379).clusterNode("127.0.0.1", 6380); + + /** + * @see DATAREDIS-315 + */ + @Test + public void shouldInitClientCorrectlyWhenClusterConfigPresent() { + + LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(CLUSTER_CONFIG); + connectionFactory.afterPropertiesSet(); + + assertThat(getField(connectionFactory, "client"), instanceOf(RedisClusterClient.class)); + } +} diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java index aa605c964..134a5abd5 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2015 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. @@ -15,14 +15,28 @@ */ package org.springframework.data.redis.connection.lettuce; +import static org.hamcrest.core.Is.*; +import static org.hamcrest.core.IsCollectionContaining.*; import static org.hamcrest.core.IsEqual.*; +import static org.hamcrest.core.IsNull.*; import static org.junit.Assert.*; +import static org.springframework.data.redis.connection.ClusterTestVariables.*; +import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; +import java.util.List; import org.junit.Test; +import org.springframework.data.redis.connection.RedisClusterNode; +import org.springframework.data.redis.connection.RedisClusterNode.Flag; +import org.springframework.data.redis.connection.RedisClusterNode.LinkState; import org.springframework.data.redis.core.types.RedisClientInfo; +import com.lambdaworks.redis.RedisURI; +import com.lambdaworks.redis.cluster.models.partitions.Partitions; +import com.lambdaworks.redis.cluster.models.partitions.RedisClusterNode.NodeFlag; + /** * @author Christoph Strobl */ @@ -61,4 +75,40 @@ public class LettuceConvertersUnitTests { assertThat(LettuceConverters.toListOfRedisClientInformation(sb.toString()).size(), equalTo(2)); } + /** + * @see DATAREDIS-315 + */ + @Test + public void partitionsToClusterNodesShouldReturnEmptyCollectionWhenPartionsDoesNotContainElements() { + assertThat(LettuceConverters.partitionsToClusterNodes(new Partitions()), notNullValue()); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void partitionsToClusterNodesShouldConvertPartitionCorrctly() { + + Partitions partitions = new Partitions(); + + com.lambdaworks.redis.cluster.models.partitions.RedisClusterNode partition = new com.lambdaworks.redis.cluster.models.partitions.RedisClusterNode(); + partition.setNodeId(CLUSTER_NODE_1.getId()); + partition.setConnected(true); + partition.setFlags(new HashSet(Arrays.asList(NodeFlag.MASTER, NodeFlag.MYSELF))); + partition.setUri(RedisURI.create("redis://" + CLUSTER_HOST + ":" + MASTER_NODE_1_PORT)); + partition.setSlots(Arrays. asList(1, 2, 3, 4, 5)); + + partitions.addPartition(partition); + + List nodes = LettuceConverters.partitionsToClusterNodes(partitions); + assertThat(nodes.size(), is(1)); + + RedisClusterNode node = nodes.get(0); + assertThat(node.getHost(), is(CLUSTER_HOST)); + assertThat(node.getPort(), is(MASTER_NODE_1_PORT)); + assertThat(node.getFlags(), hasItems(Flag.MASTER, Flag.MYSELF)); + assertThat(node.getId(), is(CLUSTER_NODE_1.getId())); + assertThat(node.getLinkState(), is(LinkState.CONNECTED)); + assertThat(node.getSlotRange().getSlots(), hasItems(1, 2, 3, 4, 5)); + } } diff --git a/src/test/java/org/springframework/data/redis/core/DefaultClusterOperationsUnitTests.java b/src/test/java/org/springframework/data/redis/core/DefaultClusterOperationsUnitTests.java new file mode 100644 index 000000000..610c5c4ce --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/DefaultClusterOperationsUnitTests.java @@ -0,0 +1,376 @@ +/* + * Copyright 2015 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.core; + +import static org.hamcrest.core.Is.*; +import static org.hamcrest.core.IsCollectionContaining.*; +import static org.hamcrest.core.IsNull.*; +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.dao.DataAccessException; +import org.springframework.data.redis.connection.RedisClusterCommands.AddSlots; +import org.springframework.data.redis.connection.RedisClusterConnection; +import org.springframework.data.redis.connection.RedisClusterNode; +import org.springframework.data.redis.connection.RedisClusterNode.SlotRange; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisServerCommands.MigrateOption; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * @author Christoph Strobl + */ +@RunWith(MockitoJUnitRunner.class) +public class DefaultClusterOperationsUnitTests { + + static final RedisClusterNode NODE_1 = RedisClusterNode.newRedisClusterNode().listeningAt("127.0.0.1", 6379) + .withId("d1861060fe6a534d42d8a19aeb36600e18785e04").build(); + + static final RedisClusterNode NODE_2 = RedisClusterNode.newRedisClusterNode().listeningAt("127.0.0.1", 6380) + .withId("0f2ee5df45d18c50aca07228cc18b1da96fd5e84").build(); + + @Mock RedisConnectionFactory connectionFactory; + @Mock RedisClusterConnection connection; + + RedisSerializer serializer; + + DefaultClusterOperations clusterOps; + + @Before + public void setUp() { + + when(connectionFactory.getConnection()).thenReturn(connection); + when(connectionFactory.getClusterConnection()).thenReturn(connection); + + serializer = new StringRedisSerializer(); + + RedisTemplate template = new RedisTemplate(); + template.setConnectionFactory(connectionFactory); + template.setValueSerializer(serializer); + template.setKeySerializer(serializer); + template.afterPropertiesSet(); + + this.clusterOps = new DefaultClusterOperations(template); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void keysShouldDelegateToConnectionCorrectly() { + + Set keys = new HashSet(Arrays.asList(serializer.serialize("key-1"), serializer.serialize("key-2"))); + when(connection.keys(any(RedisClusterNode.class), any(byte[].class))).thenReturn(keys); + + assertThat(clusterOps.keys(NODE_1, "*"), hasItems("key-1", "key-2")); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = IllegalArgumentException.class) + public void keysShouldThrowExceptionWhenNodeIsNull() { + clusterOps.keys(null, "*"); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void keysShouldReturnEmptySetWhenNoKeysAvailable() { + + when(connection.keys(any(RedisClusterNode.class), any(byte[].class))).thenReturn(null); + + assertThat(clusterOps.keys(NODE_1, "*"), notNullValue()); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void randomKeyShouldDelegateToConnection() { + + when(connection.randomKey(any(RedisClusterNode.class))).thenReturn(serializer.serialize("key-1")); + + assertThat(clusterOps.randomKey(NODE_1), is("key-1")); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = IllegalArgumentException.class) + public void randomKeyShouldThrowExceptionWhenNodeIsNull() { + clusterOps.randomKey(null); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void randomKeyShouldReturnNullWhenNoKeyAvailable() { + + when(connection.randomKey(any(RedisClusterNode.class))).thenReturn(null); + + assertThat(clusterOps.randomKey(NODE_1), nullValue()); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void pingShouldDelegateToConnection() { + + when(connection.ping(any(RedisClusterNode.class))).thenReturn("PONG"); + + assertThat(clusterOps.ping(NODE_1), is("PONG")); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = IllegalArgumentException.class) + public void pingShouldThrowExceptionWhenNodeIsNull() { + clusterOps.ping(null); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void addSlotsShouldDelegateToConnection() { + + clusterOps.addSlots(NODE_1, 1, 2, 3); + + verify(connection, times(1)).clusterAddSlots(eq(NODE_1), Mockito. anyVararg()); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = IllegalArgumentException.class) + public void addSlotsShouldThrowExceptionWhenNodeIsNull() { + clusterOps.addSlots(null); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void addSlotsWithRangeShouldDelegateToConnection() { + + clusterOps.addSlots(NODE_1, new SlotRange(1, 3)); + + verify(connection, times(1)).clusterAddSlots(eq(NODE_1), Mockito. anyVararg()); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = IllegalArgumentException.class) + public void addSlotsWithRangeShouldThrowExceptionWhenRangeIsNull() { + clusterOps.addSlots(NODE_1, (SlotRange) null); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void bgSaveShouldDelegateToConnection() { + + clusterOps.bgSave(NODE_1); + + verify(connection, times(1)).bgSave(eq(NODE_1)); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = IllegalArgumentException.class) + public void bgSaveShouldThrowExceptionWhenNodeIsNull() { + clusterOps.bgSave(null); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void meetShouldDelegateToConnection() { + + clusterOps.meet(NODE_1); + + verify(connection, times(1)).clusterMeet(eq(NODE_1)); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = IllegalArgumentException.class) + public void meetShouldThrowExceptionWhenNodeIsNull() { + clusterOps.meet(null); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void forgetShouldDelegateToConnection() { + + clusterOps.forget(NODE_1); + + verify(connection, times(1)).clusterForget(eq(NODE_1)); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = IllegalArgumentException.class) + public void forgetShouldThrowExceptionWhenNodeIsNull() { + clusterOps.forget(null); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void flushDbShouldDelegateToConnection() { + + clusterOps.flushDb(NODE_1); + + verify(connection, times(1)).flushDb(eq(NODE_1)); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = IllegalArgumentException.class) + public void flushDbShouldThrowExceptionWhenNodeIsNull() { + clusterOps.flushDb(null); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void getSlavesShouldDelegateToConnection() { + + clusterOps.getSlaves(NODE_1); + + verify(connection, times(1)).clusterGetSlaves(eq(NODE_1)); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = IllegalArgumentException.class) + public void getSlavesShouldThrowExceptionWhenNodeIsNull() { + clusterOps.getSlaves(null); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void saveShouldDelegateToConnection() { + + clusterOps.save(NODE_1); + + verify(connection, times(1)).save(eq(NODE_1)); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = IllegalArgumentException.class) + public void saveShouldThrowExceptionWhenNodeIsNull() { + clusterOps.save(null); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void shutdownShouldDelegateToConnection() { + + clusterOps.shutdown(NODE_1); + + verify(connection, times(1)).shutdown(eq(NODE_1)); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = IllegalArgumentException.class) + public void shutdownShouldThrowExceptionWhenNodeIsNull() { + clusterOps.shutdown(null); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void executeShouldDelegateToConnection() { + + final byte[] key = serializer.serialize("foo"); + clusterOps.execute(new RedisClusterCallback() { + + @Override + public String doInRedis(RedisClusterConnection connection) throws DataAccessException { + return serializer.deserialize(connection.get(key)); + } + }); + + verify(connection, times(1)).get(eq(key)); + } + + /** + * @see DATAREDIS-315 + */ + @Test(expected = IllegalArgumentException.class) + public void executeShouldThrowExceptionWhenCallbackIsNull() { + clusterOps.execute(null); + } + + /** + * @see DATAREDIS-315 + */ + @Test + public void reshardShouldExecuteCommandsCorrectly() { + + byte[] key = "foo".getBytes(); + when(connection.clusterGetKeysInSlot(eq(100), anyInt())).thenReturn(Collections.singletonList(key)); + clusterOps.reshard(NODE_1, 100, NODE_2); + + verify(connection, times(1)).clusterSetSlot(eq(NODE_2), eq(100), eq(AddSlots.IMPORTING)); + verify(connection, times(1)).clusterSetSlot(eq(NODE_1), eq(100), eq(AddSlots.MIGRATING)); + verify(connection, times(1)).clusterGetKeysInSlot(eq(100), anyInt()); + verify(connection, times(1)).migrate(any(byte[].class), eq(NODE_1), eq(0), eq(MigrateOption.COPY)); + verify(connection, times(1)).clusterSetSlot(eq(NODE_2), eq(100), eq(AddSlots.NODE)); + + } +} diff --git a/src/test/java/org/springframework/data/redis/core/MultithreadedRedisTemplateTests.java b/src/test/java/org/springframework/data/redis/core/MultithreadedRedisTemplateTests.java index 6990cc501..4eefdbc75 100644 --- a/src/test/java/org/springframework/data/redis/core/MultithreadedRedisTemplateTests.java +++ b/src/test/java/org/springframework/data/redis/core/MultithreadedRedisTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2015 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. @@ -23,10 +23,12 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import org.junit.AfterClass; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; +import org.springframework.data.redis.ConnectionFactoryTracker; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; @@ -43,6 +45,12 @@ public class MultithreadedRedisTemplateTests { public MultithreadedRedisTemplateTests(RedisConnectionFactory factory) { this.factory = factory; + ConnectionFactoryTracker.add(factory); + } + + @AfterClass + public static void cleanUp() { + ConnectionFactoryTracker.cleanUp(); } @Parameters @@ -57,7 +65,7 @@ public class MultithreadedRedisTemplateTests { lettuce.afterPropertiesSet(); SrpConnectionFactory srp = new SrpConnectionFactory(); - srp.setPort(6479); + srp.setPort(6379); srp.afterPropertiesSet(); return Arrays.asList(new Object[][] { { jedis }, { lettuce }, { srp } }); diff --git a/src/test/java/org/springframework/data/redis/core/RedisClusterTemplateTests.java b/src/test/java/org/springframework/data/redis/core/RedisClusterTemplateTests.java new file mode 100644 index 000000000..2d7d740bb --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/RedisClusterTemplateTests.java @@ -0,0 +1,241 @@ +/* + * Copyright 2015 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.core; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.junit.ClassRule; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.redis.LongObjectFactory; +import org.springframework.data.redis.ObjectFactory; +import org.springframework.data.redis.Person; +import org.springframework.data.redis.PersonObjectFactory; +import org.springframework.data.redis.RawObjectFactory; +import org.springframework.data.redis.StringObjectFactory; +import org.springframework.data.redis.connection.RedisClusterConfiguration; +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.serializer.GenericToStringSerializer; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.OxmSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.data.redis.test.util.RedisClusterRule; +import org.springframework.oxm.xstream.XStreamMarshaller; + +/** + * @author Christoph Strobl + */ +public class RedisClusterTemplateTests extends RedisTemplateTests { + + static final List CLUSTER_NODES = Arrays.asList("127.0.0.1:7379", "127.0.0.1:7380", "127.0.0.1:7381"); + + public RedisClusterTemplateTests(RedisTemplate redisTemplate, ObjectFactory keyFactory, + ObjectFactory valueFactory) { + super(redisTemplate, keyFactory, valueFactory); + } + + public static @ClassRule RedisClusterRule clusterAvaialbale = new RedisClusterRule(); + + @Test(expected = InvalidDataAccessApiUsageException.class) + @Ignore("Pipeline not supported in cluster mode") + public void testExecutePipelinedNonNullRedisCallback() { + super.testExecutePipelinedNonNullRedisCallback(); + } + + @Test + @Ignore("Pipeline not supported in cluster mode") + public void testExecutePipelinedTx() { + super.testExecutePipelinedTx(); + } + + @Test + @Ignore("Watch only supported on same connection...") + public void testWatch() { + super.testWatch(); + } + + @Test + @Ignore("Watch only supported on same connection...") + public void testUnwatch() { + super.testUnwatch(); + } + + @Test + @Ignore("EXEC only supported on same connection...") + public void testExec() { + super.testExec(); + } + + @Test(expected = InvalidDataAccessApiUsageException.class) + @Ignore("Pipeline not supported in cluster mode") + public void testExecutePipelinedNonNullSessionCallback() { + super.testExecutePipelinedNonNullSessionCallback(); + } + + @Test + @Ignore("PubSub not supported in cluster mode") + public void testConvertAndSend() { + super.testConvertAndSend(); + } + + @Test + @Ignore("Watch only supported on same connection...") + public void testExecConversionDisabled() { + super.testExecConversionDisabled(); + } + + @Test + @Ignore("Discard only supported on same connection...") + public void testDiscard() { + super.testDiscard(); + } + + @Test + @Ignore("Pipleline not supported in cluster mode") + public void testExecutePipelined() { + super.testExecutePipelined(); + } + + @Test + @Ignore("Watch only supported on same connection...") + public void testWatchMultipleKeys() { + super.testWatchMultipleKeys(); + } + + @Test + @Ignore("This one fails when using GET options on numbers") + public void testSortBulkMapper() { + super.testSortBulkMapper(); + } + + @Parameters + public static Collection testParams() { + + ObjectFactory stringFactory = new StringObjectFactory(); + ObjectFactory longFactory = new LongObjectFactory(); + ObjectFactory rawFactory = new RawObjectFactory(); + ObjectFactory personFactory = new PersonObjectFactory(); + + // XStream serializer + XStreamMarshaller xstream = new XStreamMarshaller(); + try { + xstream.afterPropertiesSet(); + } catch (Exception ex) { + throw new RuntimeException("Cannot init XStream", ex); + } + + OxmSerializer serializer = new OxmSerializer(xstream, xstream); + Jackson2JsonRedisSerializer jackson2JsonSerializer = new Jackson2JsonRedisSerializer(Person.class); + + // JEDIS + JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(new RedisClusterConfiguration( + CLUSTER_NODES)); + + jedisConnectionFactory.afterPropertiesSet(); + + RedisTemplate jedisStringTemplate = new RedisTemplate(); + jedisStringTemplate.setDefaultSerializer(new StringRedisSerializer()); + jedisStringTemplate.setConnectionFactory(jedisConnectionFactory); + jedisStringTemplate.afterPropertiesSet(); + + RedisTemplate jedisLongTemplate = new RedisTemplate(); + jedisLongTemplate.setKeySerializer(new StringRedisSerializer()); + jedisLongTemplate.setValueSerializer(new GenericToStringSerializer(Long.class)); + jedisLongTemplate.setConnectionFactory(jedisConnectionFactory); + jedisLongTemplate.afterPropertiesSet(); + + RedisTemplate jedisRawTemplate = new RedisTemplate(); + jedisRawTemplate.setEnableDefaultSerializer(false); + jedisRawTemplate.setConnectionFactory(jedisConnectionFactory); + jedisRawTemplate.afterPropertiesSet(); + + RedisTemplate jedisPersonTemplate = new RedisTemplate(); + jedisPersonTemplate.setConnectionFactory(jedisConnectionFactory); + jedisPersonTemplate.afterPropertiesSet(); + + RedisTemplate jedisXstreamStringTemplate = new RedisTemplate(); + jedisXstreamStringTemplate.setConnectionFactory(jedisConnectionFactory); + jedisXstreamStringTemplate.setDefaultSerializer(serializer); + jedisXstreamStringTemplate.afterPropertiesSet(); + + RedisTemplate jedisJackson2JsonPersonTemplate = new RedisTemplate(); + jedisJackson2JsonPersonTemplate.setConnectionFactory(jedisConnectionFactory); + jedisJackson2JsonPersonTemplate.setValueSerializer(jackson2JsonSerializer); + jedisJackson2JsonPersonTemplate.afterPropertiesSet(); + + // LETTUCE + + LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(new RedisClusterConfiguration( + CLUSTER_NODES)); + + lettuceConnectionFactory.afterPropertiesSet(); + + RedisTemplate lettuceStringTemplate = new RedisTemplate(); + lettuceStringTemplate.setDefaultSerializer(new StringRedisSerializer()); + lettuceStringTemplate.setConnectionFactory(lettuceConnectionFactory); + lettuceStringTemplate.afterPropertiesSet(); + + RedisTemplate lettuceLongTemplate = new RedisTemplate(); + lettuceLongTemplate.setKeySerializer(new StringRedisSerializer()); + lettuceLongTemplate.setValueSerializer(new GenericToStringSerializer(Long.class)); + lettuceLongTemplate.setConnectionFactory(lettuceConnectionFactory); + lettuceLongTemplate.afterPropertiesSet(); + + RedisTemplate lettuceRawTemplate = new RedisTemplate(); + lettuceRawTemplate.setEnableDefaultSerializer(false); + lettuceRawTemplate.setConnectionFactory(lettuceConnectionFactory); + lettuceRawTemplate.afterPropertiesSet(); + + RedisTemplate lettucePersonTemplate = new RedisTemplate(); + lettucePersonTemplate.setConnectionFactory(lettuceConnectionFactory); + lettucePersonTemplate.afterPropertiesSet(); + + RedisTemplate lettuceXstreamStringTemplate = new RedisTemplate(); + lettuceXstreamStringTemplate.setConnectionFactory(lettuceConnectionFactory); + lettuceXstreamStringTemplate.setDefaultSerializer(serializer); + lettuceXstreamStringTemplate.afterPropertiesSet(); + + RedisTemplate lettuceJackson2JsonPersonTemplate = new RedisTemplate(); + lettuceJackson2JsonPersonTemplate.setConnectionFactory(lettuceConnectionFactory); + lettuceJackson2JsonPersonTemplate.setValueSerializer(jackson2JsonSerializer); + lettuceJackson2JsonPersonTemplate.afterPropertiesSet(); + + return Arrays.asList(new Object[][] { // + + // JEDIS + { jedisStringTemplate, stringFactory, stringFactory }, // + { jedisLongTemplate, stringFactory, longFactory }, // + { jedisRawTemplate, rawFactory, rawFactory }, // + { jedisPersonTemplate, stringFactory, personFactory }, // + { jedisXstreamStringTemplate, stringFactory, stringFactory }, // + { jedisJackson2JsonPersonTemplate, stringFactory, personFactory }, // + + // LETTUCE + { lettuceStringTemplate, stringFactory, stringFactory }, // + { lettuceLongTemplate, stringFactory, longFactory }, // + { lettuceRawTemplate, rawFactory, rawFactory }, // + { lettucePersonTemplate, stringFactory, personFactory }, // + { lettuceXstreamStringTemplate, stringFactory, stringFactory }, // + { lettuceJackson2JsonPersonTemplate, stringFactory, personFactory } // + }); + } + +} diff --git a/src/test/java/org/springframework/data/redis/core/RedisTemplateTests.java b/src/test/java/org/springframework/data/redis/core/RedisTemplateTests.java index 61b0f0927..784ab8dc4 100644 --- a/src/test/java/org/springframework/data/redis/core/RedisTemplateTests.java +++ b/src/test/java/org/springframework/data/redis/core/RedisTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * Copyright 2013-2015 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. @@ -35,6 +35,7 @@ import java.util.concurrent.TimeUnit; import org.hamcrest.core.IsNot; import org.junit.After; +import org.junit.AfterClass; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -42,6 +43,7 @@ import org.junit.runners.Parameterized.Parameters; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.redis.ConnectionFactoryTracker; import org.springframework.data.redis.ObjectFactory; import org.springframework.data.redis.RedisTestProfileValueSource; import org.springframework.data.redis.SettingsUtils; @@ -68,17 +70,19 @@ import org.springframework.data.redis.serializer.StringRedisSerializer; @RunWith(Parameterized.class) public class RedisTemplateTests { - @Autowired private RedisTemplate redisTemplate; + @Autowired protected RedisTemplate redisTemplate; - private ObjectFactory keyFactory; + protected ObjectFactory keyFactory; - private ObjectFactory valueFactory; + protected ObjectFactory valueFactory; public RedisTemplateTests(RedisTemplate redisTemplate, ObjectFactory keyFactory, ObjectFactory valueFactory) { this.redisTemplate = redisTemplate; this.keyFactory = keyFactory; this.valueFactory = valueFactory; + + ConnectionFactoryTracker.add(redisTemplate.getConnectionFactory()); } @After @@ -91,6 +95,11 @@ public class RedisTemplateTests { }); } + @AfterClass + public static void cleanUp() { + ConnectionFactoryTracker.cleanUp(); + } + @Parameters public static Collection testParams() { return AbstractOperationsTestParams.testParams(); @@ -445,16 +454,16 @@ public class RedisTemplateTests { @Test public void testExpireMillisNotSupported() throws Exception { + assumeTrue(RedisTestProfileValueSource.matches("runLongTests", "true")); + assumeTrue(redisTemplate.getConnectionFactory() instanceof JredisConnectionFactory); + final K key1 = keyFactory.instance(); V value1 = valueFactory.instance(); + assumeTrue(key1 instanceof String && value1 instanceof String); - // JRedis does not support pExpire - JredisConnectionFactory factory = new JredisConnectionFactory(); - factory.setHostName(SettingsUtils.getHost()); - factory.setPort(SettingsUtils.getPort()); - factory.afterPropertiesSet(); - final StringRedisTemplate template2 = new StringRedisTemplate(factory); + + final StringRedisTemplate template2 = new StringRedisTemplate(redisTemplate.getConnectionFactory()); template2.boundValueOps((String) key1).set((String) value1); template2.expire((String) key1, 10, TimeUnit.MILLISECONDS); Thread.sleep(15); @@ -489,15 +498,15 @@ public class RedisTemplateTests { @Test public void testGetExpireMillisNotSupported() { + + assumeTrue(redisTemplate.getConnectionFactory() instanceof JedisConnectionFactory); + final K key1 = keyFactory.instance(); V value1 = valueFactory.instance(); + assumeTrue(key1 instanceof String && value1 instanceof String); - // Jedis does not support pTtl - JedisConnectionFactory factory = new JedisConnectionFactory(); - factory.setHostName(SettingsUtils.getHost()); - factory.setPort(SettingsUtils.getPort()); - factory.afterPropertiesSet(); - final StringRedisTemplate template2 = new StringRedisTemplate(factory); + + final StringRedisTemplate template2 = new StringRedisTemplate(redisTemplate.getConnectionFactory()); template2.boundValueOps((String) key1).set((String) value1); template2.expire((String) key1, 5, TimeUnit.SECONDS); long expire = template2.getExpire((String) key1, TimeUnit.MILLISECONDS); @@ -520,16 +529,16 @@ public class RedisTemplateTests { @Test public void testExpireAtMillisNotSupported() { + assumeTrue(RedisTestProfileValueSource.matches("runLongTests", "true")); + assumeTrue(redisTemplate.getConnectionFactory() instanceof JedisConnectionFactory); + final K key1 = keyFactory.instance(); V value1 = valueFactory.instance(); + assumeTrue(key1 instanceof String && value1 instanceof String); - // Jedis does not support pExpireAt - JedisConnectionFactory factory = new JedisConnectionFactory(); - factory.setHostName(SettingsUtils.getHost()); - factory.setPort(SettingsUtils.getPort()); - factory.afterPropertiesSet(); - final StringRedisTemplate template2 = new StringRedisTemplate(factory); + + final StringRedisTemplate template2 = new StringRedisTemplate(redisTemplate.getConnectionFactory()); template2.boundValueOps((String) key1).set((String) value1); template2.expireAt((String) key1, new Date(System.currentTimeMillis() + 5l)); // Just ensure this works as expected, pExpireAt just adds some precision over expireAt diff --git a/src/test/java/org/springframework/data/redis/test/util/MinimumRedisVersionRule.java b/src/test/java/org/springframework/data/redis/test/util/MinimumRedisVersionRule.java index 1ce20f3e1..d5ae2cf6e 100644 --- a/src/test/java/org/springframework/data/redis/test/util/MinimumRedisVersionRule.java +++ b/src/test/java/org/springframework/data/redis/test/util/MinimumRedisVersionRule.java @@ -17,7 +17,7 @@ package org.springframework.data.redis.test.util; import java.io.IOException; -import org.junit.internal.AssumptionViolatedException; +import org.junit.AssumptionViolatedException; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; @@ -50,36 +50,36 @@ public class MinimumRedisVersionRule implements TestRule { private static synchronized Version readServerVersion() { - Jedis jedis = new Jedis(SettingsUtils.getHost(), SettingsUtils.getPort()); - Version version = Version.UNKNOWN; + Jedis jedis = null; try { - - jedis.connect(); + jedis = new Jedis(SettingsUtils.getHost(), SettingsUtils.getPort()); String info = jedis.info(); String versionString = (String) JedisConverters.stringToProps().convert(info).get("redis_version"); version = RedisVersionUtils.parseVersion(versionString); } finally { - try { + if (jedis != null) { + try { - jedis.disconnect(); - if (jedis.getClient().getSocket().isConnected()) { - // force socket to be closed - jedis.getClient().getSocket().close(); - try { - // need to wait a bit - Thread.sleep(100); - } catch (InterruptedException e) { - // just ignore it + jedis.disconnect(); + if (jedis.getClient().getSocket().isConnected()) { + // force socket to be closed + jedis.getClient().getSocket().close(); + try { + // need to wait a bit + Thread.sleep(5); + } catch (InterruptedException e) { + Thread.interrupted(); + } } - } - } catch (IOException e1) { - // ignore as well + } catch (IOException e1) { + // ignore as well + } + jedis.close(); } - jedis.close(); } return version; diff --git a/src/test/java/org/springframework/data/redis/test/util/MockitoUtils.java b/src/test/java/org/springframework/data/redis/test/util/MockitoUtils.java new file mode 100644 index 000000000..a19b1be3b --- /dev/null +++ b/src/test/java/org/springframework/data/redis/test/util/MockitoUtils.java @@ -0,0 +1,104 @@ +/* + * Copyright 2015 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.test.util; + +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.hamcrest.Matcher; +import org.mockito.internal.invocation.InvocationMatcher; +import org.mockito.internal.verification.api.VerificationData; +import org.mockito.invocation.Invocation; +import org.mockito.verification.VerificationMode; +import org.springframework.util.StringUtils; + +/** + * @author Christoph Strobl + * @since 1.7 + */ +public class MockitoUtils { + + /** + * Verifies a given method is called a total number of times across all given mocks. + * + * @param method + * @param mode + * @param mocks + */ + @SuppressWarnings({ "rawtypes", "serial" }) + public static void verifyInvocationsAcross(final String method, final VerificationMode mode, Object... mocks) { + + mode.verify(new VerificationDataImpl(getInvocations(method, mocks), new InvocationMatcher(null, Collections + . singletonList(org.mockito.internal.matchers.Any.ANY)) { + + @Override + public boolean matches(Invocation actual) { + return true; + } + + @Override + public String toString() { + return String.format("%s for method: %s", mode, method); + } + + })); + } + + private static List getInvocations(String method, Object... mocks) { + + List invocations = new ArrayList(); + for (Object mock : mocks) { + + if (StringUtils.hasText(method)) { + + for (Invocation invocation : mockingDetails(mock).getInvocations()) { + if (invocation.getMethod().getName().equals(method)) { + invocations.add(invocation); + } + } + } else { + invocations.addAll(mockingDetails(mock).getInvocations()); + } + } + return invocations; + } + + static class VerificationDataImpl implements VerificationData { + + private final List invocations; + private final InvocationMatcher wanted; + + public VerificationDataImpl(List invocations, InvocationMatcher wanted) { + this.invocations = invocations; + this.wanted = wanted; + } + + @Override + public List getAllInvocations() { + return invocations; + } + + @Override + public InvocationMatcher getWanted() { + return wanted; + } + + } + +} diff --git a/src/test/java/org/springframework/data/redis/test/util/RedisClientRule.java b/src/test/java/org/springframework/data/redis/test/util/RedisClientRule.java index 3f4901e49..d92841acb 100644 --- a/src/test/java/org/springframework/data/redis/test/util/RedisClientRule.java +++ b/src/test/java/org/springframework/data/redis/test/util/RedisClientRule.java @@ -15,7 +15,7 @@ */ package org.springframework.data.redis.test.util; -import org.junit.internal.AssumptionViolatedException; +import org.junit.AssumptionViolatedException; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; diff --git a/src/test/java/org/springframework/data/redis/test/util/RedisClusterRule.java b/src/test/java/org/springframework/data/redis/test/util/RedisClusterRule.java new file mode 100644 index 000000000..21d8a02f6 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/test/util/RedisClusterRule.java @@ -0,0 +1,87 @@ +/* + * Copyright 2015 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.test.util; + +import static org.hamcrest.CoreMatchers.*; + +import org.junit.Assume; +import org.junit.rules.ExternalResource; +import org.junit.rules.TestRule; +import org.springframework.data.redis.connection.RedisClusterConfiguration; +import org.springframework.data.redis.connection.RedisNode; +import org.springframework.data.redis.connection.jedis.JedisConverters; + +import redis.clients.jedis.Jedis; + +/** + * Simple {@link TestRule} implementation that check Redis is running in cluster mode. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class RedisClusterRule extends ExternalResource { + + private RedisClusterConfiguration clusterConfig; + private String mode; + + /** + * Create and init {@link RedisClusterRule} with default configuration ({@code host=127.0.0.1 port=7379}). + */ + public RedisClusterRule() { + this(new RedisClusterConfiguration().clusterNode("127.0.0.1", 7379)); + } + + /** + * Create and init {@link RedisClientRule} with given configuration. + * + * @param config + */ + public RedisClusterRule(RedisClusterConfiguration config) { + this.clusterConfig = config; + init(); + } + + /* + * (non-Javadoc) + * @see org.junit.rules.ExternalResource#before() + */ + @Override + protected void before() throws Throwable { + Assume.assumeThat(mode, is("cluster")); + } + + private void init() { + + if (clusterConfig == null) { + return; + } + + for (RedisNode node : clusterConfig.getClusterNodes()) { + + Jedis jedis = null; + try { + + jedis = new Jedis(node.getHost(), node.getPort()); + mode = JedisConverters.toProperties(jedis.info()).getProperty("redis_mode"); + return; + } catch (Exception e) { + // ignore and move on + } finally { + jedis.close(); + } + } + } +} diff --git a/src/test/java/org/springframework/data/redis/test/util/RedisSentinelRule.java b/src/test/java/org/springframework/data/redis/test/util/RedisSentinelRule.java index 86680ee3a..dda3b7e15 100644 --- a/src/test/java/org/springframework/data/redis/test/util/RedisSentinelRule.java +++ b/src/test/java/org/springframework/data/redis/test/util/RedisSentinelRule.java @@ -15,7 +15,10 @@ */ package org.springframework.data.redis.test.util; -import org.junit.internal.AssumptionViolatedException; +import java.util.HashMap; +import java.util.Map; + +import org.junit.AssumptionViolatedException; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; @@ -39,6 +42,8 @@ public class RedisSentinelRule implements TestRule { private RedisSentinelConfiguration sentinelConfig; private SentinelsAvailable requiredSentinels; + private Map cache = new HashMap(); + protected RedisSentinelRule(RedisSentinelConfiguration config) { this.sentinelConfig = config; } @@ -128,7 +133,12 @@ public class RedisSentinelRule implements TestRule { int failed = 0; for (RedisNode node : sentinelConfig.getSentinels()) { - if (!isAvailable(node)) { + + if (cache.isEmpty() || !cache.containsKey(node.asString())) { + cache.put(node.asString(), isAvailable(node)); + } + + if (!cache.get(node.asString())) { failed++; } } @@ -159,7 +169,6 @@ public class RedisSentinelRule implements TestRule { try { jedis = new Jedis(node.getHost(), node.getPort()); - jedis.connect(); jedis.ping(); return true; @@ -173,7 +182,7 @@ public class RedisSentinelRule implements TestRule { jedis.disconnect(); if (jedis.getClient().getSocket().isConnected()) { jedis.getClient().getSocket().close(); - Thread.sleep(100); + Thread.sleep(5); } jedis.close();