From fd361a186899c2aef124d2801fb78ef733d05baa Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 21 Jul 2014 21:09:10 +0200 Subject: [PATCH] DATAREDIS-330 - Add support for SENTINEL commands. add support for commands: SENTINEL FAILOVER master SENTINEL SLAVES master SENTINEL REMOVE name SENTINEL MONITOR name ip port quorum Original pull request: #92. --- .../src/reference/docbook/reference/redis.xml | 2 + .../connection/AbstractRedisConnection.java | 115 +++ .../DefaultStringRedisConnection.java | 5 + .../redis/connection/RedisConnection.java | 6 + .../connection/RedisConnectionFactory.java | 9 +- .../data/redis/connection/RedisNode.java | 65 +- .../connection/RedisSentinelCommands.java | 64 ++ .../connection/RedisSentinelConnection.java | 31 + .../data/redis/connection/RedisServer.java | 213 +++++ .../redis/connection/convert/Converters.java | 7 +- .../convert/MapToPropertiesConverter.java | 40 + .../connection/jedis/JedisConnection.java | 37 +- .../jedis/JedisConnectionFactory.java | 28 + .../connection/jedis/JedisConverters.java | 35 + .../jedis/JedisSentinelConnection.java | 162 ++++ .../connection/jredis/JredisConnection.java | 7 +- .../jredis/JredisConnectionFactory.java | 6 + .../connection/lettuce/LettuceConnection.java | 8 +- .../lettuce/LettuceConnectionFactory.java | 6 + .../redis/connection/srp/SrpConnection.java | 8 +- .../connection/srp/SrpConnectionFactory.java | 6 + .../connection/RedisConnectionUnitTests.java | 788 ++++++++++++++++++ .../JedisConnectionIntegrationTests.java | 19 + .../jedis/JedisConnectionUnitTestSuite.java | 12 +- .../jedis/JedisConvertersUnitTests.java | 83 ++ .../JedisSentinelConnectionUnitTests.java | 198 +++++ .../jedis/JedisSentinelIntegrationTests.java | 52 +- .../redis/test/util/RedisSentinelRule.java | 71 +- .../test/util/RequiresRedisSentinel.java | 36 + 29 files changed, 2071 insertions(+), 48 deletions(-) create mode 100644 src/main/java/org/springframework/data/redis/connection/AbstractRedisConnection.java create mode 100644 src/main/java/org/springframework/data/redis/connection/RedisSentinelCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/RedisSentinelConnection.java create mode 100644 src/main/java/org/springframework/data/redis/connection/RedisServer.java create mode 100644 src/main/java/org/springframework/data/redis/connection/convert/MapToPropertiesConverter.java create mode 100644 src/main/java/org/springframework/data/redis/connection/jedis/JedisSentinelConnection.java create mode 100644 src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java create mode 100644 src/test/java/org/springframework/data/redis/connection/jedis/JedisSentinelConnectionUnitTests.java create mode 100644 src/test/java/org/springframework/data/redis/test/util/RequiresRedisSentinel.java diff --git a/docs/src/reference/docbook/reference/redis.xml b/docs/src/reference/docbook/reference/redis.xml index 365da3407..dd07b9c17 100644 --- a/docs/src/reference/docbook/reference/redis.xml +++ b/docs/src/reference/docbook/reference/redis.xml @@ -222,6 +222,8 @@ public RedisConnectionFactory jedisConnectionFactory() { return new JedisConnectionFactory(sentinelConfig); } ]]> + Sometimes direct interaction with the one of the Sentinels is required. Using RedisConnectionFactory.getSentinelConnection() or + RedisConnection.getSentinelCommands() gives you access to the first active Sentinel configured.
diff --git a/src/main/java/org/springframework/data/redis/connection/AbstractRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/AbstractRedisConnection.java new file mode 100644 index 000000000..c7e324ab7 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/AbstractRedisConnection.java @@ -0,0 +1,115 @@ +/* + * Copyright 2014 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.io.IOException; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.dao.InvalidDataAccessResourceUsageException; +import org.springframework.data.redis.RedisSystemException; + +/** + * @author Christoph Strobl + * @since 1.4 + */ +public abstract class AbstractRedisConnection implements RedisConnection { + + private RedisSentinelConfiguration sentinelConfiguration; + private ConcurrentHashMap connectionCache = new ConcurrentHashMap(); + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisConnection#getSentinelCommands() + */ + @Override + public RedisSentinelConnection getSentinelConnection() { + + if (!hasRedisSentinelConfigured()) { + throw new InvalidDataAccessResourceUsageException("No sentinels configured."); + } + + RedisNode node = selectActiveSentinel(); + RedisSentinelConnection connection = connectionCache.get(node); + if (connection == null || !connection.isOpen()) { + connection = getSentinelConnection(node); + connectionCache.putIfAbsent(node, connection); + } + return connection; + } + + public void setSentinelConfiguration(RedisSentinelConfiguration sentinelConfiguration) { + this.sentinelConfiguration = sentinelConfiguration; + } + + public boolean hasRedisSentinelConfigured() { + return this.sentinelConfiguration != null; + } + + private RedisNode selectActiveSentinel() { + + for (RedisNode node : this.sentinelConfiguration.getSentinels()) { + if (isActive(node)) { + return node; + } + } + + throw new InvalidDataAccessApiUsageException("Could not find any active sentinels"); + } + + /** + * Check if node is active by sending ping. + * + * @param node + * @return + */ + protected boolean isActive(RedisNode node) { + return false; + } + + /** + * Get {@link RedisSentinelCommands} connected to given node. + * + * @param sentinel + * @return + */ + protected RedisSentinelConnection getSentinelConnection(RedisNode sentinel) { + throw new UnsupportedOperationException("Sentinel is not supported by this client."); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisConnection#close() + */ + @Override + public void close() throws DataAccessException { + + if (!connectionCache.isEmpty()) { + for (RedisNode node : connectionCache.keySet()) { + RedisSentinelConnection connection = connectionCache.remove(node); + if (connection.isOpen()) { + try { + connection.close(); + } catch (IOException e) { + throw new RedisSystemException("Failed to close sentinel connection", e); + } + } + } + } + } + +} 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 5c29f4773..cdbdcf611 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java @@ -2380,4 +2380,9 @@ public class DefaultStringRedisConnection implements StringRedisConnection { new TupleConverter()); } + @Override + public RedisSentinelConnection getSentinelConnection() { + return delegate.getSentinelConnection(); + } + } diff --git a/src/main/java/org/springframework/data/redis/connection/RedisConnection.java b/src/main/java/org/springframework/data/redis/connection/RedisConnection.java index a445696dd..2171ca5dd 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisConnection.java @@ -92,4 +92,10 @@ public interface RedisConnection extends RedisCommands { * @return the result of the executed commands. */ List closePipeline() throws RedisPipelineException; + + /** + * @return + * @since 1.4 + */ + RedisSentinelConnection getSentinelConnection(); } 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 6c0df25da..1b286a862 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisConnectionFactory.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2013 the original author or authors. + * Copyright 2011-2014 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. @@ -22,6 +22,7 @@ import org.springframework.dao.support.PersistenceExceptionTranslator; * Thread-safe factory of Redis connections. * * @author Costin Leau + * @author Christoph Strobl */ public interface RedisConnectionFactory extends PersistenceExceptionTranslator { @@ -41,4 +42,10 @@ public interface RedisConnectionFactory extends PersistenceExceptionTranslator { * @return Whether or not to convert pipeline and tx results */ boolean getConvertPipelineAndTxResults(); + + /** + * @return + * @since 1.4 + */ + RedisSentinelConnection getSentinelConnection(); } 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 a269549c2..819b7799c 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisNode.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisNode.java @@ -15,22 +15,37 @@ */ package org.springframework.data.redis.connection; +import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; /** * @author Christoph Strobl + * @author Thomas Darimont + * * @since 1.4 */ -public class RedisNode { +public class RedisNode implements NamedNode { - private final String host; - private final int port; + private String name; + private String host; + private int port; + /** + * Creates a new {@link RedisNode} with the given {@code host}, {@code port}. + * + * @param host must not be {@literal null} + * @param port + */ public RedisNode(String host, int port) { + + Assert.notNull(host,"host must not be null!"); + this.host = host; this.port = port; } + protected RedisNode() {} + public String getHost() { return host; } @@ -43,6 +58,15 @@ public class RedisNode { return host + ":" + port; } + @Override + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + @Override public String toString() { return asString(); @@ -77,7 +101,42 @@ public class RedisNode { return false; } + if (!ObjectUtils.nullSafeEquals(this.name, other.name)) { + return false; + } + return true; } + /** + * @author Christoph Strobl + * + * @since 1.4 + */ + public static class RedisNodeBuilder { + + private RedisNode node; + + public RedisNodeBuilder() { + node = new RedisNode(); + } + + public RedisNodeBuilder withName(String name) { + node.name = name; + return this; + } + + public RedisNodeBuilder listeningAt(String host, int port) { + + Assert.hasText(host, "Hostname must not be empty or null."); + node.host = host; + node.port = port; + return this; + } + + public RedisNode build() { + return this.node; + } + } + } diff --git a/src/main/java/org/springframework/data/redis/connection/RedisSentinelCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisSentinelCommands.java new file mode 100644 index 000000000..0fcc40f61 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/RedisSentinelCommands.java @@ -0,0 +1,64 @@ +/* + * Copyright 2014 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; + +/** + * @author Christoph Strobl + * @since 1.4 + */ +public interface RedisSentinelCommands { + + /** + * Force a failover as if the {@literal master} was not reachable. + * + * @param master must not be {@literal null}. + */ + void failover(NamedNode master); + + /** + * Get a {@link Collection} of monitored masters and their state. + * + * @return Collection of {@link RedisServer}s. Never {@literal null}. + */ + Collection masters(); + + /** + * Show list of slaves for given {@literal master}. + * + * @param master must not be {@literal null}. + * @return Collection of {@link RedisServer}s. Never {@literal null}. + */ + Collection slaves(NamedNode master); + + /** + * Removes given {@literal master}. The server will no longer be monitored and will no longer be returned by + * {@link #masters()}. + * + * @param master must not be {@literal null}. + */ + void remove(NamedNode master); + + /** + * Tell sentinel to start monitoring a new {@literal master} with the specified {@link RedisServer#getName()}, + * {@link RedisServer#getHost()}, {@link RedisServer#getPort()}, and {@link RedisServer#getQuorum()}. + * + * @param master must not be {@literal null}. + */ + void monitor(RedisServer master); + +} diff --git a/src/main/java/org/springframework/data/redis/connection/RedisSentinelConnection.java b/src/main/java/org/springframework/data/redis/connection/RedisSentinelConnection.java new file mode 100644 index 000000000..8b8075005 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/RedisSentinelConnection.java @@ -0,0 +1,31 @@ +/* + * Copyright 2014 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.io.Closeable; + +/** + * @author Christoph Strobl + * @since 1.4 + */ +public interface RedisSentinelConnection extends RedisSentinelCommands, Closeable { + + /** + * @return true if connected to server + */ + boolean isOpen(); + +} diff --git a/src/main/java/org/springframework/data/redis/connection/RedisServer.java b/src/main/java/org/springframework/data/redis/connection/RedisServer.java new file mode 100644 index 000000000..0e52bd2a4 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/RedisServer.java @@ -0,0 +1,213 @@ +/* + * Copyright 2014 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.util.Assert; +import org.springframework.util.StringUtils; + +/** + * @author Christoph Strobl + * @author Thomas Darimont + * + * @since 1.4 + */ +public class RedisServer extends RedisNode { + + public static enum INFO { + + NAME("name"), // + HOST("ip"), // + PORT("port"), // + RUN_ID("runid"), // + FLAGS("flags"), // + PENDING_COMMANDS("pending-commands"), // + LAST_PING_SENT("last-ping-sent"), // + LAST_OK_PING_REPLY("last-ok-ping-reply"), // + DOWN_AFTER_MILLISECONDS("down-after-milliseconds"), // + INFO_REFRESH("info-refresh"), // + ROLE_REPORTED("role-reported"), // + ROLE_REPORTED_TIME("role-reported-time"), // + CONFIG_EPOCH("config-epoch"), // + NUMBER_SLAVES("num-slaves"), // + NUMBER_OTHER_SENTINELS("multi"), // + BUFFER_LENGTH("qbuf"), // + BUFFER_FREE_SPACE("qbuf-free"), // + OUTPUT_BUFFER_LENGTH("obl"), // + OUTPUT_LIST_LENGTH("number-other-sentinels"), // + QUORUM("quorum"), // + FAILOVER_TIMEOUT("failover-timeout"), // + PARALLEL_SYNCS("parallel-syncs"); // + + String key; + + INFO(String key) { + this.key = key; + } + } + + private Properties properties; + + /** + * Creates a new {@link RedisServer} with the given {@code host}, {@code port}. + * + * @param host must not be {@literal null} + * @param port + */ + public RedisServer(String host, int port) { + this(host, port, new Properties()); + } + + /** + * Creates a new {@link RedisServer} with the given {@code host}, {@code port} and {@code properties}. + * + * @param host must not be {@literal null} + * @param port + * @param properties may be {@literal null} + */ + public RedisServer(String host, int port, Properties properties) { + + super(host, port); + this.properties = properties; + + String name = host + ":" + port; + if (properties != null && properties.containsKey(INFO.NAME.key)) { + name = get(INFO.NAME); + } + + setName(name); + } + + /** + * Creates a new {@link RedisServer} from the given properties. + * + * @param properties + * @return + */ + public static RedisServer newServerFrom(Properties properties) { + + String host = properties.getProperty(INFO.HOST.key, "127.0.0.1"); + int port = Integer.parseInt(properties.getProperty(INFO.PORT.key, "26379")); + + return new RedisServer(host, port, properties); + } + + public void setQuorum(Long quorum) { + + if (quorum == null) { + this.properties.remove(INFO.QUORUM.key); + return; + } + + this.properties.put(INFO.QUORUM.key, quorum.toString()); + } + + public String getRunId() { + return get(INFO.RUN_ID); + } + + public String getFlags() { + return get(INFO.FLAGS); + } + + public boolean isMaster() { + + String role = getRoleReported(); + if (!StringUtils.hasText(role)) { + return false; + } + return role.equalsIgnoreCase("master"); + } + + public Long getPendingCommands() { + return getLongValueOf(INFO.PENDING_COMMANDS); + } + + public Long getLastPingSent() { + return getLongValueOf(INFO.LAST_PING_SENT); + } + + public Long getLastOkPingReply() { + return getLongValueOf(INFO.LAST_OK_PING_REPLY); + } + + public Long getDownAfterMilliseconds() { + return getLongValueOf(INFO.DOWN_AFTER_MILLISECONDS); + } + + public Long getInfoRefresh() { + return getLongValueOf(INFO.INFO_REFRESH); + } + + public String getRoleReported() { + return get(INFO.ROLE_REPORTED); + } + + public Long roleReportedTime() { + return getLongValueOf(INFO.ROLE_REPORTED_TIME); + } + + public Long getConfigEpoch() { + return getLongValueOf(INFO.CONFIG_EPOCH); + } + + public Long getNumberSlaves() { + return getLongValueOf(INFO.NUMBER_SLAVES); + } + + public Long getNumberOtherSentinels() { + return getLongValueOf(INFO.NUMBER_OTHER_SENTINELS); + } + + public Long getQuorum() { + return getLongValueOf(INFO.QUORUM); + } + + public Long getFailoverTimeout() { + return getLongValueOf(INFO.FAILOVER_TIMEOUT); + } + + public Long getParallelSyncs() { + return getLongValueOf(INFO.PARALLEL_SYNCS); + } + + /** + * @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 client 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 information for 'empty' / 'null' key."); + return this.properties.getProperty(key); + } + + private Long getLongValueOf(INFO info) { + + String value = get(info); + return value == null ? null : Long.valueOf(value); + } +} 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 ff930bed8..fa7bedeb3 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 @@ -17,6 +17,7 @@ package org.springframework.data.redis.connection.convert; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Properties; import java.util.Set; @@ -38,6 +39,7 @@ abstract public class Converters { private static final Converter STRING_TO_PROPS = new StringToPropertiesConverter(); 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; public static Converter stringToProps() { return STRING_TO_PROPS; @@ -55,6 +57,10 @@ abstract public class Converters { return STRING_TO_PROPS.convert(source); } + public static Properties toProperties(Map source) { + return MAP_TO_PROPERTIES.convert(source); + } + public static Boolean toBoolean(Long source) { return LONG_TO_BOOLEAN.convert(source); } @@ -76,7 +82,6 @@ abstract public class Converters { return tupleArgs; } - /** * Returns the timestamp constructed from the given {@code seconds} and {@code microseconds}. * diff --git a/src/main/java/org/springframework/data/redis/connection/convert/MapToPropertiesConverter.java b/src/main/java/org/springframework/data/redis/connection/convert/MapToPropertiesConverter.java new file mode 100644 index 000000000..4c04af967 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/convert/MapToPropertiesConverter.java @@ -0,0 +1,40 @@ +/* + * Copyright 2014 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 java.util.Map; +import java.util.Properties; + +import org.springframework.core.convert.converter.Converter; + +/** + * @author Christoph Strobl + * @since 1.4 + */ +public enum MapToPropertiesConverter implements Converter, Properties> { + INSTANCE; + + @Override + public Properties convert(Map source) { + + Properties p = new Properties(); + if (source != null) { + p.putAll(source); + } + return p; + } + +} 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 d946c0c1b..c7aef8b58 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 @@ -34,10 +34,11 @@ import org.springframework.dao.DataAccessException; import org.springframework.data.redis.ExceptionTranslationStrategy; import org.springframework.data.redis.FallbackExceptionTranslationStrategy; import org.springframework.data.redis.RedisConnectionFailureException; +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.RedisConnection; +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.RedisZSetCommands; @@ -85,7 +86,7 @@ import redis.clients.util.Pool; * @author Thomas Darimont * @author Jungtaek Lim */ -public class JedisConnection implements RedisConnection { +public class JedisConnection extends AbstractRedisConnection { private static final Field CLIENT_FIELD; private static final Method SEND_COMMAND; @@ -234,6 +235,7 @@ public class JedisConnection implements RedisConnection { } public void close() throws DataAccessException { + super.close(); // return the connection to the pool if (pool != null) { if (!broken) { @@ -3114,4 +3116,35 @@ public class JedisConnection implements RedisConnection { return args; } + + @Override + protected boolean isActive(RedisNode node) { + + if (node == null) { + return false; + } + + Jedis temp = null; + try { + temp = getJedis(node); + temp.connect(); + return temp.ping().equalsIgnoreCase("pong"); + } catch (Exception e) { + return false; + } finally { + if (temp != null) { + temp.disconnect(); + temp.close(); + } + } + } + + @Override + protected JedisSentinelConnection getSentinelConnection(RedisNode sentinel) { + return new JedisSentinelConnection(getJedis(sentinel)); + } + + protected Jedis getJedis(RedisNode node) { + return new Jedis(node.getHost(), node.getPort()); + } } 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 c57e8243a..a6ce19275 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 @@ -26,6 +26,7 @@ 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.InvalidDataAccessResourceUsageException; import org.springframework.data.redis.ExceptionTranslationStrategy; import org.springframework.data.redis.PassThroughExceptionTranslationStrategy; import org.springframework.data.redis.RedisConnectionFailureException; @@ -33,6 +34,7 @@ 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 org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -413,6 +415,32 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, return sentinelConfig != null; } + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisConnectionFactory#getSentinelConnection() + */ + @Override + public RedisSentinelConnection getSentinelConnection() { + + if (!isRedisSentinelAware()) { + throw new InvalidDataAccessResourceUsageException("No Sentinels configured"); + } + + return new JedisSentinelConnection(getActiveSentinel()); + } + + private Jedis getActiveSentinel() { + + Assert.notNull(this.sentinelConfig); + for (RedisNode node : this.sentinelConfig.getSentinels()) { + Jedis jedis = new Jedis(node.getHost(), node.getPort()); + if (jedis.ping().equalsIgnoreCase("pong")) { + return jedis; + } + } + + throw new InvalidDataAccessResourceUsageException("no sentinel found"); + } + private Set convertToJedisSentinelSet(Collection nodes) { if (CollectionUtils.isEmpty(nodes)) { 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 7cf24a214..c779d0168 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 @@ -15,15 +15,18 @@ */ package org.springframework.data.redis.connection.jedis; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Properties; 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.RedisListCommands.Position; +import org.springframework.data.redis.connection.RedisServer; import org.springframework.data.redis.connection.RedisStringCommands.BitOperation; import org.springframework.data.redis.connection.RedisZSetCommands.Tuple; import org.springframework.data.redis.connection.SortParameters; @@ -36,6 +39,7 @@ import org.springframework.data.redis.connection.convert.SetConverter; import org.springframework.data.redis.connection.convert.StringToRedisClientInfoConverter; 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.BinaryClient.LIST_POSITION; @@ -61,6 +65,7 @@ abstract public class JedisConverters extends Converters { private static final Converter EXCEPTION_CONVERTER = new JedisExceptionConverter(); private static final Converter> STRING_TO_CLIENT_INFO_CONVERTER = new StringToRedisClientInfoConverter(); private static final Converter TUPLE_CONVERTER; + private static final Converter PROPERTIES_TO_SENTINEL; private static final ListConverter TUPLE_LIST_TO_TUPLE_LIST_CONVERTER; static { @@ -80,6 +85,14 @@ 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); + PROPERTIES_TO_SENTINEL = new Converter() { + + @Override + public RedisServer convert(Properties source) { + return source != null ? RedisServer.newServerFrom(source) : null; + + } + }; } public static Converter stringToBytes() { @@ -157,6 +170,28 @@ abstract public class JedisConverters extends Converters { return STRING_TO_CLIENT_INFO_CONVERTER.convert(source.split("\\r?\\n")); } + /** + * @param source + * @return + * @since 1.4 + */ + public static List toListOfRedisServer(List> source) { + + if (CollectionUtils.isEmpty(source)) { + return Collections.emptyList(); + } + + List sentinels = new ArrayList(); + for (Map info : source) { + sentinels.add(RedisServer.newServerFrom(Converters.toProperties(info))); + } + return sentinels; + } + + public static DataAccessException toDataAccessException(Exception ex) { + return EXCEPTION_CONVERTER.convert(ex); + } + public static LIST_POSITION toListPosition(Position source) { Assert.notNull("list positions are mandatory"); return (Position.AFTER.equals(source) ? LIST_POSITION.AFTER : LIST_POSITION.BEFORE); diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisSentinelConnection.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisSentinelConnection.java new file mode 100644 index 000000000..2f351308b --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisSentinelConnection.java @@ -0,0 +1,162 @@ +/* + * Copyright 2014 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.io.IOException; +import java.util.List; + +import org.springframework.data.redis.connection.NamedNode; +import org.springframework.data.redis.connection.RedisNode; +import org.springframework.data.redis.connection.RedisSentinelCommands; +import org.springframework.data.redis.connection.RedisSentinelConnection; +import org.springframework.data.redis.connection.RedisServer; +import org.springframework.util.Assert; + +import redis.clients.jedis.Jedis; + +/** + * @author Christoph Strobl + * @since 1.4 + */ +public class JedisSentinelConnection implements RedisSentinelConnection { + + private Jedis jedis; + + public JedisSentinelConnection(RedisNode sentinel) { + this(sentinel.getHost(), sentinel.getPort()); + } + + public JedisSentinelConnection(String host, int port) { + this(new Jedis(host, port)); + } + + public JedisSentinelConnection(Jedis jedis) { + + Assert.notNull(jedis, "Cannot created JedisSentinelConnection using 'null' as client."); + this.jedis = jedis; + init(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisSentinelCommands#failover(org.springframework.data.redis.connection.NamedNode) + */ + @Override + public void failover(NamedNode master) { + + Assert.notNull(master, "Redis node master must not be 'null' for failover."); + Assert.hasText(master.getName(), "Redis master name must not be 'null' or empty for failover."); + jedis.sentinelFailover(master.getName()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisSentinelCommands#masters() + */ + @Override + public List masters() { + return JedisConverters.toListOfRedisServer(jedis.sentinelMasters()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisSentinelCommands#slaves(org.springframework.data.redis.connection.NamedNode) + */ + @Override + public List slaves(NamedNode master) { + + Assert.notNull(master, "Master node cannot be 'null' when loading slaves."); + return slaves(master.getName()); + } + + /** + * @param masterName + * @see RedisSentinelCommands#slaves(NamedNode) + * @return + */ + public List slaves(String masterName) { + + Assert.hasText(masterName, "Name of redis master cannot be 'null' or empty when loading slaves."); + return JedisConverters.toListOfRedisServer(jedis.sentinelSlaves(masterName)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisSentinelCommands#remove(org.springframework.data.redis.connection.NamedNode) + */ + @Override + public void remove(NamedNode master) { + + Assert.notNull(master, "Master node cannot be 'null' when trying to remove."); + remove(master.getName()); + } + + /** + * @param masterName + * @see RedisSentinelCommands#remove(NamedNode) + */ + public void remove(String masterName) { + + Assert.hasText(masterName, "Name of redis master cannot be 'null' or empty when trying to remove."); + jedis.sentinelRemove(masterName); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisSentinelCommands#monitor(org.springframework.data.redis.connection.RedisServer) + */ + @Override + public void monitor(RedisServer server) { + + Assert.notNull(server, "Cannot monitor 'null' server."); + Assert.hasText(server.getName(), "Name of server to monitor must not be 'null' or empty."); + Assert.hasText(server.getHost(), "Host must not be 'null' for server to monitor."); + Assert.notNull(server.getPort(), "Port must not be 'null' for server to monitor."); + Assert.notNull(server.getQuorum(), "Quorum must not be 'null' for server to monitor."); + jedis.sentinelMonitor(server.getName(), server.getHost(), server.getPort().intValue(), server.getQuorum() + .intValue()); + } + + /* + * (non-Javadoc) + * @see java.io.Closeable#close() + */ + @Override + public void close() throws IOException { + jedis.close(); + } + + private void init() { + if (!jedis.isConnected()) { + doInit(jedis); + } + } + + /** + * Do what ever is required to establish the connection to redis. + * + * @param jedis + */ + protected void doInit(Jedis jedis) { + jedis.connect(); + } + + @Override + public boolean isOpen() { + return jedis != null && jedis.isConnected(); + } + +} 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 25926ac63..8d1e0ab15 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 @@ -37,10 +37,10 @@ import org.jredis.protocol.Command; import org.jredis.ri.alphazero.JRedisSupport; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.RedisSystemException; +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.RedisConnection; import org.springframework.data.redis.connection.ReturnType; import org.springframework.data.redis.connection.SortParameters; import org.springframework.data.redis.connection.Subscription; @@ -59,7 +59,7 @@ import org.springframework.util.ReflectionUtils; * @author Christoph Strobl * @author Thomas Darimont */ -public class JredisConnection implements RedisConnection { +public class JredisConnection extends AbstractRedisConnection { private static final Method SERVICE_REQUEST; @@ -120,6 +120,8 @@ public class JredisConnection implements RedisConnection { } public void close() throws RedisSystemException { + super.close(); + if (isClosed()) { return; } @@ -1275,4 +1277,5 @@ public class JredisConnection implements RedisConnection { public Cursor> hScan(byte[] key, ScanOptions options) { throw new UnsupportedOperationException("'HSCAN' command is not uspported for jredis"); } + } 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 6d339820d..ba7ce0039 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 @@ -28,6 +28,7 @@ import org.springframework.dao.DataAccessException; import org.springframework.data.redis.connection.Pool; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisSentinelConnection; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -201,4 +202,9 @@ public class JredisConnectionFactory implements InitializingBean, DisposableBean public boolean getConvertPipelineAndTxResults() { return false; } + + @Override + public RedisSentinelConnection getSentinelConnection() { + throw new UnsupportedOperationException(); + } } 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 487d708ea..598cdc8ff 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 @@ -42,12 +42,12 @@ import org.springframework.dao.DataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.QueryTimeoutException; import org.springframework.data.redis.ExceptionTranslationStrategy; -import org.springframework.data.redis.RedisConnectionFailureException; import org.springframework.data.redis.FallbackExceptionTranslationStrategy; +import org.springframework.data.redis.RedisConnectionFailureException; +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.RedisConnection; import org.springframework.data.redis.connection.RedisPipelineException; import org.springframework.data.redis.connection.RedisSubscribedConnectionException; import org.springframework.data.redis.connection.ReturnType; @@ -100,7 +100,7 @@ import com.lambdaworks.redis.pubsub.RedisPubSubConnection; * @author Christoph Strobl * @author Thomas Darimont */ -public class LettuceConnection implements RedisConnection { +public class LettuceConnection extends AbstractRedisConnection { private static final ExceptionTranslationStrategy EXCEPTION_TRANSLATION = new FallbackExceptionTranslationStrategy( LettuceConverters.exceptionConverter()); @@ -358,6 +358,8 @@ public class LettuceConnection implements RedisConnection { } public void close() throws DataAccessException { + super.close(); + isClosed = true; if (asyncDedicatedConn != null) { 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 a0062681d..3b28c0bbb 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 @@ -29,6 +29,7 @@ import org.springframework.data.redis.RedisConnectionFailureException; import org.springframework.data.redis.connection.Pool; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisSentinelConnection; import org.springframework.util.Assert; import com.lambdaworks.redis.RedisAsyncConnection; @@ -338,4 +339,9 @@ public class LettuceConnectionFactory implements InitializingBean, DisposableBea client.setDefaultTimeout(timeout, TimeUnit.MILLISECONDS); return client; } + + @Override + public RedisSentinelConnection getSentinelConnection() { + throw new UnsupportedOperationException(); + } } 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 c8d23f8c3..c64ed90f1 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 @@ -32,12 +32,12 @@ import java.util.concurrent.Future; import org.springframework.core.convert.converter.Converter; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.ExceptionTranslationStrategy; -import org.springframework.data.redis.RedisConnectionFailureException; import org.springframework.data.redis.FallbackExceptionTranslationStrategy; +import org.springframework.data.redis.RedisConnectionFailureException; +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.RedisConnection; import org.springframework.data.redis.connection.RedisPipelineException; import org.springframework.data.redis.connection.RedisSubscribedConnectionException; import org.springframework.data.redis.connection.ReturnType; @@ -69,7 +69,7 @@ import com.google.common.util.concurrent.ListenableFuture; * @author Christoph Strobl * @author Thomas Darimont */ -public class SrpConnection implements RedisConnection { +public class SrpConnection extends AbstractRedisConnection { private static final ExceptionTranslationStrategy EXCEPTION_TRANSLATION = new FallbackExceptionTranslationStrategy( SrpConverters.exceptionConverter()); @@ -265,6 +265,8 @@ public class SrpConnection implements RedisConnection { } public void close() throws DataAccessException { + + super.close(); isClosed = true; queue.remove(this); 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 9f187a737..ceece540d 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 @@ -26,6 +26,7 @@ import org.springframework.data.redis.ExceptionTranslationStrategy; import org.springframework.data.redis.PassThroughExceptionTranslationStrategy; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisSentinelConnection; /** * Connection factory creating Redis Protocol based connections. @@ -159,4 +160,9 @@ public class SrpConnectionFactory implements InitializingBean, DisposableBean, R public void setConvertPipelineAndTxResults(boolean convertPipelineAndTxResults) { this.convertPipelineAndTxResults = convertPipelineAndTxResults; } + + @Override + public RedisSentinelConnection getSentinelConnection() { + throw new UnsupportedOperationException(); + } } diff --git a/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java b/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java new file mode 100644 index 000000000..87ee308de --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java @@ -0,0 +1,788 @@ +/* + * Copyright 2014 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.mockito.Mockito.*; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.dao.DataAccessException; +import org.springframework.data.redis.connection.RedisNode.RedisNodeBuilder; +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.ObjectUtils; + +/** + * @author Christoph Strobl + */ +public class RedisConnectionUnitTests { + + private final RedisNode SENTINEL_1 = new RedisNodeBuilder().listeningAt("localhost", 23679).build(); + private AbstractDelegatingRedisConnectionStub connection; + private RedisSentinelConnection sentinelConnectionMock; + + @Before + public void setUp() { + sentinelConnectionMock = mock(RedisSentinelConnection.class); + + connection = new AbstractDelegatingRedisConnectionStub(mock(AbstractRedisConnection.class, CALLS_REAL_METHODS)); + connection.setSentinelConfiguration(new RedisSentinelConfiguration().master("mymaster").sentinel(SENTINEL_1)); + connection.setSentinelConnection(sentinelConnectionMock); + } + + /** + * @see DATAREDIS-330 + */ + @Test + public void shouldCloseSentinelConnectionAlongWithRedisConnection() throws IOException { + + when(sentinelConnectionMock.isOpen()).thenReturn(true).thenReturn(false); + + connection.setActiveNode(SENTINEL_1); + connection.getSentinelConnection(); + connection.close(); + + verify(sentinelConnectionMock, times(1)).close(); + } + + /** + * @see DATAREDIS-330 + */ + @Test + public void shouldNotTryToCloseSentinelConnectionsWhenAlreadyClosed() throws IOException { + + when(sentinelConnectionMock.isOpen()).thenReturn(true); + when(sentinelConnectionMock.isOpen()).thenReturn(false); + + connection.setActiveNode(SENTINEL_1); + connection.getSentinelConnection(); + connection.close(); + + verify(sentinelConnectionMock, never()).close(); + } + + static class AbstractDelegatingRedisConnectionStub extends AbstractRedisConnection { + + RedisConnection delegate; + RedisNode activeNode; + RedisSentinelConnection sentinelConnection; + + public AbstractDelegatingRedisConnectionStub(RedisConnection delegate) { + this.delegate = delegate; + } + + @Override + protected boolean isActive(RedisNode node) { + return ObjectUtils.nullSafeEquals(activeNode, node); + } + + public void setActiveNode(RedisNode activeNode) { + this.activeNode = activeNode; + } + + public void setSentinelConnection(RedisSentinelConnection sentinelConnection) { + this.sentinelConnection = sentinelConnection; + } + + public boolean isSubscribed() { + return delegate.isSubscribed(); + } + + public void scriptFlush() { + delegate.scriptFlush(); + } + + public void select(int dbIndex) { + delegate.select(dbIndex); + } + + public void multi() { + delegate.multi(); + } + + public Long rPush(byte[] key, byte[]... values) { + return delegate.rPush(key, values); + } + + public byte[] get(byte[] key) { + return delegate.get(key); + } + + public void scriptKill() { + delegate.scriptKill(); + } + + public Long sAdd(byte[] key, byte[]... values) { + return delegate.sAdd(key, values); + } + + public Boolean exists(byte[] key) { + return delegate.exists(key); + } + + public Subscription getSubscription() { + return delegate.getSubscription(); + } + + public byte[] echo(byte[] message) { + return delegate.echo(message); + } + + public Boolean hSet(byte[] key, byte[] field, byte[] value) { + return delegate.hSet(key, field, value); + } + + public void bgWriteAof() { + delegate.bgWriteAof(); + } + + public Object execute(String command, byte[]... args) { + return delegate.execute(command, args); + } + + public String scriptLoad(byte[] script) { + return delegate.scriptLoad(script); + } + + public byte[] getSet(byte[] key, byte[] value) { + return delegate.getSet(key, value); + } + + public List exec() { + return delegate.exec(); + } + + public Long lPush(byte[] key, byte[]... value) { + return delegate.lPush(key, value); + } + + public Long del(byte[]... keys) { + return delegate.del(keys); + } + + public void close() throws DataAccessException { + super.close(); + } + + public String ping() { + return delegate.ping(); + } + + public Long sRem(byte[] key, byte[]... values) { + return delegate.sRem(key, values); + } + + public Boolean zAdd(byte[] key, double score, byte[] value) { + return delegate.zAdd(key, score, value); + } + + public Long publish(byte[] channel, byte[] message) { + return delegate.publish(channel, message); + } + + public Boolean hSetNX(byte[] key, byte[] field, byte[] value) { + return delegate.hSetNX(key, field, value); + } + + public void bgReWriteAof() { + delegate.bgReWriteAof(); + } + + public List mGet(byte[]... keys) { + return delegate.mGet(keys); + } + + public boolean isClosed() { + return delegate.isClosed(); + } + + public Long rPushX(byte[] key, byte[] value) { + return delegate.rPushX(key, value); + } + + public DataType type(byte[] key) { + return delegate.type(key); + } + + public List scriptExists(String... scriptShas) { + return delegate.scriptExists(scriptShas); + } + + public byte[] sPop(byte[] key) { + return delegate.sPop(key); + } + + public void bgSave() { + delegate.bgSave(); + } + + public void set(byte[] key, byte[] value) { + delegate.set(key, value); + } + + public void discard() { + delegate.discard(); + } + + public Object getNativeConnection() { + return delegate.getNativeConnection(); + } + + public Long zAdd(byte[] key, Set tuples) { + return delegate.zAdd(key, tuples); + } + + public void subscribe(MessageListener listener, byte[]... channels) { + delegate.subscribe(listener, channels); + } + + public Set keys(byte[] pattern) { + return delegate.keys(pattern); + } + + public byte[] hGet(byte[] key, byte[] field) { + return delegate.hGet(key, field); + } + + public Long lPushX(byte[] key, byte[] value) { + return delegate.lPushX(key, value); + } + + public Long lastSave() { + return delegate.lastSave(); + } + + public void watch(byte[]... keys) { + delegate.watch(keys); + } + + public Boolean sMove(byte[] srcKey, byte[] destKey, byte[] value) { + return delegate.sMove(srcKey, destKey, value); + } + + public Boolean setNX(byte[] key, byte[] value) { + return delegate.setNX(key, value); + } + + public T eval(byte[] script, ReturnType returnType, int numKeys, byte[]... keysAndArgs) { + return delegate.eval(script, returnType, numKeys, keysAndArgs); + } + + public boolean isQueueing() { + return delegate.isQueueing(); + } + + public Cursor scan(ScanOptions options) { + return delegate.scan(options); + } + + public void save() { + delegate.save(); + } + + public List hMGet(byte[] key, byte[]... fields) { + return delegate.hMGet(key, fields); + } + + public Long zRem(byte[] key, byte[]... values) { + return delegate.zRem(key, values); + } + + public Long lLen(byte[] key) { + return delegate.lLen(key); + } + + public void unwatch() { + delegate.unwatch(); + } + + public Long dbSize() { + return delegate.dbSize(); + } + + public void setEx(byte[] key, long seconds, byte[] value) { + delegate.setEx(key, seconds, value); + } + + public Long sCard(byte[] key) { + return delegate.sCard(key); + } + + public byte[] randomKey() { + return delegate.randomKey(); + } + + public T evalSha(String scriptSha, ReturnType returnType, int numKeys, byte[]... keysAndArgs) { + return delegate.evalSha(scriptSha, returnType, numKeys, keysAndArgs); + } + + public List lRange(byte[] key, long begin, long end) { + return delegate.lRange(key, begin, end); + } + + public void hMSet(byte[] key, Map hashes) { + delegate.hMSet(key, hashes); + } + + public Double zIncrBy(byte[] key, double increment, byte[] value) { + return delegate.zIncrBy(key, increment, value); + } + + public void flushDb() { + delegate.flushDb(); + } + + public Boolean sIsMember(byte[] key, byte[] value) { + return delegate.sIsMember(key, value); + } + + public void pSubscribe(MessageListener listener, byte[]... patterns) { + delegate.pSubscribe(listener, patterns); + } + + public void rename(byte[] oldName, byte[] newName) { + delegate.rename(oldName, newName); + } + + public boolean isPipelined() { + return delegate.isPipelined(); + } + + public void pSetEx(byte[] key, long milliseconds, byte[] value) { + delegate.pSetEx(key, milliseconds, value); + } + + public void flushAll() { + delegate.flushAll(); + } + + public void lTrim(byte[] key, long begin, long end) { + delegate.lTrim(key, begin, end); + } + + public Long hIncrBy(byte[] key, byte[] field, long delta) { + return delegate.hIncrBy(key, field, delta); + } + + public Set sInter(byte[]... keys) { + return delegate.sInter(keys); + } + + public Boolean renameNX(byte[] oldName, byte[] newName) { + return delegate.renameNX(oldName, newName); + } + + public Long zRank(byte[] key, byte[] value) { + return delegate.zRank(key, value); + } + + public Properties info() { + return delegate.info(); + } + + public void openPipeline() { + delegate.openPipeline(); + } + + public void mSet(Map tuple) { + delegate.mSet(tuple); + } + + public byte[] lIndex(byte[] key, long index) { + return delegate.lIndex(key, index); + } + + public Long sInterStore(byte[] destKey, byte[]... keys) { + return delegate.sInterStore(destKey, keys); + } + + public Double hIncrBy(byte[] key, byte[] field, double delta) { + return delegate.hIncrBy(key, field, delta); + } + + public Long zRevRank(byte[] key, byte[] value) { + return delegate.zRevRank(key, value); + } + + public Boolean expire(byte[] key, long seconds) { + return delegate.expire(key, seconds); + } + + public Properties info(String section) { + return delegate.info(section); + } + + public Boolean mSetNX(Map tuple) { + return delegate.mSetNX(tuple); + } + + public Long lInsert(byte[] key, Position where, byte[] pivot, byte[] value) { + return delegate.lInsert(key, where, pivot, value); + } + + public Set sUnion(byte[]... keys) { + return delegate.sUnion(keys); + } + + public void shutdown() { + delegate.shutdown(); + } + + public Boolean pExpire(byte[] key, long millis) { + return delegate.pExpire(key, millis); + } + + public Boolean hExists(byte[] key, byte[] field) { + return delegate.hExists(key, field); + } + + public Set zRange(byte[] key, long begin, long end) { + return delegate.zRange(key, begin, end); + } + + public void shutdown(ShutdownOption option) { + delegate.shutdown(option); + } + + public Long sUnionStore(byte[] destKey, byte[]... keys) { + return delegate.sUnionStore(destKey, keys); + } + + public Long incr(byte[] key) { + return delegate.incr(key); + } + + public Long hDel(byte[] key, byte[]... fields) { + return delegate.hDel(key, fields); + } + + public Boolean expireAt(byte[] key, long unixTime) { + return delegate.expireAt(key, unixTime); + } + + public List getConfig(String pattern) { + return delegate.getConfig(pattern); + } + + public void lSet(byte[] key, long index, byte[] value) { + delegate.lSet(key, index, value); + } + + public Set zRangeWithScores(byte[] key, long begin, long end) { + return delegate.zRangeWithScores(key, begin, end); + } + + public Long incrBy(byte[] key, long value) { + return delegate.incrBy(key, value); + } + + public Set sDiff(byte[]... keys) { + return delegate.sDiff(keys); + } + + public Long hLen(byte[] key) { + return delegate.hLen(key); + } + + public List closePipeline() throws RedisPipelineException { + return delegate.closePipeline(); + } + + public void setConfig(String param, String value) { + delegate.setConfig(param, value); + } + + public Boolean pExpireAt(byte[] key, long unixTimeInMillis) { + return delegate.pExpireAt(key, unixTimeInMillis); + } + + public Long lRem(byte[] key, long count, byte[] value) { + return delegate.lRem(key, count, value); + } + + public Double incrBy(byte[] key, double value) { + return delegate.incrBy(key, value); + } + + public Long sDiffStore(byte[] destKey, byte[]... keys) { + return delegate.sDiffStore(destKey, keys); + } + + public Set zRangeByScore(byte[] key, double min, double max) { + return delegate.zRangeByScore(key, min, max); + } + + public Set hKeys(byte[] key) { + return delegate.hKeys(key); + } + + public void resetConfigStats() { + delegate.resetConfigStats(); + } + + public Long decr(byte[] key) { + return delegate.decr(key); + } + + public List hVals(byte[] key) { + return delegate.hVals(key); + } + + public Boolean persist(byte[] key) { + return delegate.persist(key); + } + + public byte[] lPop(byte[] key) { + return delegate.lPop(key); + } + + public Set sMembers(byte[] key) { + return delegate.sMembers(key); + } + + public Long decrBy(byte[] key, long value) { + return delegate.decrBy(key, value); + } + + public Set zRangeByScoreWithScores(byte[] key, double min, double max) { + return delegate.zRangeByScoreWithScores(key, min, max); + } + + public Long time() { + return delegate.time(); + } + + public Map hGetAll(byte[] key) { + return delegate.hGetAll(key); + } + + public Boolean move(byte[] key, int dbIndex) { + return delegate.move(key, dbIndex); + } + + public byte[] sRandMember(byte[] key) { + return delegate.sRandMember(key); + } + + public byte[] rPop(byte[] key) { + return delegate.rPop(key); + } + + public void killClient(String host, int port) { + delegate.killClient(host, port); + } + + public Long append(byte[] key, byte[] value) { + return delegate.append(key, value); + } + + public Cursor> hScan(byte[] key, ScanOptions options) { + return delegate.hScan(key, options); + } + + public List sRandMember(byte[] key, long count) { + return delegate.sRandMember(key, count); + } + + public Long ttl(byte[] key) { + return delegate.ttl(key); + } + + public List bLPop(int timeout, byte[]... keys) { + return delegate.bLPop(timeout, keys); + } + + public Set zRangeByScore(byte[] key, double min, double max, long offset, long count) { + return delegate.zRangeByScore(key, min, max, offset, count); + } + + public byte[] getRange(byte[] key, long begin, long end) { + return delegate.getRange(key, begin, end); + } + + public void setClientName(byte[] name) { + delegate.setClientName(name); + } + + public Long pTtl(byte[] key) { + return delegate.pTtl(key); + } + + public Cursor sScan(byte[] key, ScanOptions options) { + return delegate.sScan(key, options); + } + + public String getClientName() { + return delegate.getClientName(); + } + + public List sort(byte[] key, SortParameters params) { + return delegate.sort(key, params); + } + + public List bRPop(int timeout, byte[]... keys) { + return delegate.bRPop(timeout, keys); + } + + public void setRange(byte[] key, byte[] value, long offset) { + delegate.setRange(key, value, offset); + } + + public Set zRangeByScoreWithScores(byte[] key, double min, double max, long offset, long count) { + return delegate.zRangeByScoreWithScores(key, min, max, offset, count); + } + + public List getClientList() { + return delegate.getClientList(); + } + + public Long sort(byte[] key, SortParameters params, byte[] storeKey) { + return delegate.sort(key, params, storeKey); + } + + public Boolean getBit(byte[] key, long offset) { + return delegate.getBit(key, offset); + } + + public void slaveOf(String host, int port) { + delegate.slaveOf(host, port); + } + + public byte[] rPopLPush(byte[] srcKey, byte[] dstKey) { + return delegate.rPopLPush(srcKey, dstKey); + } + + public Set zRevRange(byte[] key, long begin, long end) { + return delegate.zRevRange(key, begin, end); + } + + public byte[] dump(byte[] key) { + return delegate.dump(key); + } + + public Boolean setBit(byte[] key, long offset, boolean value) { + return delegate.setBit(key, offset, value); + } + + public void slaveOfNoOne() { + delegate.slaveOfNoOne(); + } + + public void restore(byte[] key, long ttlInMillis, byte[] serializedValue) { + delegate.restore(key, ttlInMillis, serializedValue); + } + + public byte[] bRPopLPush(int timeout, byte[] srcKey, byte[] dstKey) { + return delegate.bRPopLPush(timeout, srcKey, dstKey); + } + + public Set zRevRangeWithScores(byte[] key, long begin, long end) { + return delegate.zRevRangeWithScores(key, begin, end); + } + + public Long bitCount(byte[] key) { + return delegate.bitCount(key); + } + + public Set zRevRangeByScore(byte[] key, double min, double max) { + return delegate.zRevRangeByScore(key, min, max); + } + + public Long bitCount(byte[] key, long begin, long end) { + return delegate.bitCount(key, begin, end); + } + + public Set zRevRangeByScoreWithScores(byte[] key, double min, double max) { + return delegate.zRevRangeByScoreWithScores(key, min, max); + } + + public Long bitOp(BitOperation op, byte[] destination, byte[]... keys) { + return delegate.bitOp(op, destination, keys); + } + + public Long strLen(byte[] key) { + return delegate.strLen(key); + } + + public Set zRevRangeByScore(byte[] key, double min, double max, long offset, long count) { + return delegate.zRevRangeByScore(key, min, max, offset, count); + } + + public Set zRevRangeByScoreWithScores(byte[] key, double min, double max, long offset, long count) { + return delegate.zRevRangeByScoreWithScores(key, min, max, offset, count); + } + + public Long zCount(byte[] key, double min, double max) { + return delegate.zCount(key, min, max); + } + + public Long zCard(byte[] key) { + return delegate.zCard(key); + } + + public Double zScore(byte[] key, byte[] value) { + return delegate.zScore(key, value); + } + + public Long zRemRange(byte[] key, long begin, long end) { + return delegate.zRemRange(key, begin, end); + } + + public Long zRemRangeByScore(byte[] key, double min, double max) { + return delegate.zRemRangeByScore(key, min, max); + } + + public Long zUnionStore(byte[] destKey, byte[]... sets) { + return delegate.zUnionStore(destKey, sets); + } + + public Long zUnionStore(byte[] destKey, Aggregate aggregate, int[] weights, byte[]... sets) { + return delegate.zUnionStore(destKey, aggregate, weights, sets); + } + + public Long zInterStore(byte[] destKey, byte[]... sets) { + return delegate.zInterStore(destKey, sets); + } + + public Long zInterStore(byte[] destKey, Aggregate aggregate, int[] weights, byte[]... sets) { + return delegate.zInterStore(destKey, aggregate, weights, sets); + } + + public Cursor zScan(byte[] key, ScanOptions options) { + return delegate.zScan(key, options); + } + + public RedisConnection getDelegate() { + return delegate; + } + + @Override + protected RedisSentinelConnection getSentinelConnection(RedisNode sentinel) { + if (ObjectUtils.nullSafeEquals(this.activeNode, sentinel)) { + return this.sentinelConnection; + } + return null; + } + } +} diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionIntegrationTests.java index 811cb4a32..1fb5d5d65 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionIntegrationTests.java @@ -30,6 +30,7 @@ import org.hamcrest.core.AllOf; import org.hamcrest.core.IsCollectionContaining; import org.hamcrest.core.IsInstanceOf; import org.junit.After; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.dao.InvalidDataAccessApiUsageException; @@ -40,9 +41,13 @@ import org.springframework.data.redis.connection.DefaultStringTuple; import org.springframework.data.redis.connection.Message; import org.springframework.data.redis.connection.MessageListener; import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisSentinelConfiguration; import org.springframework.data.redis.connection.ReturnType; import org.springframework.data.redis.connection.StringRedisConnection.StringTuple; +import org.springframework.data.redis.test.util.RedisSentinelRule; +import org.springframework.data.redis.test.util.RedisSentinelRule.SentinelsAvailable; import org.springframework.data.redis.test.util.RelaxedJUnit4ClassRunner; +import org.springframework.data.redis.test.util.RequiresRedisSentinel; import org.springframework.test.annotation.IfProfileValue; import org.springframework.test.context.ContextConfiguration; @@ -60,6 +65,8 @@ import redis.clients.jedis.JedisPoolConfig; @ContextConfiguration public class JedisConnectionIntegrationTests extends AbstractConnectionIntegrationTests { + public @Rule RedisSentinelRule sentinelRule = RedisSentinelRule.withDefaultConfig().dynamicModeSelection(); + @After public void tearDown() { try { @@ -366,4 +373,16 @@ public class JedisConnectionIntegrationTests extends AbstractConnectionIntegrati assertTrue(String.format("difference between millis=%s and ttl=%s should not be greater than 20ms but is %s", millis, ttl, millis - ttl), millis - ttl < 20L); } + + /** + * @see DATAREDIS-330 + */ + @Test + @RequiresRedisSentinel(SentinelsAvailable.ONE_ACTIVE) + public void shouldReturnSentinelCommandsWhenWhenActiveSentinelFound() { + + ((JedisConnection) byteConnection).setSentinelConfiguration(new RedisSentinelConfiguration().master("mymaster") + .sentinel("127.0.0.1", 26379).sentinel("127.0.0.1", 26380)); + assertThat(connection.getSentinelConnection(), notNullValue()); + } } diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionUnitTestSuite.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionUnitTestSuite.java index 2648a6a91..2f088f10b 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionUnitTestSuite.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionUnitTestSuite.java @@ -29,6 +29,7 @@ import org.junit.runner.RunWith; import org.junit.runners.Suite; import org.junit.runners.Suite.SuiteClasses; import org.mockito.ArgumentCaptor; +import org.springframework.dao.InvalidDataAccessResourceUsageException; import org.springframework.data.redis.connection.AbstractConnectionUnitTestBase; import org.springframework.data.redis.connection.RedisServerCommands.ShutdownOption; import org.springframework.data.redis.connection.jedis.JedisConnectionUnitTestSuite.JedisConnectionPipelineUnitTests; @@ -115,8 +116,6 @@ public class JedisConnectionUnitTestSuite { } /** - * <<<<<<< HEAD - * * @see DATAREDIS-267 */ @Test @@ -163,6 +162,15 @@ public class JedisConnectionUnitTestSuite { connection.slaveOfNoOne(); verifyNativeConnectionInvocation().slaveofNoOne(); } + + /** + * @see DATAREDIS-330 + */ + @Test(expected = InvalidDataAccessResourceUsageException.class) + public void shouldThrowExceptionWhenAccessingRedisSentinelsCommandsWhenNoSentinelsConfigured() { + connection.getSentinelConnection(); + } + } public static class JedisConnectionPipelineUnitTests extends JedisConnectionUnitTests { 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 ccf3e5955..5f9780ad6 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 @@ -15,12 +15,19 @@ */ package org.springframework.data.redis.connection.jedis; +import static org.hamcrest.core.Is.*; import static org.hamcrest.core.IsEqual.*; +import static org.hamcrest.core.IsNull.*; import static org.junit.Assert.*; +import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.junit.Test; +import org.springframework.data.redis.connection.RedisServer; import org.springframework.data.redis.core.types.RedisClientInfo; /** @@ -59,4 +66,80 @@ public class JedisConvertersUnitTests { assertThat(JedisConverters.toListOfRedisClientInformation(sb.toString()).size(), equalTo(2)); } + + /** + * @see DATAREDIS-330 + */ + @Test + public void convertsSingleMapToRedisServerReturnsCollectionCorrectly() { + + Map values = getRedisServerInfoMap("mymaster", 23697); + List servers = JedisConverters.toListOfRedisServer(Collections.singletonList(values)); + + assertThat(servers.size(), is(1)); + verifyRedisServerInfo(servers.get(0), values); + } + + /** + * @see DATAREDIS-330 + */ + @Test + public void convertsMultipleMapsToRedisServerReturnsCollectionCorrectly() { + + List> vals = Arrays.asList(getRedisServerInfoMap("mymaster", 23697), + getRedisServerInfoMap("yourmaster", 23680)); + List servers = JedisConverters.toListOfRedisServer(vals); + + assertThat(servers.size(), is(vals.size())); + for (int i = 0; i < vals.size(); i++) { + verifyRedisServerInfo(servers.get(i), vals.get(i)); + } + } + + /** + * @see DATAREDIS-330 + */ + @Test + public void convertsRedisServersCorrectlyWhenGivenAnEmptyList() { + assertThat(JedisConverters.toListOfRedisServer(Collections.> emptyList()), notNullValue()); + } + + /** + * @see DATAREDIS-330 + */ + @Test + public void convertsRedisServersCorrectlyWhenGivenNull() { + assertThat(JedisConverters.toListOfRedisServer(null), notNullValue()); + } + + private void verifyRedisServerInfo(RedisServer server, Map values) { + + for (Map.Entry entry : values.entrySet()) { + assertThat(server.get(entry.getKey()), equalTo(entry.getValue())); + } + } + + private Map getRedisServerInfoMap(String name, int port) { + Map map = new HashMap(); + map.put("name", name); + map.put("ip", "127.0.0.1"); + map.put("port", Integer.toString(port)); + map.put("runid", "768c2926e5148d208713bf17cd5821e10f5388e2"); + map.put("flags", "master"); + map.put("pending-commands", "0"); + map.put("last-ping-sent", "0"); + map.put("last-ok-ping-reply", "534"); + map.put("last-ping-reply", "534"); + map.put("down-after-milliseconds", "30000"); + map.put("info-refresh", "147"); + map.put("role-reported", "master"); + map.put("role-reported-time", "41248339"); + map.put("config-epoch", "7"); + map.put("num-slaves", "2"); + map.put("num-other-sentinels", "2"); + map.put("quorum", "2"); + map.put("failover-timeout", "180000"); + map.put("parallel-syncs", "1"); + return map; + } } diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisSentinelConnectionUnitTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisSentinelConnectionUnitTests.java new file mode 100644 index 000000000..c34530cfd --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisSentinelConnectionUnitTests.java @@ -0,0 +1,198 @@ +/* + * Copyright 2014 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.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.redis.connection.RedisNode; +import org.springframework.data.redis.connection.RedisNode.RedisNodeBuilder; +import org.springframework.data.redis.connection.RedisServer; + +import redis.clients.jedis.Jedis; + +/** + * @author Christoph Strobl + */ +@RunWith(MockitoJUnitRunner.class) +public class JedisSentinelConnectionUnitTests { + + private @Mock Jedis jedisMock; + + private JedisSentinelConnection connection; + + @Before + public void setUp() { + this.connection = new JedisSentinelConnection(jedisMock); + } + + /** + * @see DATAREDIS-330 + */ + @Test + public void shouldConnectAfterCreation() { + verify(jedisMock, times(1)).connect(); + } + + /** + * @see DATAREDIS-330 + */ + @SuppressWarnings("resource") + @Test + public void shouldNotConnectIfAlreadyConnected() { + + Jedis yetAnotherJedisMock = mock(Jedis.class); + when(yetAnotherJedisMock.isConnected()).thenReturn(true); + + new JedisSentinelConnection(yetAnotherJedisMock); + + verify(yetAnotherJedisMock, never()).connect(); + } + + /** + * @see DATAREDIS-330 + */ + @Test + public void failoverShouldBeSentCorrectly() { + + connection.failover(new RedisNodeBuilder().withName("mymaster").build()); + verify(jedisMock, times(1)).sentinelFailover(eq("mymaster")); + } + + /** + * @see DATAREDIS-330 + */ + @Test(expected = IllegalArgumentException.class) + public void failoverShouldThrowExceptionIfMasterNodeIsNull() { + connection.failover(null); + } + + /** + * @see DATAREDIS-330 + */ + @Test(expected = IllegalArgumentException.class) + public void failoverShouldThrowExceptionIfMasterNodeNameIsEmpty() { + connection.failover(new RedisNodeBuilder().build()); + } + + /** + * @see DATAREDIS-330 + */ + @Test + public void mastersShouldReadMastersCorrectly() { + + connection.masters(); + verify(jedisMock, times(1)).sentinelMasters(); + } + + /** + * @see DATAREDIS-330 + */ + @Test + public void shouldReadSlavesCorrectly() { + + connection.slaves("mymaster"); + verify(jedisMock, times(1)).sentinelSlaves(eq("mymaster")); + } + + /** + * @see DATAREDIS-330 + */ + @Test + public void shouldReadSlavesCorrectlyWhenGivenNamedNode() { + + connection.slaves(new RedisNodeBuilder().withName("mymaster").build()); + verify(jedisMock, times(1)).sentinelSlaves(eq("mymaster")); + } + + /** + * @see DATAREDIS-330 + */ + @Test(expected = IllegalArgumentException.class) + public void readSlavesShouldThrowExceptionWhenGivenEmptyMasterName() { + connection.slaves(""); + } + + /** + * @see DATAREDIS-330 + */ + @Test(expected = IllegalArgumentException.class) + public void readSlavesShouldThrowExceptionWhenGivenNull() { + connection.slaves((RedisNode) null); + } + + /** + * @see DATAREDIS-330 + */ + @Test(expected = IllegalArgumentException.class) + public void readSlavesShouldThrowExceptionWhenNodeWithoutName() { + connection.slaves(new RedisNodeBuilder().build()); + } + + /** + * @see DATAREDIS-330 + */ + @Test + public void shouldRemoveMasterCorrectlyWhenGivenNamedNode() { + + connection.remove(new RedisNodeBuilder().withName("mymaster").build()); + verify(jedisMock, times(1)).sentinelRemove(eq("mymaster")); + } + + /** + * @see DATAREDIS-330 + */ + @Test(expected = IllegalArgumentException.class) + public void removeShouldThrowExceptionWhenGivenEmptyMasterName() { + connection.remove(""); + } + + /** + * @see DATAREDIS-330 + */ + @Test(expected = IllegalArgumentException.class) + public void removeShouldThrowExceptionWhenGivenNull() { + connection.remove((RedisNode) null); + } + + /** + * @see DATAREDIS-330 + */ + @Test(expected = IllegalArgumentException.class) + public void removeShouldThrowExceptionWhenNodeWithoutName() { + connection.remove(new RedisNodeBuilder().build()); + } + + /** + * @see DATAREDIS-330 + */ + @Test + public void monitorShouldBeSentCorrectly() { + + RedisServer server = new RedisServer("127.0.0.1", 6382); + server.setName("anothermaster"); + server.setQuorum(3L); + + connection.monitor(server); + verify(jedisMock, times(1)).sentinelMonitor(eq("anothermaster"), eq("127.0.0.1"), eq(6382), eq(3)); + } + +} diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisSentinelIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisSentinelIntegrationTests.java index 2b4f0df44..f30a66c9f 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisSentinelIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisSentinelIntegrationTests.java @@ -15,6 +15,12 @@ */ package org.springframework.data.redis.connection.jedis; +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.assertThat; + +import java.util.Collection; +import java.util.List; + import org.junit.After; import org.junit.Before; import org.junit.Ignore; @@ -23,17 +29,29 @@ import org.junit.Test; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.redis.connection.AbstractConnectionIntegrationTests; import org.springframework.data.redis.connection.RedisSentinelConfiguration; +import org.springframework.data.redis.connection.RedisSentinelConnection; +import org.springframework.data.redis.connection.RedisServer; import org.springframework.data.redis.connection.ReturnType; import org.springframework.data.redis.test.util.RedisSentinelRule; import org.springframework.test.annotation.IfProfileValue; /** * @author Christoph Strobl + * @author Thomas Darimont */ public class JedisSentinelIntegrationTests extends AbstractConnectionIntegrationTests { - - private static final RedisSentinelConfiguration SENTINEL_CONFIG = new RedisSentinelConfiguration().master("mymaster") - .sentinel("127.0.0.1", 26379).sentinel("127.0.0.1", 26380); + + private static final String MASTER_NAME = "mymaster"; + private static final RedisServer SENTINEL_0 = new RedisServer("127.0.0.1", 26379); + private static final RedisServer SENTINEL_1 = new RedisServer("127.0.0.1", 26380); + + private static final RedisServer SLAVE_0 = new RedisServer("127.0.0.1", 6380); + private static final RedisServer SLAVE_1 = new RedisServer("127.0.0.1", 6381); + + private static final RedisSentinelConfiguration SENTINEL_CONFIG = new RedisSentinelConfiguration() // + .master(MASTER_NAME) + .sentinel(SENTINEL_0) + .sentinel(SENTINEL_1); public @Rule RedisSentinelRule sentinelRule = RedisSentinelRule.forConfig(SENTINEL_CONFIG).oneActive(); @@ -102,4 +120,32 @@ public class JedisSentinelIntegrationTests extends AbstractConnectionIntegration public void testErrorInTx() { super.testErrorInTx(); } + + /** + * @see DATAREDIS-330 + */ + @Test + public void shouldReadMastersCorrectly() { + + List servers = (List) connectionFactory.getSentinelConnection().masters(); + assertThat(servers.size(), is(1)); + assertThat(servers.get(0).getName(),is(MASTER_NAME)); + } + + /** + * @see DATAREDIS-330 + */ + @Test + public void shouldReadSlavesOfMastersCorrectly() { + + RedisSentinelConnection sentinelConnection = connectionFactory.getSentinelConnection(); + + List servers = (List) sentinelConnection.masters(); + assertThat(servers.size(), is(1)); + + Collection slaves = sentinelConnection.slaves(servers.get(0)); + assertThat(slaves.size(), is(2)); + assertThat(slaves, hasItems(SLAVE_0, SLAVE_1)); + } + } 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 e386d1fdb..147f6c423 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 @@ -29,24 +29,18 @@ import redis.clients.jedis.Jedis; */ public class RedisSentinelRule implements TestRule { - enum VerificationMode { - ALL_ACTIVE, ONE_ACTIVE, NO_SENTINEL + public enum SentinelsAvailable { + ALL_ACTIVE, ONE_ACTIVE, NONE_ACTIVE } private static final RedisSentinelConfiguration DEFAULT_SENTINEL_CONFIG = new RedisSentinelConfiguration() .master("mymaster").sentinel("127.0.0.1", 26379).sentinel("127.0.0.1", 26380).sentinel("127.0.0.1", 26381); private RedisSentinelConfiguration sentinelConfig; - private VerificationMode mode; + private SentinelsAvailable requiredSentinels; protected RedisSentinelRule(RedisSentinelConfiguration config) { - this(config, VerificationMode.ALL_ACTIVE); - } - - protected RedisSentinelRule(RedisSentinelConfiguration config, VerificationMode mode) { - this.sentinelConfig = config; - this.mode = mode; } /** @@ -69,8 +63,8 @@ public class RedisSentinelRule implements TestRule { } public RedisSentinelRule sentinelsDisabled() { - - this.mode = VerificationMode.NO_SENTINEL; + + this.requiredSentinels = SentinelsAvailable.NONE_ACTIVE; return this; } @@ -80,8 +74,8 @@ public class RedisSentinelRule implements TestRule { * @return */ public RedisSentinelRule allActive() { - - this.mode = VerificationMode.ALL_ACTIVE; + + this.requiredSentinels = SentinelsAvailable.ALL_ACTIVE; return this; } @@ -91,25 +85,46 @@ public class RedisSentinelRule implements TestRule { * @return */ public RedisSentinelRule oneActive() { - - this.mode = VerificationMode.ONE_ACTIVE; + + this.requiredSentinels = SentinelsAvailable.ONE_ACTIVE; + return this; + } + + /** + * Will only check {@link RedisSentinelConfiguration} configuration in case {@link RequiresRedisSentinel} is detected + * on test method. + * + * @return + */ + public RedisSentinelRule dynamicModeSelection() { + this.requiredSentinels = null; return this; } @Override - public Statement apply(final Statement base, Description description) { + public Statement apply(final Statement base, final Description description) { return new Statement() { @Override public void evaluate() throws Throwable { - verify(); + + if (description.isTest()) { + RequiresRedisSentinel sentinels = description.getAnnotation(RequiresRedisSentinel.class); + if (RedisSentinelRule.this.requiredSentinels != null || sentinels != null) { + verify(sentinels != null ? sentinels.value() : RedisSentinelRule.this.requiredSentinels); + } + + } else { + verify(RedisSentinelRule.this.requiredSentinels); + } + base.evaluate(); } }; } - private void verify() { + private void verify(SentinelsAvailable verificationMode) { int failed = 0; for (RedisNode node : sentinelConfig.getSentinels()) { @@ -119,19 +134,19 @@ public class RedisSentinelRule implements TestRule { } if (failed > 0) { - if (VerificationMode.ALL_ACTIVE.equals(mode)) { + if (SentinelsAvailable.ALL_ACTIVE.equals(verificationMode)) { throw new AssumptionViolatedException(String.format( "Expected all Redis Sentinels to respone but %s of %s did not responde", failed, sentinelConfig .getSentinels().size())); } - if (VerificationMode.ONE_ACTIVE.equals(mode) && sentinelConfig.getSentinels().size() - 1 < failed) { + if (SentinelsAvailable.ONE_ACTIVE.equals(verificationMode) && sentinelConfig.getSentinels().size() - 1 < failed) { throw new AssumptionViolatedException( "Expected at least one sentinel to respond but it seems all are offline - Game Over!"); } } - if (VerificationMode.NO_SENTINEL.equals(mode) && failed != sentinelConfig.getSentinels().size()) { + if (SentinelsAvailable.NONE_ACTIVE.equals(verificationMode) && failed != sentinelConfig.getSentinels().size()) { throw new AssumptionViolatedException(String.format( "Expected to have no sentinels online but found that %s are still alive.", (sentinelConfig.getSentinels() .size() - failed))); @@ -139,30 +154,30 @@ public class RedisSentinelRule implements TestRule { } private boolean isAvailable(RedisNode node) { - + Jedis jedis = null; try { - + jedis = new Jedis(node.getHost(), node.getPort()); jedis.connect(); jedis.ping(); } catch (Exception e) { - + return false; } finally { - + if (jedis != null) { try { - + jedis.disconnect(); jedis.close(); } catch (Exception e) { - + e.printStackTrace(); } } } - + return true; } } diff --git a/src/test/java/org/springframework/data/redis/test/util/RequiresRedisSentinel.java b/src/test/java/org/springframework/data/redis/test/util/RequiresRedisSentinel.java new file mode 100644 index 000000000..ebc7d20b8 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/test/util/RequiresRedisSentinel.java @@ -0,0 +1,36 @@ +/* + * Copyright 2014 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 java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.data.redis.test.util.RedisSentinelRule.SentinelsAvailable; + +/** + * @author Christoph Strobl + */ +@Documented +@Target({ ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface RequiresRedisSentinel { + + SentinelsAvailable value() default SentinelsAvailable.ALL_ACTIVE; + +}