org.redisson.command.RedisExecutor.java Source code

Java tutorial

Introduction

Here is the source code for org.redisson.command.RedisExecutor.java

Source

/**
 * Copyright (c) 2013-2019 Nikita Koksharov
 *
 * 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.redisson.command;

import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;

import org.redisson.RedissonReference;
import org.redisson.RedissonShutdownException;
import org.redisson.ScanResult;
import org.redisson.api.RFuture;
import org.redisson.cache.ReferenceCacheMap;
import org.redisson.client.RedisAskException;
import org.redisson.client.RedisConnection;
import org.redisson.client.RedisException;
import org.redisson.client.RedisLoadingException;
import org.redisson.client.RedisMovedException;
import org.redisson.client.RedisResponseTimeoutException;
import org.redisson.client.RedisTimeoutException;
import org.redisson.client.RedisTryAgainException;
import org.redisson.client.WriteRedisConnectionException;
import org.redisson.client.codec.BaseCodec;
import org.redisson.client.codec.Codec;
import org.redisson.client.protocol.CommandData;
import org.redisson.client.protocol.CommandsData;
import org.redisson.client.protocol.RedisCommand;
import org.redisson.client.protocol.RedisCommands;
import org.redisson.client.protocol.ScoredEntry;
import org.redisson.client.protocol.decoder.ListScanResult;
import org.redisson.client.protocol.decoder.MapScanResult;
import org.redisson.connection.ConnectionManager;
import org.redisson.connection.MasterSlaveConnectionManager;
import org.redisson.connection.NodeSource;
import org.redisson.connection.NodeSource.Redirect;
import org.redisson.liveobject.core.RedissonObjectBuilder;
import org.redisson.misc.LogHelper;
import org.redisson.misc.RPromise;
import org.redisson.misc.RedissonPromise;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.Timeout;
import io.netty.util.TimerTask;
import io.netty.util.concurrent.FutureListener;

/**
 * 
 * @author Nikita Koksharov
 *
 * @param <V> type of value
 * @param <R> type of returned value
 */
@SuppressWarnings({ "NestedIfDepth" })
public class RedisExecutor<V, R> {

    static final Logger log = LoggerFactory.getLogger(RedisExecutor.class);

    final boolean readOnlyMode;
    final RedisCommand<V> command;
    final Object[] params;
    final RPromise<R> mainPromise;
    final boolean ignoreRedirect;
    final RedissonObjectBuilder objectBuilder;
    final ConnectionManager connectionManager;

    NodeSource source;
    Codec codec;
    volatile int attempt;
    volatile Timeout timeout;
    volatile BiConsumer<R, Throwable> mainPromiseListener;
    volatile ChannelFuture writeFuture;
    volatile RedisException exception;

    int attempts;
    long retryInterval;
    long responseTimeout;

    public RedisExecutor(boolean readOnlyMode, NodeSource source, Codec codec, RedisCommand<V> command,
            Object[] params, RPromise<R> mainPromise, boolean ignoreRedirect, ConnectionManager connectionManager,
            RedissonObjectBuilder objectBuilder) {
        super();
        this.readOnlyMode = readOnlyMode;
        this.source = source;
        this.codec = codec;
        this.command = command;
        this.params = params;
        this.mainPromise = mainPromise;
        this.ignoreRedirect = ignoreRedirect;
        this.connectionManager = connectionManager;
        this.objectBuilder = objectBuilder;

        this.attempts = connectionManager.getConfig().getRetryAttempts();
        this.retryInterval = connectionManager.getConfig().getRetryInterval();
        this.responseTimeout = connectionManager.getConfig().getTimeout();
    }

