org.springframework.data.redis.connection.lettuce.LettuceConnection.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.data.redis.connection.lettuce.LettuceConnection.java

Source

/*
 * Copyright 2011-2019 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
 *
 *      https://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 static io.lettuce.core.protocol.CommandType.*;

import io.lettuce.core.AbstractRedisClient;
import io.lettuce.core.LettuceFutures;
import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisException;
import io.lettuce.core.RedisFuture;
import io.lettuce.core.RedisURI;
import io.lettuce.core.TransactionResult;
import io.lettuce.core.api.StatefulConnection;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.async.RedisAsyncCommands;
import io.lettuce.core.api.sync.RedisCommands;
import io.lettuce.core.cluster.api.StatefulRedisClusterConnection;
import io.lettuce.core.cluster.api.async.RedisClusterAsyncCommands;
import io.lettuce.core.cluster.api.sync.RedisClusterCommands;
import io.lettuce.core.codec.ByteArrayCodec;
import io.lettuce.core.codec.RedisCodec;
import io.lettuce.core.output.*;
import io.lettuce.core.protocol.Command;
import io.lettuce.core.protocol.CommandArgs;
import io.lettuce.core.protocol.CommandType;
import io.lettuce.core.pubsub.StatefulRedisPubSubConnection;
import io.lettuce.core.sentinel.api.StatefulRedisSentinelConnection;
import lombok.RequiredArgsConstructor;

import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

import org.springframework.beans.BeanUtils;
import org.springframework.core.convert.converter.Converter;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.dao.QueryTimeoutException;
import org.springframework.data.redis.ExceptionTranslationStrategy;
import org.springframework.data.redis.FallbackExceptionTranslationStrategy;
import org.springframework.data.redis.connection.*;
import org.springframework.data.redis.connection.convert.TransactionResultConverter;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionProvider.TargetAware;
import org.springframework.data.redis.connection.lettuce.LettuceResult.LettuceResultBuilder;
import org.springframework.data.redis.connection.lettuce.LettuceResult.LettuceStatusResult;
import org.springframework.data.redis.core.RedisCommand;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;

/**
 * {@code RedisConnection} implementation on top of <a href="https://github.com/mp911de/lettuce">Lettuce</a> Redis
 * client.
 *
 * @author Costin Leau
 * @author Jennifer Hickey
 * @author Christoph Strobl
 * @author Thomas Darimont
 * @author David Liu
 * @author Mark Paluch
 * @author Ninad Divadkar
 */
public class LettuceConnection extends AbstractRedisConnection {

    static final RedisCodec<byte[], byte[]> CODEC = ByteArrayCodec.INSTANCE;

    private static final ExceptionTranslationStrategy EXCEPTION_TRANSLATION = new FallbackExceptionTranslationStrategy(
            LettuceConverters.exceptionConverter());
    private static final TypeHints typeHints = new TypeHints();

    private final int defaultDbIndex;
    private int dbIndex;

    private final LettuceConnectionProvider connectionProvider;
    private final @Nullable StatefulConnection<byte[], byte[]> asyncSharedConn;
    private @Nullable StatefulConnection<byte[], byte[]> asyncDedicatedConn;

    private final long timeout;

    // refers only to main connection as pubsub happens on a different one
    private boolean isClosed = false;
    private boolean isMulti = false;
    private boolean isPipelined = false;
    private @Nullable List<LettuceResult> ppline;
    private final Queue<FutureResult<?>> txResults = new LinkedList<>();
    private volatile @Nullable LettuceSubscription subscription;
    /** flag indicating whether the connection needs to be dropped or not */
    private boolean convertPipelineAndTxResults = true;

    LettuceResult newLettuceResult(Future<?> resultHolder) {
        return newLettuceResult(resultHolder, (val) -> val);
    }

    @SuppressWarnings("unchecked")
    <T, R> LettuceResult<T, R> newLettuceResult(Future<T> resultHolder, Converter<T, R> converter) {

        return LettuceResultBuilder.forResponse(resultHolder).mappedWith((Converter) converter)
                .convertPipelineAndTxResults(convertPipelineAndTxResults).build();
    }

    @SuppressWarnings("unchecked")
    <T, R> LettuceResult<T, R> newLettuceResult(Future<T> resultHolder, Converter<T, R> converter,
            Supplier<R> defaultValue) {

        return LettuceResultBuilder.forResponse(resultHolder).mappedWith((Converter) converter)
                .convertPipelineAndTxResults(convertPipelineAndTxResults).defaultNullTo(defaultValue).build();
    }

    <T, R> LettuceResult<T, R> newLettuceStatusResult(Future<T> resultHolder) {
        return LettuceResultBuilder.<T, R>forResponse(resultHolder).buildStatusResult();
    }

    private class LettuceTransactionResultConverter<T> extends TransactionResultConverter<T> {
        public LettuceTransactionResultConverter(Queue<FutureResult<T>> txResults,
                Converter<Exception, DataAccessException> exceptionConverter) {
            super(txResults, exceptionConverter);
        }

