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 RedisTemplate
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;
+ }
+}