From f240befa734de9471745a20c466f7623ef4e67f1 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 13 Mar 2024 14:22:28 +0100 Subject: [PATCH 1/2] Prepare issue branch. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 507b76dd15..219f179eeb 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-redis - 3.3.0-SNAPSHOT + 3.3.0-GH-2866-SNAPSHOT Spring Data Redis Spring Data module for Redis From ad99d24a29e3319a336086c01e1ed0b76cb6063c Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 13 Mar 2024 15:05:14 +0100 Subject: [PATCH 2/2] 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 --- .../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 f9c8215d9d..4e83c271cf 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 @@ * 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 RedisClusterConfiguration getClusterConfiguration() { 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 void afterPropertiesSet() { this.clientConfig = createClientConfig(getDatabase(), getRedisUsername(), getRedisPassword()); - if (isAutoStartup()) { + if (isEarlyStartup()) { start(); } } @@ -724,21 +790,6 @@ public void stop() { } } - @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 2b6ac76503..c406faaa1e 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 @@ * 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 void setShareNativeConnection(boolean shareNativeConnection) { /** * 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 RedisClusterConfiguration getClusterConfiguration() { 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 void stop() { 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 boolean isRunning() { @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 950a09aacc..f6e3f3f9e4 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 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 @@ void afterPropertiesTriggersConnectionInitialization() { 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 2c29c12e52..b4dd713fbe 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 @@ void createRedisConfigurationWithValidRedisUriString() { .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;