From dfac3a282b98bd480c5acf778dbfbce994051dad Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 31 Mar 2020 18:08:28 +0200 Subject: [PATCH] Add configuration to enable Redis Cluster topology refresh This commit adds two options to enable a refresh of the cluster topology using Lettuce. Closes gh-15630 --- .../redis/LettuceConnectionConfiguration.java | 25 ++++++- .../data/redis/RedisProperties.java | 49 +++++++++++++- .../redis/RedisAutoConfigurationTests.java | 67 ++++++++++++++++++- 3 files changed, 137 insertions(+), 4 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java index d1c7a8bd4e7..6cc58a468da 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java @@ -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. @@ -18,7 +18,12 @@ package org.springframework.boot.autoconfigure.data.redis; 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; @@ -26,6 +31,7 @@ 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; @@ -88,6 +94,7 @@ class LettuceConnectionConfiguration extends RedisConnectionConfiguration { 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(); @@ -120,6 +127,22 @@ class LettuceConnectionConfiguration extends RedisConnectionConfiguration { 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()) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisProperties.java index b8e6cd54185..9786af22884 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisProperties.java @@ -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. @@ -354,6 +354,8 @@ public class RedisProperties { */ private Pool pool; + private final Cluster cluster = new Cluster(); + public Duration getShutdownTimeout() { return this.shutdownTimeout; } @@ -370,6 +372,51 @@ public class RedisProperties { 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; + } + + } + + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java index 68c4ffb0822..db3526620fb 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java @@ -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. @@ -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; @@ -54,7 +62,7 @@ import static org.assertj.core.api.Assertions.assertThat; */ class RedisAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class)); @Test @@ -230,6 +238,61 @@ class RedisAutoConfigurationTests { ); } + @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 ContextConsumer assertClientOptions( + Class expectedType, Consumer 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"); }