    public void execute() {
        if (mainPromise.isCancelled()) {
            free();
            return;
        }

        if (!connectionManager.getShutdownLatch().acquire()) {
            free();
            mainPromise.tryFailure(new RedissonShutdownException("Redisson is shutdown"));
            return;
        }

        codec = getCodec(codec);

        RFuture<RedisConnection> connectionFuture = getConnection();

        RPromise<R> attemptPromise = new RedissonPromise<R>();
        mainPromiseListener = (r, e) -> {
            if (mainPromise.isCancelled() && connectionFuture.cancel(false)) {
                log.debug("Connection obtaining canceled for {}", command);
                timeout.cancel();
                if (attemptPromise.cancel(false)) {
                    free();
                }
            }
        };

        if (attempt == 0) {
            mainPromise.onComplete((r, e) -> {
                if (this.mainPromiseListener != null) {
                    this.mainPromiseListener.accept(r, e);
                }
            });
        }

        scheduleRetryTimeout(connectionFuture, attemptPromise);

        connectionFuture.onComplete((connection, e) -> {
            if (connectionFuture.isCancelled()) {
                connectionManager.getShutdownLatch().release();
                return;
            }

            if (!connectionFuture.isSuccess()) {
                connectionManager.getShutdownLatch().release();
                exception = convertException(connectionFuture);
                return;
            }

            if (attemptPromise.isDone() || mainPromise.isDone()) {
                releaseConnection(attemptPromise, connectionFuture);
                return;
            }

            sendCommand(attemptPromise, connection);

            writeFuture.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    checkWriteFuture(writeFuture, attemptPromise, connection);
                }
            });

