Skip to content

Commit

Permalink
Add configuration to enable Redis Cluster topology refresh
Browse files Browse the repository at this point in the history
This commit adds two options to enable a refresh of the cluster
topology using Lettuce.

Closes spring-projectsgh-15630
  • Loading branch information
snicoll committed Apr 1, 2020
1 parent d8cead5 commit dfac3a2
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
Expand All @@ -18,14 +18,20 @@

import java.net.UnknownHostException;

import io.lettuce.core.ClientOptions;
import io.lettuce.core.RedisClient;
import io.lettuce.core.TimeoutOptions;
import io.lettuce.core.cluster.ClusterClientOptions;
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions;
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions.Builder;
import io.lettuce.core.resource.ClientResources;
import io.lettuce.core.resource.DefaultClientResources;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties.Lettuce.Cluster.Refresh;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties.Pool;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand Down Expand Up @@ -88,6 +94,7 @@ private LettuceClientConfiguration getLettuceClientConfiguration(
if (StringUtils.hasText(getProperties().getUrl())) {
customizeConfigurationFromUrl(builder);
}
builder.clientOptions(initializeClientOptionsBuilder().timeoutOptions(TimeoutOptions.enabled()).build());
builder.clientResources(clientResources);
builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return builder.build();
Expand Down Expand Up @@ -120,6 +127,22 @@ private LettuceClientConfigurationBuilder applyProperties(
return builder;
}

private ClientOptions.Builder initializeClientOptionsBuilder() {
if (getProperties().getCluster() != null) {
ClusterClientOptions.Builder builder = ClusterClientOptions.builder();
Refresh refreshProperties = getProperties().getLettuce().getCluster().getRefresh();
Builder refreshBuilder = ClusterTopologyRefreshOptions.builder();
if (refreshProperties.getPeriod() != null) {
refreshBuilder.enablePeriodicRefresh(refreshProperties.getPeriod());
}
if (refreshProperties.isAdaptive()) {
refreshBuilder.enableAllAdaptiveRefreshTriggers();
}
return builder.topologyRefreshOptions(refreshBuilder.build());
}
return ClientOptions.builder();
}

private void customizeConfigurationFromUrl(LettuceClientConfiguration.LettuceClientConfigurationBuilder builder) {
ConnectionInfo connectionInfo = parseUrl(getProperties().getUrl());
if (connectionInfo.isUseSsl()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
Expand Down Expand Up @@ -354,6 +354,8 @@ public static class Lettuce {
*/
private Pool pool;

private final Cluster cluster = new Cluster();

public Duration getShutdownTimeout() {
return this.shutdownTimeout;
}
Expand All @@ -370,6 +372,51 @@ public void setPool(Pool pool) {
this.pool = pool;
}

public Cluster getCluster() {
return this.cluster;
}

public static class Cluster {

private final Refresh refresh = new Refresh();

public Refresh getRefresh() {
return this.refresh;
}

public static class Refresh {

/**
* Cluster topology refresh period.
*/
private Duration period;

/**
* Whether adaptive topology refreshing using all available refresh
* triggers should be used.
*/
private boolean adaptive;

public Duration getPeriod() {
return this.period;
}

public void setPeriod(Duration period) {
this.period = period;
}

public boolean isAdaptive() {
return this.adaptive;
}

public void setAdaptive(boolean adaptive) {
this.adaptive = adaptive;
}

}

}

}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
Expand All @@ -17,19 +17,27 @@
package org.springframework.boot.autoconfigure.data.redis;

import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import io.lettuce.core.ClientOptions;
import io.lettuce.core.cluster.ClusterClientOptions;
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions.RefreshTrigger;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.junit.jupiter.api.Test;

import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.ContextConsumer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisNode;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration.LettuceClientConfigurationBuilder;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
Expand All @@ -54,7 +62,7 @@
*/
class RedisAutoConfigurationTests {

private ApplicationContextRunner contextRunner = new ApplicationContextRunner()
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class));

@Test
Expand Down Expand Up @@ -230,6 +238,61 @@ void testRedisConfigurationWithClusterAndPassword() {
);
}

@Test
void testRedisConfigurationCreateClientOptionsByDefault() {
this.contextRunner.run(assertClientOptions(ClientOptions.class, (options) -> {
assertThat(options.getTimeoutOptions().isApplyConnectionTimeout()).isTrue();
assertThat(options.getTimeoutOptions().isTimeoutCommands()).isTrue();
}));
}

@Test
void testRedisConfigurationWithClusterCreateClusterClientOptions() {
this.contextRunner.withPropertyValues("spring.redis.cluster.nodes=127.0.0.1:27379,127.0.0.1:27380")
.run(assertClientOptions(ClusterClientOptions.class, (options) -> {
assertThat(options.getTimeoutOptions().isApplyConnectionTimeout()).isTrue();
assertThat(options.getTimeoutOptions().isTimeoutCommands()).isTrue();
}));
}

@Test
void testRedisConfigurationWithClusterRefreshPeriod() {
this.contextRunner
.withPropertyValues("spring.redis.cluster.nodes=127.0.0.1:27379,127.0.0.1:27380",
"spring.redis.lettuce.cluster.refresh.period=30s")
.run(assertClientOptions(ClusterClientOptions.class,
(options) -> assertThat(options.getTopologyRefreshOptions().getRefreshPeriod())
.hasSeconds(30)));
}

@Test
void testRedisConfigurationWithClusterAdaptiveRefresh() {
this.contextRunner
.withPropertyValues("spring.redis.cluster.nodes=127.0.0.1:27379,127.0.0.1:27380",
"spring.redis.lettuce.cluster.refresh.adaptive=true")
.run(assertClientOptions(ClusterClientOptions.class,
(options) -> assertThat(options.getTopologyRefreshOptions().getAdaptiveRefreshTriggers())
.isEqualTo(EnumSet.allOf(RefreshTrigger.class))));
}

@Test
void testRedisConfigurationWithClusterRefreshPeriodHasNoEffectWithNonClusteredConfiguration() {
this.contextRunner.withPropertyValues("spring.redis.cluster.refresh.period=30s").run(assertClientOptions(
ClientOptions.class, (options) -> assertThat(options.getClass()).isEqualTo(ClientOptions.class)));
}

private <T extends ClientOptions> ContextConsumer<AssertableApplicationContext> assertClientOptions(
Class<T> expectedType, Consumer<T> options) {
return (context) -> {
LettuceClientConfiguration clientConfiguration = context.getBean(LettuceConnectionFactory.class)
.getClientConfiguration();
assertThat(clientConfiguration.getClientOptions()).isPresent();
ClientOptions clientOptions = clientConfiguration.getClientOptions().get();
assertThat(clientOptions.getClass()).isEqualTo(expectedType);
options.accept(expectedType.cast(clientOptions));
};
}

private LettucePoolingClientConfiguration getPoolingClientConfiguration(LettuceConnectionFactory factory) {
return (LettucePoolingClientConfiguration) ReflectionTestUtils.getField(factory, "clientConfiguration");
}
Expand Down

0 comments on commit dfac3a2

Please sign in to comment.