        @Override
        public List<Object> convert(List<Object> execResults) {
            // Lettuce Empty list means null (watched variable modified)
            if (execResults.isEmpty()) {
                return null;
            }
            return super.convert(execResults);
        }
    }

    /**
     * Instantiates a new lettuce connection.
     *
     * @param timeout The connection timeout (in milliseconds)
     * @param client The {@link RedisClient} to use when instantiating a native connection
     */
    public LettuceConnection(long timeout, RedisClient client) {
        this(null, timeout, client, null);
    }

    /**
     * Instantiates a new lettuce connection.
     *
     * @param timeout The connection timeout (in milliseconds) * @param client The {@link RedisClient} to use when
     *          instantiating a pub/sub connection
     * @param pool The connection pool to use for all other native connections
     * @deprecated since 2.0, use pooling via {@link LettucePoolingClientConfiguration}.
     */
    @Deprecated
    public LettuceConnection(long timeout, RedisClient client, LettucePool pool) {
        this(null, timeout, client, pool);
    }

    /**
     * Instantiates a new lettuce connection.
     *
     * @param sharedConnection A native connection that is shared with other {@link LettuceConnection}s. Will not be used
     *          for transactions or blocking operations
     * @param timeout The connection timeout (in milliseconds)
     * @param client The {@link RedisClient} to use when making pub/sub, blocking, and tx connections
     */
    public LettuceConnection(@Nullable StatefulRedisConnection<byte[], byte[]> sharedConnection, long timeout,
            RedisClient client) {
        this(sharedConnection, timeout, client, null);
    }

    /**
     * Instantiates a new lettuce connection.
     *
     * @param sharedConnection A native connection that is shared with other {@link LettuceConnection}s. Should not be
     *          used for transactions or blocking operations
     * @param timeout The connection timeout (in milliseconds)
     * @param client The {@link RedisClient} to use when making pub/sub connections
     * @param pool The connection pool to use for blocking and tx operations
     * @deprecated since 2.0, use
     *             {@link #LettuceConnection(StatefulRedisConnection, LettuceConnectionProvider, long, int)}
     */
    @Deprecated
    public LettuceConnection(@Nullable StatefulRedisConnection<byte[], byte[]> sharedConnection, long timeout,
            RedisClient client, @Nullable LettucePool pool) {

        this(sharedConnection, timeout, client, pool, 0);
    }

    /**
     * @param sharedConnection A native connection that is shared with other {@link LettuceConnection}s. Should not be
     *          used for transactions or blocking operations.
     * @param timeout The connection timeout (in milliseconds)
     * @param client The {@link RedisClient} to use when making pub/sub connections.
     * @param pool The connection pool to use for blocking and tx operations.
     * @param defaultDbIndex The db index to use along with {@link RedisClient} when establishing a dedicated connection.
     * @since 1.7
     * @deprecated since 2.0, use
     *             {@link #LettuceConnection(StatefulRedisConnection, LettuceConnectionProvider, long, int)}
     */
    @Deprecated
    public LettuceConnection(@Nullable StatefulRedisConnection<byte[], byte[]> sharedConnection, long timeout,
            @Nullable AbstractRedisClient client, @Nullable LettucePool pool, int defaultDbIndex) {

        if (pool != null) {
            this.connectionProvider = new LettucePoolConnectionProvider(pool);
        } else {
            this.connectionProvider = new StandaloneConnectionProvider((RedisClient) client, CODEC);
        }

        this.asyncSharedConn = sharedConnection;
        this.timeout = timeout;
        this.defaultDbIndex = defaultDbIndex;
        this.dbIndex = this.defaultDbIndex;
    }

    /**
     * @param sharedConnection A native connection that is shared with other {@link LettuceConnection}s. Should not be
     *          used for transactions or blocking operations.
     * @param connectionProvider connection provider to obtain and release native connections.
     * @param timeout The connection timeout (in milliseconds)
     * @param defaultDbIndex The db index to use along with {@link RedisClient} when establishing a dedicated connection.
     * @since 2.0
     */
    public LettuceConnection(@Nullable StatefulRedisConnection<byte[], byte[]> sharedConnection,
            LettuceConnectionProvider connectionProvider, long timeout, int defaultDbIndex) {
        this((StatefulConnection<byte[], byte[]>) sharedConnection, connectionProvider, timeout, defaultDbIndex);
    }

    /**
     * @param sharedConnection A native connection that is shared with other {@link LettuceConnection}s. Should not be
     *          used for transactions or blocking operations.
     * @param connectionProvider connection provider to obtain and release native connections.
     * @param timeout The connection timeout (in milliseconds)
     * @param defaultDbIndex The db index to use along with {@link RedisClient} when establishing a dedicated connection.
     * @since 2.1
     */
    LettuceConnection(@Nullable StatefulConnection<byte[], byte[]> sharedConnection,
            LettuceConnectionProvider connectionProvider, long timeout, int defaultDbIndex) {

        Assert.notNull(connectionProvider, "LettuceConnectionProvider must not be null.");

        this.asyncSharedConn = sharedConnection;
        this.connectionProvider = connectionProvider;
        this.timeout = timeout;
        this.defaultDbIndex = defaultDbIndex;
        this.dbIndex = this.defaultDbIndex;
    }

