diff --git a/.gitignore b/.gitignore index c4002795b..8553effaf 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ pom.xml .settings .idea out +work +*.rdb diff --git a/.travis.yml b/.travis.yml index bf5930883..06ccbed5b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,4 @@ jdk: - oraclejdk7 - openjdk6 - oraclejdk8 -services: - - redis-server -script: "gradle clean build -DrunLongTests=true" \ No newline at end of file +script: make test diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..217fd7bb0 --- /dev/null +++ b/Makefile @@ -0,0 +1,99 @@ +# 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. +# 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. + +REDIS_VERSION:=2.8.13 + +####### +# Redis +####### +.PRECIOUS: work/redis-%.conf + +work/redis-%.conf: + @mkdir -p $(@D) + + echo port $* >> $@ + echo daemonize yes >> $@ + echo pidfile $(shell pwd)/work/redis-$*.pid >> $@ + echo logfile $(shell pwd)/work/redis-$*.log >> $@ + echo save \"\" >> $@ + echo slaveof 127.0.0.1 6379 >> $@ + +# Handled separately because it's the master and all others are slaves +work/redis-6379.conf: + @mkdir -p $(@D) + + echo port 6379 >> $@ + echo daemonize yes >> $@ + echo pidfile $(shell pwd)/work/redis-6379.pid >> $@ + echo logfile $(shell pwd)/work/redis-6379.log >> $@ + echo save \"\" >> $@ + +work/redis-%.pid: work/redis-%.conf work/redis/bin/redis-server + work/redis/bin/redis-server $< + +redis-start: work/redis-6379.pid work/redis-6380.pid work/redis-6381.pid + +redis-stop: stop-6379 stop-6380 stop-6381 + +########## +# Sentinel +########## +.PRECIOUS: work/sentinel-%.conf + +work/sentinel-%.conf: + @mkdir -p $(@D) + + echo port $* >> $@ + echo daemonize yes >> $@ + echo pidfile $(shell pwd)/work/sentinel-$*.pid >> $@ + echo logfile $(shell pwd)/work/sentinel-$*.log >> $@ + echo save \"\" >> $@ + echo sentinel monitor mymaster 127.0.0.1 6379 2 >> $@ + +work/sentinel-%.pid: work/sentinel-%.conf work/redis-6379.pid work/redis/bin/redis-server + work/redis/bin/redis-server $< --sentinel + +sentinel-start: work/sentinel-26379.pid work/sentinel-26380.pid work/sentinel-26381.pid + +sentinel-stop: stop-26379 stop-26380 stop-26381 + +######## +# Global +######## +clean: + rm -rf work/*.conf work/*.log + +clobber: + rm -rf work + +work/redis/bin/redis-cli work/redis/bin/redis-server: + @mkdir -p work/redis + + curl -sSL https://github.com/antirez/redis/archive/$(REDIS_VERSION).tar.gz | tar xzf - -C work + $(MAKE) -C work/redis-$(REDIS_VERSION) -j + $(MAKE) -C work/redis-$(REDIS_VERSION) PREFIX=$(shell pwd)/work/redis install + rm -rf work/redis-$(REDIS_VERSION) + +start: redis-start sentinel-start + +stop-%: work/redis/bin/redis-cli + -work/redis/bin/redis-cli -p $* shutdown + +stop: redis-stop sentinel-stop + +test: + $(MAKE) start + sleep 2 + $(PWD)/gradlew clean build -DrunLongTests=true + $(MAKE) stop diff --git a/docs/src/reference/docbook/reference/redis.xml b/docs/src/reference/docbook/reference/redis.xml index e1f6fd03e..365da3407 100644 --- a/docs/src/reference/docbook/reference/redis.xml +++ b/docs/src/reference/docbook/reference/redis.xml @@ -209,6 +209,21 @@ +
+ Redis Sentinel Support + For dealing with high available Redis there is support for Redis Sentinel using RedisSentinelConfiguration. + Please note that currently only Jedis supports Redis Sentinel. + +
+
Working with Objects through <classname>RedisTemplate</classname> diff --git a/src/main/java/org/springframework/data/redis/connection/NamedNode.java b/src/main/java/org/springframework/data/redis/connection/NamedNode.java new file mode 100644 index 000000000..e5c0be670 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/NamedNode.java @@ -0,0 +1,25 @@ +/* + * 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; + +/** + * @author Christoph Strobl + * @since 1.4 + */ +public interface NamedNode { + + String getName(); +} diff --git a/src/main/java/org/springframework/data/redis/connection/RedisNode.java b/src/main/java/org/springframework/data/redis/connection/RedisNode.java new file mode 100644 index 000000000..a269549c2 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/RedisNode.java @@ -0,0 +1,83 @@ +/* + * 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 org.springframework.util.ObjectUtils; + +/** + * @author Christoph Strobl + * @since 1.4 + */ +public class RedisNode { + + private final String host; + private final int port; + + public RedisNode(String host, int port) { + this.host = host; + this.port = port; + } + + public String getHost() { + return host; + } + + public Integer getPort() { + return port; + } + + public String asString() { + return host + ":" + port; + } + + @Override + public String toString() { + return asString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ObjectUtils.nullSafeHashCode(host); + result = prime * result + ObjectUtils.nullSafeHashCode(port); + return result; + } + + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + if (obj == null || !(obj instanceof RedisNode)) { + return false; + } + + RedisNode other = (RedisNode) obj; + + if (!ObjectUtils.nullSafeEquals(this.host, other.host)) { + return false; + } + + if (!ObjectUtils.nullSafeEquals(this.port, other.port)) { + return false; + } + + return true; + } + +} diff --git a/src/main/java/org/springframework/data/redis/connection/RedisSentinelConfiguration.java b/src/main/java/org/springframework/data/redis/connection/RedisSentinelConfiguration.java new file mode 100644 index 000000000..2a2571ecf --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/RedisSentinelConfiguration.java @@ -0,0 +1,154 @@ +/* + * 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.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.util.Assert; + +/** + * Configuration class used for setting up {@link RedisConnection} via {@link RedisConnectionFactory} using connecting + * to Redis Sentinel(s). Useful when setting up a high availability Redis + * environment. + * + * @author Christoph Strobl + * @since 1.4 + */ +public class RedisSentinelConfiguration { + + private NamedNode master; + private Set sentinels; + + /** + * Creates new {@link RedisSentinelConfiguration}. + */ + public RedisSentinelConfiguration() { + this.sentinels = new LinkedHashSet(); + } + + /** + * Set {@literal Sentinels} to connect to. + * + * @param sentinels must not be {@literal null}. + */ + public void setSentinels(Iterable sentinels) { + + Assert.notNull(sentinels, "Cannot set sentinels to 'null'."); + this.sentinels.clear(); + for (RedisNode sentinel : sentinels) { + addSentinel(sentinel); + } + } + + /** + * Returns an {@link Collections#unmodifiableSet(Set)} of {@literal Sentinels}. + * + * @return {@link Set} of sentinels. Never {@literal null}. + */ + public Set getSentinels() { + return Collections.unmodifiableSet(sentinels); + } + + /** + * Add sentinel. + * + * @param sentinel must not be {@literal null}. + */ + public void addSentinel(RedisNode sentinel) { + + Assert.notNull(sentinel, "Sentinel must not be 'null'."); + this.sentinels.add(sentinel); + } + + /** + * Set the master node via its name. + * + * @param name must not be {@literal null}. + */ + public void setMaster(final String name) { + + Assert.notNull(name, "Name of sentinel master must not be null."); + setMaster(new NamedNode() { + + @Override + public String getName() { + return name; + } + }); + } + + /** + * Set the master. + * + * @param master must not be {@literal null}. + */ + public void setMaster(NamedNode master) { + + Assert.notNull("Sentinel master node must not be 'null'."); + this.master = master; + } + + /** + * Get the {@literal Sentinel} master node. + * + * @return + */ + public NamedNode getMaster() { + return master; + } + + /** + * @see #setMaster(String) + * @param master + * @return + */ + public RedisSentinelConfiguration master(String master) { + this.setMaster(master); + return this; + } + + /** + * @see #setMaster(NamedNode) + * @param master + * @return + */ + public RedisSentinelConfiguration master(NamedNode master) { + this.setMaster(master); + return this; + } + + /** + * @see #addSentinel(RedisNode) + * @param sentinel + * @return + */ + public RedisSentinelConfiguration sentinel(RedisNode sentinel) { + this.addSentinel(sentinel); + return this; + } + + /** + * @see #sentinel(RedisNode) + * @param host + * @param port + * @return + */ + public RedisSentinelConfiguration sentinel(String host, Integer port) { + return sentinel(new RedisNode(host, port)); + } +} 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 336fd49ae..c57e8243a 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 @@ -16,6 +16,11 @@ package org.springframework.data.redis.connection.jedis; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.DisposableBean; @@ -26,20 +31,26 @@ import org.springframework.data.redis.PassThroughExceptionTranslationStrategy; import org.springframework.data.redis.RedisConnectionFailureException; 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.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.JedisSentinelPool; import redis.clients.jedis.JedisShardInfo; import redis.clients.jedis.Protocol; +import redis.clients.util.Pool; /** * Connection factory creating Jedis based connections. * * @author Costin Leau * @author Thomas Darimont + * @author Christoph Strobl */ public class JedisConnectionFactory implements InitializingBean, DisposableBean, RedisConnectionFactory { @@ -53,10 +64,11 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, private int timeout = Protocol.DEFAULT_TIMEOUT; private String password; private boolean usePool = true; - private JedisPool pool = null; + private Pool pool; private JedisPoolConfig poolConfig = new JedisPoolConfig(); private int dbIndex = 0; private boolean convertPipelineAndTxResults = true; + private RedisSentinelConfiguration sentinelConfig; /** * Constructs a new JedisConnectionFactory instance with default settings (default connection pooling, no @@ -80,7 +92,31 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, * @param poolConfig pool configuration */ public JedisConnectionFactory(JedisPoolConfig poolConfig) { - this.poolConfig = poolConfig; + this(null, poolConfig); + } + + /** + * Constructs a new {@link JedisConnectionFactory} instance using the given {@link JedisPoolConfig} applied to + * {@link JedisSentinelPool}. + * + * @param sentinelConfig + * @since 1.4 + */ + public JedisConnectionFactory(RedisSentinelConfiguration sentinelConfig) { + this(sentinelConfig, null); + } + + /** + * Constructs a new {@link JedisConnectionFactory} instance using the given {@link JedisPoolConfig} applied to + * {@link JedisSentinelPool}. + * + * @param sentinelConfig + * @param poolConfig pool configuration. Defaulted to new instance if {@literal null}. + * @since 1.4 + */ + public JedisConnectionFactory(RedisSentinelConfiguration sentinelConfig, JedisPoolConfig poolConfig) { + this.sentinelConfig = sentinelConfig; + this.poolConfig = poolConfig != null ? poolConfig : new JedisPoolConfig(); } /** @@ -114,6 +150,10 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, return connection; } + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ public void afterPropertiesSet() { if (shardInfo == null) { shardInfo = new JedisShardInfo(hostName, port); @@ -128,11 +168,46 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, } if (usePool) { - pool = new JedisPool(poolConfig, shardInfo.getHost(), shardInfo.getPort(), shardInfo.getTimeout(), - shardInfo.getPassword()); + this.pool = createPool(); } } + private Pool createPool() { + + if (isRedisSentinelAware()) { + return createRedisSentinelPool(this.sentinelConfig); + } + return createRedisPool(); + } + + /** + * Creates {@link JedisSentinelPool}. + * + * @param config + * @return + * @since 1.4 + */ + protected Pool createRedisSentinelPool(RedisSentinelConfiguration config) { + return new JedisSentinelPool(config.getMaster().getName(), convertToJedisSentinelSet(config.getSentinels()), + getPoolConfig() != null ? getPoolConfig() : new JedisPoolConfig(), getShardInfo().getTimeout(), getShardInfo() + .getPassword()); + } + + /** + * Creates {@link JedisPool}. + * + * @return + * @since 1.4 + */ + protected Pool createRedisPool() { + return new JedisPool(getPoolConfig(), getShardInfo().getHost(), getShardInfo().getPort(), getShardInfo() + .getTimeout(), getShardInfo().getPassword()); + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.DisposableBean#destroy() + */ public void destroy() { if (usePool && pool != null) { try { @@ -144,6 +219,10 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, } } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisConnectionFactory#getConnection() + */ public JedisConnection getConnection() { Jedis jedis = fetchJedisConnector(); JedisConnection connection = (usePool ? new JedisConnection(jedis, pool, dbIndex) : new JedisConnection(jedis, @@ -152,6 +231,10 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, return postProcessConnection(connection); } + /* + * (non-Javadoc) + * @see org.springframework.dao.support.PersistenceExceptionTranslator#translateExceptionIfPossible(java.lang.RuntimeException) + */ public DataAccessException translateExceptionIfPossible(RuntimeException ex) { return EXCEPTION_TRANSLATION.translate(ex); } @@ -321,4 +404,28 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, public void setConvertPipelineAndTxResults(boolean convertPipelineAndTxResults) { this.convertPipelineAndTxResults = convertPipelineAndTxResults; } + + /** + * @return true when {@link RedisSentinelConfiguration} is present. + * @since 1.4 + */ + public boolean isRedisSentinelAware() { + return sentinelConfig != null; + } + + private Set convertToJedisSentinelSet(Collection nodes) { + + if (CollectionUtils.isEmpty(nodes)) { + return Collections.emptySet(); + } + + Set convertedNodes = new LinkedHashSet(nodes.size()); + for (RedisNode node : nodes) { + if (node != null) { + convertedNodes.add(node.asString()); + } + } + return convertedNodes; + } + } diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryTests.java new file mode 100644 index 000000000..a67e48986 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryTests.java @@ -0,0 +1,57 @@ +/* + * 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.hamcrest.core.IsEqual.*; +import static org.junit.Assert.*; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.springframework.data.redis.connection.RedisSentinelConfiguration; +import org.springframework.data.redis.test.util.RedisSentinelRule; + +/** + * @author Christoph Strobl + */ +public class JedisConnectionFactoryTests { + + private static final RedisSentinelConfiguration SENTINEL_CONFIG = new RedisSentinelConfiguration().master("mymaster") + .sentinel("127.0.0.1", 26379).sentinel("127.0.0.1", 26380); + private JedisConnectionFactory factory; + + public @Rule RedisSentinelRule sentinelRule = RedisSentinelRule.forConfig(SENTINEL_CONFIG).oneActive(); + + @Before + public void setUp() { + factory = new JedisConnectionFactory(SENTINEL_CONFIG); + factory.afterPropertiesSet(); + } + + @After + public void tearDown() { + factory.destroy(); + } + + /** + * @see DATAREDIS-324 + */ + @Test + public void shouldSendCommandCorrectlyViaConnectionFactoryUsingSentinel() { + assertThat(factory.getConnection().ping(), equalTo("PONG")); + } +} 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 new file mode 100644 index 000000000..50aa8bcb8 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryUnitTests.java @@ -0,0 +1,71 @@ +/* + * 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.Mockito.*; + +import org.junit.Test; +import org.mockito.Matchers; +import org.springframework.data.redis.connection.RedisSentinelConfiguration; + +import redis.clients.jedis.JedisPoolConfig; + +/** + * @author Christoph Strobl + */ +public class JedisConnectionFactoryUnitTests { + + private JedisConnectionFactory connectionFactory; + + private static final RedisSentinelConfiguration SINGLE_SENTINEL_CONFIG = new RedisSentinelConfiguration().master( + "mymaster").sentinel("127.0.0.1", 26379); + + /** + * @see DATAREDIS-324 + */ + @Test + public void shouldInitSentinelPoolWhenSentinelConfigPresent() { + + connectionFactory = initSpyedConnectionFactory(SINGLE_SENTINEL_CONFIG, new JedisPoolConfig()); + connectionFactory.afterPropertiesSet(); + + verify(connectionFactory, times(1)).createRedisSentinelPool(Matchers.eq(SINGLE_SENTINEL_CONFIG)); + verify(connectionFactory, never()).createRedisPool(); + } + + /** + * @see DATAREDIS-324 + */ + @Test + public void shouldInitJedisPoolWhenNoSentinelConfigPresent() { + + connectionFactory = initSpyedConnectionFactory(null, new JedisPoolConfig()); + connectionFactory.afterPropertiesSet(); + + verify(connectionFactory, times(1)).createRedisPool(); + verify(connectionFactory, never()).createRedisSentinelPool(Matchers.any(RedisSentinelConfiguration.class)); + } + + private JedisConnectionFactory initSpyedConnectionFactory(RedisSentinelConfiguration sentinelConfig, + JedisPoolConfig poolConfig) { + + // we have to use a spy here as jedis would start connecting to redis sentinels when the pool is created. + JedisConnectionFactory factorySpy = spy(new JedisConnectionFactory(sentinelConfig, poolConfig)); + doReturn(null).when(factorySpy).createRedisSentinelPool(Matchers.any(RedisSentinelConfiguration.class)); + doReturn(null).when(factorySpy).createRedisPool(); + return factorySpy; + } +} 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 new file mode 100644 index 000000000..2b4f0df44 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisSentinelIntegrationTests.java @@ -0,0 +1,105 @@ +/* + * 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 org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +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.ReturnType; +import org.springframework.data.redis.test.util.RedisSentinelRule; +import org.springframework.test.annotation.IfProfileValue; + +/** + * @author Christoph Strobl + */ +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); + + public @Rule RedisSentinelRule sentinelRule = RedisSentinelRule.forConfig(SENTINEL_CONFIG).oneActive(); + + @Before + public void setUp() { + JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(SENTINEL_CONFIG); + jedisConnectionFactory.afterPropertiesSet(); + connectionFactory = jedisConnectionFactory; + super.setUp(); + } + + @After + public void tearDown() { + super.tearDown(); + ((JedisConnectionFactory) connectionFactory).destroy(); + } + + @Test + @Ignore + public void testScriptKill() throws Exception { + super.testScriptKill(); + } + + @Test(expected = InvalidDataAccessApiUsageException.class) + @IfProfileValue(name = "redisVersion", value = "2.6+") + public void testEvalReturnSingleError() { + connection.eval("return redis.call('expire','foo')", ReturnType.BOOLEAN, 0); + } + + @Test(expected = InvalidDataAccessApiUsageException.class) + @IfProfileValue(name = "redisVersion", value = "2.6+") + public void testEvalArrayScriptError() { + super.testEvalArrayScriptError(); + } + + @Test(expected = InvalidDataAccessApiUsageException.class) + @IfProfileValue(name = "redisVersion", value = "2.6+") + public void testEvalShaNotFound() { + connection.evalSha("somefakesha", ReturnType.VALUE, 2, "key1", "key2"); + } + + @Test(expected = InvalidDataAccessApiUsageException.class) + @IfProfileValue(name = "redisVersion", value = "2.6+") + public void testEvalShaArrayError() { + super.testEvalShaArrayError(); + } + + @Test(expected = InvalidDataAccessApiUsageException.class) + @IfProfileValue(name = "redisVersion", value = "2.6+") + public void testRestoreBadData() { + super.testRestoreBadData(); + } + + @Test(expected = InvalidDataAccessApiUsageException.class) + @IfProfileValue(name = "redisVersion", value = "2.6+") + public void testRestoreExistingKey() { + super.testRestoreExistingKey(); + } + + @Test(expected = InvalidDataAccessApiUsageException.class) + public void testExecWithoutMulti() { + super.testExecWithoutMulti(); + } + + @Test(expected = InvalidDataAccessApiUsageException.class) + public void testErrorInTx() { + super.testErrorInTx(); + } +} diff --git a/src/test/java/org/springframework/data/redis/listener/PubSubTests.java b/src/test/java/org/springframework/data/redis/listener/PubSubTests.java index c99fffa2f..0ef1fd724 100644 --- a/src/test/java/org/springframework/data/redis/listener/PubSubTests.java +++ b/src/test/java/org/springframework/data/redis/listener/PubSubTests.java @@ -15,11 +15,9 @@ */ package org.springframework.data.redis.listener; -import static org.hamcrest.CoreMatchers.hasItems; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertThat; -import static org.junit.Assume.assumeTrue; +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.junit.Assume.*; import java.util.Arrays; import java.util.Collection; @@ -33,6 +31,7 @@ import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -44,6 +43,7 @@ import org.springframework.data.redis.ObjectFactory; import org.springframework.data.redis.RedisTestProfileValueSource; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.listener.adapter.MessageListenerAdapter; +import org.springframework.data.redis.test.util.RedisSentinelRule; /** * Base test class for PubSub integration tests @@ -54,6 +54,8 @@ import org.springframework.data.redis.listener.adapter.MessageListenerAdapter; @RunWith(Parameterized.class) public class PubSubTests { + public @Rule RedisSentinelRule sentinelRule = RedisSentinelRule.withDefaultConfig().sentinelsDisabled(); + private static final String CHANNEL = "pubsub::test"; protected RedisMessageListenerContainer container; 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 new file mode 100644 index 000000000..e386d1fdb --- /dev/null +++ b/src/test/java/org/springframework/data/redis/test/util/RedisSentinelRule.java @@ -0,0 +1,168 @@ +/* + * 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 org.junit.internal.AssumptionViolatedException; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.springframework.data.redis.connection.RedisNode; +import org.springframework.data.redis.connection.RedisSentinelConfiguration; + +import redis.clients.jedis.Jedis; + +/** + * @author Christoph Strobl + */ +public class RedisSentinelRule implements TestRule { + + enum VerificationMode { + ALL_ACTIVE, ONE_ACTIVE, NO_SENTINEL + } + + 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; + + protected RedisSentinelRule(RedisSentinelConfiguration config) { + this(config, VerificationMode.ALL_ACTIVE); + } + + protected RedisSentinelRule(RedisSentinelConfiguration config, VerificationMode mode) { + + this.sentinelConfig = config; + this.mode = mode; + } + + /** + * Create new {@link RedisSentinelRule} for given {@link RedisSentinelConfiguration}. + * + * @param config + * @return + */ + public static RedisSentinelRule forConfig(RedisSentinelConfiguration config) { + return new RedisSentinelRule(config != null ? config : DEFAULT_SENTINEL_CONFIG); + } + + /** + * Create new {@link RedisSentinelRule} using default configuration. + * + * @return + */ + public static RedisSentinelRule withDefaultConfig() { + return new RedisSentinelRule(DEFAULT_SENTINEL_CONFIG); + } + + public RedisSentinelRule sentinelsDisabled() { + + this.mode = VerificationMode.NO_SENTINEL; + return this; + } + + /** + * Verifies all {@literal Sentinel} nodes are available. + * + * @return + */ + public RedisSentinelRule allActive() { + + this.mode = VerificationMode.ALL_ACTIVE; + return this; + } + + /** + * Verifies at least one {@literal Sentinel} node is available. + * + * @return + */ + public RedisSentinelRule oneActive() { + + this.mode = VerificationMode.ONE_ACTIVE; + return this; + } + + @Override + public Statement apply(final Statement base, Description description) { + + return new Statement() { + + @Override + public void evaluate() throws Throwable { + verify(); + base.evaluate(); + } + }; + } + + private void verify() { + + int failed = 0; + for (RedisNode node : sentinelConfig.getSentinels()) { + if (!isAvailable(node)) { + failed++; + } + } + + if (failed > 0) { + if (VerificationMode.ALL_ACTIVE.equals(mode)) { + 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) { + 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()) { + throw new AssumptionViolatedException(String.format( + "Expected to have no sentinels online but found that %s are still alive.", (sentinelConfig.getSentinels() + .size() - failed))); + } + } + + 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; + } +}