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

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

There is a newer version: 3.45.1
Show newest version
/**
 * Copyright (c) 2013-2020 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.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.RedisConnection;
import org.redisson.client.RedisTimeoutException;
import org.redisson.client.codec.Codec;
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.ConnectionManager;
import org.redisson.connection.MasterSlaveEntry;
import org.redisson.connection.NodeSource;
import org.redisson.liveobject.core.RedissonObjectBuilder;
import org.redisson.misc.CountableListener;
import org.redisson.misc.RPromise;
import org.redisson.misc.RedissonPromise;
import org.redisson.pubsub.AsyncSemaphore;

import java.util.*;
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 java.util.stream.Collectors;

/**
 * 
 * @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 AsyncSemaphore semaphore = new AsyncSemaphore(0);
    private AtomicInteger index = new AtomicInteger();

    private ConcurrentMap commands = new ConcurrentHashMap<>();
    private ConcurrentMap connections = new ConcurrentHashMap<>();
    
    private BatchOptions options = BatchOptions.defaults();
    
    private Map, List> nestedServices = new ConcurrentHashMap<>();

    private AtomicBoolean executed = new AtomicBoolean();

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

    public void setObjectBuilder(RedissonObjectBuilder objectBuilder) {
        this.objectBuilder = objectBuilder;
    }
    
    public BatchOptions getOptions() {
        return options;
    }

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

    public RFuture executeAsyncVoid() {
        RedissonPromise promise = new RedissonPromise();
        RFuture> resFuture = executeAsync();
        resFuture.onComplete((res, e) -> {
            if (e == null) {
                promise.trySuccess(null);
            } else {
                promise.tryFailure(e);
            }
        });
        return promise;
    }

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

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

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

        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> promise = new RedissonPromise<>();
        RPromise voidPromise = new RedissonPromise();
        if (this.options.isSkipResult()
                && this.options.getSyncSlaves() == 0) {
            voidPromise.onComplete((res, e) -> {
                executed.set(true);
                commands.clear();
                nestedServices.clear();
                promise.trySuccess(new BatchResult<>(Collections.emptyList(), 0));
            });
        } else {
            voidPromise.onComplete((res, ex) -> {
                executed.set(true);
                if (ex != null) {
                    promise.tryFailure(ex);

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

                    commands.clear();
                    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())
                            && !this.options.isSkipResult()) {
                        
                        if (commandEntry.getPromise().isCancelled()) {
                            continue;
                        }
                        
                        Object entryResult = commandEntry.getPromise().getNow();
                        try {
                            entryResult = RedisExecutor.tryHandleReference(objectBuilder, entryResult);
                        } catch (ReflectiveOperationException exc) {
                            log.error("Unable to handle reference from " + entryResult, exc);
                        }
                        responses.add(entryResult);
                    }
                }
                
                BatchResult result = new BatchResult(responses, syncedSlaves);
                promise.trySuccess(result);

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

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

        for (Map.Entry, List> entry : nestedServices.entrySet()) {
            slots.incrementAndGet();
            for (CommandBatchService service : entry.getValue()) {
                service.executeAsync();
            }
            
            entry.getKey().onComplete((res, e) -> {
                handle(voidPromise, slots, entry.getKey());
            });
        }
        
        for (Map.Entry e : commands.entrySet()) {
            RedisCommonBatchExecutor executor = new RedisCommonBatchExecutor(new NodeSource(e.getKey()), voidPromise,
                                                    connectionManager, this.options, e.getValue(), slots);
            executor.execute();
        }
        return promise;
    }

    private  RFuture executeRedisBasedQueue() {
        int permits = 0;
        for (Entry entry : commands.values()) {
            permits += entry.getCommands().size();
        }
        
        RPromise resultPromise = new RedissonPromise();
        Timeout timeout;
        if (semaphore.getCounter() < permits) {
            long responseTimeout;
            if (options.getResponseTimeout() > 0) {
                responseTimeout = options.getResponseTimeout();
            } else {
                responseTimeout = connectionManager.getConfig().getTimeout();
            }

            timeout = connectionManager.newTimeout(new TimerTask() {
                @Override
                public void run(Timeout timeout) throws Exception {
                    resultPromise.tryFailure(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);
        } else {
            timeout = null;
        }

        semaphore.acquire(new Runnable() {
            @Override
            public void run() {
                if (timeout != null) {
                    timeout.cancel();
                }

                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;
                }
                
                RPromise>> mainPromise = new RedissonPromise<>();
                Map> result = new ConcurrentHashMap<>();
                CountableListener>> listener = new CountableListener<>(mainPromise, result);
                listener.setCounter(connections.size());
                for (Map.Entry entry : commands.entrySet()) {
                    RPromise> execPromise = new RedissonPromise<>();
                    async(entry.getValue().isReadOnlyMode(), new NodeSource(entry.getKey()), connectionManager.getCodec(), RedisCommands.EXEC, 
                            new Object[] {}, execPromise, false);
                    execPromise.onComplete((r, ex) -> {
                        if (ex != null) {
                            mainPromise.tryFailure(ex);
                            return;
                        }

                        BatchCommandData lastCommand = (BatchCommandData) entry.getValue().getCommands().peekLast();
                        result.put(entry.getKey(), r);
                        if (RedisCommands.WAIT.getName().equals(lastCommand.getCommand().getName())) {
                            lastCommand.getPromise().onComplete((res, e) -> {
                                if (e != null) {
                                    mainPromise.tryFailure(e);
                                    return;
                                }
                                
                                execPromise.onComplete(listener);
                            });
                        } else {
                            execPromise.onComplete(listener);
                        }
                    });
                }
                
                mainPromise.onComplete((res, ex) -> {
                    executed.set(true);
                    if (ex != null) {
                        resultPromise.tryFailure(ex);
                        return;
                    }
                    
                    try {
                        for (java.util.Map.Entry> entry : res.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();
                                if (resultIter.hasNext()) {
                                    promise.trySuccess(resultIter.next());
                                } else {
                                    // fix for https://github.com/redisson/redisson/issues/2212
                                    promise.trySuccess(null);
                                }
                            }
                        }
                        
                        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 = RedisExecutor.tryHandleReference(objectBuilder, entryResult);
                                responses.add(entryResult);
                            }
                        }
                        BatchResult r = new BatchResult(responses, syncedSlaves);
                        resultPromise.trySuccess((R) r);
                    } catch (Exception e) {
                        resultPromise.tryFailure(e);
                    }
                });
            }
        }, permits);
        return 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());
    }

    protected void handle(RPromise mainPromise, AtomicInteger slots, RFuture future) {
        if (future.isSuccess()) {
            if (slots.decrementAndGet() == 0) {
                mainPromise.trySuccess(null);
            }
        } else {
            mainPromise.tryFailure(future.cause());
        }
    }
    
    @Override
    protected boolean isEvalCacheActive() {
        return false;
    }
    

}