From 7a94beb4673079eaadbbc510768036064194acd6 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 13 Mar 2024 15:05:14 +0100 Subject: [PATCH] Allow RedisConnectionFactories to be initialized as part of the context lifecycle. Lettuce and Jedis connection factories now can be configured to initialize early during afterPropertiesSet or configured whether the component should be auto-started by the container. By default, connection factories auto-startup early. Closes #2866 Original Pull Request: #2868 --- .../jedis/JedisConnectionFactory.java | 91 ++++++++++++---- .../lettuce/LettuceConnectionFactory.java | 102 +++++++++++++----- .../JedisConnectionFactoryUnitTests.java | 16 +++ .../LettuceConnectionFactoryUnitTests.java | 16 +++ 4 files changed, 180 insertions(+), 45 deletions(-) diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java index f9c8215d9..4e83c271c 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java @@ -78,8 +78,9 @@ import org.springframework.util.ObjectUtils; * This connection factory implements {@link InitializingBean} and {@link SmartLifecycle} for flexible lifecycle * control. It must be {@link #afterPropertiesSet() initialized} and {@link #start() started} before you can obtain a * connection. {@link #afterPropertiesSet() Initialization} {@link SmartLifecycle#start() starts} this bean - * {@link #isAutoStartup() by default}. You can {@link SmartLifecycle#stop()} and {@link SmartLifecycle#start() restart} - * this connection factory if needed. + * {@link #isEarlyStartup() early} by default. You can {@link SmartLifecycle#stop()} and {@link SmartLifecycle#start() + * restart} this connection factory if needed. Disabling {@link #isEarlyStartup() early startup} leaves lifecycle + * management to the container refresh if {@link #isAutoStartup() auto-startup} is enabled. *

* Note that {@link JedisConnection} and its {@link JedisClusterConnection clustered variant} are not Thread-safe and * instances should not be shared across threads. Refer to the @@ -103,9 +104,10 @@ public class JedisConnectionFactory private static final ExceptionTranslationStrategy EXCEPTION_TRANSLATION = new PassThroughExceptionTranslationStrategy( JedisExceptionConverter.INSTANCE); - private boolean convertPipelineAndTxResults = true; - private int phase = 0; // in between min and max values + private boolean autoStartup = true; + private boolean earlyStartup = true; + private boolean convertPipelineAndTxResults = true; private final AtomicReference state = new AtomicReference<>(State.CREATED); @@ -571,6 +573,70 @@ public class JedisConnectionFactory return RedisConfiguration.isClusterConfiguration(configuration) ? (RedisClusterConfiguration) configuration : null; } + @Override + public int getPhase() { + return this.phase; + } + + /** + * Specify the lifecycle phase for pausing and resuming this executor. The default is {@code 0}. + * + * @since 3.2 + * @see SmartLifecycle#getPhase() + */ + public void setPhase(int phase) { + this.phase = phase; + } + + /** + * @since 3.3 + */ + @Override + public boolean isAutoStartup() { + return this.autoStartup; + } + + /** + * Configure if this Lifecycle connection factory should get started automatically by the container at the time that + * the containing ApplicationContext gets refreshed. + *

