All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.redisson.command.CommandBatchService Maven / Gradle / Ivy

/**
 * Copyright 2018 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.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Deque;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import org.redisson.RedissonShutdownException;
import org.redisson.api.BatchOptions;
import org.redisson.api.BatchOptions.ExecutionMode;
import org.redisson.api.BatchResult;
import org.redisson.api.RFuture;
import org.redisson.client.RedisAskException;
import org.redisson.client.RedisConnection;
import org.redisson.client.RedisLoadingException;
import org.redisson.client.RedisMovedException;
import org.redisson.client.RedisTimeoutException;
import org.redisson.client.RedisTryAgainException;
import org.redisson.client.WriteRedisConnectionException;
import org.redisson.client.codec.Codec;
import org.redisson.client.codec.StringCodec;
import org.redisson.client.protocol.BatchCommandData;
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.connection.ConnectionManager;
import org.redisson.connection.MasterSlaveEntry;
import org.redisson.connection.NodeSource;
import org.redisson.connection.NodeSource.Redirect;
import org.redisson.misc.CountableListener;
import org.redisson.misc.RPromise;
import org.redisson.misc.RedissonPromise;
import org.redisson.pubsub.AsyncSemaphore;

import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.util.Timeout;
import io.netty.util.TimerTask;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.FutureListener;
import io.netty.util.internal.PlatformDependent;

/**
 * 
 * @author Nikita Koksharov
 *
 */
public class CommandBatchService extends CommandAsyncService {

    public static class ConnectionEntry {

        boolean firstCommand = true;
        RFuture connectionFuture;
        
        public RFuture getConnectionFuture() {
            return connectionFuture;
        }
        
        public void setConnectionFuture(RFuture connectionFuture) {
            this.connectionFuture = connectionFuture;
        }

        public boolean isFirstCommand() {
            return firstCommand;
        }

        public void setFirstCommand(boolean firstCommand) {
            this.firstCommand = firstCommand;
        }
        
    }
    
    public static class Entry {

        Deque> commands = new LinkedBlockingDeque>();
        volatile boolean readOnlyMode = true;

        public Deque> getCommands() {
            return commands;
        }

        public void setReadOnlyMode(boolean readOnlyMode) {
            this.readOnlyMode = readOnlyMode;
        }

        public boolean isReadOnlyMode() {
            return readOnlyMode;
        }
        

        public void clearErrors() {
            for (BatchCommandData commandEntry : commands) {
                commandEntry.clearError();
            }
        }

    }

    private final AtomicInteger index = new AtomicInteger();

    private ConcurrentMap commands = PlatformDependent.newConcurrentHashMap();
    private ConcurrentMap connections = PlatformDependent.newConcurrentHashMap();
    
    private BatchOptions options;
    
    private Map, List> nestedServices = PlatformDependent.newConcurrentHashMap();

    private AtomicBoolean executed = new AtomicBoolean();

    public CommandBatchService(ConnectionManager connectionManager) {
        super(connectionManager);
    }
    
    public CommandBatchService(ConnectionManager connectionManager, BatchOptions options) {
        super(connectionManager);
        this.options = options;
    }
    
    public BatchOptions getOptions() {
        return options;
    }

    public void add(RFuture future, List services) {
        nestedServices.put(future, services);
    }
    
    @Override
    protected  void async(boolean readOnlyMode, NodeSource nodeSource,
            Codec codec, RedisCommand command, Object[] params, RPromise mainPromise, int attempt, boolean ignoreRedirect, RFuture connFuture) {
        if (executed.get()) {
            throw new IllegalStateException("Batch already has been executed!");
        }
        if (nodeSource.getEntry() != null) {
            Entry entry = commands.get(nodeSource.getEntry());
            if (entry == null) {
                entry = new Entry();
                Entry oldEntry = commands.putIfAbsent(nodeSource.getEntry(), entry);
                if (oldEntry != null) {
                    entry = oldEntry;
                }
            }
            
            if (!readOnlyMode) {
                entry.setReadOnlyMode(false);
            }
            
            Object[] batchParams = null;
            if (!isRedisBasedQueue()) {
                batchParams = params;
            }
            BatchCommandData commandData = new BatchCommandData(mainPromise, codec, command, batchParams, index.incrementAndGet());
            entry.getCommands().add(commandData);
        }
        
        if (!isRedisBasedQueue()) {
            return;
        }
        
        if (!readOnlyMode && this.options.getExecutionMode() == ExecutionMode.REDIS_READ_ATOMIC) {
            throw new IllegalStateException("Data modification commands can't be used with queueStore=REDIS_READ_ATOMIC");
        }
        
        super.async(readOnlyMode, nodeSource, codec, command, params, mainPromise, attempt, true, connFuture);
    }
    
