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

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

Go to download

Easy Redis Java client and Real-Time Data Platform. Valkey compatible. Sync/Async/RxJava3/Reactive API. Client side caching. Over 50 Redis based Java objects and services: JCache API, Apache Tomcat, Hibernate, Spring, Set, Multimap, SortedSet, Map, List, Queue, Deque, Semaphore, Lock, AtomicLong, Map Reduce, Bloom filter, Scheduler, RPC

The newest version!
/**
 * Copyright (c) 2013-2024 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 io.netty.util.ReferenceCountUtil;
import io.netty.util.Timeout;
import io.netty.util.TimerTask;
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.RedisClient;
import org.redisson.client.RedisConnection;
import org.redisson.client.RedisTimeoutException;
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.RedisCommand;
import org.redisson.client.protocol.RedisCommands;
import org.redisson.connection.ClientConnectionsEntry;
import org.redisson.connection.ConnectionManager;
import org.redisson.connection.MasterSlaveEntry;
import org.redisson.connection.NodeSource;
import org.redisson.liveobject.core.RedissonObjectBuilder;
import org.redisson.misc.CompletableFutureWrapper;

import java.time.Duration;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

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

    public static class ConnectionEntry {

        boolean firstCommand = true;
        final CompletableFuture connectionFuture;
        Runnable cancelCallback;

        public ConnectionEntry(CompletableFuture connectionFuture) {
            this.connectionFuture = connectionFuture;
        }

        public CompletableFuture getConnectionFuture() {
            return connectionFuture;
        }

        public boolean isFirstCommand() {
            return firstCommand;
        }

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

        public Runnable getCancelCallback() {
            return cancelCallback;
        }

        public void setCancelCallback(Runnable cancelCallback) {
            this.cancelCallback = cancelCallback;
        }

    }
    
    public static class Entry {

        final List> evalCommands = new LinkedList<>();
        final Deque> commands = new ConcurrentLinkedDeque<>();
        volatile boolean readOnlyMode = true;

        public void addCommand(BatchCommandData command) {
            if (RedisCommands.EVAL_OBJECT.getName().equals(command.getCommand().getName())) {
                evalCommands.add(command);
            }
            commands.add(command);
        }
        public List> getEvalCommands() {
            return evalCommands;
        }

        public void addFirstCommand(BatchCommandData command) {
            commands.addFirst(command);
        }

        public void add(BatchCommandData command) {
            commands.add(command);
        }

        public Deque> getCommands() {
            return commands;
        }

        public void sortCommands() {
            int index = 0;
            boolean sorted = true;
            for (BatchCommandData command : commands) {
                if (command.getIndex() > index) {
                    index = command.getIndex();
                } else {
                    sorted = false;
                    break;
                }
            }
            if (sorted) {
                return;
            }

            BatchCommandData[] cmds = commands.toArray(new BatchCommandData[0]);
            Arrays.sort(cmds);
            commands.clear();
            Collections.addAll(commands, cmds);
        }

        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 final ConcurrentMap commands = new ConcurrentHashMap<>();
    private Map aggregatedCommands = Collections.emptyMap();
    private final ConcurrentMap connections = new ConcurrentHashMap<>();
    
    private final BatchOptions options;
    
    private final Map, List> nestedServices = new ConcurrentHashMap<>();

    private final AtomicBoolean executed = new AtomicBoolean();

    private final long retryInterval;
    private final int retryAttempts;

    public CommandBatchService(CommandAsyncExecutor executor) {
        this(executor, RedissonObjectBuilder.ReferenceType.DEFAULT);
    }

    public CommandBatchService(CommandAsyncExecutor executor, RedissonObjectBuilder.ReferenceType referenceType) {
        this(executor.getConnectionManager(), BatchOptions.defaults(), executor.getObjectBuilder(), referenceType);
    }

    public CommandBatchService(CommandAsyncExecutor executor, BatchOptions options) {
        this(executor.getConnectionManager(), options, executor.getObjectBuilder(), RedissonObjectBuilder.ReferenceType.DEFAULT);
    }

    public CommandBatchService(CommandAsyncExecutor executor, BatchOptions options, RedissonObjectBuilder.ReferenceType referenceType) {
        this(executor.getConnectionManager(), options, executor.getObjectBuilder(), referenceType);
    }

    private CommandBatchService(ConnectionManager connectionManager, BatchOptions options,
                                    RedissonObjectBuilder objectBuilder, RedissonObjectBuilder.ReferenceType referenceType) {
        super(connectionManager, objectBuilder, referenceType);
        this.options = options;

        if (options.getRetryAttempts() >= 0) {
            this.retryAttempts = options.getRetryAttempts();
        } else {
            this.retryAttempts = connectionManager.getServiceManager().getConfig().getRetryAttempts();
        }

        if (options.getRetryInterval() > 0) {
            this.retryInterval = this.options.getRetryInterval();
        } else {
            this.retryInterval = connectionManager.getServiceManager().getConfig().getRetryInterval();
        }
    }

    public BatchOptions getOptions() {
        return options;
    }

    public void add(CompletableFuture future, List services) {
        nestedServices.put(future, services);
    }
    
    @Override
    public  RFuture async(boolean readOnlyMode, NodeSource nodeSource,
            Codec codec, RedisCommand command, Object[] params, boolean ignoreRedirect, boolean noRetry) {
        CompletableFuture mainPromise = createPromise();
        if (isRedisBasedQueue()) {
            boolean isReadOnly = options.getExecutionMode() == ExecutionMode.REDIS_READ_ATOMIC;
            RedisExecutor executor = new RedisQueuedBatchExecutor<>(isReadOnly, nodeSource, codec, command, params, mainPromise,
                    false, connectionManager, objectBuilder, commands, connections, options, index, executed,
                    referenceType, noRetry, aggregatedCommands);
            executor.execute();
        } else {
            RedisExecutor executor = new RedisBatchExecutor<>(readOnlyMode, nodeSource, codec, command, params, mainPromise, 
                    false, connectionManager, objectBuilder, commands, options, index, executed, referenceType, noRetry);
            executor.execute();
        }
        return new CompletableFutureWrapper<>(mainPromise);
    }
        
    @Override
    public  CompletableFuture createPromise() {
        if (isRedisBasedQueue()) {
            return new BatchPromise<>();
        }
        
        return new CompletableFuture<>();
    }

    public void discard() {
        get(discardAsync());
    }

    public RFuture discardAsync() {
        if (executed.get()) {
            throw new IllegalStateException("Batch already executed!");
        }

        executed.set(true);
        if (isRedisBasedQueue()) {
            return writeAllVoidAsync(RedisCommands.DISCARD);
        }

        commands.values().stream()
                        .flatMap(e -> e.getCommands().stream())
                        .flatMap(c -> Arrays.stream(c.getParams()))
                        .forEach(obj -> ReferenceCountUtil.safeRelease(obj));
        return new CompletableFutureWrapper<>((Void) null);
    }
    
    public BatchResult execute() {
        RFuture> f = executeAsync();
        return get(f);
    }

    public RFuture executeAsyncVoid() {
        CompletableFuture> resFuture = executeAsync().toCompletableFuture();
        CompletableFuture s = resFuture.thenApply(res -> null);
        return new CompletableFutureWrapper<>(s);
    }

    public boolean isExecuted() {
        return executed.get();
    }

    public RFuture> executeAsync() {
        if (executed.get()) {
            throw new IllegalStateException("Batch already executed!");
        }
        
        if (commands.isEmpty() && nestedServices.isEmpty()) {
            executed.set(true);
            BatchResult result = new BatchResult<>(Collections.emptyList(), 0);
            return new CompletableFutureWrapper<>(result);
        }

        if (isRedisBasedQueue()) {
            return executeRedisBasedQueue();
        }

        CompletableFuture> promise = new CompletableFuture<>();
        CompletableFuture> voidPromise = new CompletableFuture<>();
        if (this.options.isSkipResult()
                && this.options.getSyncSlaves() == 0) {
            voidPromise.whenComplete((res, ex) -> {
                executed.set(true);

                if (ex != null) {
                    for (Entry e : commands.values()) {
                        e.getCommands().forEach(t -> t.tryFailure(ex));
                    }

                    promise.completeExceptionally(ex);

                    commands.clear();
                    nestedServices.clear();
                    return;
                }

                commands.clear();
                nestedServices.clear();
                promise.complete(new BatchResult<>(Collections.emptyList(), 0));
            });
        } else {
            voidPromise.whenComplete((res, ex) -> {
                executed.set(true);
                if (ex != null) {
                    for (Entry e : commands.values()) {
                        e.getCommands().forEach(t -> t.tryFailure(ex));
                    }

                    promise.completeExceptionally(ex);

                    commands.clear();
                    nestedServices.clear();
                    return;
                }

                try {
                    List responses = new ArrayList<>();
                    int syncedSlaves = 0;

                    if (!res.isEmpty()) {
                        List entries = new ArrayList();
                        for (Entry e : res.values()) {
                            entries.addAll(e.getCommands());
                        }
                        Collections.sort(entries);
                        for (BatchCommandData commandEntry : entries) {
                            if (isWaitCommand(commandEntry)) {
                                if (commandEntry.getCommand().getName().equals(RedisCommands.WAIT.getName())) {
                                    syncedSlaves += ((CompletableFuture) commandEntry.getPromise()).getNow(0);
                                } else {
                                    List list = ((CompletableFuture>) commandEntry.getPromise()).getNow(Arrays.asList(0, 0));
                                    syncedSlaves += list.get(1);
                                }
                            } else if (!commandEntry.getCommand().getName().equals(RedisCommands.MULTI.getName())
                                    && !commandEntry.getCommand().getName().equals(RedisCommands.EXEC.getName())
                                    && !this.options.isSkipResult()) {

                                if (commandEntry.getPromise().isCancelled()) {
                                    continue;
                                }

                                Object entryResult = commandEntry.getPromise().getNow(null);
                                try {
                                    if (objectBuilder != null) {
                                        entryResult = objectBuilder.tryHandleReference(entryResult, referenceType);
                                    }
                                } catch (ReflectiveOperationException exc) {
                                    log.error("Unable to handle reference from {}", entryResult, exc);
                                }
                                responses.add(entryResult);
                            }
                        }
                    }
                    if (!nestedServices.isEmpty()) {
                        for (CompletableFuture f : nestedServices.keySet()) {
                            responses.add(f.getNow(null));
                        }
                    }

                    BatchResult result = new BatchResult<>(responses, syncedSlaves);
                    promise.complete(result);
                } catch (Exception e) {
                    promise.completeExceptionally(ex);
                    throw e;
                }

                commands.clear();
                nestedServices.clear();
            });
        }

        execute(voidPromise);
        return new CompletableFutureWrapper<>(promise);
    }

    private void execute(CompletableFuture> voidPromise) {
        AtomicInteger attempt = new AtomicInteger();
        CompletableFuture> future = new CompletableFuture<>();
        resolveCommandsInMemory(attempt, future);
        future.whenComplete((r, ex) -> {
            try {
                if (ex != null) {
                    voidPromise.completeExceptionally(ex);
                    return;
                }

                AtomicInteger slots = new AtomicInteger(r.size());

                for (Map.Entry, List> entry : nestedServices.entrySet()) {
                    slots.incrementAndGet();
                    for (CommandBatchService service : entry.getValue()) {
                        service.executeAsync();
                    }

                    entry.getKey().whenComplete((res, e) -> {
                        if (e == null) {
                            if (slots.decrementAndGet() == 0) {
                                voidPromise.complete(r);
                            }
                        } else {
                            if (entry.getKey().isCancelled()) {
                                voidPromise.completeExceptionally(e);
                            } else {
                                voidPromise.completeExceptionally(e.getCause());
                            }
                        }
                    });
                }

                CompletionStage f = loadScripts(r);
                f.whenComplete((res, ex1) -> {
                    try {
                        if (ex1 != null) {
                            voidPromise.completeExceptionally(ex1.getCause());
                            return;
                        }

                        for (Map.Entry e : r.entrySet()) {
                            Entry entry = e.getValue();
                            if (this.options.getExecutionMode() != ExecutionMode.IN_MEMORY) {
                                BatchCommandData multiCommand = new BatchCommandData<>(RedisCommands.MULTI, new Object[] {}, index.incrementAndGet());
                                entry.addFirstCommand(multiCommand);
                                BatchCommandData execCommand = new BatchCommandData<>(RedisCommands.EXEC, new Object[] {}, index.incrementAndGet());
                                entry.add(execCommand);
                            }

                            if (this.options.isSkipResult()) {
                                BatchCommandData offCommand = new BatchCommandData<>(RedisCommands.CLIENT_REPLY, new Object[] { "OFF" }, index.incrementAndGet());
                                entry.addFirstCommand(offCommand);
                                BatchCommandData onCommand = new BatchCommandData<>(RedisCommands.CLIENT_REPLY, new Object[] { "ON" }, index.incrementAndGet());
                                entry.add(onCommand);
                            }

                            if (this.options.getSyncSlaves() > 0) {
                                BatchCommandData waitCommand;
                                if (this.options.isSyncAOF()) {
                                    waitCommand = new BatchCommandData<>(RedisCommands.WAITAOF,
                                            new Object[]{this.options.getSyncLocals(), this.options.getSyncSlaves(), this.options.getSyncTimeout()}, index.incrementAndGet());
                                } else {
                                    waitCommand = new BatchCommandData<>(RedisCommands.WAIT,
                                            new Object[]{this.options.getSyncSlaves(), this.options.getSyncTimeout()}, index.incrementAndGet());
                                }
                                entry.add(waitCommand);
                            }

                            BatchOptions options = BatchOptions.defaults()
                                    .executionMode(this.options.getExecutionMode())
                                    .responseTimeout(this.options.getResponseTimeout(), TimeUnit.MILLISECONDS)
                                    .retryAttempts(Math.max(0, retryAttempts - attempt.get()))
                                    .retryInterval(retryInterval, TimeUnit.MILLISECONDS);

                            if (this.options.isSkipResult()) {
                                options.skipResult();
                            }

                            if (this.options.isSyncAOF()) {
                                options.syncAOF(this.options.getSyncLocals(), this.options.getSyncSlaves(), Duration.ofMillis(this.options.getSyncTimeout()));
                            } else {
                                options.sync(this.options.getSyncSlaves(), Duration.ofMillis(this.options.getSyncTimeout()));
                            }

                            CompletableFuture mainPromise = new CompletableFuture<>();
                            mainPromise.whenComplete((res1, ex2) -> {
                                if (ex2 != null) {
                                    voidPromise.completeExceptionally(ex2);
                                    return;
                                }

                                voidPromise.complete(r);
                            });
                            RedisCommonBatchExecutor executor = new RedisCommonBatchExecutor(e.getKey(), mainPromise,
                                    connectionManager, options, e.getValue(), slots, referenceType, false);
                            executor.execute();
                        }
                    } catch (Exception e) {
                        voidPromise.completeExceptionally(e);
                    }
                });
            } catch (Exception e) {
                voidPromise.completeExceptionally(e);
            }
        });
    }

    private CompletionStage loadScripts(Map r) {
        if (!connectionManager.getServiceManager().getCfg().isUseScriptCache()) {
            return CompletableFuture.completedFuture(null);
        }

        // TODO BatchOptions.defaults().skipResult() for Redis 3.2+
        CommandBatchService bb = new CommandBatchService(connectionManager, BatchOptions.defaults(), objectBuilder, referenceType);
        Map> newScripts = new HashMap<>();
        for (Map.Entry e : r.entrySet()) {
            for (BatchCommandData data : e.getValue().getEvalCommands()) {
                RedisCommand command = data.getCommand();

                String script = (String) data.getParams()[0];
                MasterSlaveEntry entry = e.getKey().getEntry();
                if (!connectionManager.getServiceManager().isCached(entry.getClient().getAddr(), script)) {
                    Set newShas = newScripts.computeIfAbsent(entry, k -> new HashSet<>());
                    if (newShas.add(script)) {
                        if (e.getValue().isReadOnlyMode()) {
                            bb.executeAllAsync(entry, RedisCommands.SCRIPT_LOAD, script);
                        } else {
                            bb.writeAsync(entry, StringCodec.INSTANCE, RedisCommands.SCRIPT_LOAD, script);
                        }
                    }
                }

                RedisCommand cmd = new RedisCommand(command, "EVALSHA");
                data.updateCommand(cmd);
                String sha1 = getServiceManager().calcSHA(script);
                data.getParams()[0] = sha1;
            }
        }

        return bb.executeAsync().thenAccept(res -> {
            for (Map.Entry> e : newScripts.entrySet()) {
                connectionManager.getServiceManager().cacheScripts(e.getKey().getClient().getAddr(), e.getValue());
            }
        });
    }

    private void resolveCommands(AtomicInteger attempt, CompletableFuture> future) {
        Map result = new HashMap<>();
        for (Map.Entry e : commands.entrySet()) {
            MasterSlaveEntry entry = getEntry(e.getKey());
            if (entry == null) {
                if (attempt.incrementAndGet() == retryAttempts + 1) {
                    future.completeExceptionally(connectionManager.getServiceManager().createNodeNotFoundException(e.getKey()));
                    return;
                }

                connectionManager.getServiceManager().newTimeout(task -> {
                    resolveCommands(attempt, future);
                }, retryInterval, TimeUnit.MILLISECONDS);
                return;
            }
            Entry ee = result.computeIfAbsent(entry, k -> new Entry());
            if (!e.getValue().isReadOnlyMode()) {
                ee.setReadOnlyMode(false);
            }
            ee.getCommands().addAll(e.getValue().getCommands());
            ee.getEvalCommands().addAll(e.getValue().getEvalCommands());
        }
        for (Entry entry : result.values()) {
            entry.sortCommands();
        }
        future.complete(result);
    }

    private void resolveCommandsInMemory(AtomicInteger attempt, CompletableFuture> future) {
        Map result = new HashMap<>();
        for (Map.Entry e : commands.entrySet()) {
            MasterSlaveEntry entry = getEntry(e.getKey());
            if (entry == null) {
                if (attempt.incrementAndGet() == retryAttempts + 1) {
                    future.completeExceptionally(connectionManager.getServiceManager().createNodeNotFoundException(e.getKey()));
                    return;
                }

                connectionManager.getServiceManager().newTimeout(task -> {
                    resolveCommandsInMemory(attempt, future);
                }, retryInterval, TimeUnit.MILLISECONDS);
                return;
            }

            RedisClient client = e.getKey().getRedisClient();
            if (client != null) {
                ClientConnectionsEntry ce = entry.getEntry(client);
                if (ce == null || ce.isFreezed()) {
                    client = null;
                }
            }

            Entry ee = result.computeIfAbsent(new NodeSource(entry, client), k -> new Entry());
            if (!e.getValue().isReadOnlyMode()) {
                ee.setReadOnlyMode(false);
            }
            ee.getCommands().addAll(e.getValue().getCommands());
            ee.getEvalCommands().addAll(e.getValue().getEvalCommands());
        }
        for (Entry entry : result.values()) {
            entry.sortCommands();
        }
        future.complete(result);
    }

    private MasterSlaveEntry getEntry(NodeSource source) {
        if (source.getSlot() != null) {
            return connectionManager.getWriteEntry(source.getSlot());
        }
        return source.getEntry();
    }

    protected Throwable cause(CompletableFuture future) {
        try {
            future.getNow(null);
            return null;
        } catch (CompletionException ex2) {
            return ex2.getCause();
        } catch (CancellationException ex1) {
            return ex1;
        }
    }

    @SuppressWarnings("MethodLength")
    private  RFuture executeRedisBasedQueue() {
        CompletableFuture resultPromise = new CompletableFuture();
        long responseTimeout;
        if (options.getResponseTimeout() > 0) {
            responseTimeout = options.getResponseTimeout();
        } else {
            responseTimeout = connectionManager.getServiceManager().getConfig().getTimeout();
        }

        Timeout timeout = connectionManager.getServiceManager().newTimeout(new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                connections.values().forEach(c -> {
                    c.getCancelCallback().run();
                });

                resultPromise.completeExceptionally(new RedisTimeoutException("Response timeout for queued commands " + responseTimeout + ": " +
                        commands.values().stream()
                                .flatMap(e -> e.getCommands().stream().map(d -> d.getCommand()))
                                .collect(Collectors.toList())));
            }
        }, responseTimeout, TimeUnit.MILLISECONDS);

        CompletableFuture allFutures = CompletableFuture.allOf(commands.values().stream()
                                                                            .flatMap(m -> m.getCommands()
                                                                                                .stream().map(c -> ((BatchPromise) c.getPromise()).getSentPromise()))
                                                                            .toArray(CompletableFuture[]::new));

        allFutures.whenComplete((fr, exc) -> {
                if (!timeout.cancel()) {
                    return;
                }

                for (Entry entry : commands.values()) {
                    for (BatchCommandData command : entry.getCommands()) {
                        if (command.getPromise().isDone() && command.getPromise().isCompletedExceptionally()) {

                            resultPromise.completeExceptionally(cause(command.getPromise()));
                            break;
                        }
                    }
                }
                
                if (resultPromise.isDone()) {
                    return;
                }
                
                Map> result = new ConcurrentHashMap<>();
                AtomicInteger attempt = new AtomicInteger();
                CompletableFuture> resolvedEntriesFuture = new CompletableFuture<>();
                resolveCommands(attempt, resolvedEntriesFuture);
                resolvedEntriesFuture.whenComplete((map, ee) -> {
                    if (ee != null) {
                        resultPromise.completeExceptionally(ee);
                        return;
                    }

                    AtomicInteger slots = new AtomicInteger(nestedServices.size());
                    CompletableFuture nestedServicesFuture;
                    if (nestedServices.isEmpty()) {
                        nestedServicesFuture = CompletableFuture.completedFuture(null);
                    } else {
                        nestedServicesFuture = new CompletableFuture<>();
                    }

                    for (Map.Entry, List> entry : nestedServices.entrySet()) {
                        for (CommandBatchService service : entry.getValue()) {
                            service.executeAsync();
                        }

                        entry.getKey().whenComplete((res, e) -> {
                            if (e == null) {
                                if (slots.decrementAndGet() == 0) {
                                    nestedServicesFuture.complete(null);
                                }
                            } else {
                                if (entry.getKey().isCancelled()) {
                                    nestedServicesFuture.completeExceptionally(e);
                                } else {
                                    nestedServicesFuture.completeExceptionally(e.getCause());
                                }
                            }
                        });
                    }

                    nestedServicesFuture.whenComplete((r1, exc2) -> {
                        if (exc2 != null) {
                            resultPromise.completeExceptionally(exc2);
                            return;
                        }

                        aggregatedCommands = map;

                        List> futures = new ArrayList<>(map.size());
                        for (Map.Entry entry : aggregatedCommands.entrySet()) {
                            boolean isReadOnly = options.getExecutionMode() == ExecutionMode.REDIS_READ_ATOMIC;
                            CompletableFuture> execPromise = createPromise();
                            RedisExecutor, List> executor = new RedisQueuedBatchExecutor<>(isReadOnly, new NodeSource(entry.getKey()), codec,
                                    RedisCommands.EXEC, new Object[] {}, execPromise,
                                    false, connectionManager, objectBuilder, commands, connections,
                                    options, index, executed, referenceType, false, aggregatedCommands);
                            executor.execute();

                            CompletionStage f = execPromise.thenCompose(r -> {
                                BatchCommandData lastCommand = entry.getValue().getCommands().peekLast();
                                result.put(entry.getKey(), r);

                                if (RedisCommands.WAIT.getName().equals(lastCommand.getCommand().getName())) {
                                    return lastCommand.getPromise().thenApply(i -> null);
                                }
                                return CompletableFuture.completedFuture(null);
                            });
                            futures.add(f.toCompletableFuture());
                        }

                        CompletableFuture future = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
                        future.whenComplete((res, ex) -> {
                            executed.set(true);
                            if (ex != null) {
                                resultPromise.completeExceptionally(ex);
                                return;
                            }

                            try {
                                for (java.util.Map.Entry> entry : result.entrySet()) {
                                    Entry commandEntry = aggregatedCommands.get(entry.getKey());
                                    Iterator resultIter = entry.getValue().iterator();
                                    for (BatchCommandData data : commandEntry.getCommands()) {
                                        if (data.getCommand().getName().equals(RedisCommands.EXEC.getName())) {
                                            break;
                                        }

                                        CompletableFuture promise = (CompletableFuture) data.getPromise();
                                        if (resultIter.hasNext()) {
                                            promise.complete(resultIter.next());
                                        } else {
                                            // fix for https://github.com/redisson/redisson/issues/2212
                                            promise.complete(null);
                                        }
                                    }
                                }

                                List entries = new ArrayList<>();
                                for (Entry e : aggregatedCommands.values()) {
                                    entries.addAll(e.getCommands());
                                }
                                Collections.sort(entries);
                                List responses = new ArrayList<>(entries.size());
                                int syncedSlaves = 0;
                                for (BatchCommandData commandEntry : entries) {
                                    if (isWaitCommand(commandEntry)) {
                                        if (commandEntry.getCommand().getName().equals(RedisCommands.WAIT.getName())) {
                                            syncedSlaves += ((CompletableFuture) commandEntry.getPromise()).getNow(0);
                                        } else {
                                            List list = ((CompletableFuture>) commandEntry.getPromise()).getNow(Arrays.asList(0, 0));
                                            syncedSlaves += list.get(1);
                                        }
                                    } else if (!commandEntry.getCommand().getName().equals(RedisCommands.MULTI.getName())
                                            && !commandEntry.getCommand().getName().equals(RedisCommands.EXEC.getName())) {
                                        Object entryResult = commandEntry.getPromise().getNow(null);
                                        if (objectBuilder != null) {
                                            entryResult = objectBuilder.tryHandleReference(entryResult, referenceType);
                                        }
                                        responses.add(entryResult);
                                    }
                                }

                                if (!nestedServices.isEmpty()) {
                                    for (CompletableFuture f : nestedServices.keySet()) {
                                        responses.add(f.getNow(null));
                                    }
                                }

                                BatchResult r = new BatchResult<>(responses, syncedSlaves);
                                resultPromise.complete((R) r);
                            } catch (Exception e) {
                                resultPromise.completeExceptionally(e);
                            }
                        });
                    });
                });
        });
        return new CompletableFutureWrapper<>(resultPromise);
    }

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

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

    @Override
    protected boolean isEvalCacheActive() {
        return false;
    }

    @Override
    protected CommandBatchService createCommandBatchService(int availableSlaves) {
        return this;
    }

}