/*
* Copyright 2011-2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.connection.lettuce;
import io.lettuce.core.AbstractRedisClient;
import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisException;
import io.lettuce.core.RedisURI;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.cluster.RedisClusterClient;
import io.lettuce.core.resource.ClientResources;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
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.dao.DataAccessException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.dao.InvalidDataAccessResourceUsageException;
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.Pool;
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.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisNode;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.RedisSentinelConnection;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* Connection factory creating <a href="http://github.com/mp911de/lettuce">Lettuce</a>-based connections.
* <p>
* This factory creates a new {@link LettuceConnection} on each call to {@link #getConnection()}. Multiple
* {@link LettuceConnection}s share a single thread-safe native connection by default.
* <p>
* The shared native connection is never closed by {@link LettuceConnection}, therefore it is not validated by default
* on {@link #getConnection()}. Use {@link #setValidateConnection(boolean)} to change this behavior if necessary. Inject
* a {@link Pool} to pool dedicated connections. If shareNativeConnection is true, the pool will be used to select a
* connection for blocking and tx operations only, which should not share a connection. If native connection sharing is
* disabled, the selected connection will be used for all operations.
*
* @author Costin Leau
* @author Jennifer Hickey
* @author Thomas Darimont
* @author Christoph Strobl
* @author Mark Paluch
* @author Balázs Németh
*/
public class LettuceConnectionFactory
implements InitializingBean, DisposableBean, RedisConnectionFactory, ReactiveRedisConnectionFactory {
public static final String PING_REPLY = "PONG";
private static final ExceptionTranslationStrategy EXCEPTION_TRANSLATION = new PassThroughExceptionTranslationStrategy(
LettuceConverters.exceptionConverter());
private final Log log = LogFactory.getLog(getClass());
private String hostName = "localhost";
private int port = 6379;
private AbstractRedisClient client;
private long timeout = TimeUnit.MILLISECONDS.convert(60, TimeUnit.SECONDS);
private long shutdownTimeout = TimeUnit.MILLISECONDS.convert(2, TimeUnit.SECONDS);
private boolean validateConnection = false;
private boolean shareNativeConnection = true;
private StatefulRedisConnection<byte[], byte[]> connection;
private LettucePool pool;
private int dbIndex = 0;
/** Synchronization monitor for the shared Connection */
private final Object connectionMonitor = new Object();
private String password;
private boolean convertPipelineAndTxResults = true;
private RedisSentinelConfiguration sentinelConfiguration;
private RedisClusterConfiguration clusterConfiguration;
private ClusterCommandExecutor clusterCommandExecutor;
private ClientResources clientResources;
private boolean useSsl = false;
private boolean verifyPeer = true;
private boolean startTls = false;
/**
* Constructs a new <code>LettuceConnectionFactory</code> instance with default settings.
*/
public LettuceConnectionFactory() {}
/**
* Constructs a new <code>LettuceConnectionFactory</code> instance with default settings.
*/
public LettuceConnectionFactory(String host, int port) {
this.hostName = host;
this.port = port;
}
/**
* Constructs a new {@link LettuceConnectionFactory} instance using the given {@link RedisSentinelConfiguration}
*
* @param sentinelConfiguration
* @since 1.6
*/
public LettuceConnectionFactory(RedisSentinelConfiguration sentinelConfiguration) {
this.sentinelConfiguration = sentinelConfiguration;
}
/**
* Constructs a new {@link LettuceConnectionFactory} instance using the given {@link RedisClusterConfiguration}
* applied to create a {@link RedisClusterClient}.
*
* @param clusterConfig
* @since 1.7
*/
public LettuceConnectionFactory(RedisClusterConfiguration clusterConfig) {
this.clusterConfiguration = clusterConfig;
}
public LettuceConnectionFactory(LettucePool pool) {
this.pool = pool;
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
public void afterPropertiesSet() {
this.client = createRedisClient();
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.DisposableBean#destroy()
*/
public void destroy() {
resetConnection();
try {
client.shutdown(shutdownTimeout, shutdownTimeout, TimeUnit.MILLISECONDS);
} catch (Exception e) {
if (log.isWarnEnabled()) {
log.warn((client != null ? ClassUtils.getShortName(client.getClass()) : "LettuceClient")
+ " did not shut down gracefully.", e);
}
}
if (clusterCommandExecutor != null) {
try {
clusterCommandExecutor.destroy();
} catch (Exception ex) {
log.warn("Cannot properly close cluster command executor", ex);
}
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnectionFactory#getConnection()
*/
public RedisConnection getConnection() {
if (isClusterAware()) {
return getClusterConnection();
}
LettuceConnection connection = new LettuceConnection(getSharedConnection(), timeout, client, pool, dbIndex);
connection.setConvertPipelineAndTxResults(convertPipelineAndTxResults);
return connection;
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnectionFactory#getClusterConnection()
*/
@Override
public RedisClusterConnection getClusterConnection() {
if (!isClusterAware()) {
throw new InvalidDataAccessApiUsageException("Cluster is not configured!");
}
return new LettuceClusterConnection((RedisClusterClient) client, clusterCommandExecutor);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.ReactiveRedisConnectionFactory#getReactiveConnection()
*/
@Override
public LettuceReactiveRedisConnection getReactiveConnection() {
return new LettuceReactiveRedisConnection(client);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.ReactiveRedisConnectionFactory#getReactiveClusterConnection()
*/
@Override
public LettuceReactiveRedisClusterConnection getReactiveClusterConnection() {
if (!isClusterAware()) {
throw new InvalidDataAccessApiUsageException("Cluster is not configured!");
}
return new LettuceReactiveRedisClusterConnection((RedisClusterClient) client);
}
public void initConnection() {
synchronized (this.connectionMonitor) {
if (this.connection != null) {
resetConnection();
}
this.connection = createLettuceConnector();
}
}
/**
* Reset the underlying shared Connection, to be reinitialized on next access.
*/
public void resetConnection() {
synchronized (this.connectionMonitor) {
if (this.connection != null) {
this.connection.close();
}
this.connection = null;
}
}
/**
* Validate the shared Connection and reinitialize if invalid
*/
public void validateConnection() {
synchronized (this.connectionMonitor) {
boolean valid = false;
if (connection.isOpen()) {
try {
connection.sync().ping();
valid = true;
} catch (Exception e) {
log.debug("Validation failed", e);
}
}
if (!valid) {
log.warn("Validation of shared connection failed. Creating a new connection.");
initConnection();
}
}
}
public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
return EXCEPTION_TRANSLATION.translate(ex);
}
/**
* Returns the current host.
*
* @return the host.
*/
public String getHostName() {
return hostName;
}
/**
* Sets the host.
*
* @param host the host to set.
*/
public void setHostName(String host) {
this.hostName = host;
}
/**
* Returns the current port.
*
* @return the port.
*/
public int getPort() {
return port;
}
/**
* Sets the port.
*
* @param port the port to set.
*/
public void setPort(int port) {
this.port = port;
}
/**
* Returns the connection timeout (in milliseconds).
*
* @return connection timeout.
*/
public long getTimeout() {
return timeout;
}
/**
* Sets the connection timeout (in milliseconds).
*
* @param timeout connection timeout.
*/
public void setTimeout(long timeout) {
this.timeout = timeout;
}
/**
* Sets to use SSL connection.
*
* @param useSsl {@literal true} to use SSL.
*/
public void setUseSsl(boolean useSsl) {
this.useSsl = useSsl;
}
/**
* Returns whether to use SSL.
*
* @return use of SSL.
*/
public boolean isUseSsl() {
return useSsl;
}
/**
* Sets to use verify certificate validity/hostname check when SSL is used.
*
* @param verifyPeer {@literal false} not to verify hostname.
*/
public void setVerifyPeer(boolean verifyPeer) {
this.verifyPeer = verifyPeer;
}
/**
* Returns whether to verify certificate validity/hostname check when SSL is used.
*
* @return verify peers when using SSL.
*/
public boolean isVerifyPeer() {
return verifyPeer;
}
/**
* Returns whether to issue a StartTLS.
*
* @return use of StartTLS.
*/
public boolean isStartTls() {
return startTls;
}
/**
* Sets to issue StartTLS.
*
* @param startTls {@literal true} to issue StartTLS.
*/
public void setStartTls(boolean startTls) {
this.startTls = startTls;
}
/**
* Indicates if validation of the native Lettuce connection is enabled.
*
* @return connection validation enabled.
*/
public boolean getValidateConnection() {
return validateConnection;
}
/**
* Enables validation of the shared native Lettuce connection on calls to {@link #getConnection()}. A new connection
* will be created and used if validation fails.
* <p>
* Lettuce will automatically reconnect until close is called, which should never happen through
* {@link LettuceConnection} if a shared native connection is used, therefore the default is false.
* <p>
* Setting this to true will result in a round-trip call to the server on each new connection, so this setting should
* only be used if connection sharing is enabled and there is code that is actively closing the native Lettuce
* connection.
*
* @param validateConnection enable connection validation.
*/
public void setValidateConnection(boolean validateConnection) {
this.validateConnection = validateConnection;
}
/**
* Indicates if multiple {@link LettuceConnection}s should share a single native connection.
*
* @return native connection shared.
*/
public boolean getShareNativeConnection() {
return shareNativeConnection;
}
/**
* Enables multiple {@link LettuceConnection}s to share a single native connection. If set to false, every operation
* on {@link LettuceConnection} will open and close a socket.
*
* @param shareNativeConnection enable connection sharing.
*/
public void setShareNativeConnection(boolean shareNativeConnection) {
this.shareNativeConnection = shareNativeConnection;
}
/**
* Returns the index of the database.
*
* @return the database index.
*/
public int getDatabase() {
return dbIndex;
}
/**
* Sets the index of the database used by this connection factory. Default is 0.
*
* @param index database index
*/
public void setDatabase(int index) {
Assert.isTrue(index >= 0, "invalid DB index (a positive index required)");
this.dbIndex = index;
}
/**
* Returns the password used for authenticating with the Redis server.
*
* @return password for authentication.
*/
public String getPassword() {
return password;
}
/**
* Sets the password used for authenticating with the Redis server.
*
* @param password the password to set
*/
public void setPassword(String password) {
this.password = password;
}
/**
* Returns the shutdown timeout for shutting down the RedisClient (in milliseconds).
*
* @return shutdown timeout.
* @since 1.6
*/
public long getShutdownTimeout() {
return shutdownTimeout;
}
/**
* Sets the shutdown timeout for shutting down the RedisClient (in milliseconds).
*
* @param shutdownTimeout the shutdown timeout.
* @since 1.6
*/
public void setShutdownTimeout(long shutdownTimeout) {
this.shutdownTimeout = shutdownTimeout;
}
/**
* Get the {@link ClientResources} to reuse infrastructure.
*
* @return {@literal null} if not set.
* @since 1.7
*/
public ClientResources getClientResources() {
return clientResources;
}
/**
* Sets the {@link ClientResources} to reuse the client infrastructure. <br />
* Set to {@literal null} to not share resources.
*
* @param clientResources can be {@literal null}.
* @since 1.7
*/
public void setClientResources(ClientResources clientResources) {
this.clientResources = clientResources;
}
/**
* Specifies if pipelined 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.
*
* @return Whether or not to convert pipeline and tx results.
*/
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.
*
* @param convertPipelineAndTxResults Whether or not to convert pipeline and tx results.
*/
public void setConvertPipelineAndTxResults(boolean convertPipelineAndTxResults) {
this.convertPipelineAndTxResults = convertPipelineAndTxResults;
}
/**
* @return true when {@link RedisSentinelConfiguration} is present.
* @since 1.5
*/
public boolean isRedisSentinelAware() {
return sentinelConfiguration != null;
}
/**
* @return true when {@link RedisClusterConfiguration} is present.
* @since 1.7
*/
public boolean isClusterAware() {
return clusterConfiguration != null;
}
protected StatefulRedisConnection<byte[], byte[]> getSharedConnection() {
if (shareNativeConnection) {
synchronized (this.connectionMonitor) {
if (this.connection == null) {
initConnection();
}
if (validateConnection) {
validateConnection();
}
return this.connection;
}
} else {
return null;
}
}
protected StatefulRedisConnection<byte[], byte[]> createLettuceConnector() {
try {
StatefulRedisConnection<byte[], byte[]> connection = null;
if (client instanceof RedisClient) {
connection = ((RedisClient) client).connect(LettuceConnection.CODEC);
if (dbIndex > 0) {
connection.sync().select(dbIndex);
}
} else {
connection = null;
}
return connection;
} catch (RedisException e) {
throw new RedisConnectionFailureException("Unable to connect to Redis on " + getHostName() + ":" + getPort(), e);
}
}
private AbstractRedisClient createRedisClient() {
if (isRedisSentinelAware()) {
RedisURI redisURI = getSentinelRedisURI();
if (clientResources == null) {
return RedisClient.create(redisURI);
}
return RedisClient.create(clientResources, redisURI);
}
if (isClusterAware()) {
List<RedisURI> initialUris = new ArrayList<>();
for (RedisNode node : this.clusterConfiguration.getClusterNodes()) {
initialUris.add(createRedisURIAndApplySettings(node.getHost(), node.getPort()));
}
RedisClusterClient clusterClient = clientResources != null
? RedisClusterClient.create(clientResources, initialUris) : RedisClusterClient.create(initialUris);
this.clusterCommandExecutor = new ClusterCommandExecutor(
new LettuceClusterConnection.LettuceClusterTopologyProvider(clusterClient),
new LettuceClusterConnection.LettuceClusterNodeResourceProvider(clusterClient), EXCEPTION_TRANSLATION);
return clusterClient;
}
if (pool != null) {
return pool.getClient();
}
RedisURI uri = createRedisURIAndApplySettings(hostName, port);
return clientResources != null ? RedisClient.create(clientResources, uri) : RedisClient.create(uri);
}
private RedisURI getSentinelRedisURI() {
RedisURI redisUri = LettuceConverters.sentinelConfigurationToRedisURI(sentinelConfiguration);
if (StringUtils.hasText(password)) {
redisUri.setPassword(password);
}
return redisUri;
}
private RedisURI createRedisURIAndApplySettings(String host, int port) {
RedisURI.Builder builder = RedisURI.Builder.redis(host, port);
if (StringUtils.hasText(password)) {
builder.withPassword(password);
}
builder.withSsl(useSsl);
builder.withVerifyPeer(verifyPeer);
builder.withStartTls(startTls);
builder.withTimeout(timeout, TimeUnit.MILLISECONDS);
return builder.build();
}
@Override
public RedisSentinelConnection getSentinelConnection() {
if (!(client instanceof RedisClient)) {
throw new InvalidDataAccessResourceUsageException("Unable to connect to sentinels using " + client.getClass());
}
return new LettuceSentinelConnection(((RedisClient) client).connectSentinel());
}
}