/*
* 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.jedis;
import redis.clients.jedis.BinaryJedis;
import redis.clients.jedis.BinaryJedisPubSub;
import redis.clients.jedis.Builder;
import redis.clients.jedis.Client;
import redis.clients.jedis.Connection;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.Protocol.Command;
import redis.clients.jedis.Queable;
import redis.clients.jedis.Response;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.exceptions.JedisDataException;
import redis.clients.util.Pool;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import org.springframework.core.convert.converter.Converter;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.redis.ExceptionTranslationStrategy;
import org.springframework.data.redis.FallbackExceptionTranslationStrategy;
import org.springframework.data.redis.RedisConnectionFailureException;
import org.springframework.data.redis.connection.*;
import org.springframework.data.redis.connection.convert.TransactionResultConverter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
* {@code RedisConnection} implementation on top of <a href="http://github.com/xetorthio/jedis">Jedis</a> library.
*
* @author Costin Leau
* @author Jennifer Hickey
* @author Christoph Strobl
* @author Thomas Darimont
* @author Jungtaek Lim
* @author Konstantin Shchepanovskyi
* @author David Liu
* @author Milan Agatonovic
* @author Mark Paluch
* @author Ninad Divadkar
*/
public class JedisConnection extends AbstractRedisConnection {
private static final Field CLIENT_FIELD;
private static final Method SEND_COMMAND;
private static final Method GET_RESPONSE;
private static final ExceptionTranslationStrategy EXCEPTION_TRANSLATION = new FallbackExceptionTranslationStrategy(
JedisConverters.exceptionConverter());
static {
CLIENT_FIELD = ReflectionUtils.findField(BinaryJedis.class, "client", Client.class);
ReflectionUtils.makeAccessible(CLIENT_FIELD);
try {
Class<?> commandType = ClassUtils.isPresent("redis.clients.jedis.ProtocolCommand", null)
? ClassUtils.forName("redis.clients.jedis.ProtocolCommand", null)
: ClassUtils.forName("redis.clients.jedis.Protocol$Command", null);
SEND_COMMAND = ReflectionUtils.findMethod(Connection.class, "sendCommand",
new Class[] { commandType, byte[][].class });
} catch (Exception e) {
throw new NoClassDefFoundError(
"Could not find required flavor of command required by 'redis.clients.jedis.Connection#sendCommand'.");
}
ReflectionUtils.makeAccessible(SEND_COMMAND);
GET_RESPONSE = ReflectionUtils.findMethod(Queable.class, "getResponse", Builder.class);
ReflectionUtils.makeAccessible(GET_RESPONSE);
}
private final Jedis jedis;
private final Client client;
private Transaction transaction;
private final Pool<Jedis> pool;
/**
* flag indicating whether the connection needs to be dropped or not
*/
private boolean broken = false;
private volatile JedisSubscription subscription;
private volatile Pipeline pipeline;
private final int dbIndex;
private final String clientName;
private boolean convertPipelineAndTxResults = true;
private List<FutureResult<Response<?>>> pipelinedResults = new ArrayList<>();
private Queue<FutureResult<Response<?>>> txResults = new LinkedList<>();
class JedisResult extends FutureResult<Response<?>> {
public <T> JedisResult(Response<T> resultHolder, Converter<T, ?> converter) {
super(resultHolder, converter);
}
public <T> JedisResult(Response<T> resultHolder) {
super(resultHolder);
}
@SuppressWarnings("unchecked")
public Object get() {
if (convertPipelineAndTxResults && converter != null) {
return converter.convert(resultHolder.get());
}
return resultHolder.get();
}
}
private class JedisStatusResult extends JedisResult {
public JedisStatusResult(Response<?> resultHolder) {
super(resultHolder);
setStatus(true);
}
}
/**
* Constructs a new <code>JedisConnection</code> instance.
*
* @param jedis Jedis entity
*/
public JedisConnection(Jedis jedis) {
this(jedis, null, 0);
}
/**
* Constructs a new <code>JedisConnection</code> instance backed by a jedis pool.
*
* @param jedis
* @param pool can be null, if no pool is used
* @param dbIndex
*/
public JedisConnection(Jedis jedis, Pool<Jedis> pool, int dbIndex) {
this(jedis, pool, dbIndex, null);
}
/**
* Constructs a new <code>JedisConnection</code> instance backed by a jedis pool.
*
* @param jedis
* @param pool can be null, if no pool is used
* @param dbIndex
* @param clientName the client name, can be {@literal null}.
* @since 1.8
*/
protected JedisConnection(Jedis jedis, Pool<Jedis> pool, int dbIndex, String clientName) {
// extract underlying connection for batch operations
client = (Client) ReflectionUtils.getField(CLIENT_FIELD, jedis);
this.jedis = jedis;
this.pool = pool;
this.dbIndex = dbIndex;
this.clientName = clientName;
// select the db
// if this fail, do manual clean-up before propagating the exception
// as we're inside the constructor
if (dbIndex > 0) {
try {
select(dbIndex);
} catch (DataAccessException ex) {
close();
throw ex;
}
}
}
protected DataAccessException convertJedisAccessException(Exception ex) {
if (ex instanceof NullPointerException) {
// An NPE before flush will leave data in the OutputStream of a pooled connection
broken = true;
}
DataAccessException exception = EXCEPTION_TRANSLATION.translate(ex);
if (exception instanceof RedisConnectionFailureException) {
broken = true;
}
return exception;
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnection#keyCommands()
*/
@Override
public RedisKeyCommands keyCommands() {
return new JedisKeyCommands(this);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnection#stringCommands()
*/
@Override
public RedisStringCommands stringCommands() {
return new JedisStringCommands(this);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnection#listCommands()
*/
@Override
public RedisListCommands listCommands() {
return new JedisListCommands(this);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnection#setCommands()
*/
@Override
public RedisSetCommands setCommands() {
return new JedisSetCommands(this);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnection#zSetCommands()
*/
@Override
public RedisZSetCommands zSetCommands() {
return new JedisZSetCommands(this);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnection#hashCommands()
*/
@Override
public RedisHashCommands hashCommands() {
return new JedisHashCommands(this);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnection#geoCommands()
*/
@Override
public RedisGeoCommands geoCommands() {
return new JedisGeoCommands(this);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnection#scriptingCommands()
*/
@Override
public RedisScriptingCommands scriptingCommands() {
return new JedisScriptingCommands(this);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnection#serverCommands()
*/
@Override
public RedisServerCommands serverCommands() {
return new JedisServerCommands(this);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnection#hyperLogLogCommands()
*/
@Override
public RedisHyperLogLogCommands hyperLogLogCommands() {
return new JedisHyperLogLogCommands(this);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisCommands#execute(java.lang.String, byte[][])
*/
@Override
public Object execute(String command, byte[]... args) {
Assert.hasText(command, "a valid command needs to be specified");
try {
List<byte[]> mArgs = new ArrayList<>();
if (!ObjectUtils.isEmpty(args)) {
Collections.addAll(mArgs, args);
}
ReflectionUtils.invokeMethod(SEND_COMMAND, client, Command.valueOf(command.trim().toUpperCase()),
mArgs.toArray(new byte[mArgs.size()][]));
if (isQueueing() || isPipelined()) {
Object target = (isPipelined() ? pipeline : transaction);
@SuppressWarnings("unchecked")
Response<Object> result = (Response<Object>) ReflectionUtils.invokeMethod(GET_RESPONSE, target,
new Builder<Object>() {
public Object build(Object data) {
return data;
}
public String toString() {
return "Object";
}
});
if (isPipelined()) {
pipeline(new JedisResult(result));
} else {
transaction(new JedisResult(result));
}
return null;
}
return client.getOne();
} catch (Exception ex) {
throw convertJedisAccessException(ex);
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.AbstractRedisConnection#close()
*/
@Override
public void close() throws DataAccessException {
super.close();
// return the connection to the pool
if (pool != null) {
if (!broken) {
// reset the connection
try {
if (dbIndex > 0) {
jedis.select(0);
}
pool.returnResource(jedis);
return;
} catch (Exception ex) {
DataAccessException dae = convertJedisAccessException(ex);
if (broken) {
pool.returnBrokenResource(jedis);
} else {
pool.returnResource(jedis);
}
throw dae;
}
} else {
pool.returnBrokenResource(jedis);
return;
}
}
// else close the connection normally (doing the try/catch dance)
Exception exc = null;
if (isQueueing()) {
try {
client.quit();
} catch (Exception ex) {
exc = ex;
}
try {
client.disconnect();
} catch (Exception ex) {
exc = ex;
}
return;
}
try {
jedis.quit();
} catch (Exception ex) {
exc = ex;
}
try {
jedis.disconnect();
} catch (Exception ex) {
exc = ex;
}
if (exc != null)
throw convertJedisAccessException(exc);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnection#getNativeConnection()
*/
@Override
public Jedis getNativeConnection() {
return jedis;
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnection#isClosed()
*/
@Override
public boolean isClosed() {
try {
return !jedis.isConnected();
} catch (Exception ex) {
throw convertJedisAccessException(ex);
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnection#isQueueing()
*/
@Override
public boolean isQueueing() {
return client.isInMulti();
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnection#isPipelined()
*/
@Override
public boolean isPipelined() {
return (pipeline != null);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnection#openPipeline()
*/
@Override
public void openPipeline() {
if (pipeline == null) {
pipeline = jedis.pipelined();
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnection#closePipeline()
*/
@Override
public List<Object> closePipeline() {
if (pipeline != null) {
try {
return convertPipelineResults();
} finally {
pipeline = null;
pipelinedResults.clear();
}
}
return Collections.emptyList();
}
private List<Object> convertPipelineResults() {
List<Object> results = new ArrayList<>();
pipeline.sync();
Exception cause = null;
for (FutureResult<Response<?>> result : pipelinedResults) {
try {
Object data = result.get();
if (!convertPipelineAndTxResults || !(result.isStatus())) {
results.add(data);
}
} catch (JedisDataException e) {
DataAccessException dataAccessException = convertJedisAccessException(e);
if (cause == null) {
cause = dataAccessException;
}
results.add(dataAccessException);
} catch (DataAccessException e) {
if (cause == null) {
cause = e;
}
results.add(e);
}
}
if (cause != null) {
throw new RedisPipelineException(cause, results);
}
return results;
}
void pipeline(FutureResult<Response<?>> result) {
if (isQueueing()) {
transaction(result);
} else {
pipelinedResults.add(result);
}
}
void transaction(FutureResult<Response<?>> result) {
txResults.add(result);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnectionCommands#echo(byte[])
*/
@Override
public byte[] echo(byte[] message) {
try {
if (isPipelined()) {
pipeline(new JedisResult(pipeline.echo(message)));
return null;
}
if (isQueueing()) {
transaction(new JedisResult(transaction.echo(message)));
return null;
}
return jedis.echo(message);
} catch (Exception ex) {
throw convertJedisAccessException(ex);
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnectionCommands#ping()
*/
@Override
public String ping() {
try {
if (isPipelined()) {
pipeline(new JedisResult(pipeline.ping()));
return null;
}
if (isQueueing()) {
transaction(new JedisResult(transaction.ping()));
return null;
}
return jedis.ping();
} catch (Exception ex) {
throw convertJedisAccessException(ex);
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisTxCommands#discard()
*/
@Override
public void discard() {
try {
if (isPipelined()) {
pipeline(new JedisStatusResult(pipeline.discard()));
return;
}
transaction.discard();
} catch (Exception ex) {
throw convertJedisAccessException(ex);
} finally {
txResults.clear();
transaction = null;
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisTxCommands#exec()
*/
@Override
public List<Object> exec() {
try {
if (isPipelined()) {
pipeline(new JedisResult(pipeline.exec(),
new TransactionResultConverter<>(new LinkedList<>(txResults), JedisConverters.exceptionConverter())));
return null;
}
if (transaction == null) {
throw new InvalidDataAccessApiUsageException("No ongoing transaction. Did you forget to call multi?");
}
List<Object> results = transaction.exec();
return convertPipelineAndTxResults && !CollectionUtils.isEmpty(results)
? new TransactionResultConverter<>(txResults, JedisConverters.exceptionConverter()).convert(results)
: results;
} catch (Exception ex) {
throw convertJedisAccessException(ex);
} finally {
txResults.clear();
transaction = null;
}
}
public Pipeline getPipeline() {
return pipeline;
}
public Transaction getTransaction() {
return transaction;
}
public Jedis getJedis() {
return jedis;
}
JedisResult newJedisResult(Response<?> response) {
return new JedisResult(response);
}
<T> JedisResult newJedisResult(Response<T> response, Converter<T, ?> converter) {
return new JedisResult(response, converter);
}
JedisStatusResult newStatusResult(Response<?> response) {
return new JedisStatusResult(response);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisTxCommands#multi()
*/
@Override
public void multi() {
if (isQueueing()) {
return;
}
try {
if (isPipelined()) {
pipeline.multi();
return;
}
this.transaction = jedis.multi();
} catch (Exception ex) {
throw convertJedisAccessException(ex);
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConnectionCommands#select(int)
*/
@Override
public void select(int dbIndex) {
try {
if (isPipelined()) {
pipeline(new JedisStatusResult(pipeline.select(dbIndex)));
return;
}
if (isQueueing()) {
transaction(new JedisStatusResult(transaction.select(dbIndex)));
return;
}
jedis.select(dbIndex);
} catch (Exception ex) {
throw convertJedisAccessException(ex);
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisTxCommands#unwatch()
*/
@Override
public void unwatch() {
try {
jedis.unwatch();
} catch (Exception ex) {
throw convertJedisAccessException(ex);
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisTxCommands#watch(byte[][])
*/
@Override
public void watch(byte[]... keys) {
if (isQueueing()) {
throw new UnsupportedOperationException();
}
try {
for (byte[] key : keys) {
if (isPipelined()) {
pipeline(new JedisStatusResult(pipeline.watch(key)));
} else {
jedis.watch(key);
}
}
} catch (Exception ex) {
throw convertJedisAccessException(ex);
}
}
//
// Pub/Sub functionality
//
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisPubSubCommands#publish(byte[], byte[])
*/
@Override
public Long publish(byte[] channel, byte[] message) {
try {
if (isPipelined()) {
pipeline(new JedisResult(pipeline.publish(channel, message)));
return null;
}
if (isQueueing()) {
transaction(new JedisResult(transaction.publish(channel, message)));
return null;
}
return jedis.publish(channel, message);
} catch (Exception ex) {
throw convertJedisAccessException(ex);
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisPubSubCommands#getSubscription()
*/
@Override
public Subscription getSubscription() {
return subscription;
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisPubSubCommands#isSubscribed()
*/
@Override
public boolean isSubscribed() {
return (subscription != null && subscription.isAlive());
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisPubSubCommands#pSubscribe(org.springframework.data.redis.connection.MessageListener, byte[][])
*/
@Override
public void pSubscribe(MessageListener listener, byte[]... patterns) {
if (isSubscribed()) {
throw new RedisSubscribedConnectionException(
"Connection already subscribed; use the connection Subscription to cancel or add new channels");
}
if (isQueueing()) {
throw new UnsupportedOperationException();
}
if (isPipelined()) {
throw new UnsupportedOperationException();
}
try {
BinaryJedisPubSub jedisPubSub = new JedisMessageListener(listener);
subscription = new JedisSubscription(listener, jedisPubSub, null, patterns);
jedis.psubscribe(jedisPubSub, patterns);
} catch (Exception ex) {
throw convertJedisAccessException(ex);
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisPubSubCommands#subscribe(org.springframework.data.redis.connection.MessageListener, byte[][])
*/
@Override
public void subscribe(MessageListener listener, byte[]... channels) {
if (isSubscribed()) {
throw new RedisSubscribedConnectionException(
"Connection already subscribed; use the connection Subscription to cancel or add new channels");
}
if (isQueueing()) {
throw new UnsupportedOperationException();
}
if (isPipelined()) {
throw new UnsupportedOperationException();
}
try {
BinaryJedisPubSub jedisPubSub = new JedisMessageListener(listener);
subscription = new JedisSubscription(listener, jedisPubSub, channels, null);
jedis.subscribe(jedisPubSub, channels);
} catch (Exception ex) {
throw convertJedisAccessException(ex);
}
}
/**
* Specifies if pipelined results should be converted to the expected data type. If false, results of
* {@link #closePipeline()} and {@link #exec()} will be of the type returned by the Jedis driver
*
* @param convertPipelineAndTxResults Whether or not to convert pipeline and tx results
*/
public void setConvertPipelineAndTxResults(boolean convertPipelineAndTxResults) {
this.convertPipelineAndTxResults = convertPipelineAndTxResults;
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.AbstractRedisConnection#isActive(org.springframework.data.redis.connection.RedisNode)
*/
@Override
protected boolean isActive(RedisNode node) {
if (node == null) {
return false;
}
Jedis temp = null;
try {
temp = getJedis(node);
temp.connect();
return temp.ping().equalsIgnoreCase("pong");
} catch (Exception e) {
return false;
} finally {
if (temp != null) {
temp.disconnect();
temp.close();
}
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.AbstractRedisConnection#getSentinelConnection(org.springframework.data.redis.connection.RedisNode)
*/
@Override
protected JedisSentinelConnection getSentinelConnection(RedisNode sentinel) {
return new JedisSentinelConnection(getJedis(sentinel));
}
protected Jedis getJedis(RedisNode node) {
Jedis jedis = new Jedis(node.getHost(), node.getPort());
if (StringUtils.hasText(clientName)) {
jedis.clientSetname(clientName);
}
return jedis;
}
}