Skip to content

Add factory method to LettuceConnectionFactory to create RedisConfiguration from RedisURI #2117

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

/**
Expand Down Expand Up @@ -97,6 +98,7 @@
* @author Ruben Cervilla
* @author Luis De Bello
* @author Andrea Como
* @author Chris Bono
*/
public class LettuceConnectionFactory
implements InitializingBean, DisposableBean, RedisConnectionFactory, ReactiveRedisConnectionFactory {
Expand Down Expand Up @@ -279,6 +281,32 @@ public LettuceConnectionFactory(RedisClusterConfiguration clusterConfiguration,
this.configuration = clusterConfiguration;
}

/**
* Creates a {@link RedisConfiguration} based on a {@link RedisURI} according to the following:
* <ul>
* <li>If {@code redisURI} has sentinel info a {@link RedisSentinelConfiguration} is returned</li>
* <li>If {@code redisURI} has socket info a {@link RedisSocketConfiguration} is returned</li>
* <li>Otherwise a {@link RedisStandaloneConfiguration} is returned</li>
* </ul>
*
* @param redisURI the connection info in the format of a RedisURI
* @return an appropriate {@link RedisConfiguration} instance representing the Redis URI.
*/
public static RedisConfiguration createRedisConfiguration(RedisURI redisURI) {

Assert.notNull(redisURI, "RedisURI must not be null");

if (!ObjectUtils.isEmpty(redisURI.getSentinels())) {
return LettuceConverters.redisUriToSentinelConfiguration(redisURI);
}

if (!ObjectUtils.isEmpty(redisURI.getSocket())) {
return LettuceConverters.redisUriToSocketConfiguration(redisURI);
}

return LettuceConverters.redisUriToStandaloneConfiguration(redisURI);
}

/*
* (non-Javadoc)
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,12 @@
import org.springframework.data.geo.Metric;
import org.springframework.data.geo.Metrics;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.connection.*;
import org.springframework.data.redis.connection.BitFieldSubCommands.BitFieldGet;
import org.springframework.data.redis.connection.BitFieldSubCommands.BitFieldIncrBy;
import org.springframework.data.redis.connection.BitFieldSubCommands.BitFieldIncrBy.Overflow;
import org.springframework.data.redis.connection.BitFieldSubCommands.BitFieldSet;
import org.springframework.data.redis.connection.BitFieldSubCommands.BitFieldSubCommand;
import org.springframework.data.redis.connection.DefaultTuple;
import org.springframework.data.redis.connection.RedisClusterNode;
import org.springframework.data.redis.connection.RedisClusterNode.Flag;
import org.springframework.data.redis.connection.RedisClusterNode.LinkState;
import org.springframework.data.redis.connection.RedisClusterNode.SlotRange;
Expand All @@ -49,17 +47,10 @@
import org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs;
import org.springframework.data.redis.connection.RedisListCommands.Direction;
import org.springframework.data.redis.connection.RedisListCommands.Position;
import org.springframework.data.redis.connection.RedisNode;
import org.springframework.data.redis.connection.RedisNode.NodeType;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.RedisServer;
import org.springframework.data.redis.connection.RedisStringCommands.SetOption;
import org.springframework.data.redis.connection.RedisZSetCommands;
import org.springframework.data.redis.connection.RedisZSetCommands.Range.Boundary;
import org.springframework.data.redis.connection.RedisZSetCommands.Tuple;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.connection.SortParameters;
import org.springframework.data.redis.connection.SortParameters.Order;
import org.springframework.data.redis.connection.convert.Converters;
import org.springframework.data.redis.connection.convert.ListConverter;
Expand All @@ -85,6 +76,7 @@
* @author Mark Paluch
* @author Ninad Divadkar
* @author dengliming
* @author Chris Bono
*/
public abstract class LettuceConverters extends Converters {

Expand Down Expand Up @@ -535,6 +527,86 @@ public static RedisURI sentinelConfigurationToRedisURI(RedisSentinelConfiguratio
return builder.build();
}

/**
* Converts a {@link RedisURI} to its corresponding {@link RedisSentinelConfiguration}.
*
* @param redisURI the uri containing the Redis Sentinel connection info
* @return a {@link RedisSentinelConfiguration} representing the Redis Sentinel information in the Redis URI.
* @since 2.6
*/
static RedisSentinelConfiguration redisUriToSentinelConfiguration(RedisURI redisURI) {

Assert.notNull(redisURI, "RedisURI is required");

RedisSentinelConfiguration sentinelConfiguration = new RedisSentinelConfiguration();
if (!ObjectUtils.isEmpty(redisURI.getSentinelMasterId())) {
sentinelConfiguration.setMaster(redisURI.getSentinelMasterId());
}
sentinelConfiguration.setDatabase(redisURI.getDatabase());

for (RedisURI sentinelNodeRedisUri : redisURI.getSentinels()) {
RedisNode sentinelNode = new RedisNode(sentinelNodeRedisUri.getHost(), sentinelNodeRedisUri.getPort());
if (sentinelNodeRedisUri.getPassword() != null) {
sentinelConfiguration.setSentinelPassword(sentinelNodeRedisUri.getPassword());
}
sentinelConfiguration.addSentinel(sentinelNode);
}

applyAuthentication(redisURI, sentinelConfiguration);

return sentinelConfiguration;
}

/**
* Converts a {@link RedisURI} to its corresponding {@link RedisSocketConfiguration}.
*
* @param redisURI the uri containing the Redis connection info using a local unix domain socket
* @return a {@link RedisSocketConfiguration} representing the connection information in the Redis URI.
* @since 2.6
*/
static RedisSocketConfiguration redisUriToSocketConfiguration(RedisURI redisURI) {

Assert.notNull(redisURI, "RedisURI is required");

RedisSocketConfiguration socketConfiguration = new RedisSocketConfiguration();
socketConfiguration.setSocket(redisURI.getSocket());
socketConfiguration.setDatabase(redisURI.getDatabase());

applyAuthentication(redisURI, socketConfiguration);

return socketConfiguration;
}

/**
* Converts a {@link RedisURI} to its corresponding {@link RedisStandaloneConfiguration}.
*
* @param redisURI the uri containing the Redis connection info
* @return a {@link RedisStandaloneConfiguration} representing the connection information in the Redis URI.
* @since 2.6
*/
static RedisStandaloneConfiguration redisUriToStandaloneConfiguration(RedisURI redisURI) {

Assert.notNull(redisURI, "RedisURI is required");

RedisStandaloneConfiguration standaloneConfiguration = new RedisStandaloneConfiguration();
standaloneConfiguration.setHostName(redisURI.getHost());
standaloneConfiguration.setPort(redisURI.getPort());
standaloneConfiguration.setDatabase(redisURI.getDatabase());

applyAuthentication(redisURI, standaloneConfiguration);

return standaloneConfiguration;
}

private static void applyAuthentication(RedisURI redisURI, RedisConfiguration.WithAuthentication redisConfiguration) {
if (StringUtils.hasText(redisURI.getUsername())) {
redisConfiguration.setUsername(redisURI.getUsername());
}
if (redisURI.getPassword() != null) {
redisConfiguration.setPassword(redisURI.getPassword());
}
}

public static byte[] toBytes(@Nullable String source) {
if (source == null) {
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/*
* Copyright 2015-2021 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
*
* https://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.lettuce;

import static org.assertj.core.api.Assertions.*;

import io.lettuce.core.RedisURI;

import org.assertj.core.api.recursive.comparison.RecursiveComparisonConfiguration;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import org.springframework.data.redis.connection.RedisConfiguration;
import org.springframework.data.redis.connection.RedisNode;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.RedisSocketConfiguration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;

/**
* Unit tests for the {@link LettuceConnectionFactory#createRedisConfiguration(RedisURI)} factory method.
*
* @author Chris Bono
*/
class LettuceConnectionFactoryCreateRedisConfigurationUnitTests {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few things to note about this test:

  1. I opted to test the functionality at LettuceConnectionFactory.createRedisConfiguration rather than directly on the LettuceConverters.redisUriTo<T>Configuration as the tests would have been almost identical. The coverage is the same doing it this way. The assumption is that the LC methods are an internal impl of this API. Also, because the LC methods are static, just unit testing the routing in LCF to LC (that is all that the LCF really is doing) would have been problematic to mock.

  2. I broke it out into its own specific LCF unit test as the other one is getting pretty big.

  3. I leveraged @Nested to group the "flavor" of RedisConfiguration under test.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest introducing proper equals/hashCode methods, that's something we can do during the merge.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeh good call. I was wrapped up in Lettuce specific-land that I did not realize the config objects were not lettuce constructs - they are clearly spring data redis and I could have added EHC rather than the AssertJ field by field config approach.


@Test
void requiresRedisURI() {
assertThatThrownBy(() -> LettuceConnectionFactory.createRedisConfiguration(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("RedisURI must not be null");
}

@Nested // GH-2117
class CreateRedisSentinelConfiguration {

@Test
void minimalFieldsSetOnRedisURI() {
RedisURI redisURI = RedisURI.create("redis-sentinel://myserver?sentinelMasterId=5150");
RedisSentinelConfiguration expectedSentinelConfiguration = new RedisSentinelConfiguration();
expectedSentinelConfiguration.setMaster("5150");
expectedSentinelConfiguration.addSentinel(new RedisNode("myserver", 26379));

RedisConfiguration sentinelConfiguration = LettuceConnectionFactory.createRedisConfiguration(redisURI);

assertThat(sentinelConfiguration).usingRecursiveComparison(sentinelCompareConfig())
.isInstanceOf(RedisSentinelConfiguration.class)
.isEqualTo(expectedSentinelConfiguration);
}

@Test
void allFieldsSetOnRedisURI() {
RedisURI redisURI = RedisURI.create("redis-sentinel://fooUser:fooPass@myserver1:111,myserver2:222/7?sentinelMasterId=5150");
// Set the passwords directly on the sentinels so that it gets picked up by converter
char[] sentinelPass = "changeme".toCharArray();
redisURI.getSentinels().forEach(sentinelRedisUri -> sentinelRedisUri.setPassword(sentinelPass));
RedisSentinelConfiguration expectedSentinelConfiguration = new RedisSentinelConfiguration();
expectedSentinelConfiguration.setMaster("5150");
expectedSentinelConfiguration.setDatabase(7);
expectedSentinelConfiguration.setUsername("fooUser");
expectedSentinelConfiguration.setPassword("fooPass");
expectedSentinelConfiguration.setSentinelPassword(sentinelPass);
expectedSentinelConfiguration.addSentinel(new RedisNode("myserver1", 111));
expectedSentinelConfiguration.addSentinel(new RedisNode("myserver2", 222));

RedisConfiguration sentinelConfiguration = LettuceConnectionFactory.createRedisConfiguration(redisURI);

assertThat(sentinelConfiguration).usingRecursiveComparison(sentinelCompareConfig())
.isInstanceOf(RedisSentinelConfiguration.class)
.isEqualTo(expectedSentinelConfiguration);
}

// RedisSentinelConfiguration does not provide equals impl
private RecursiveComparisonConfiguration sentinelCompareConfig() {
return RecursiveComparisonConfiguration.builder().withComparedFields(
"master",
"username",
"password",
"sentinelPassword",
"database",
"sentinels")
.build();
}
}

@Nested // GH-2117
class CreateRedisSocketConfiguration {

@Test
void minimalFieldsSetOnRedisURI() {
RedisURI redisURI = RedisURI.builder()
.socket("mysocket")
.build();
RedisSocketConfiguration expectedSocketConfiguration = new RedisSocketConfiguration();
expectedSocketConfiguration.setSocket("mysocket");

RedisConfiguration socketConfiguration = LettuceConnectionFactory.createRedisConfiguration(redisURI);

assertThat(socketConfiguration).usingRecursiveComparison(socketCompareConfig())
.isInstanceOf(RedisSocketConfiguration.class)
.isEqualTo(expectedSocketConfiguration);
}

@Test
void allFieldsSetOnRedisURI() {
RedisURI redisURI = RedisURI.builder()
.socket("mysocket")
.withAuthentication("fooUser", "fooPass".toCharArray())
.withDatabase(7)
.build();
RedisSocketConfiguration expectedSocketConfiguration = new RedisSocketConfiguration();
expectedSocketConfiguration.setSocket("mysocket");
expectedSocketConfiguration.setUsername("fooUser");
expectedSocketConfiguration.setPassword("fooPass");
expectedSocketConfiguration.setDatabase(7);

RedisConfiguration socketConfiguration = LettuceConnectionFactory.createRedisConfiguration(redisURI);

assertThat(socketConfiguration).usingRecursiveComparison(socketCompareConfig())
.isInstanceOf(RedisSocketConfiguration.class)
.isEqualTo(expectedSocketConfiguration);
}

// RedisSocketConfiguration does not provide equals impl
private RecursiveComparisonConfiguration socketCompareConfig() {
return RecursiveComparisonConfiguration.builder().withComparedFields(
"socket",
"username",
"password",
"database")
.build();
}
}

@Nested // GH-2117
class CreateRedisStandaloneConfiguration {

@Test
void minimalFieldsSetOnRedisURI() {
RedisURI redisURI = RedisURI.create("redis://myserver");
RedisStandaloneConfiguration expectedStandaloneConfiguration = new RedisStandaloneConfiguration();
expectedStandaloneConfiguration.setHostName("myserver");

RedisConfiguration StandaloneConfiguration = LettuceConnectionFactory.createRedisConfiguration(redisURI);

assertThat(StandaloneConfiguration).usingRecursiveComparison(standaloneCompareConfig())
.isInstanceOf(RedisStandaloneConfiguration.class)
.isEqualTo(expectedStandaloneConfiguration);
}

@Test
void allFieldsSetOnRedisURI() {
RedisURI redisURI = RedisURI.create("redis://fooUser:fooPass@myserver1:111/7");
RedisStandaloneConfiguration expectedStandaloneConfiguration = new RedisStandaloneConfiguration();
expectedStandaloneConfiguration.setHostName("myserver1");
expectedStandaloneConfiguration.setPort(111);
expectedStandaloneConfiguration.setDatabase(7);
expectedStandaloneConfiguration.setUsername("fooUser");
expectedStandaloneConfiguration.setPassword("fooPass");

RedisConfiguration StandaloneConfiguration = LettuceConnectionFactory.createRedisConfiguration(redisURI);

assertThat(StandaloneConfiguration).usingRecursiveComparison(standaloneCompareConfig())
.isInstanceOf(RedisStandaloneConfiguration.class)
.isEqualTo(expectedStandaloneConfiguration);

}

// RedisStandaloneConfiguration does not provide equals impl
private RecursiveComparisonConfiguration standaloneCompareConfig() {
return RecursiveComparisonConfiguration.builder().withComparedFields(
"host",
"port",
"username",
"password",
"database")
.build();
}
}

}