    AsyncSemaphore semaphore = new AsyncSemaphore(0);
    
    @Override
    protected  RPromise createPromise() {
        if (isRedisBasedQueue()) {
            return new BatchPromise(executed);
        }
        return super.createPromise();
    }
    
    @Override
    protected  void releaseConnection(NodeSource source, RFuture connectionFuture,
            boolean isReadOnly, RPromise attemptPromise, AsyncDetails details) {
        if (!isRedisBasedQueue() || RedisCommands.EXEC.getName().equals(details.getCommand().getName())) {
            super.releaseConnection(source, connectionFuture, isReadOnly, attemptPromise, details);
        }
    }
    
    @Override
    protected  void handleSuccess(final AsyncDetails details, RPromise promise, RedisCommand command, R res) {
        if (RedisCommands.EXEC.getName().equals(command.getName())) {
            super.handleSuccess(details, promise, command, res);
            return;
        }
        if (RedisCommands.DISCARD.getName().equals(command.getName())) {
            super.handleSuccess(details, promise, command, null);
            if (executed.compareAndSet(false, true)) {
                details.getConnectionFuture().getNow().forceFastReconnectAsync().addListener(new FutureListener() {
                    @Override
                    public void operationComplete(Future future) throws Exception {
                        CommandBatchService.super.releaseConnection(details.getSource(), details.getConnectionFuture(), details.isReadOnlyMode(), details.getAttemptPromise(), details);
                    }
                });
            }
            return;
        }

        if (isRedisBasedQueue()) {
            BatchPromise batchPromise = (BatchPromise) promise;
            RPromise sentPromise = (RPromise) batchPromise.getSentPromise();
            super.handleSuccess(details, sentPromise, command, null);
            semaphore.release();
        }
    }
    
    @Override
    protected  void handleError(final AsyncDetails details, RPromise promise, Throwable cause) {
        if (isRedisBasedQueue() && promise instanceof BatchPromise) {
            BatchPromise batchPromise = (BatchPromise) promise;
            RPromise sentPromise = (RPromise) batchPromise.getSentPromise();
            sentPromise.tryFailure(cause);
            promise.tryFailure(cause);
            if (executed.compareAndSet(false, true)) {
                details.getConnectionFuture().getNow().forceFastReconnectAsync().addListener(new FutureListener() {
                    @Override
                    public void operationComplete(Future future) throws Exception {
                        CommandBatchService.super.releaseConnection(details.getSource(), details.getConnectionFuture(), details.isReadOnlyMode(), details.getAttemptPromise(), details);
                    }
                });
            }
            semaphore.release();
            return;
        }

        super.handleError(details, promise, cause);
    }
    