            releaseConnection(attemptPromise, connectionFuture);
        });

        attemptPromise.onComplete((r, e) -> {
            checkAttemptPromise(attemptPromise, connectionFuture);
        });
    }

    private void scheduleRetryTimeout(RFuture<RedisConnection> connectionFuture, RPromise<R> attemptPromise) {
        if (retryInterval == 0 || attempts == 0) {
            this.timeout = MasterSlaveConnectionManager.DUMMY_TIMEOUT;
            return;
        }

        TimerTask retryTimerTask = new TimerTask() {

            @Override
            public void run(Timeout t) throws Exception {
                if (attemptPromise.isDone()) {
                    return;
                }

                if (connectionFuture.cancel(false)) {
                    if (exception == null) {
                        exception = new RedisTimeoutException(
                                "Unable to get connection! Try to increase 'nettyThreads' and/or connection pool size settings"
                                        + "Node source: " + source + ", command: "
                                        + LogHelper.toString(command, params) + " after " + attempt
                                        + " retry attempts");
                    }
                } else {
                    if (connectionFuture.isSuccess()) {
                        if (writeFuture == null || !writeFuture.isDone()) {
                            if (attempt == attempts) {
                                if (writeFuture != null && writeFuture.cancel(false)) {
                                    if (exception == null) {
                                        exception = new RedisTimeoutException(
                                                "Unable to send command! Try to increase 'nettyThreads' and/or connection pool size settings "
                                                        + "Node source: " + source + ", connection: "
                                                        + connectionFuture.getNow() + ", command: "
                                                        + LogHelper.toString(command, params) + " after " + attempts
                                                        + " retry attempts");
                                    }
                                    attemptPromise.tryFailure(exception);
                                }
                                return;
                            }
                            attempt++;

                            Timeout timeout;
                            if (retryInterval > 0 && attempts > 0) {
                                timeout = connectionManager.newTimeout(this, retryInterval, TimeUnit.MILLISECONDS);
                            } else {
                                timeout = MasterSlaveConnectionManager.DUMMY_TIMEOUT;
                            }
                            RedisExecutor.this.timeout = timeout;
                            return;
                        }

                        if (writeFuture.isDone() && writeFuture.isSuccess()) {
                            return;
                        }
                    }
                }

                if (mainPromise.isCancelled()) {
                    if (attemptPromise.cancel(false)) {
                        free();
                    }
                    return;
                }

                if (attempt == attempts) {
                    if (exception == null) {
                        exception = new RedisTimeoutException(
                                "Unable to send command! Try to increase 'nettyThreads' and/or connection pool size settings. Node source: "
                                        + source + ", command: " + LogHelper.toString(command, params) + " after "
                                        + attempts + " retry attempts");
                    }
                    attemptPromise.tryFailure(exception);
                    return;
                }
                if (!attemptPromise.cancel(false)) {
                    return;
                }

                attempt++;
                if (log.isDebugEnabled()) {
                    log.debug("attempt {} for command {} and params {}", attempt, command,
                            LogHelper.toString(params));
                }

                mainPromiseListener = null;

                execute();
            }

        };

        timeout = connectionManager.newTimeout(retryTimerTask, retryInterval, TimeUnit.MILLISECONDS);
    }

    protected void free() {
        free(params);
    }

    protected void free(Object[] params) {
        for (Object obj : params) {
            ReferenceCountUtil.safeRelease(obj);
        }
    }

    private void checkWriteFuture(ChannelFuture future, RPromise<R> attemptPromise, RedisConnection connection) {
        if (future.isCancelled() || attemptPromise.isDone()) {
            return;
        }

        if (!future.isSuccess()) {
            exception = new WriteRedisConnectionException(
                    "Unable to send command! Node source: " + source + ", connection: " + connection + ", command: "
                            + LogHelper.toString(command, params) + " after " + attempt + " retry attempts",
                    future.cause());
            if (attempt == attempts) {
                if (!attemptPromise.tryFailure(exception)) {
                    log.error(exception.getMessage());
                }
            }
            return;
        }

        timeout.cancel();

        long timeoutTime = responseTimeout;
        if (command != null && (RedisCommands.BLOCKING_COMMAND_NAMES.contains(command.getName())
                || RedisCommands.BLOCKING_COMMANDS.contains(command))) {
            Long popTimeout = null;
            if (RedisCommands.BLOCKING_COMMANDS.contains(command)) {
                boolean found = false;
                for (Object param : params) {
                    if (found) {
                        popTimeout = Long.valueOf(param.toString()) / 1000;
                        break;
                    }
                    if ("BLOCK".equals(param)) {
                        found = true;
                    }
                }
            } else {
                popTimeout = Long.valueOf(params[params.length - 1].toString());
            }

            handleBlockingOperations(attemptPromise, connection, popTimeout);
            if (popTimeout == 0) {
                return;
            }
            timeoutTime += popTimeout * 1000;
            // add 1 second due to issue https://github.com/antirez/redis/issues/874
            timeoutTime += 1000;
        }

        long timeoutAmount = timeoutTime;
        TimerTask timeoutTask = new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                if (attempt < attempts) {
                    if (!attemptPromise.cancel(false)) {
                        return;
                    }

                    attempt++;
                    if (log.isDebugEnabled()) {
                        log.debug("attempt {} for command {} and params {}", attempt, command,
                                LogHelper.toString(params));
                    }

                    mainPromiseListener = null;

                    execute();
                    return;
                }

                attemptPromise.tryFailure(new RedisResponseTimeoutException("Redis server response timeout ("
                        + timeoutAmount + " ms) occured" + " after " + attempts + " retry attempts. Command: "
                        + LogHelper.toString(command, params) + ", channel: " + connection.getChannel()));
            }
        };

        timeout = connectionManager.newTimeout(timeoutTask, timeoutTime, TimeUnit.MILLISECONDS);
    }

    private void handleBlockingOperations(RPromise<R> attemptPromise, RedisConnection connection, Long popTimeout) {
        FutureListener<Void> listener = f -> {
            mainPromise.tryFailure(new RedissonShutdownException("Redisson is shutdown"));
        };

        Timeout scheduledFuture;
        if (popTimeout != 0) {
            // handling cases when connection has been lost
            scheduledFuture = connectionManager.newTimeout(new TimerTask() {
                @Override
                public void run(Timeout timeout) throws Exception {
                    if (attemptPromise.trySuccess(null)) {
                        connection.forceFastReconnectAsync();
                    }
                }
            }, popTimeout, TimeUnit.SECONDS);
        } else {
            scheduledFuture = null;
        }

        mainPromise.onComplete((res, e) -> {
            if (scheduledFuture != null) {
                scheduledFuture.cancel();
            }

            synchronized (listener) {
                connectionManager.getShutdownPromise().removeListener(listener);
            }

            // handling cancel operation for blocking commands
            if (mainPromise.isCancelled() && !attemptPromise.isDone()) {
                log.debug("Canceled blocking operation {} used {}", command, connection);
                connection.forceFastReconnectAsync().onComplete((r, ex) -> {
                    attemptPromise.cancel(true);
                });
                return;
            }

            if (e instanceof RedissonShutdownException) {
                attemptPromise.tryFailure(e);
            }
        });

        synchronized (listener) {
            if (!mainPromise.isDone()) {
                connectionManager.getShutdownPromise().addListener(listener);
            }
        }
    }

    protected void checkAttemptPromise(RPromise<R> attemptFuture, RFuture<RedisConnection> connectionFuture) {
        timeout.cancel();
        if (attemptFuture.isCancelled()) {
            return;
        }

        try {
            mainPromiseListener = null;

            if (attemptFuture.cause() instanceof RedisMovedException && !ignoreRedirect) {
                RedisMovedException ex = (RedisMovedException) attemptFuture.cause();
                if (source.getRedirect() == Redirect.MOVED) {
                    mainPromise.tryFailure(new RedisException("MOVED redirection loop detected. Node "
                            + source.getAddr() + " has further redirect to " + ex.getUrl()));
                    return;
                }

                onException();

                source = new NodeSource(ex.getSlot(), connectionManager.applyNatMap(ex.getUrl()), Redirect.MOVED);
                execute();
                return;
            }

            if (attemptFuture.cause() instanceof RedisAskException && !ignoreRedirect) {
                RedisAskException ex = (RedisAskException) attemptFuture.cause();

                onException();

                source = new NodeSource(ex.getSlot(), connectionManager.applyNatMap(ex.getUrl()), Redirect.ASK);
                execute();
                return;
            }

            if (attemptFuture.cause() instanceof RedisLoadingException
                    || attemptFuture.cause() instanceof RedisTryAgainException) {
                if (attempt < attempts) {
                    onException();
                    connectionManager.newTimeout(new TimerTask() {
                        @Override
                        public void run(Timeout timeout) throws Exception {
                            attempt++;
                            execute();
                        }
                    }, Math.min(responseTimeout, 1000), TimeUnit.MILLISECONDS);
                    return;
                }
            }

            free();

            handleResult(attemptFuture, connectionFuture);

        } catch (Exception e) {
            handleError(connectionFuture, e);
        }
    }

    protected void handleResult(RPromise<R> attemptPromise, RFuture<RedisConnection> connectionFuture)
            throws ReflectiveOperationException {
        if (attemptPromise.isSuccess()) {
            R res = attemptPromise.getNow();
            if (res instanceof ScanResult) {
                ((ScanResult) res).setRedisClient(connectionFuture.getNow().getRedisClient());
            }

            handleSuccess(mainPromise, connectionFuture, res);
        } else {
            handleError(connectionFuture, attemptPromise.cause());
        }
    }

    protected void onException() {
    }

    protected void handleError(RFuture<RedisConnection> connectionFuture, Throwable cause) {
        mainPromise.tryFailure(cause);
    }

    protected void handleSuccess(RPromise<R> promise, RFuture<RedisConnection> connectionFuture, R res)
            throws ReflectiveOperationException {
        if (objectBuilder != null) {
            handleReference(promise, res);
        } else {
            promise.trySuccess(res);
        }
    }

    private void handleReference(RPromise<R> promise, R res) throws ReflectiveOperationException {
        promise.trySuccess((R) tryHandleReference(objectBuilder, res));
    }

    public static Object tryHandleReference(RedissonObjectBuilder objectBuilder, Object o)
            throws ReflectiveOperationException {
        boolean hasConversion = false;
        if (o instanceof List) {
            List<Object> r = (List<Object>) o;
            for (int i = 0; i < r.size(); i++) {
                Object ref = tryHandleReference0(objectBuilder, r.get(i));
                if (ref != r.get(i)) {
                    r.set(i, ref);
                }
            }
            return o;
        } else if (o instanceof Set) {
            Set<Object> set = (Set<Object>) o;
            Set<Object> r = (Set<Object>) o;
            boolean useNewSet = o instanceof LinkedHashSet;
            try {
                set = (Set<Object>) o.getClass().getConstructor().newInstance();
            } catch (Exception exception) {
                set = new LinkedHashSet<Object>();
            }
            for (Object i : r) {
                Object ref = tryHandleReference0(objectBuilder, i);
                //Not testing for ref changes because r.add(ref) below needs to
                //fail on the first iteration to be able to perform fall back 
                //if failure happens.
                //
                //Assuming the failure reason is systematic such as put method
                //is not supported or implemented, and not an occasional issue 
                //like only one element fails.
                if (useNewSet) {
                    set.add(ref);
                } else {
                    try {
                        r.add(ref);
                        set.add(i);
                    } catch (Exception e) {
                        //r is not supporting add operation, like 
                        //LinkedHashMap$LinkedEntrySet and others.
                        //fall back to use a new set.
                        useNewSet = true;
                        set.add(ref);
                    }
                }
                hasConversion |= ref != i;
            }

            if (!hasConversion) {
                return o;
            } else if (useNewSet) {
                return set;
            } else if (!set.isEmpty()) {
                r.removeAll(set);
            }
            return o;
        } else if (o instanceof Map) {
            Map<Object, Object> r = (Map<Object, Object>) o;
            for (Map.Entry<Object, Object> e : r.entrySet()) {
                if (e.getKey() instanceof RedissonReference || e.getValue() instanceof RedissonReference) {
                    Object key = e.getKey();
                    Object value = e.getValue();
                    if (e.getKey() instanceof RedissonReference) {
                        key = fromReference(objectBuilder, e.getKey());
                        r.remove(e.getKey());
                    }
                    if (e.getValue() instanceof RedissonReference) {
                        value = fromReference(objectBuilder, e.getValue());
                    }
                    r.put(key, value);
                }
            }

            return o;
        } else if (o instanceof ListScanResult) {
            tryHandleReference(objectBuilder, ((ListScanResult) o).getValues());
            return o;
        } else if (o instanceof MapScanResult) {
            MapScanResult scanResult = (MapScanResult) o;
            Map oldMap = ((MapScanResult) o).getMap();
            Map map = (Map) tryHandleReference(objectBuilder, oldMap);
            if (map != oldMap) {
                MapScanResult<Object, Object> newScanResult = new MapScanResult<Object, Object>(scanResult.getPos(),
                        map);
                newScanResult.setRedisClient(scanResult.getRedisClient());
                return newScanResult;
            } else {
                return o;
            }
        } else {
            return tryHandleReference0(objectBuilder, o);
        }
    }

    private static Object tryHandleReference0(RedissonObjectBuilder objectBuilder, Object o)
            throws ReflectiveOperationException {
        if (o instanceof RedissonReference) {
            return fromReference(objectBuilder, o);
        } else if (o instanceof ScoredEntry && ((ScoredEntry) o).getValue() instanceof RedissonReference) {
            ScoredEntry<?> se = (ScoredEntry<?>) o;
            return new ScoredEntry(se.getScore(), fromReference(objectBuilder, se.getValue()));
        } else if (o instanceof Map.Entry) {
            Map.Entry old = (Map.Entry) o;
            Object key = tryHandleReference0(objectBuilder, old.getKey());
            Object value = tryHandleReference0(objectBuilder, old.getValue());
            if (value != old.getValue() || key != old.getKey()) {
                return new AbstractMap.SimpleEntry(key, value);
            }
        }
        return o;
    }

    private static Object fromReference(RedissonObjectBuilder objectBuilder, Object res)
            throws ReflectiveOperationException {
        if (objectBuilder == null) {
            return res;
        }

        return objectBuilder.fromReference((RedissonReference) res);
    }

    protected void sendCommand(RPromise<R> attemptPromise, RedisConnection connection) {
        if (source.getRedirect() == Redirect.ASK) {
            List<CommandData<?, ?>> list = new ArrayList<CommandData<?, ?>>(2);
            RPromise<Void> promise = new RedissonPromise<Void>();
            list.add(new CommandData<Void, Void>(promise, codec, RedisCommands.ASKING, new Object[] {}));
            list.add(new CommandData<V, R>(attemptPromise, codec, command, params));
            RPromise<Void> main = new RedissonPromise<Void>();
            writeFuture = connection.send(new CommandsData(main, list, false));
        } else {
            if (log.isDebugEnabled()) {
                log.debug("acquired connection for command {} and params {} from slot {} using node {}... {}",
                        command, LogHelper.toString(params), source, connection.getRedisClient().getAddr(),
                        connection);
            }
            writeFuture = connection.send(new CommandData<V, R>(attemptPromise, codec, command, params));
        }
    }

    protected void releaseConnection(RPromise<R> attemptPromise, RFuture<RedisConnection> connectionFuture) {
        attemptPromise.onComplete((res, e) -> {
            if (!connectionFuture.isSuccess()) {
                return;
            }

            RedisConnection connection = connectionFuture.getNow();
            connectionManager.getShutdownLatch().release();
            if (readOnlyMode) {
                connectionManager.releaseRead(source, connection);
            } else {
                connectionManager.releaseWrite(source, connection);
            }

            if (log.isDebugEnabled()) {
                log.debug("connection released for command {} and params {} from slot {} using connection {}",
                        command, LogHelper.toString(params), source, connection);
            }
        });
    }

    protected RFuture<RedisConnection> getConnection() {
        RFuture<RedisConnection> connectionFuture;
        if (readOnlyMode) {
            connectionFuture = connectionManager.connectionReadOp(source, command);
        } else {
            connectionFuture = connectionManager.connectionWriteOp(source, command);
        }
        return connectionFuture;
    }

    private static final Map<ClassLoader, Map<Codec, Codec>> CODECS = ReferenceCacheMap.soft(0, 0);

    protected Codec getCodec(Codec codec) {
        if (codec == null) {
            return codec;
        }

        for (Class<?> clazz : BaseCodec.SKIPPED_CODECS) {
            if (clazz.isAssignableFrom(codec.getClass())) {
                return codec;
            }
        }

        Codec codecToUse = codec;
        ClassLoader threadClassLoader = Thread.currentThread().getContextClassLoader();
        if (threadClassLoader != null) {
            Map<Codec, Codec> map = CODECS.get(threadClassLoader);
            if (map == null) {
                synchronized (CODECS) {
                    map = CODECS.get(threadClassLoader);
                    if (map == null) {
                        map = new ConcurrentHashMap<Codec, Codec>();
                        CODECS.put(threadClassLoader, map);
                    }
                }
            }
            codecToUse = map.get(codec);
            if (codecToUse == null) {
                try {
                    codecToUse = codec.getClass().getConstructor(ClassLoader.class, codec.getClass())
                            .newInstance(threadClassLoader, codec);
                } catch (NoSuchMethodException e) {
                    codecToUse = codec;
                    // skip
                } catch (Exception e) {
                    throw new IllegalStateException(e);
                }
                map.put(codec, codecToUse);
            }
        }
        return codecToUse;
    }

    protected <T> RedisException convertException(RFuture<T> future) {
        if (future.cause() instanceof RedisException) {
            return (RedisException) future.cause();
        }
        return new RedisException("Unexpected exception while processing command", future.cause());
    }

}