diff --git a/README.adoc b/README.adoc index 0979a3fb7..bcb2c0ea2 100644 --- a/README.adoc +++ b/README.adoc @@ -9,7 +9,7 @@ This modules provides integration with the https://redis.io/[Redis] store. == Features -* Connection package as low-level abstraction across multiple Redis drivers (https://github.com/lettuce-io/lettuce-core[Lettuce] and https://github.com/xetorthio/jedis[Jedis]). +* Connection package as low-level abstraction across multiple Redis drivers (https://github.com/lettuce-io/lettuce-core[Lettuce] and https://github.com/redis/jedis[Jedis]). * Exception translation to Spring’s portable Data Access exception hierarchy for Redis driver exceptions * https://docs.spring.io/spring-data/data-redis/docs/current/reference/html/#redis:template[`RedisTemplate`] that provides a high level abstraction for performing various Redis operations, exception translation and serialization support. * Pubsub support (such as a MessageListenerContainer for message-driven POJOs). diff --git a/src/main/asciidoc/reference/redis-repositories.adoc b/src/main/asciidoc/reference/redis-repositories.adoc index c29b7e3e9..b4097dfa8 100644 --- a/src/main/asciidoc/reference/redis-repositories.adoc +++ b/src/main/asciidoc/reference/redis-repositories.adoc @@ -845,6 +845,7 @@ class RedisOperationsProducer { LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(new RedisStandaloneConfiguration()); connectionFactory.afterPropertiesSet(); + connectionFactory.start(); return connectionFactory; } diff --git a/src/main/java/org/springframework/data/redis/connection/RedisConnectionFactory.java b/src/main/java/org/springframework/data/redis/connection/RedisConnectionFactory.java index 5a14fd8e6..1251399e0 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisConnectionFactory.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisConnectionFactory.java @@ -30,8 +30,8 @@ public interface RedisConnectionFactory extends PersistenceExceptionTranslator { * Returns a suitable {@link RedisConnection connection} for interacting with Redis. * * @return {@link RedisConnection connection} for interacting with Redis. - * @throws IllegalStateException if the connection factory requires initialization and the factory has not yet - * been initialized. + * @throws IllegalStateException if the connection factory requires initialization and the factory has not yet been + * initialized. */ RedisConnection getConnection(); @@ -39,8 +39,8 @@ public interface RedisConnectionFactory extends PersistenceExceptionTranslator { * Returns a suitable {@link RedisClusterConnection connection} for interacting with Redis Cluster. * * @return a {@link RedisClusterConnection connection} for interacting with Redis Cluster. - * @throws IllegalStateException if the connection factory requires initialization and the factory has not yet - * been initialized. + * @throws IllegalStateException if the connection factory requires initialization and the factory has not yet been + * initialized. * @since 1.7 */ RedisClusterConnection getClusterConnection(); @@ -48,12 +48,12 @@ public interface RedisConnectionFactory extends PersistenceExceptionTranslator { /** * Specifies if pipelined results should be converted to the expected data type. *