    @Override
    protected  void sendCommand(AsyncDetails details, RedisConnection connection) {
        if (!isRedisBasedQueue()) {
            super.sendCommand(details, connection);
            return;
        }
        
        ConnectionEntry connectionEntry = connections.get(details.getSource().getEntry());
        
        if (details.getSource().getRedirect() == Redirect.ASK) {
            List> list = new ArrayList>(2);
            RPromise promise = new RedissonPromise();
            list.add(new CommandData(promise, details.getCodec(), RedisCommands.ASKING, new Object[]{}));
            if (connectionEntry.isFirstCommand()) {
                list.add(new CommandData(promise, details.getCodec(), RedisCommands.MULTI, new Object[]{}));
                connectionEntry.setFirstCommand(false);
            }
            list.add(new CommandData(details.getAttemptPromise(), details.getCodec(), details.getCommand(), details.getParams()));
            RPromise main = new RedissonPromise();
            ChannelFuture future = connection.send(new CommandsData(main, list, true));
            details.setWriteFuture(future);
        } else {
            if (log.isDebugEnabled()) {
                log.debug("acquired connection for command {} and params {} from slot {} using node {}... {}",
                        details.getCommand(), Arrays.toString(details.getParams()), details.getSource(), connection.getRedisClient().getAddr(), connection);
            }
            
            if (connectionEntry.isFirstCommand()) {
                List> list = new ArrayList>(2);
                list.add(new CommandData(new RedissonPromise(), details.getCodec(), RedisCommands.MULTI, new Object[]{}));
                list.add(new CommandData(details.getAttemptPromise(), details.getCodec(), details.getCommand(), details.getParams()));
                RPromise main = new RedissonPromise();
                ChannelFuture future = connection.send(new CommandsData(main, list, true));
                connectionEntry.setFirstCommand(false);
                details.setWriteFuture(future);
            } else {
                if (RedisCommands.EXEC.getName().equals(details.getCommand().getName())) {
                    Entry entry = commands.get(details.getSource().getEntry());

                    List> list = new LinkedList>();

                    if (options.isSkipResult()) {
                        list.add(new CommandData(new RedissonPromise(), details.getCodec(), RedisCommands.CLIENT_REPLY, new Object[]{ "OFF" }));
                    }
                    
                    list.add(new CommandData(details.getAttemptPromise(), details.getCodec(), details.getCommand(), details.getParams()));
                    
                    if (options.isSkipResult()) {
                        list.add(new CommandData(new RedissonPromise(), details.getCodec(), RedisCommands.CLIENT_REPLY, new Object[]{ "ON" }));
                    }
                    if (options.getSyncSlaves() > 0) {
                        BatchCommandData waitCommand = new BatchCommandData(RedisCommands.WAIT, 
                                new Object[] { this.options.getSyncSlaves(), this.options.getSyncTimeout() }, index.incrementAndGet());
                        list.add(waitCommand);
                        entry.getCommands().add(waitCommand);
                    }

                    RPromise main = new RedissonPromise();
                    ChannelFuture future = connection.send(new CommandsData(main, list, new ArrayList(entry.getCommands())));
                    details.setWriteFuture(future);
                } else {
                    RPromise main = new RedissonPromise();
                    List> list = new LinkedList>();
                    list.add(new CommandData(details.getAttemptPromise(), details.getCodec(), details.getCommand(), details.getParams()));
                    ChannelFuture future = connection.send(new CommandsData(main, list, true));
                    details.setWriteFuture(future);
                }
            }
        }
    }
    
    @Override
    protected  RFuture getConnection(boolean readOnlyMode, NodeSource source,
            RedisCommand command) {
        if (!isRedisBasedQueue()) {
            return super.getConnection(readOnlyMode, source, command);
        }
        
        ConnectionEntry entry = connections.get(source.getEntry());
        if (entry == null) {
            entry = new ConnectionEntry();
            ConnectionEntry oldEntry = connections.putIfAbsent(source.getEntry(), entry);
            if (oldEntry != null) {
                entry = oldEntry;
            }
        }

        
        if (entry.getConnectionFuture() != null) {
            return entry.getConnectionFuture();
        }
        
        synchronized (this) {
            if (entry.getConnectionFuture() != null) {
                return entry.getConnectionFuture();
            }
        
            RFuture connectionFuture;
            if (this.options.getExecutionMode() == ExecutionMode.REDIS_WRITE_ATOMIC) {
                connectionFuture = connectionManager.connectionWriteOp(source, null);
            } else {
                connectionFuture = connectionManager.connectionReadOp(source, null);
            }
            connectionFuture.syncUninterruptibly();
            entry.setConnectionFuture(connectionFuture);
            return connectionFuture;
        }
    }

    public BatchResult execute() {
        RFuture> f = executeAsync(BatchOptions.defaults());
        return get(f);
    }
    
    public BatchResult execute(BatchOptions options) {
        RFuture> f = executeAsync(options);
        return get(f);
    }

    public RFuture executeAsyncVoid() {
        final RedissonPromise promise = new RedissonPromise();
        RFuture> res = executeAsync(BatchOptions.defaults());
        res.addListener(new FutureListener>() {
            @Override
            public void operationComplete(Future> future) throws Exception {
                if (future.isSuccess()) {
                    promise.trySuccess(null);
                } else {
                    promise.tryFailure(future.cause());
                }
            }
        });
        return promise;
    }
    
    public RFuture> executeAsync() {
        return executeAsync(BatchOptions.defaults());
    }
    