    protected DataAccessException convertLettuceAccessException(Exception ex) {
        return EXCEPTION_TRANSLATION.translate(ex);
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.connection.RedisConnection#geoCommands()
     */
    @Override
    public RedisGeoCommands geoCommands() {
        return new LettuceGeoCommands(this);
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.connection.RedisConnection#hashCommands()
     */
    @Override
    public RedisHashCommands hashCommands() {
        return new LettuceHashCommands(this);
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.connection.RedisConnection#hyperLogLogCommands()
     */
    @Override
    public RedisHyperLogLogCommands hyperLogLogCommands() {
        return new LettuceHyperLogLogCommands(this);
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.connection.RedisConnection#keyCommands()
     */
    @Override
    public RedisKeyCommands keyCommands() {
        return new LettuceKeyCommands(this);
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.connection.RedisConnection#listCommands()
     */
    @Override
    public RedisListCommands listCommands() {
        return new LettuceListCommands(this);
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.connection.RedisConnection#setCommands()
     */
    @Override
    public RedisSetCommands setCommands() {
        return new LettuceSetCommands(this);
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.connection.RedisConnection#scriptingCommands()
     */
    @Override
    public RedisScriptingCommands scriptingCommands() {
        return new LettuceScriptingCommands(this);
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.connection.RedisConnection#streamCommands()
     */
    @Override
    public RedisStreamCommands streamCommands() {
        return new LettuceStreamCommands(this);
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.connection.RedisConnection#stringCommands()
     */
    @Override
    public RedisStringCommands stringCommands() {
        return new LettuceStringCommands(this);
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.connection.RedisConnection#serverCommands()
     */
    @Override
    public RedisServerCommands serverCommands() {
        return new LettuceServerCommands(this);
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.connection.RedisConnection#zSetCommands()
     */
    @Override
    public RedisZSetCommands zSetCommands() {
        return new LettuceZSetCommands(this);
    }

    @Nullable
    @SuppressWarnings({ "rawtypes", "unchecked" })
    private Object await(RedisFuture<?> cmd) {

        if (isMulti) {
            return null;
        }

        return LettuceFutures.awaitOrCancel(cmd, timeout, TimeUnit.MILLISECONDS);
    }

    @Override
    public Object execute(String command, byte[]... args) {
        return execute(command, null, args);
    }

    /**
     * 'Native' or 'raw' execution of the given command along-side the given arguments.
     *
     * @see RedisConnection#execute(String, byte[]...)
     * @param command Command to execute
     * @param commandOutputTypeHint Type of Output to use, may be (may be {@literal null}).
     * @param args Possible command arguments (may be {@literal null})
     * @return execution result.
     */
    @Nullable
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public Object execute(String command, @Nullable CommandOutput commandOutputTypeHint, byte[]... args) {

        Assert.hasText(command, "a valid command needs to be specified");
        try {

            String name = command.trim().toUpperCase();
            CommandType commandType = CommandType.valueOf(name);

            validateCommandIfRunningInTransactionMode(commandType, args);

            CommandArgs<byte[], byte[]> cmdArg = new CommandArgs<>(CODEC);
            if (!ObjectUtils.isEmpty(args)) {
                cmdArg.addKeys(args);
            }

            RedisClusterAsyncCommands<byte[], byte[]> connectionImpl = getAsyncConnection();

            CommandOutput expectedOutput = commandOutputTypeHint != null ? commandOutputTypeHint
                    : typeHints.getTypeHint(commandType);
            Command cmd = new Command(commandType, expectedOutput, cmdArg);

            if (isPipelined()) {

                pipeline(newLettuceResult(connectionImpl.dispatch(cmd.getType(), cmd.getOutput(), cmd.getArgs())));
                return null;
            }

            if (isQueueing()) {

                transaction(
                        newLettuceResult(connectionImpl.dispatch(cmd.getType(), cmd.getOutput(), cmd.getArgs())));
                return null;
            }

            return await(connectionImpl.dispatch(cmd.getType(), cmd.getOutput(), cmd.getArgs()));
        } catch (RedisException ex) {
            throw convertLettuceAccessException(ex);
        }
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.connection.AbstractRedisConnection#close()
     */
    @Override
    public void close() throws DataAccessException {

        super.close();

        if (isClosed) {
            return;
        }

        isClosed = true;

        if (asyncDedicatedConn != null) {
            try {
                connectionProvider.release(asyncDedicatedConn);
            } catch (RuntimeException ex) {
                throw convertLettuceAccessException(ex);
            }
        }

        if (subscription != null) {
            if (subscription.isAlive()) {
                subscription.doClose();
            }
            subscription = null;
        }

        this.dbIndex = defaultDbIndex;
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.connection.RedisConnection#isClosed()
     */
    @Override
    public boolean isClosed() {
        return isClosed && !isSubscribed();
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.connection.RedisConnection#getNativeConnection()
     */
    @Override
    public RedisClusterAsyncCommands<byte[], byte[]> getNativeConnection() {

        LettuceSubscription subscription = this.subscription;
        return (subscription != null ? subscription.getNativeConnection().async() : getAsyncConnection());
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.connection.RedisConnection#isQueueing()
     */
    @Override
    public boolean isQueueing() {
        return isMulti;
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.connection.RedisConnection#isPipelined()
     */
    @Override
    public boolean isPipelined() {
        return isPipelined;
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.connection.RedisConnection#openPipeline()
     */
    @Override
    public void openPipeline() {
        if (!isPipelined) {
            isPipelined = true;
            ppline = new ArrayList<>();
        }
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.connection.RedisConnection#closePipeline()
     */
    @Override
    public List<Object> closePipeline() {

        if (!isPipelined) {

            return Collections.emptyList();
        }
        isPipelined = false;
        List<io.lettuce.core.protocol.RedisCommand<?, ?, ?>> futures = new ArrayList<>(ppline.size());
        for (LettuceResult<?, ?> result : ppline) {
            futures.add(result.getResultHolder());
        }

        try {
            boolean done = LettuceFutures.awaitAll(timeout, TimeUnit.MILLISECONDS,
                    futures.toArray(new RedisFuture[futures.size()]));

            List<Object> results = new ArrayList<>(futures.size());

            Exception problem = null;

            if (done) {
                for (LettuceResult<?, ?> result : ppline) {

                    if (result.getResultHolder().getOutput().hasError()) {

                        Exception err = new InvalidDataAccessApiUsageException(
                                result.getResultHolder().getOutput().getError());
                        // remember only the first error
                        if (problem == null) {
                            problem = err;
                        }
                        results.add(err);
                    } else if (!result.isStatus()) {

                        try {
                            results.add(result.conversionRequired() ? result.convert(result.get()) : result.get());
                        } catch (DataAccessException e) {
                            if (problem == null) {
                                problem = e;
                            }
                            results.add(e);
                        }
                    }
                }
            }
            ppline.clear();

            if (problem != null) {
                throw new RedisPipelineException(problem, results);
            }

            if (done) {
                return results;
            }

            throw new RedisPipelineException(new QueryTimeoutException("Redis command timed out"));
        } catch (Exception e) {
            throw new RedisPipelineException(e);
        }
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.connection.RedisServerCommands#shutdown(org.springframework.data.redis.connection.RedisServerCommands.ShutdownOption)
     */
    @Override
    public byte[] echo(byte[] message) {
        try {
            if (isPipelined()) {
                pipeline(newLettuceResult(getAsyncConnection().echo(message)));
                return null;
            }
            if (isQueueing()) {
                transaction(newLettuceResult(getAsyncConnection().echo(message)));
                return null;
            }
            return getConnection().echo(message);
        } catch (Exception ex) {
            throw convertLettuceAccessException(ex);
        }
    }

    @Override
    public String ping() {
        try {
            if (isPipelined()) {
                pipeline(newLettuceResult(getAsyncConnection().ping()));
                return null;
            }
            if (isQueueing()) {
                transaction(newLettuceResult(getAsyncConnection().ping()));
                return null;
            }
            return getConnection().ping();
        } catch (Exception ex) {
            throw convertLettuceAccessException(ex);
        }
    }

    @Override
    public void discard() {
        isMulti = false;
        try {
            if (isPipelined()) {
                pipeline(newLettuceStatusResult(getAsyncDedicatedRedisCommands().discard()));
                return;
            }
            getDedicatedRedisCommands().discard();
        } catch (Exception ex) {
            throw convertLettuceAccessException(ex);
        } finally {
            txResults.clear();
        }
    }

    @Override
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public List<Object> exec() {

        isMulti = false;

        try {
            if (isPipelined()) {
                RedisFuture<TransactionResult> exec = getAsyncDedicatedRedisCommands().exec();

                LettuceTransactionResultConverter resultConverter = new LettuceTransactionResultConverter(
                        new LinkedList<>(txResults), LettuceConverters.exceptionConverter());

                pipeline(newLettuceResult(exec, source -> resultConverter.convert(
                        LettuceConverters.transactionResultUnwrapper().convert((TransactionResult) source))));
                return null;
            }

            TransactionResult transactionResult = (getDedicatedRedisCommands()).exec();
            List<Object> results = LettuceConverters.transactionResultUnwrapper().convert(transactionResult);
            return convertPipelineAndTxResults
                    ? new LettuceTransactionResultConverter(txResults, LettuceConverters.exceptionConverter())
                            .convert(results)
                    : results;
        } catch (Exception ex) {
            throw convertLettuceAccessException(ex);
        } finally {
            txResults.clear();
        }
    }

    @Override
    public void multi() {
        if (isQueueing()) {
            return;
        }
        isMulti = true;
        try {
            if (isPipelined()) {
                getAsyncDedicatedRedisCommands().multi();
                return;
            }
            getDedicatedRedisCommands().multi();
        } catch (Exception ex) {
            throw convertLettuceAccessException(ex);
        }
    }

    @Override
    public void select(int dbIndex) {

        if (asyncSharedConn != null) {
            throw new UnsupportedOperationException(
                    "Selecting a new database not supported due to shared connection. "
                            + "Use separate ConnectionFactorys to work with multiple databases");
        }

        try {

            this.dbIndex = dbIndex;

            if (isPipelined()) {
                pipeline(new LettuceStatusResult(getAsyncConnection().dispatch(CommandType.SELECT,
                        new StatusOutput<>(ByteArrayCodec.INSTANCE),
                        new CommandArgs<>(ByteArrayCodec.INSTANCE).add(dbIndex))));
                return;
            }

            if (isQueueing()) {
                transaction(newLettuceStatusResult(getAsyncConnection().dispatch(CommandType.SELECT,
                        new StatusOutput<>(ByteArrayCodec.INSTANCE),
                        new CommandArgs<>(ByteArrayCodec.INSTANCE).add(dbIndex))));
                return;
            }
            ((RedisCommands) getConnection()).select(dbIndex);
        } catch (Exception ex) {
            throw convertLettuceAccessException(ex);
        }
    }

    @Override
    public void unwatch() {
        try {
            if (isPipelined()) {
                pipeline(newLettuceStatusResult(getAsyncDedicatedRedisCommands().unwatch()));
                return;
            }
            if (isQueueing()) {
                transaction(newLettuceStatusResult(getAsyncDedicatedRedisCommands().unwatch()));
                return;
            }
            getDedicatedRedisCommands().unwatch();
        } catch (Exception ex) {
            throw convertLettuceAccessException(ex);
        }
    }

    @Override
    public void watch(byte[]... keys) {
        if (isQueueing()) {
            throw new UnsupportedOperationException();
        }
        try {
            if (isPipelined()) {
                pipeline(newLettuceStatusResult(getAsyncDedicatedRedisCommands().watch(keys)));
                return;
            }
            if (isQueueing()) {
                transaction(new LettuceStatusResult(getAsyncDedicatedRedisCommands().watch(keys)));
                return;
            }
            getDedicatedRedisCommands().watch(keys);
        } catch (Exception ex) {
            throw convertLettuceAccessException(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(newLettuceResult(getAsyncConnection().publish(channel, message)));
                return null;
            }

            if (isQueueing()) {
                transaction(newLettuceResult(getAsyncConnection().publish(channel, message)));
                return null;
            }

            return getConnection().publish(channel, message);
        } catch (Exception ex) {
            throw convertLettuceAccessException(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) {

        checkSubscription();

        if (isQueueing() || isPipelined()) {
            throw new UnsupportedOperationException(
                    "Transaction/Pipelining is not supported for Pub/Sub subscriptions!");
        }

        try {
            subscription = initSubscription(listener);
            subscription.pSubscribe(patterns);
        } catch (Exception ex) {
            throw convertLettuceAccessException(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) {

        checkSubscription();

        if (isQueueing() || isPipelined()) {
            throw new UnsupportedOperationException(
                    "Transaction/Pipelining is not supported for Pub/Sub subscriptions!");
        }

        try {
            subscription = initSubscription(listener);
            subscription.subscribe(channels);
        } catch (Exception ex) {
            throw convertLettuceAccessException(ex);
        }
    }

    @SuppressWarnings("unchecked")
    <T> T failsafeReadScanValues(List<?> source, @SuppressWarnings("rawtypes") Converter converter) {

        try {
            return (T) (converter != null ? converter.convert(source) : source);
        } catch (IndexOutOfBoundsException e) {
            // ignore this one
        }
        return null;
    }

    /**
     * Specifies if pipelined and transaction 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 Lettuce driver
     *
     * @param convertPipelineAndTxResults Whether or not to convert pipeline and tx results
     */
    public void setConvertPipelineAndTxResults(boolean convertPipelineAndTxResults) {
        this.convertPipelineAndTxResults = convertPipelineAndTxResults;
    }

    private void checkSubscription() {
        if (isSubscribed()) {
            throw new RedisSubscribedConnectionException(
                    "Connection already subscribed; use the connection Subscription to cancel or add new channels");
        }
    }

    /**
     * {@link #close()} the current connection and open a new pub/sub connection to the Redis server.
     *
     * @return never {@literal null}.
     */
    @SuppressWarnings("unchecked")
    protected StatefulRedisPubSubConnection<byte[], byte[]> switchToPubSub() {

        close();
        return connectionProvider.getConnection(StatefulRedisPubSubConnection.class);
    }

    private LettuceSubscription initSubscription(MessageListener listener) {
        return doCreateSubscription(listener, switchToPubSub(), connectionProvider);
    }

    /**
     * Customization hook to create a {@link LettuceSubscription}.
     *
     * @param listener the {@link MessageListener} to notify.
     * @param connection Pub/Sub connection.
     * @param connectionProvider the {@link LettuceConnectionProvider} for connection release.
     * @return a {@link LettuceSubscription}.
     * @since 2.2
     */
    protected LettuceSubscription doCreateSubscription(MessageListener listener,
            StatefulRedisPubSubConnection<byte[], byte[]> connection,
            LettuceConnectionProvider connectionProvider) {
        return new LettuceSubscription(listener, connection, connectionProvider);
    }

    void pipeline(LettuceResult result) {
        if (isQueueing()) {
            transaction(result);
        } else {
            ppline.add(result);
        }
    }

    void transaction(FutureResult<?> result) {
        txResults.add(result);
    }

    RedisClusterAsyncCommands<byte[], byte[]> getAsyncConnection() {
        if (isQueueing()) {
            return getAsyncDedicatedConnection();
        }
        if (asyncSharedConn != null) {

            if (asyncSharedConn instanceof StatefulRedisConnection) {
                return ((StatefulRedisConnection<byte[], byte[]>) asyncSharedConn).async();
            }
        }
        return getAsyncDedicatedConnection();
    }

    protected RedisClusterCommands<byte[], byte[]> getConnection() {

        if (isQueueing()) {
            return getDedicatedConnection();
        }
        if (asyncSharedConn != null) {

            if (asyncSharedConn instanceof StatefulRedisConnection) {
                return ((StatefulRedisConnection<byte[], byte[]>) asyncSharedConn).sync();
            }
            if (asyncSharedConn instanceof StatefulRedisClusterConnection) {
                return ((StatefulRedisClusterConnection<byte[], byte[]>) asyncSharedConn).sync();
            }
        }
        return getDedicatedConnection();
    }

    @SuppressWarnings("unchecked")
    private RedisAsyncCommands<byte[], byte[]> getAsyncDedicatedRedisCommands() {
        return (RedisAsyncCommands) getAsyncDedicatedConnection();
    }

    protected RedisClusterAsyncCommands<byte[], byte[]> getAsyncDedicatedConnection() {

        if (asyncDedicatedConn == null) {

            asyncDedicatedConn = doGetAsyncDedicatedConnection();

            if (asyncDedicatedConn instanceof StatefulRedisConnection) {
                ((StatefulRedisConnection<byte[], byte[]>) asyncDedicatedConn).sync().select(dbIndex);
            }
        }

        if (asyncDedicatedConn instanceof StatefulRedisConnection) {
            return ((StatefulRedisConnection<byte[], byte[]>) asyncDedicatedConn).async();
        }
        if (asyncDedicatedConn instanceof StatefulRedisClusterConnection) {
            return ((StatefulRedisClusterConnection<byte[], byte[]>) asyncDedicatedConn).async();
        }

        throw new IllegalStateException(
                String.format("%s is not a supported connection type.", asyncDedicatedConn.getClass().getName()));
    }

    @SuppressWarnings("unchecked")
    private RedisCommands<byte[], byte[]> getDedicatedRedisCommands() {
        return (RedisCommands) getDedicatedConnection();
    }

    RedisClusterCommands<byte[], byte[]> getDedicatedConnection() {

        if (asyncDedicatedConn == null) {

            asyncDedicatedConn = doGetAsyncDedicatedConnection();

            if (asyncDedicatedConn instanceof StatefulRedisConnection) {
                ((StatefulRedisConnection<byte[], byte[]>) asyncDedicatedConn).sync().select(dbIndex);
            }
        }

        if (asyncDedicatedConn instanceof StatefulRedisConnection) {
            return ((StatefulRedisConnection<byte[], byte[]>) asyncDedicatedConn).sync();
        }
        if (asyncDedicatedConn instanceof StatefulRedisClusterConnection) {
            return ((StatefulRedisClusterConnection<byte[], byte[]>) asyncDedicatedConn).sync();
        }

        throw new IllegalStateException(
                String.format("%s is not a supported connection type.", asyncDedicatedConn.getClass().getName()));
    }

    @SuppressWarnings("unchecked")
    protected StatefulConnection<byte[], byte[]> doGetAsyncDedicatedConnection() {
        return connectionProvider.getConnection(StatefulConnection.class);
    }

    io.lettuce.core.ScanCursor getScanCursor(long cursorId) {
        return io.lettuce.core.ScanCursor.of(Long.toString(cursorId));
    }

    private void validateCommandIfRunningInTransactionMode(CommandType cmd, byte[]... args) {

        if (this.isQueueing()) {
            validateCommand(cmd, args);
        }
    }

    private void validateCommand(CommandType cmd, @Nullable byte[]... args) {

        RedisCommand redisCommand = RedisCommand.failsafeCommandLookup(cmd.name());
        if (!RedisCommand.UNKNOWN.equals(redisCommand) && redisCommand.requiresArguments()) {
            try {
                redisCommand.validateArgumentCount(args != null ? args.length : 0);
            } catch (IllegalArgumentException e) {
                throw new InvalidDataAccessApiUsageException(
                        String.format("Validation failed for %s command.", cmd), e);
            }
        }
    }

    @Override
    protected boolean isActive(RedisNode node) {

        StatefulRedisSentinelConnection<String, String> connection = null;
        try {
            connection = getConnection(node);
            return connection.sync().ping().equalsIgnoreCase("pong");
        } catch (Exception e) {
            return false;
        } finally {
            if (connection != null) {
                connectionProvider.release(connection);
            }
        }
    }

    private RedisURI getRedisURI(RedisNode node) {
        return RedisURI.Builder.redis(node.getHost(), node.getPort()).build();
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.connection.AbstractRedisConnection#getSentinelConnection(org.springframework.data.redis.connection.RedisNode)
     */
    @Override
    protected RedisSentinelConnection getSentinelConnection(RedisNode sentinel) {

        StatefulRedisSentinelConnection<String, String> connection = getConnection(sentinel);
        return new LettuceSentinelConnection(connection);
    }

    @SuppressWarnings("unchecked")
    private StatefulRedisSentinelConnection<String, String> getConnection(RedisNode sentinel) {
        return ((TargetAware) connectionProvider).getConnection(StatefulRedisSentinelConnection.class,
                getRedisURI(sentinel));
    }

    LettuceConnectionProvider getConnectionProvider() {
        return connectionProvider;
    }

    /**
     * {@link TypeHints} provide {@link CommandOutput} information for a given {@link CommandType}.
     *
     * @since 1.2.1
     */
    static class TypeHints {

        @SuppressWarnings("rawtypes") //
        private static final Map<CommandType, Class<? extends CommandOutput>> COMMAND_OUTPUT_TYPE_MAPPING = new HashMap<>();

        @SuppressWarnings("rawtypes") //
        private static final Map<Class<?>, Constructor<CommandOutput>> CONSTRUCTORS = new ConcurrentHashMap<>();

        {
            // INTEGER
            COMMAND_OUTPUT_TYPE_MAPPING.put(BITCOUNT, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(BITOP, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(BITPOS, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(DBSIZE, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(DECR, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(DECRBY, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(DEL, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(GETBIT, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(HDEL, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(HINCRBY, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(HLEN, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(INCR, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(INCRBY, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(LINSERT, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(LLEN, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(LPUSH, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(LPUSHX, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(LREM, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(PTTL, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(PUBLISH, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(RPUSH, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(RPUSHX, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(SADD, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(SCARD, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(SDIFFSTORE, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(SETBIT, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(SETRANGE, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(SINTERSTORE, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(SREM, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(SUNIONSTORE, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(STRLEN, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(TTL, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(ZADD, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(ZCOUNT, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(ZINTERSTORE, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(ZRANK, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(ZREM, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(ZREMRANGEBYRANK, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(ZREMRANGEBYSCORE, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(ZREVRANK, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(ZUNIONSTORE, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(PFCOUNT, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(PFMERGE, IntegerOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(PFADD, IntegerOutput.class);

            // DOUBLE
            COMMAND_OUTPUT_TYPE_MAPPING.put(HINCRBYFLOAT, DoubleOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(INCRBYFLOAT, DoubleOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(MGET, ValueListOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(ZINCRBY, DoubleOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(ZSCORE, DoubleOutput.class);

            // MAP
            COMMAND_OUTPUT_TYPE_MAPPING.put(HGETALL, MapOutput.class);

            // KEY LIST
            COMMAND_OUTPUT_TYPE_MAPPING.put(HKEYS, KeyListOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(KEYS, KeyListOutput.class);

            // KEY VALUE
            COMMAND_OUTPUT_TYPE_MAPPING.put(BRPOP, KeyValueOutput.class);

            // SINGLE VALUE
            COMMAND_OUTPUT_TYPE_MAPPING.put(BRPOPLPUSH, ValueOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(ECHO, ValueOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(GET, ValueOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(GETRANGE, ValueOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(GETSET, ValueOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(HGET, ValueOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(LINDEX, ValueOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(LPOP, ValueOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(RANDOMKEY, ValueOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(RENAME, ValueOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(RPOP, ValueOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(RPOPLPUSH, ValueOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(SPOP, ValueOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(SRANDMEMBER, ValueOutput.class);

            // STATUS VALUE
            COMMAND_OUTPUT_TYPE_MAPPING.put(BGREWRITEAOF, StatusOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(BGSAVE, StatusOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(CLIENT, StatusOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(DEBUG, StatusOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(DISCARD, StatusOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(FLUSHALL, StatusOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(FLUSHDB, StatusOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(HMSET, StatusOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(INFO, StatusOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(LSET, StatusOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(LTRIM, StatusOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(MIGRATE, StatusOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(MSET, StatusOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(QUIT, StatusOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(RESTORE, StatusOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(SAVE, StatusOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(SELECT, StatusOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(SET, StatusOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(SETEX, StatusOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(SHUTDOWN, StatusOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(SLAVEOF, StatusOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(SYNC, StatusOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(TYPE, StatusOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(WATCH, StatusOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(UNWATCH, StatusOutput.class);

            // VALUE LIST
            COMMAND_OUTPUT_TYPE_MAPPING.put(HMGET, ValueListOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(MGET, ValueListOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(HVALS, ValueListOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(LRANGE, ValueListOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(SORT, ValueListOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(ZRANGE, ValueListOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(ZRANGEBYSCORE, ValueListOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(ZREVRANGE, ValueListOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(ZREVRANGEBYSCORE, ValueListOutput.class);

            // BOOLEAN
            COMMAND_OUTPUT_TYPE_MAPPING.put(EXISTS, BooleanOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(EXPIRE, BooleanOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(EXPIREAT, BooleanOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(HEXISTS, BooleanOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(HSET, BooleanOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(HSETNX, BooleanOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(MOVE, BooleanOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(MSETNX, BooleanOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(PERSIST, BooleanOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(PEXPIRE, BooleanOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(PEXPIREAT, BooleanOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(RENAMENX, BooleanOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(SETNX, BooleanOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(SISMEMBER, BooleanOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(SMOVE, BooleanOutput.class);

            // MULTI
            COMMAND_OUTPUT_TYPE_MAPPING.put(EXEC, MultiOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(MULTI, MultiOutput.class);

            // DATE
            COMMAND_OUTPUT_TYPE_MAPPING.put(LASTSAVE, DateOutput.class);

            // VALUE SET
            COMMAND_OUTPUT_TYPE_MAPPING.put(SDIFF, ValueSetOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(SINTER, ValueSetOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(SMEMBERS, ValueSetOutput.class);
            COMMAND_OUTPUT_TYPE_MAPPING.put(SUNION, ValueSetOutput.class);
        }

        /**
         * Returns the {@link CommandOutput} mapped for given {@link CommandType} or {@link ByteArrayOutput} as default.
         *
         * @param type
         * @return {@link ByteArrayOutput} as default when no matching {@link CommandOutput} available.
         */
        @SuppressWarnings("rawtypes")
        public CommandOutput getTypeHint(CommandType type) {
            return getTypeHint(type, new ByteArrayOutput<>(CODEC));
        }

        /**
         * Returns the {@link CommandOutput} mapped for given {@link CommandType} given {@link CommandOutput} as default.
         *
         * @param type
         * @return
         */
        @SuppressWarnings("rawtypes")
        public CommandOutput getTypeHint(CommandType type, CommandOutput defaultType) {

            if (type == null || !COMMAND_OUTPUT_TYPE_MAPPING.containsKey(type)) {
                return defaultType;
            }
            CommandOutput<?, ?, ?> outputType = instanciateCommandOutput(COMMAND_OUTPUT_TYPE_MAPPING.get(type));
            return outputType != null ? outputType : defaultType;
        }

        @SuppressWarnings({ "rawtypes", "unchecked" })
        private CommandOutput<?, ?, ?> instanciateCommandOutput(Class<? extends CommandOutput> type) {

            Assert.notNull(type, "Cannot create instance for 'null' type.");
            Constructor<CommandOutput> constructor = CONSTRUCTORS.get(type);
            if (constructor == null) {
                constructor = (Constructor<CommandOutput>) ClassUtils.getConstructorIfAvailable(type,
                        RedisCodec.class);
                CONSTRUCTORS.put(type, constructor);
            }
            return BeanUtils.instantiateClass(constructor, CODEC);
        }
    }

    @RequiredArgsConstructor
    static class LettucePoolConnectionProvider implements LettuceConnectionProvider {

        private final LettucePool pool;

        /*
         * (non-Javadoc)
         * @see org.springframework.data.redis.connection.lettuce.LettuceConnectionProvider#getConnection(java.lang.Class)
         */
        @Override
        public <T extends StatefulConnection<?, ?>> T getConnection(Class<T> connectionType) {
            return connectionType.cast(pool.getResource());
        }

        /*
         * (non-Javadoc)
         * @see org.springframework.data.redis.connection.lettuce.LettuceConnectionProvider#getConnectionAsync(java.lang.Class)
         */
        @Override
        public <T extends StatefulConnection<?, ?>> CompletionStage<T> getConnectionAsync(Class<T> connectionType) {
            throw new UnsupportedOperationException("Async operations not supported!");
        }

        /*
         * (non-Javadoc)
         * @see org.springframework.data.redis.connection.lettuce.LettuceConnectionProvider#release(io.lettuce.core.api.StatefulConnection)
         */
        @Override
        @SuppressWarnings("unchecked")
        public void release(StatefulConnection<?, ?> connection) {

            if (connection.isOpen()) {

                if (connection instanceof StatefulRedisConnection) {
                    StatefulRedisConnection<?, ?> redisConnection = (StatefulRedisConnection<?, ?>) connection;
                    if (redisConnection.isMulti()) {
                        redisConnection.async().discard();
                    }
                }
                pool.returnResource((StatefulConnection<byte[], byte[]>) connection);
            } else {
                pool.returnBrokenResource((StatefulConnection<byte[], byte[]>) connection);
            }
        }
    }
}