- * If {@literal false}, results of {@link RedisConnection#closePipeline()} and {@link RedisConnection#exec()} - * will be of the type returned by the underlying driver. This method is mostly for backwards compatibility - * with {@literal 1.0}. It is generally always a good idea to allow results to be converted and deserialized. - * In fact, this is now the default behavior. + * If {@literal false}, results of {@link RedisConnection#closePipeline()} and {@link RedisConnection#exec()} will be + * of the type returned by the underlying driver. This method is mostly for backwards compatibility with + * {@literal 1.0}. It is generally always a good idea to allow results to be converted and deserialized. In fact, this + * is now the default behavior. * - * @return a boolen indicating whether to convert pipeline and transaction results. + * @return {@code true} to convert pipeline and transaction results; {@code false} otherwise. */ boolean getConvertPipelineAndTxResults(); @@ -61,8 +61,8 @@ public interface RedisConnectionFactory extends PersistenceExceptionTranslator { * Returns a suitable {@link RedisSentinelConnection connection} for interacting with Redis Sentinel. * * @return a {@link RedisSentinelConnection connection} for interacting with Redis Sentinel. - * @throws IllegalStateException if the connection factory requires initialization and the factory has not yet - * been initialized. + * @throws IllegalStateException if the connection factory requires initialization and the factory has not yet been + * initialized. * @since 1.4 */ RedisSentinelConnection getSentinelConnection(); 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 51741a008..e0b96cb41 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 @@ -64,7 +64,7 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; /** - * Connection factory creating Jedis based connections. + * Connection factory creating Jedis based connections. *

* {@link JedisConnectionFactory} should be configured using an environmental configuration and the * {@link JedisClientConfiguration client configuration}. Jedis supports the following environmental configurations: @@ -74,8 +74,9 @@ import org.springframework.util.ObjectUtils; *

  • {@link RedisClusterConfiguration}
  • * *

    - * This connection factory must be {@link #afterPropertiesSet() initialized} prior to {@link #getConnection obtaining - * connections}. + * This connection factory must be {@link #afterPropertiesSet() initialized} and {@link SmartLifecycle#start() started} + * prior to {@link #getConnection obtaining connections}. You can {@link SmartLifecycle#stop()} and + * {@link SmartLifecycle#start() restart} this connection factory if needed. * * @author Costin Leau * @author Thomas Darimont @@ -86,30 +87,36 @@ import org.springframework.util.ObjectUtils; * @see JedisClientConfiguration * @see Jedis */ -public class JedisConnectionFactory implements RedisConnectionFactory, InitializingBean, DisposableBean, SmartLifecycle { +public class JedisConnectionFactory + implements RedisConnectionFactory, InitializingBean, DisposableBean, SmartLifecycle { private final static Log log = LogFactory.getLog(JedisConnectionFactory.class); private static final ExceptionTranslationStrategy EXCEPTION_TRANSLATION = new PassThroughExceptionTranslationStrategy( JedisExceptionConverter.INSTANCE); private final JedisClientConfiguration clientConfiguration; - private JedisClientConfig clientConfig = DefaultJedisClientConfig.builder().build(); - private @Nullable Pool pool; + private boolean convertPipelineAndTxResults = true; private RedisStandaloneConfiguration standaloneConfig = new RedisStandaloneConfiguration("localhost", Protocol.DEFAULT_PORT); private @Nullable RedisConfiguration configuration; - private @Nullable JedisCluster cluster; - private @Nullable ClusterTopologyProvider topologyProvider; - private @Nullable ClusterCommandExecutor clusterCommandExecutor; - + /** + * Lifecycle state of this factory. + */ enum State { CREATED, STARTING, STARTED, STOPPING, STOPPED, DESTROYED; } - private AtomicReference state = new AtomicReference<>(State.CREATED); + private final AtomicReference state = new AtomicReference<>(State.CREATED); + + private JedisClientConfig clientConfig = DefaultJedisClientConfig.builder().build(); + + private @Nullable Pool pool; + private @Nullable JedisCluster cluster; + private @Nullable ClusterTopologyProvider topologyProvider; + private @Nullable ClusterCommandExecutor clusterCommandExecutor; /** * Constructs a new {@link JedisConnectionFactory} instance with default settings (default connection pooling). @@ -253,115 +260,6 @@ public class JedisConnectionFactory implements RedisConnectionFactory, Initializ this.configuration = clusterConfig; } - /** - * Returns a Jedis instance to be used as a Redis connection. The instance can be newly created or retrieved from a - * pool. - * - * @return Jedis instance ready for wrapping into a {@link RedisConnection}. - */ - protected Jedis fetchJedisConnector() { - try { - - if (getUsePool() && pool != null) { - return pool.getResource(); - } - - Jedis jedis = createJedis(); - // force initialization (see Jedis issue #82) - jedis.connect(); - - return jedis; - } catch (Exception ex) { - throw new RedisConnectionFailureException("Cannot get Jedis connection", ex); - } - } - - private Jedis createJedis() { - return new Jedis(new HostAndPort(getHostName(), getPort()), this.clientConfig); - } - - /** - * Post process a newly retrieved connection. Useful for decorating or executing initialization commands on a new - * connection. This implementation simply returns the connection. - * - * @param connection the jedis connection. - * @return processed connection - */ - protected JedisConnection postProcessConnection(JedisConnection connection) { - return connection; - } - - @Override - public void start() { - - State current = state.getAndUpdate(state -> { - if (State.CREATED.equals(state) || State.STOPPED.equals(state)) { - return State.STARTING; - } - return state; - }); - - if (State.CREATED.equals(current) || State.STOPPED.equals(current)) { - - if (getUsePool() && !isRedisClusterAware()) { - this.pool = createPool(); - } - - if (isRedisClusterAware()) { - - this.cluster = createCluster(); - this.topologyProvider = createTopologyProvider(this.cluster); - this.clusterCommandExecutor = new ClusterCommandExecutor(this.topologyProvider, - new JedisClusterConnection.JedisClusterNodeResourceProvider(this.cluster, this.topologyProvider), - EXCEPTION_TRANSLATION); - } - - state.set(State.STARTED); - } - } - - @Override - public void stop() { - - if (state.compareAndSet(State.STARTED, State.STOPPING)) { - if (getUsePool() && !isRedisClusterAware()) { - if (pool != null) { - try { - this.pool.close(); - } catch (Exception ex) { - log.warn("Cannot properly close Jedis pool", ex); - } - this.pool = null; - } - } - - if(this.clusterCommandExecutor != null) { - try { - this.clusterCommandExecutor.destroy(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - if (this.cluster != null) { - - this.topologyProvider = null; - - try { - cluster.close(); - } catch (Exception ex) { - log.warn("Cannot properly close Jedis cluster", ex); - } - } - state.set(State.STOPPED); - } - } - - @Override - public boolean isRunning() { - return State.STARTED.equals(state.get()); - } - @Override public void afterPropertiesSet() { clientConfig = createClientConfig(getDatabase(), getRedisUsername(), getRedisPassword()); @@ -399,6 +297,76 @@ public class JedisConnectionFactory implements RedisConnectionFactory, Initializ return builder.build(); } + @Override + public void start() { + + State current = state + .getAndUpdate(state -> State.CREATED.equals(state) || State.STOPPED.equals(state) ? State.STARTING : state); + + if (State.CREATED.equals(current) || State.STOPPED.equals(current)) { + + if (getUsePool() && !isRedisClusterAware()) { + this.pool = createPool(); + } + + if (isRedisClusterAware()) { + + this.cluster = createCluster(); + this.topologyProvider = createTopologyProvider(this.cluster); + this.clusterCommandExecutor = new ClusterCommandExecutor(this.topologyProvider, + new JedisClusterConnection.JedisClusterNodeResourceProvider(this.cluster, this.topologyProvider), + EXCEPTION_TRANSLATION); + } + + state.set(State.STARTED); + } + } + + @Override + public void stop() { + + if (state.compareAndSet(State.STARTED, State.STOPPING)) { + + if (getUsePool() && !isRedisClusterAware()) { + if (pool != null) { + try { + this.pool.close(); + this.pool = null; + } catch (Exception ex) { + log.warn("Cannot properly close Jedis pool", ex); + } + } + } + + if (this.clusterCommandExecutor != null) { + try { + this.clusterCommandExecutor.destroy(); + this.clusterCommandExecutor = null; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + if (this.cluster != null) { + + this.topologyProvider = null; + + try { + this.cluster.close(); + this.cluster = null; + } catch (Exception ex) { + log.warn("Cannot properly close Jedis cluster", ex); + } + } + state.set(State.STOPPED); + } + } + + @Override + public boolean isRunning() { + return State.STARTED.equals(state.get()); + } + private Pool createPool() { if (isRedisSentinelAware()) { @@ -473,12 +441,14 @@ public class JedisConnectionFactory implements RedisConnectionFactory, Initializ return new JedisCluster(hostAndPort, this.clientConfig, redirects, poolConfig); } + @Override public void destroy() { stop(); state.set(State.DESTROYED); } + @Override public RedisConnection getConnection() { assertInitialized(); @@ -498,9 +468,48 @@ public class JedisConnectionFactory implements RedisConnectionFactory, Initializ JedisConnection connection = (getUsePool() ? new JedisConnection(jedis, pool, this.clientConfig, sentinelConfig) : new JedisConnection(jedis, null, this.clientConfig, sentinelConfig)); connection.setConvertPipelineAndTxResults(convertPipelineAndTxResults); + return postProcessConnection(connection); } + /** + * Returns a Jedis instance to be used as a Redis connection. The instance can be newly created or retrieved from a + * pool. + * + * @return Jedis instance ready for wrapping into a {@link RedisConnection}. + */ + protected Jedis fetchJedisConnector() { + try { + + if (getUsePool() && pool != null) { + return pool.getResource(); + } + + Jedis jedis = createJedis(); + // force initialization (see Jedis issue #82) + jedis.connect(); + + return jedis; + } catch (Exception ex) { + throw new RedisConnectionFailureException("Cannot get Jedis connection", ex); + } + } + + private Jedis createJedis() { + return new Jedis(new HostAndPort(getHostName(), getPort()), this.clientConfig); + } + + /** + * Post process a newly retrieved connection. Useful for decorating or executing initialization commands on a new + * connection. This implementation simply returns the connection. + * + * @param connection the jedis connection. + * @return processed connection + */ + protected JedisConnection postProcessConnection(JedisConnection connection) { + return connection; + } + @Override public RedisClusterConnection getClusterConnection() { @@ -509,9 +518,24 @@ public class JedisConnectionFactory implements RedisConnectionFactory, Initializ if (!isRedisClusterAware()) { throw new InvalidDataAccessApiUsageException("Cluster is not configured"); } - return new JedisClusterConnection(this.cluster, this.clusterCommandExecutor, this.topologyProvider); + + return postProcessConnection( + new JedisClusterConnection(this.cluster, this.clusterCommandExecutor, this.topologyProvider)); } + /** + * Post process a newly retrieved connection. Useful for decorating or executing initialization commands on a new + * connection. This implementation simply returns the connection. + * + * @param connection the jedis connection. + * @return processed connection. + * @since 3.2 + */ + protected JedisClusterConnection postProcessConnection(JedisClusterConnection connection) { + return connection; + } + + @Override public DataAccessException translateExceptionIfPossible(RuntimeException ex) { return EXCEPTION_TRANSLATION.translate(ex); } @@ -790,22 +814,24 @@ public class JedisConnectionFactory implements RedisConnectionFactory, Initializ } /** - * Specifies if pipelined results should be converted to the expected data type. If false, results of + * 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 * Jedis driver. * - * @return Whether or not to convert pipeline and tx results. + * @return {@code true} to convert pipeline and transaction results; {@code false} otherwise. */ + @Override public boolean getConvertPipelineAndTxResults() { return convertPipelineAndTxResults; } /** - * Specifies if pipelined results should be converted to the expected data type. If false, results of + * 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 * Jedis driver. * - * @param convertPipelineAndTxResults Whether or not to convert pipeline and tx results. + * @param convertPipelineAndTxResults {@code true} to convert pipeline and transaction results; {@code false} + * otherwise. */ public void setConvertPipelineAndTxResults(boolean convertPipelineAndTxResults) { this.convertPipelineAndTxResults = convertPipelineAndTxResults; @@ -910,7 +936,8 @@ public class JedisConnectionFactory implements RedisConnectionFactory, Initializ } switch (current) { - case CREATED, STOPPED -> throw new IllegalStateException(String.format("JedisConnectionFactory has been %s. Use start() to initialize it", current)); + case CREATED, STOPPED -> throw new IllegalStateException( + String.format("JedisConnectionFactory has been %s. Use start() to initialize it", current)); case DESTROYED -> throw new IllegalStateException( "JedisConnectionFactory was destroyed and cannot be used anymore"); default -> throw new IllegalStateException(String.format("JedisConnectionFactory is %s", current)); diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/package-info.java b/src/main/java/org/springframework/data/redis/connection/jedis/package-info.java index e3a02a282..c32ac1cab 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/package-info.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/package-info.java @@ -1,5 +1,5 @@ /** - * Connection package for Jedis library. + * Connection package for Jedis library. */ @org.springframework.lang.NonNullApi @org.springframework.lang.NonNullFields 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 2a77891a2..4ffaeea41 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 @@ -15,53 +15,7 @@ */ package org.springframework.data.redis.connection.lettuce; -import static org.springframework.data.redis.connection.lettuce.LettuceConnection.CODEC; -import static org.springframework.data.redis.connection.lettuce.LettuceConnection.PipeliningFlushPolicy; - -import java.nio.ByteBuffer; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; -import java.util.stream.Collectors; - -import org.springframework.beans.factory.DisposableBean; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.context.SmartLifecycle; -import org.springframework.dao.DataAccessException; -import org.springframework.dao.InvalidDataAccessApiUsageException; -import org.springframework.data.redis.ExceptionTranslationStrategy; -import org.springframework.data.redis.PassThroughExceptionTranslationStrategy; -import org.springframework.data.redis.RedisConnectionFailureException; -import org.springframework.data.redis.connection.ClusterCommandExecutor; -import org.springframework.data.redis.connection.ClusterTopologyProvider; -import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory; -import org.springframework.data.redis.connection.RedisClusterConfiguration; -import org.springframework.data.redis.connection.RedisClusterConnection; -import org.springframework.data.redis.connection.RedisConfiguration; -import org.springframework.data.redis.connection.RedisConfiguration.ClusterConfiguration; -import org.springframework.data.redis.connection.RedisConfiguration.DomainSocketConfiguration; -import org.springframework.data.redis.connection.RedisConfiguration.WithDatabaseIndex; -import org.springframework.data.redis.connection.RedisConfiguration.WithPassword; -import org.springframework.data.redis.connection.RedisConnection; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.connection.RedisPassword; -import org.springframework.data.redis.connection.RedisSentinelConfiguration; -import org.springframework.data.redis.connection.RedisSentinelConnection; -import org.springframework.data.redis.connection.RedisSocketConfiguration; -import org.springframework.data.redis.connection.RedisStandaloneConfiguration; -import org.springframework.data.redis.connection.RedisStaticMasterReplicaConfiguration; -import org.springframework.data.util.Optionals; -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; +import static org.springframework.data.redis.connection.lettuce.LettuceConnection.*; import io.lettuce.core.AbstractRedisClient; import io.lettuce.core.ClientOptions; @@ -78,8 +32,39 @@ import io.lettuce.core.cluster.api.StatefulRedisClusterConnection; import io.lettuce.core.codec.RedisCodec; import io.lettuce.core.resource.ClientResources; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.stream.Collectors; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.SmartLifecycle; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.redis.ExceptionTranslationStrategy; +import org.springframework.data.redis.PassThroughExceptionTranslationStrategy; +import org.springframework.data.redis.RedisConnectionFailureException; +import org.springframework.data.redis.connection.*; +import org.springframework.data.redis.connection.RedisConfiguration.ClusterConfiguration; +import org.springframework.data.redis.connection.RedisConfiguration.DomainSocketConfiguration; +import org.springframework.data.redis.connection.RedisConfiguration.WithDatabaseIndex; +import org.springframework.data.redis.connection.RedisConfiguration.WithPassword; +import org.springframework.data.util.Optionals; +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; /** * Connection factory creating Lettuce-based connections. @@ -104,8 +89,9 @@ import org.apache.commons.logging.LogFactory; *

  • {@link RedisClusterConfiguration}
  • * *

    - * This connection factory must be {@link #afterPropertiesSet() initialized} prior to {@link #getConnection obtaining - * connections}. + * This connection factory must be {@link #afterPropertiesSet() initialized} and {@link SmartLifecycle#start() started} + * prior to {@link #getConnection obtaining connections}. You can {@link SmartLifecycle#stop()} and + * {@link SmartLifecycle#start() restart} this connection factory if needed. * * @author Costin Leau * @author Jennifer Hickey @@ -125,32 +111,38 @@ public class LettuceConnectionFactory implements RedisConnectionFactory, Reactiv LettuceExceptionConverter.INSTANCE); private final Log log = LogFactory.getLog(getClass()); - private final LettuceClientConfiguration clientConfiguration; - private @Nullable AbstractRedisClient client; - private @Nullable LettuceConnectionProvider connectionProvider; - private @Nullable LettuceConnectionProvider reactiveConnectionProvider; - private boolean validateConnection = false; - private boolean shareNativeConnection = true; - private boolean eagerInitialization = false; - private @Nullable SharedConnection connection; - private @Nullable SharedConnection reactiveConnection; /** Synchronization monitor for the shared Connection */ private final Object connectionMonitor = new Object(); - private boolean convertPipelineAndTxResults = true; + + private final LettuceClientConfiguration clientConfiguration; private RedisStandaloneConfiguration standaloneConfig = new RedisStandaloneConfiguration("localhost", 6379); - private PipeliningFlushPolicy pipeliningFlushPolicy = PipeliningFlushPolicy.flushEachCommand(); private @Nullable RedisConfiguration configuration; - private @Nullable ClusterCommandExecutor clusterCommandExecutor; + private boolean validateConnection = false; + private boolean shareNativeConnection = true; + private boolean eagerInitialization = false; + private boolean convertPipelineAndTxResults = true; + private PipeliningFlushPolicy pipeliningFlushPolicy = PipeliningFlushPolicy.flushEachCommand(); + + /** + * Lifecycle state of this factory. + */ enum State { CREATED, STARTING, STARTED, STOPPING, STOPPED, DESTROYED; } - private AtomicReference state = new AtomicReference<>(State.CREATED); + private final AtomicReference state = new AtomicReference<>(State.CREATED); + + private @Nullable AbstractRedisClient client; + private @Nullable LettuceConnectionProvider connectionProvider; + private @Nullable LettuceConnectionProvider reactiveConnectionProvider; + private @Nullable SharedConnection connection; + private @Nullable SharedConnection reactiveConnection; + private @Nullable ClusterCommandExecutor clusterCommandExecutor; /** * Constructs a new {@link LettuceConnectionFactory} instance with default settings. @@ -341,12 +333,8 @@ public class LettuceConnectionFactory implements RedisConnectionFactory, Reactiv @Override public void start() { - State current = state.getAndUpdate(state -> { - if (State.CREATED.equals(state) || State.STOPPED.equals(state)) { - return State.STARTING; - } - return state; - }); + State current = state + .getAndUpdate(state -> State.CREATED.equals(state) || State.STOPPED.equals(state) ? State.STARTING : state); if (State.CREATED.equals(current) || State.STOPPED.equals(current)) { @@ -376,23 +364,33 @@ public class LettuceConnectionFactory implements RedisConnectionFactory, Reactiv public void stop() { if (state.compareAndSet(State.STARTED, State.STOPPING)) { - resetConnection(); - dispose(connectionProvider); - dispose(reactiveConnectionProvider); - try { - Duration quietPeriod = clientConfiguration.getShutdownQuietPeriod(); - Duration timeout = clientConfiguration.getShutdownTimeout(); - client.shutdown(quietPeriod.toMillis(), timeout.toMillis(), TimeUnit.MILLISECONDS); - state.set(State.STOPPED); - } catch (Exception e) { - if (log.isWarnEnabled()) { - log.warn((client != null ? ClassUtils.getShortName(client.getClass()) : "LettuceClient") - + " did not shut down gracefully.", e); + resetConnection(); + + dispose(connectionProvider); + connectionProvider = null; + + dispose(reactiveConnectionProvider); + reactiveConnectionProvider = null; + + if (client != null) { + + try { + Duration quietPeriod = clientConfiguration.getShutdownQuietPeriod(); + Duration timeout = clientConfiguration.getShutdownTimeout(); + + client.shutdown(quietPeriod.toMillis(), timeout.toMillis(), TimeUnit.MILLISECONDS); + client = null; + } catch (Exception e) { + + if (log.isWarnEnabled()) { + log.warn(ClassUtils.getShortName(client.getClass()) + " did not shut down gracefully.", e); + } } } - state.set(State.STOPPED); } + + state.set(State.STOPPED); } @Override @@ -421,7 +419,7 @@ public class LettuceConnectionFactory implements RedisConnectionFactory, Reactiv state.set(State.DESTROYED); } - private void dispose(LettuceConnectionProvider connectionProvider) { + private void dispose(@Nullable LettuceConnectionProvider connectionProvider) { if (connectionProvider instanceof DisposableBean) { try { @@ -435,9 +433,10 @@ public class LettuceConnectionFactory implements RedisConnectionFactory, Reactiv } } + @Override public RedisConnection getConnection() { - assertInitialized(); + assertStarted(); if (isClusterAware()) { return getClusterConnection(); @@ -452,7 +451,7 @@ public class LettuceConnectionFactory implements RedisConnectionFactory, Reactiv @Override public RedisClusterConnection getClusterConnection() { - assertInitialized(); + assertStarted(); if (!isClusterAware()) { throw new InvalidDataAccessApiUsageException("Cluster is not configured"); @@ -517,7 +516,7 @@ public class LettuceConnectionFactory implements RedisConnectionFactory, Reactiv @Override public LettuceReactiveRedisConnection getReactiveConnection() { - assertInitialized(); + assertStarted(); if (isClusterAware()) { return getReactiveClusterConnection(); @@ -531,7 +530,7 @@ public class LettuceConnectionFactory implements RedisConnectionFactory, Reactiv @Override public LettuceReactiveRedisClusterConnection getReactiveClusterConnection() { - assertInitialized(); + assertStarted(); if (!isClusterAware()) { throw new InvalidDataAccessApiUsageException("Cluster is not configured"); @@ -581,7 +580,7 @@ public class LettuceConnectionFactory implements RedisConnectionFactory, Reactiv */ public void validateConnection() { - assertInitialized(); + assertStarted(); getOrCreateSharedConnection().validateConnection(); getOrCreateSharedReactiveConnection().validateConnection(); @@ -611,6 +610,7 @@ public class LettuceConnectionFactory implements RedisConnectionFactory, Reactiv } } + @Override public DataAccessException translateExceptionIfPossible(RuntimeException ex) { return EXCEPTION_TRANSLATION.translate(ex); } @@ -888,7 +888,7 @@ public class LettuceConnectionFactory implements RedisConnectionFactory, Reactiv */ @Nullable public AbstractRedisClient getNativeClient() { - assertInitialized(); + assertStarted(); return this.client; } @@ -1042,22 +1042,24 @@ public class LettuceConnectionFactory implements RedisConnectionFactory, Reactiv } /** - * Specifies if pipelined results should be converted to the expected data type. If false, results of + * 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 * Lettuce driver. * - * @return Whether or not to convert pipeline and tx results. + * @return {@code true} to convert pipeline and transaction results; {@code false} otherwise. */ + @Override public boolean getConvertPipelineAndTxResults() { return convertPipelineAndTxResults; } /** - * Specifies if pipelined and transaction results should be converted to the expected data type. If false, results of - * {@link LettuceConnection#closePipeline()} and {LettuceConnection#exec()} will be of the type returned by the - * Lettuce driver. + * Specifies if pipelined and transaction 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 Lettuce driver. * - * @param convertPipelineAndTxResults Whether or not to convert pipeline and tx results. + * @param convertPipelineAndTxResults {@code true} to convert pipeline and transaction results; {@code false} + * otherwise. */ public void setConvertPipelineAndTxResults(boolean convertPipelineAndTxResults) { this.convertPipelineAndTxResults = convertPipelineAndTxResults; @@ -1156,8 +1158,8 @@ public class LettuceConnectionFactory implements RedisConnectionFactory, Reactiv protected LettuceConnectionProvider doCreateConnectionProvider(AbstractRedisClient client, RedisCodec codec) { return isStaticMasterReplicaAware() ? createStaticMasterReplicaConnectionProvider((RedisClient) client, codec) - : isClusterAware() ? createClusterConnectionProvider((RedisClusterClient) client, codec) - : createStandaloneConnectionProvider((RedisClient) client, codec); + : isClusterAware() ? createClusterConnectionProvider((RedisClusterClient) client, codec) + : createStandaloneConnectionProvider((RedisClient) client, codec); } @SuppressWarnings("all") @@ -1165,9 +1167,8 @@ public class LettuceConnectionFactory implements RedisConnectionFactory, Reactiv RedisCodec codec) { List nodes = ((RedisStaticMasterReplicaConfiguration) this.configuration).getNodes().stream() - .map(it -> createRedisURIAndApplySettings(it.getHostName(), it.getPort())) - .peek(it -> it.setDatabase(getDatabase())) - .collect(Collectors.toList()); + .map(it -> createRedisURIAndApplySettings(it.getHostName(), it.getPort())) + .peek(it -> it.setDatabase(getDatabase())).collect(Collectors.toList()); return new StaticMasterReplicaConnectionProvider(client, codec, nodes, getClientConfiguration().getReadFrom().orElse(null)); @@ -1184,15 +1185,13 @@ public class LettuceConnectionFactory implements RedisConnectionFactory, Reactiv protected AbstractRedisClient createClient() { return isStaticMasterReplicaAware() ? createStaticMasterReplicaClient() - : isRedisSentinelAware() ? createSentinelClient() - : isClusterAware() ? createClusterClient() - : createBasicClient(); + : isRedisSentinelAware() ? createSentinelClient() + : isClusterAware() ? createClusterClient() : createBasicClient(); } private RedisClient createStaticMasterReplicaClient() { - RedisClient redisClient = this.clientConfiguration.getClientResources() - .map(RedisClient::create) + RedisClient redisClient = this.clientConfiguration.getClientResources().map(RedisClient::create) .orElseGet(RedisClient::create); this.clientConfiguration.getClientOptions().ifPresent(redisClient::setOptions); @@ -1251,8 +1250,7 @@ public class LettuceConnectionFactory implements RedisConnectionFactory, Reactiv ClusterConfiguration configuration = (ClusterConfiguration) this.configuration; - configuration.getClusterNodes().stream() - .map(node -> createRedisURIAndApplySettings(node.getHost(), node.getPort())) + configuration.getClusterNodes().stream().map(node -> createRedisURIAndApplySettings(node.getHost(), node.getPort())) .forEach(initialUris::add); RedisClusterClient clusterClient = this.clientConfiguration.getClientResources() @@ -1268,12 +1266,9 @@ public class LettuceConnectionFactory implements RedisConnectionFactory, Reactiv Optional clientOptions = this.clientConfiguration.getClientOptions(); - ClusterClientOptions clusterClientOptions = clientOptions - .filter(ClusterClientOptions.class::isInstance) - .map(ClusterClientOptions.class::cast) - .orElseGet(() -> clientOptions - .map(it -> ClusterClientOptions.builder(it).build()) - .orElseGet(ClusterClientOptions::create)); + ClusterClientOptions clusterClientOptions = clientOptions.filter(ClusterClientOptions.class::isInstance) + .map(ClusterClientOptions.class::cast).orElseGet(() -> clientOptions + .map(it -> ClusterClientOptions.builder(it).build()).orElseGet(ClusterClientOptions::create)); if (configuration.getMaxRedirects() != null) { return clusterClientOptions.mutate().maxRedirects(configuration.getMaxRedirects()).build(); @@ -1290,15 +1285,14 @@ public class LettuceConnectionFactory implements RedisConnectionFactory, Reactiv : createRedisURIAndApplySettings(getHostName(), getPort()); RedisClient redisClient = this.clientConfiguration.getClientResources() - .map(clientResources -> RedisClient.create(clientResources, uri)) - .orElseGet(() -> RedisClient.create(uri)); + .map(clientResources -> RedisClient.create(clientResources, uri)).orElseGet(() -> RedisClient.create(uri)); this.clientConfiguration.getClientOptions().ifPresent(redisClient::setOptions); return redisClient; } - private void assertInitialized() { + private void assertStarted() { State current = state.get(); @@ -1307,7 +1301,8 @@ public class LettuceConnectionFactory implements RedisConnectionFactory, Reactiv } switch (current) { - case CREATED, STOPPED -> throw new IllegalStateException(String.format("LettuceConnectionFactory has been %s. Use start() to initialize it", current)); + case CREATED, STOPPED -> throw new IllegalStateException( + String.format("LettuceConnectionFactory has been %s. Use start() to initialize it", current)); case DESTROYED -> throw new IllegalStateException( "LettuceConnectionFactory was destroyed and cannot be used anymore"); default -> throw new IllegalStateException(String.format("LettuceConnectionFactory is %s", current)); @@ -1359,14 +1354,14 @@ public class LettuceConnectionFactory implements RedisConnectionFactory, Reactiv getRedisPassword().toOptional().ifPresent(builder::withPassword); } - clientConfiguration.getRedisCredentialsProviderFactory().ifPresent(factory -> - builder.withAuthentication(factory.createCredentialsProvider(this.configuration))); + clientConfiguration.getRedisCredentialsProviderFactory() + .ifPresent(factory -> builder.withAuthentication(factory.createCredentialsProvider(this.configuration))); } @Override public RedisSentinelConnection getSentinelConnection() { - assertInitialized(); + assertStarted(); return new LettuceSentinelConnection(connectionProvider); } diff --git a/src/test/java/org/springframework/data/redis/ConnectionFactoryTracker.java b/src/test/java/org/springframework/data/redis/ConnectionFactoryTracker.java index 489fab2c0..946097bd2 100644 --- a/src/test/java/org/springframework/data/redis/ConnectionFactoryTracker.java +++ b/src/test/java/org/springframework/data/redis/ConnectionFactoryTracker.java @@ -41,8 +41,8 @@ public abstract class ConnectionFactoryTracker { throw new UnsupportedOperationException("Cannot track managed resource"); } - if(factory instanceof SmartLifecycle smartLifecycle) { - if(!smartLifecycle.isRunning() && smartLifecycle.isAutoStartup()) { + if (factory instanceof SmartLifecycle smartLifecycle) { + if (!smartLifecycle.isRunning() && smartLifecycle.isAutoStartup()) { smartLifecycle.start(); } } diff --git a/src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java b/src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java index d531eef94..326fc78a3 100644 --- a/src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java +++ b/src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java @@ -15,10 +15,10 @@ */ package org.springframework.data.redis.cache; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.assertj.core.api.Assumptions.assumeThat; +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assumptions.*; + +import io.netty.util.concurrent.DefaultThreadFactory; import java.io.Serializable; import java.nio.charset.StandardCharsets; @@ -37,7 +37,6 @@ import java.util.function.Consumer; import java.util.stream.IntStream; import org.junit.jupiter.api.BeforeEach; - import org.springframework.cache.Cache.ValueWrapper; import org.springframework.cache.interceptor.SimpleKey; import org.springframework.cache.interceptor.SimpleKeyGenerator; @@ -51,8 +50,6 @@ import org.springframework.data.redis.test.extension.parametrized.MethodSource; import org.springframework.data.redis.test.extension.parametrized.ParameterizedRedisTest; import org.springframework.lang.Nullable; -import io.netty.util.concurrent.DefaultThreadFactory; - /** * Tests for {@link RedisCache} with {@link DefaultRedisCacheWriter} using different {@link RedisSerializer} and * {@link RedisConnectionFactory} pairs. @@ -443,7 +440,7 @@ public class RedisCacheTests { Object key = SimpleKeyGenerator .generateKey(Collections.singletonList(new InvalidKey(sample.getFirstname(), sample.getBirthdate()))); - assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> cache.put(key, sample)); + assertThatIllegalStateException().isThrownBy(() -> cache.put(key, sample)); } @ParameterizedRedisTest // GH-2079 diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryIntegrationTests.java index 61aabedfa..acbc683e1 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactoryIntegrationTests.java @@ -19,7 +19,6 @@ import static org.assertj.core.api.Assertions.*; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; - import org.springframework.data.redis.SettingsUtils; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; @@ -84,12 +83,14 @@ class JedisConnectionFactoryIntegrationTests { factory.stop(); assertThat(factory.isRunning()).isFalse(); - assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> factory.getConnection()); + assertThatIllegalStateException().isThrownBy(() -> factory.getConnection()); factory.start(); assertThat(factory.isRunning()).isTrue(); try (RedisConnection connection = factory.getConnection()) { assertThat(connection.ping()).isEqualTo("PONG"); } + + factory.destroy(); } } 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 1fdbe738a..f5cf14a83 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,7 +35,6 @@ 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; @@ -334,7 +333,7 @@ class JedisConnectionFactoryUnitTests { connectionFactory.afterPropertiesSet(); assertThat(connectionFactory.isRunning()).isFalse(); - assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> connectionFactory.getConnection()); + assertThatIllegalStateException().isThrownBy(() -> connectionFactory.getConnection()); } private JedisConnectionFactory initSpyedConnectionFactory(RedisSentinelConfiguration sentinelConfig, diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryTests.java index 3f5d34840..7583a58e8 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryTests.java @@ -575,10 +575,13 @@ class LettuceConnectionFactoryTests { assertThat(factory.isRunning()).isTrue(); factory.stop(); + assertThat(factory.isRunning()).isFalse(); - assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> factory.getConnection()); + assertThatIllegalStateException().isThrownBy(() -> factory.getConnection()); + factory.start(); assertThat(factory.isRunning()).isTrue(); + try (RedisConnection connection = factory.getConnection()) { assertThat(connection.ping()).isEqualTo("PONG"); } 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 209e7cd57..e2e1058aa 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 @@ -15,20 +15,27 @@ */ package org.springframework.data.redis.connection.lettuce; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.mockito.Mockito.withSettings; -import static org.springframework.data.redis.connection.ClusterTestVariables.CLUSTER_NODE_1; -import static org.springframework.data.redis.connection.RedisConfiguration.WithHostAndPort; -import static org.springframework.data.redis.test.extension.LettuceTestClientResources.getSharedClientResources; -import static org.springframework.test.util.ReflectionTestUtils.getField; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.springframework.data.redis.connection.ClusterTestVariables.*; +import static org.springframework.data.redis.connection.RedisConfiguration.*; +import static org.springframework.data.redis.test.extension.LettuceTestClientResources.*; +import static org.springframework.test.util.ReflectionTestUtils.*; + +import io.lettuce.core.AbstractRedisClient; +import io.lettuce.core.ClientOptions; +import io.lettuce.core.RedisClient; +import io.lettuce.core.RedisURI; +import io.lettuce.core.api.StatefulConnection; +import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.cluster.ClusterClientOptions; +import io.lettuce.core.cluster.RedisClusterClient; +import io.lettuce.core.cluster.api.StatefulRedisClusterConnection; +import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands; +import io.lettuce.core.codec.ByteArrayCodec; +import io.lettuce.core.codec.RedisCodec; +import io.lettuce.core.resource.ClientResources; +import reactor.test.StepVerifier; import java.time.Duration; import java.util.Collections; @@ -40,7 +47,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatchers; - import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.factory.DisposableBean; import org.springframework.data.redis.ConnectionFactoryTracker; @@ -58,22 +64,6 @@ import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.data.redis.test.extension.LettuceTestClientResources; import org.springframework.test.util.ReflectionTestUtils; -import io.lettuce.core.AbstractRedisClient; -import io.lettuce.core.ClientOptions; -import io.lettuce.core.RedisClient; -import io.lettuce.core.RedisURI; -import io.lettuce.core.api.StatefulConnection; -import io.lettuce.core.api.StatefulRedisConnection; -import io.lettuce.core.cluster.ClusterClientOptions; -import io.lettuce.core.cluster.RedisClusterClient; -import io.lettuce.core.cluster.api.StatefulRedisClusterConnection; -import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands; -import io.lettuce.core.codec.ByteArrayCodec; -import io.lettuce.core.codec.RedisCodec; -import io.lettuce.core.resource.ClientResources; - -import reactor.test.StepVerifier; - /** * Unit tests for {@link LettuceConnectionFactory}. * @@ -1244,7 +1234,7 @@ class LettuceConnectionFactoryUnitTests { connectionFactory.afterPropertiesSet(); assertThat(connectionFactory.isRunning()).isFalse(); - assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> connectionFactory.getConnection()); + assertThatIllegalStateException().isThrownBy(() -> connectionFactory.getConnection()); } static class CustomRedisConfiguration implements RedisConfiguration, WithHostAndPort { diff --git a/src/test/java/org/springframework/data/redis/listener/RedisMessageListenerContainerUnitTests.java b/src/test/java/org/springframework/data/redis/listener/RedisMessageListenerContainerUnitTests.java index af60d8096..ebb268dcc 100644 --- a/src/test/java/org/springframework/data/redis/listener/RedisMessageListenerContainerUnitTests.java +++ b/src/test/java/org/springframework/data/redis/listener/RedisMessageListenerContainerUnitTests.java @@ -23,7 +23,6 @@ import java.util.concurrent.Executor; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; - import org.springframework.core.task.SyncTaskExecutor; import org.springframework.data.redis.RedisConnectionFailureException; import org.springframework.data.redis.connection.RedisConnection; @@ -150,6 +149,6 @@ class RedisMessageListenerContainerUnitTests { @Test // GH-964 void failsOnDuplicateInit() { - assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> container.afterPropertiesSet()); + assertThatIllegalStateException().isThrownBy(() -> container.afterPropertiesSet()); } }