    public  RFuture executeAsync(BatchOptions options) {
        if (executed.get()) {
            throw new IllegalStateException("Batch already executed!");
        }
        
        if (commands.isEmpty()) {
            executed.set(true);
            BatchResult result = new BatchResult(Collections.emptyList(), 0);
            return (RFuture) RedissonPromise.newSucceededFuture(result);
        }
        
        if (this.options == null) {
            this.options = options;
        }
        
        if (isRedisBasedQueue()) {
            int permits = 0;
            for (Entry entry : commands.values()) {
                permits += entry.getCommands().size();
            };
            
            final RPromise resultPromise = new RedissonPromise();
            semaphore.acquire(new Runnable() {
                @Override
                public void run() {
                    for (Entry entry : commands.values()) {
                        for (BatchCommandData command : entry.getCommands()) {
                            if (command.getPromise().isDone() && !command.getPromise().isSuccess()) {
                                resultPromise.tryFailure(command.getPromise().cause());
                                break;
                            }
                        }
                    }
                    
                    if (resultPromise.isDone()) {
                        return;
                    }
                    
                    final RPromise>> mainPromise = new RedissonPromise>>();
                    final Map> result = new ConcurrentHashMap>();
                    final CountableListener>> listener = new CountableListener>>(mainPromise, result);
                    listener.setCounter(connections.size());
                    for (final Map.Entry entry : commands.entrySet()) {
                        ConnectionEntry connection = connections.get(entry.getKey());
                        final RPromise> execPromise = new RedissonPromise>();
                        async(false, new NodeSource(entry.getKey()), connectionManager.getCodec(), RedisCommands.EXEC, 
                                new Object[] {}, execPromise, 0, false, connection.getConnectionFuture());
                        execPromise.addListener(new FutureListener>() {
                            @Override
                            public void operationComplete(Future> future) throws Exception {
                                if (!future.isSuccess()) {
                                    mainPromise.tryFailure(future.cause());
                                    return;
                                }

                                BatchCommandData lastCommand = (BatchCommandData) entry.getValue().getCommands().peekLast();
                                result.put(entry.getKey(), future.getNow());
                                if (RedisCommands.WAIT.getName().equals(lastCommand.getCommand().getName())) {
                                    lastCommand.getPromise().addListener(new FutureListener() {
                                        @Override
                                        public void operationComplete(Future ft) throws Exception {
                                            if (!ft.isSuccess()) {
                                                mainPromise.tryFailure(ft.cause());
                                                return;
                                            }
                                            
                                            execPromise.addListener(listener);
                                        }
                                    });
                                } else {
                                    execPromise.addListener(listener);
                                }
                            }
                        });
                    }
                    
                    executed.set(true);

                    mainPromise.addListener(new FutureListener>>() {
                        @Override
                        public void operationComplete(Future>> future) throws Exception {
                            if (!future.isSuccess()) {
                                resultPromise.tryFailure(future.cause());
                                return;
                            }
                            
                            try {
                                for (java.util.Map.Entry> entry : future.getNow().entrySet()) {
                                    Entry commandEntry = commands.get(entry.getKey());
                                    Iterator resultIter = entry.getValue().iterator();
                                    for (BatchCommandData data : commandEntry.getCommands()) {
                                        if (data.getCommand().getName().equals(RedisCommands.EXEC.getName())) {
                                            break;
                                        }
                                        RPromise promise = (RPromise) data.getPromise();
                                        promise.trySuccess(resultIter.next());
                                    }
                                }
                                
                                List entries = new ArrayList();
                                for (Entry e : commands.values()) {
                                    entries.addAll(e.getCommands());
                                }
                                Collections.sort(entries);
                                List responses = new ArrayList(entries.size());
                                int syncedSlaves = 0;
                                for (BatchCommandData commandEntry : entries) {
                                    if (isWaitCommand(commandEntry)) {
                                        syncedSlaves += (Integer) commandEntry.getPromise().getNow();
                                    } else if (!commandEntry.getCommand().getName().equals(RedisCommands.MULTI.getName())
                                            && !commandEntry.getCommand().getName().equals(RedisCommands.EXEC.getName())) {
                                        Object entryResult = commandEntry.getPromise().getNow();
                                        entryResult = tryHandleReference(entryResult);
                                        responses.add(entryResult);
                                    }
                                }
                                BatchResult result = new BatchResult(responses, syncedSlaves);
                                resultPromise.trySuccess((R)result);
                            } catch (Exception e) {
                                resultPromise.tryFailure(e);
                            }
                            
                            commands = null;
                        }
                    });
                }
            }, permits);
            return resultPromise;
        }

        executed.set(true);
        
        if (this.options.getExecutionMode() != ExecutionMode.IN_MEMORY) {
            for (Entry entry : commands.values()) {
                BatchCommandData multiCommand = new BatchCommandData(RedisCommands.MULTI, new Object[] {}, index.incrementAndGet());
                entry.getCommands().addFirst(multiCommand);
                BatchCommandData execCommand = new BatchCommandData(RedisCommands.EXEC, new Object[] {}, index.incrementAndGet());
                entry.getCommands().add(execCommand);
            }
        }
        
        if (this.options.isSkipResult()) {
            for (Entry entry : commands.values()) {
                BatchCommandData offCommand = new BatchCommandData(RedisCommands.CLIENT_REPLY, new Object[] { "OFF" }, index.incrementAndGet());
                entry.getCommands().addFirst(offCommand);
                BatchCommandData onCommand = new BatchCommandData(RedisCommands.CLIENT_REPLY, new Object[] { "ON" }, index.incrementAndGet());
                entry.getCommands().add(onCommand);
            }
        }
        
        if (this.options.getSyncSlaves() > 0) {
            for (Entry entry : commands.values()) {
                BatchCommandData waitCommand = new BatchCommandData(RedisCommands.WAIT, 
                                    new Object[] { this.options.getSyncSlaves(), this.options.getSyncTimeout() }, index.incrementAndGet());
                entry.getCommands().add(waitCommand);
            }
        }
        
        RPromise resultPromise;
        final RPromise voidPromise = new RedissonPromise();
        if (this.options.isSkipResult()) {
            voidPromise.addListener(new FutureListener() {
                @Override
                public void operationComplete(Future future) throws Exception {
//                    commands = null;
                    nestedServices.clear();
                }
            });
            resultPromise = (RPromise) voidPromise;
        } else {
            final RPromise promise = new RedissonPromise();
            voidPromise.addListener(new FutureListener() {
                @Override
                public void operationComplete(Future future) throws Exception {
                    if (!future.isSuccess()) {
                        promise.tryFailure(future.cause());
                        commands = null;
                        nestedServices.clear();
                        return;
                    }
                    
                    List entries = new ArrayList();
                    for (Entry e : commands.values()) {
                        entries.addAll(e.getCommands());
                    }
                    Collections.sort(entries);
                    List responses = new ArrayList(entries.size());
                    int syncedSlaves = 0;
                    for (BatchCommandData commandEntry : entries) {
                        if (isWaitCommand(commandEntry)) {
                            syncedSlaves = (Integer) commandEntry.getPromise().getNow();
                        } else if (!commandEntry.getCommand().getName().equals(RedisCommands.MULTI.getName())
                                && !commandEntry.getCommand().getName().equals(RedisCommands.EXEC.getName())) {
                            Object entryResult = commandEntry.getPromise().getNow();
                            entryResult = tryHandleReference(entryResult);
                            responses.add(entryResult);
                        }
                    }
                    
                    BatchResult result = new BatchResult(responses, syncedSlaves);
                    promise.trySuccess(result);
                    
                    commands = null;
                    nestedServices.clear();
                }
            });
            resultPromise = (RPromise) promise;
        }

        final AtomicInteger slots = new AtomicInteger(commands.size());
        
        for (java.util.Map.Entry, List> entry : nestedServices.entrySet()) {
            slots.incrementAndGet();
            for (CommandBatchService service : entry.getValue()) {
                service.executeAsync();
            }
            
            entry.getKey().addListener(new FutureListener() {
                @Override
                public void operationComplete(Future future) throws Exception {
                    handle(voidPromise, slots, future);
                }
            });
        }
        
        for (java.util.Map.Entry e : commands.entrySet()) {
            execute(e.getValue(), new NodeSource(e.getKey()), voidPromise, slots, 0, this.options);
        }
        return resultPromise;
    }

