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:
committed by
Thomas Darimont
parent
c36a58da7f
commit
55ff415a71
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user