+ * This connection factory defaults to early auto-startup during {@link #afterPropertiesSet()} and can potentially + * create Redis connections early on in the lifecycle. See {@link #setEarlyStartup(boolean)} for delaying connection + * creation to the ApplicationContext refresh if auto-startup is enabled. + * + * @param autoStartup {@literal true} to automatically {@link #start()} the connection factory; {@literal false} + * otherwise. + * @since 3.3 + * @see #setEarlyStartup(boolean) + * @see #start() + */ + public void setAutoStartup(boolean autoStartup) { + this.autoStartup = autoStartup; + } + + /** + * @return whether to {@link #start()} the component during {@link #afterPropertiesSet()}. + * @since 3.3 + */ + public boolean isEarlyStartup() { + return this.earlyStartup; + } + + /** + * Configure if this InitializingBean's component Lifecycle should get started early by {@link #afterPropertiesSet()} + * at the time that the bean is initialized. The component defaults to auto-startup. + *

+ * This method is related to {@link #setAutoStartup(boolean) auto-startup} and can be used to delay Redis client + * startup until the ApplicationContext refresh. Disabling early startup does not disable auto-startup. + * + * @param earlyStartup {@literal true} to early {@link #start()} the component; {@literal false} otherwise. + * @since 3.3 + * @see #setAutoStartup(boolean) + */ + public void setEarlyStartup(boolean earlyStartup) { + this.earlyStartup = earlyStartup; + } + /** * Specifies if pipelined results should be converted to the expected data type. If {@code false}, results of * {@link JedisConnection#closePipeline()} and {@link JedisConnection#exec()} will be of the type returned by the @@ -616,7 +682,7 @@ public class JedisConnectionFactory this.clientConfig = createClientConfig(getDatabase(), getRedisUsername(), getRedisPassword()); - if (isAutoStartup()) { + if (isEarlyStartup()) { start(); } } @@ -724,21 +790,6 @@ public class JedisConnectionFactory } } - @Override - public int getPhase() { - return this.phase; - } - - /** - * Specify the lifecycle phase for pausing and resuming this executor. The default is {@code 0}. - * - * @since 3.2 - * @see SmartLifecycle#getPhase() - */ - public void setPhase(int phase) { - this.phase = phase; - } - @Override public boolean isRunning() { return State.STARTED.equals(this.state.get()); diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java index 2b6ac7650..c406faaa1 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java @@ -100,8 +100,9 @@ import org.springframework.util.StringUtils; * This connection factory implements {@link InitializingBean} and {@link SmartLifecycle} for flexible lifecycle * control. It must be {@link #afterPropertiesSet() initialized} and {@link #start() started} before you can obtain a * connection. {@link #afterPropertiesSet() Initialization} {@link SmartLifecycle#start() starts} this bean - * {@link #isAutoStartup() by default}. You can {@link SmartLifecycle#stop()} and {@link SmartLifecycle#start() restart} - * this connection factory if needed. + * {@link #isEarlyStartup() early} by default. You can {@link SmartLifecycle#stop()} and {@link SmartLifecycle#start() + * restart} this connection factory if needed. Disabling {@link #isEarlyStartup() early startup} leaves lifecycle + * management to the container refresh if {@link #isAutoStartup() auto-startup} is enabled. * * @author Costin Leau * @author Jennifer Hickey @@ -121,13 +122,14 @@ public class LettuceConnectionFactory implements RedisConnectionFactory, Reactiv private static final ExceptionTranslationStrategy EXCEPTION_TRANSLATION = new PassThroughExceptionTranslationStrategy( LettuceExceptionConverter.INSTANCE); + private int phase = 0; // in between min and max values + private boolean autoStartup = true; + private boolean earlyStartup = true; private boolean convertPipelineAndTxResults = true; private boolean eagerInitialization = false; + private boolean shareNativeConnection = true; private boolean validateConnection = false; - - private int phase = 0; // in between min and max values - private @Nullable AbstractRedisClient client; private final AtomicReference state = new AtomicReference<>(State.CREATED); @@ -556,12 +558,13 @@ public class LettuceConnectionFactory implements RedisConnectionFactory, Reactiv /** * Indicates {@link #setShareNativeConnection(boolean) shared connections} should be eagerly initialized. Eager - * initialization requires a running Redis instance during application startup to allow early validation of connection - * factory configuration. Eager initialization also prevents blocking connect while using reactive API and is - * recommended for reactive API usage. + * initialization requires a running Redis instance during {@link #start() startup} to allow early validation of + * connection factory configuration. Eager initialization also prevents blocking connect while using reactive API and + * is recommended for reactive API usage. * - * @return {@link true} if the shared connection is initialized upon {@link #afterPropertiesSet()}. + * @return {@link true} if the shared connection is initialized upon {@link #start()}. * @since 2.2 + * @see #start() */ public boolean getEagerInitialization() { return this.eagerInitialization; @@ -795,6 +798,70 @@ public class LettuceConnectionFactory implements RedisConnectionFactory, Reactiv return isClusterAware() ? (RedisClusterConfiguration) this.configuration : null; } + @Override + public int getPhase() { + return this.phase; + } + + /** + * Specify the lifecycle phase for pausing and resuming this executor. The default is {@code 0}. + * + * @since 3.2 + * @see SmartLifecycle#getPhase() + */ + public void setPhase(int phase) { + this.phase = phase; + } + + /** + * @since 3.3 + */ + @Override + public boolean isAutoStartup() { + return this.autoStartup; + } + + /** + * Configure if this Lifecycle connection factory should get started automatically by the container at the time that + * the containing ApplicationContext gets refreshed. + *

+ * This connection factory defaults to early auto-startup during {@link #afterPropertiesSet()} and can potentially + * create Redis connections early on in the lifecycle. See {@link #setEarlyStartup(boolean)} for delaying connection + * creation to the ApplicationContext refresh if auto-startup is enabled. + * + * @param autoStartup {@literal true} to automatically {@link #start()} the connection factory; {@literal false} + * otherwise. + * @since 3.3 + * @see #setEarlyStartup(boolean) + * @see #start() + */ + public void setAutoStartup(boolean autoStartup) { + this.autoStartup = autoStartup; + } + + /** + * @return whether to {@link #start()} the component during {@link #afterPropertiesSet()}. + * @since 3.3 + */ + public boolean isEarlyStartup() { + return this.earlyStartup; + } + + /** + * Configure if this InitializingBean's component Lifecycle should get started early by {@link #afterPropertiesSet()} + * at the time that the bean is initialized. The component defaults to auto-startup. + *

+ * This method is related to {@link #setAutoStartup(boolean) auto-startup} and can be used to delay Redis client + * startup until the ApplicationContext refresh. Disabling early startup does not disable auto-startup. + * + * @param earlyStartup {@literal true} to early {@link #start()} the component; {@literal false} otherwise. + * @since 3.3 + * @see #setAutoStartup(boolean) + */ + public void setEarlyStartup(boolean earlyStartup) { + this.earlyStartup = earlyStartup; + } + /** * Specifies if pipelined results should be converted to the expected data type. If {@code false}, results of * {@link LettuceConnection#closePipeline()} and {LettuceConnection#exec()} will be of the type returned by the @@ -924,21 +991,6 @@ public class LettuceConnectionFactory implements RedisConnectionFactory, Reactiv state.set(State.STOPPED); } - @Override - public int getPhase() { - return this.phase; - } - - /** - * Specify the lifecycle phase for pausing and resuming this executor. The default is {@code 0}. - * - * @since 3.2 - * @see SmartLifecycle#getPhase() - */ - public void setPhase(int phase) { - this.phase = phase; - } - @Override public boolean isRunning() { return State.STARTED.equals(this.state.get()); @@ -947,7 +999,7 @@ public class LettuceConnectionFactory implements RedisConnectionFactory, Reactiv @Override public void afterPropertiesSet() { - if (isAutoStartup()) { + if (isEarlyStartup()) { start(); } } diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryUnitTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryUnitTests.java index 950a09aac..f6e3f3f9e 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryUnitTests.java @@ -35,6 +35,7 @@ import javax.net.ssl.SSLSocketFactory; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.junit.jupiter.api.Test; + import org.springframework.data.redis.connection.RedisClusterConfiguration; import org.springframework.data.redis.connection.RedisPassword; import org.springframework.data.redis.connection.RedisSentinelConfiguration; @@ -337,6 +338,21 @@ class JedisConnectionFactoryUnitTests { assertThat(connectionFactory.isRunning()).isTrue(); } + @Test // GH-2866 + void earlyStartupDoesNotStartConnectionFactory() { + + JedisConnectionFactory connectionFactory = new JedisConnectionFactory(new JedisPoolConfig()); + + connectionFactory.setEarlyStartup(false); + connectionFactory.afterPropertiesSet(); + + assertThat(connectionFactory.isEarlyStartup()).isFalse(); + assertThat(connectionFactory.isAutoStartup()).isTrue(); + assertThat(connectionFactory.isRunning()).isFalse(); + + assertThat(ReflectionTestUtils.getField(connectionFactory, "pool")).isNull(); + } + private JedisConnectionFactory initSpyedConnectionFactory(RedisSentinelConfiguration sentinelConfiguration, @Nullable JedisPoolConfig poolConfig) { diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryUnitTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryUnitTests.java index 2c29c12e5..b4dd713fb 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryUnitTests.java @@ -1260,6 +1260,22 @@ class LettuceConnectionFactoryUnitTests { .extracting(RedisStandaloneConfiguration::getPort).isEqualTo(6789); } + @Test // GH-2866 + void earlyStartupDoesNotStartConnectionFactory() { + + LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(new RedisStandaloneConfiguration(), + LettuceTestClientConfiguration.defaultConfiguration()); + connectionFactory.setEarlyStartup(false); + connectionFactory.afterPropertiesSet(); + + assertThat(connectionFactory.isEarlyStartup()).isFalse(); + assertThat(connectionFactory.isAutoStartup()).isTrue(); + assertThat(connectionFactory.isRunning()).isFalse(); + + AbstractRedisClient client = (AbstractRedisClient) getField(connectionFactory, "client"); + assertThat(client).isNull(); + } + static class CustomRedisConfiguration implements RedisConfiguration, WithHostAndPort { private String hostName;