    protected boolean isRedisBasedQueue() {
        return options != null && (this.options.getExecutionMode() == ExecutionMode.REDIS_READ_ATOMIC || this.options.getExecutionMode() == ExecutionMode.REDIS_WRITE_ATOMIC);
    }

    private void execute(final Entry entry, final NodeSource source, final RPromise mainPromise, final AtomicInteger slots, 
            final int attempt, final BatchOptions options) {
        if (mainPromise.isCancelled()) {
            free(entry);
            return;
        }

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

        final RPromise attemptPromise = new RedissonPromise();

        final AsyncDetails details = new AsyncDetails();

        final RFuture connectionFuture;
        if (entry.isReadOnlyMode()) {
            connectionFuture = connectionManager.connectionReadOp(source, null);
        } else {
            connectionFuture = connectionManager.connectionWriteOp(source, null);
        }
        
        final int attempts;
        if (options.getRetryAttempts() > 0) {
            attempts = options.getRetryAttempts();
        } else {
            attempts = connectionManager.getConfig().getRetryAttempts();
        }

        final long interval;
        if (options.getRetryInterval() > 0) {
            interval = options.getRetryInterval();
        } else {
            interval = connectionManager.getConfig().getRetryInterval();
        }
        
        final FutureListener mainPromiseListener = new FutureListener() {
            @Override
            public void operationComplete(Future future) throws Exception {
                if (future.isCancelled() && connectionFuture.cancel(false)) {
                    log.debug("Connection obtaining canceled for batch");
                    details.getTimeout().cancel();
                    if (attemptPromise.cancel(false)) {
                        free(entry);
                    }
                }
            }
        };

        final TimerTask retryTimerTask = new TimerTask() {
            @Override
            public void run(Timeout t) throws Exception {
                if (attemptPromise.isDone()) {
                    return;
                }

                if (connectionFuture.cancel(false)) {
                    if (details.getException() == null) {
                        details.setException(new RedisTimeoutException("Unable to get connection! "
                                    + "Node source: " + source + " after " + attempts + " retry attempts"));
                    }
                    connectionManager.getShutdownLatch().release();
                } else {
                    if (connectionFuture.isSuccess()) {
                        if (details.getWriteFuture() == null || !details.getWriteFuture().isDone()) {
                            if (details.getAttempt() == attempts) {
                                if (details.getWriteFuture() != null && details.getWriteFuture().cancel(false)) {
                                    if (details.getException() == null) {
                                        details.setException(new RedisTimeoutException("Unable to send batch after " + attempts + " retry attempts"));
                                    }
                                    attemptPromise.tryFailure(details.getException());
                                }
                                return;
                            }
                            details.incAttempt();
                            Timeout timeout = connectionManager.newTimeout(this, interval, TimeUnit.MILLISECONDS);
                            details.setTimeout(timeout);
                            return;
                        }
                        
                        if (details.getWriteFuture().isDone() && details.getWriteFuture().isSuccess()) {
                            return;
                        }
                    }
                }

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

                if (attempt == attempts) {
                    if (details.getException() == null) {
                        details.setException(new RedisTimeoutException("Batch command execution timeout"));
                    }
                    attemptPromise.tryFailure(details.getException());
                    return;
                }
                if (!attemptPromise.cancel(false)) {
                    return;
                }

                int count = attempt + 1;
                mainPromise.removeListener(mainPromiseListener);
                execute(entry, source, mainPromise, slots, count, options);
            }
        };

        Timeout timeout = connectionManager.newTimeout(retryTimerTask, interval, TimeUnit.MILLISECONDS);
        details.setTimeout(timeout);
        mainPromise.addListener(mainPromiseListener);

        connectionFuture.addListener(new FutureListener() {
            @Override
            public void operationComplete(Future connFuture) throws Exception {
                checkConnectionFuture(entry, source, mainPromise, attemptPromise, details, connectionFuture, options.isSkipResult(), 
                        options.getResponseTimeout(), attempts, options.getExecutionMode());
            }
        });

        attemptPromise.addListener(new FutureListener() {
            @Override
            public void operationComplete(Future future) throws Exception {
                details.getTimeout().cancel();
                if (future.isCancelled()) {
                    return;
                }

                mainPromise.removeListener(mainPromiseListener);
                
                if (future.cause() instanceof RedisMovedException) {
                    RedisMovedException ex = (RedisMovedException)future.cause();
                    entry.clearErrors();
                    NodeSource nodeSource = new NodeSource(ex.getSlot(), ex.getUrl(), Redirect.MOVED);
                    execute(entry, nodeSource, mainPromise, slots, attempt, options);
                    return;
                }
                if (future.cause() instanceof RedisAskException) {
                    RedisAskException ex = (RedisAskException)future.cause();
                    entry.clearErrors();
                    NodeSource nodeSource = new NodeSource(ex.getSlot(), ex.getUrl(), Redirect.ASK);
                    execute(entry, nodeSource, mainPromise, slots, attempt, options);
                    return;
                }
                if (future.cause() instanceof RedisLoadingException) {
                    entry.clearErrors();
                    execute(entry, source, mainPromise, slots, attempt, options);
                    return;
                }
                if (future.cause() instanceof RedisTryAgainException) {
                    entry.clearErrors();
                    connectionManager.newTimeout(new TimerTask() {
                        @Override
                        public void run(Timeout timeout) throws Exception {
                            execute(entry, source, mainPromise, slots, attempt, options);
                        }
                    }, 1, TimeUnit.SECONDS);
                    return;
                }

                free(entry);
                
                handle(mainPromise, slots, future);
            }
        });
    }

