DATAREDIS-324 - Add Redis Sentinel support.

We’ve added RedisSentinelConfiguration holding required information for connecting to redis sentinels. This can be used to set up ConnectionFeactory for HA environments.

**Using Jedis**
Providing RedisSentinelConfiguration will force the JedisConnectionFactory to use JedisSentinelPool for managing resources.

**Using Lettuce/JRedis/SRP**
There’s currently no support for sentinel in those clients.

**CI Build**
We’ve added makefile to build and set up redis instances for testing sentinel support on travis-ci. There’s already a section for redis cluster.
The cluster section is for whatever reason currently not working as the cluster nodes won’t start.
This will be fixed when we add redis-cluster support.

_side note:_ there’s an alternative fork of lettuce at mp911de/lettuce that already has sentinel support.
This commit is contained in:
Christoph Strobl
2014-07-15 09:22:04 +02:00
committed by Thomas Darimont
parent c36a58da7f
commit 55ff415a71
13 changed files with 898 additions and 12 deletions

View File

@@ -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"));
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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<T> {
public @Rule RedisSentinelRule sentinelRule = RedisSentinelRule.withDefaultConfig().sentinelsDisabled();
private static final String CHANNEL = "pubsub::test";
protected RedisMessageListenerContainer container;

View File

@@ -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;
}
}