    protected void free(final Entry entry) {
        for (BatchCommandData command : entry.getCommands()) {
            free(command.getParams());
        }
    }

    private void checkWriteFuture(Entry entry, final RPromise attemptPromise, AsyncDetails details,
            final RedisConnection connection, ChannelFuture future, long responseTimeout, int attempts) {
        if (future.isCancelled() || attemptPromise.isDone()) {
            return;
        }
        
        if (!future.isSuccess()) {
            details.setException(new WriteRedisConnectionException("Can't write command batch to channel: " + future.channel(), future.cause()));
            if (details.getAttempt() == attempts) {
                attemptPromise.tryFailure(details.getException());
            }
            return;
        }
        
        details.getTimeout().cancel();
        
        TimerTask timerTask = new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                attemptPromise.tryFailure(
                        new RedisTimeoutException("Redis server response timeout during command batch execution. Channel: " + connection.getChannel()));
            }
        };
        
        long timeout = connectionManager.getConfig().getTimeout();
        if (responseTimeout > 0) {
            timeout = responseTimeout;
        }
        Timeout timeoutTask = connectionManager.newTimeout(timerTask, timeout, TimeUnit.MILLISECONDS);
        details.setTimeout(timeoutTask);
    }

    private void checkConnectionFuture(final Entry entry, final NodeSource source,
            final RPromise mainPromise, final RPromise attemptPromise, final AsyncDetails details,
            RFuture connFuture, final boolean noResult, final long responseTimeout, final int attempts, ExecutionMode executionMode) {
        if (connFuture.isCancelled()) {
            return;
        }

        if (!connFuture.isSuccess()) {
            connectionManager.getShutdownLatch().release();
            details.setException(convertException(connFuture));
            return;
        }

        if (attemptPromise.isDone() || mainPromise.isDone()) {
            releaseConnection(source, connFuture, details.isReadOnlyMode(), attemptPromise, details);
            return;
        }
        
        final RedisConnection connection = connFuture.getNow();
        boolean isAtomic = executionMode != ExecutionMode.IN_MEMORY;
        boolean isQueued = executionMode == ExecutionMode.REDIS_READ_ATOMIC || executionMode == ExecutionMode.REDIS_WRITE_ATOMIC;

        List> list = new LinkedList>();
        if (source.getRedirect() == Redirect.ASK) {
            RPromise promise = new RedissonPromise();
            list.add(new CommandData(promise, StringCodec.INSTANCE, RedisCommands.ASKING, new Object[] {}));
        } 
        for (BatchCommandData c : entry.getCommands()) {
            if (c.getPromise().isSuccess() && !isWaitCommand(c) && !isAtomic) {
                // skip successful commands
                continue;
            }
            list.add(c);
        }
        
        ChannelFuture future = connection.send(new CommandsData(attemptPromise, list, noResult, isAtomic, isQueued));
        details.setWriteFuture(future);

        details.getWriteFuture().addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                checkWriteFuture(entry, attemptPromise, details, connection, future, responseTimeout, attempts);
            }
        });

        releaseConnection(source, connFuture, entry.isReadOnlyMode(), attemptPromise, details);
    }

    protected boolean isWaitCommand(BatchCommandData c) {
        return c.getCommand().getName().equals(RedisCommands.WAIT.getName());
    }

    protected void handle(final RPromise mainPromise, final AtomicInteger slots, Future future) {
        if (future.isSuccess()) {
            if (slots.decrementAndGet() == 0) {
                mainPromise.trySuccess(null);
            }
        } else {
            mainPromise.tryFailure(future.cause());
        }
    }

}