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

org.redisson.transaction.RedissonTransaction 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.transaction;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import org.redisson.RedissonBatch;
import org.redisson.RedissonLocalCachedMap;
import org.redisson.RedissonObject;
import org.redisson.RedissonTopic;
import org.redisson.api.BatchOptions;
import org.redisson.api.BatchResult;
import org.redisson.api.RBucket;
import org.redisson.api.RFuture;
import org.redisson.api.RLocalCachedMap;
import org.redisson.api.RMap;
import org.redisson.api.RMapCache;
import org.redisson.api.RMultimapCacheAsync;
import org.redisson.api.RSet;
import org.redisson.api.RSetCache;
import org.redisson.api.RTopic;
import org.redisson.api.RTopicAsync;
import org.redisson.api.RTransaction;
import org.redisson.api.TransactionOptions;
import org.redisson.api.listener.MessageListener;
import org.redisson.cache.LocalCachedMapDisable;
import org.redisson.cache.LocalCachedMapDisabledKey;
import org.redisson.cache.LocalCachedMapEnable;
import org.redisson.cache.LocalCachedMessageCodec;
import org.redisson.client.codec.Codec;
import org.redisson.command.CommandAsyncExecutor;
import org.redisson.command.CommandBatchService;
import org.redisson.connection.MasterSlaveEntry;
import org.redisson.misc.CountableListener;
import org.redisson.misc.RPromise;
import org.redisson.misc.RedissonPromise;
import org.redisson.transaction.operation.TransactionalOperation;
import org.redisson.transaction.operation.map.MapOperation;

import io.netty.buffer.ByteBufUtil;
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 RedissonTransaction implements RTransaction {

    private final CommandAsyncExecutor commandExecutor;
    private final AtomicBoolean executed = new AtomicBoolean();
    
    private final TransactionOptions options;
    private List operations = new CopyOnWriteArrayList();
    private Set localCaches = new HashSet();
    private final long startTime = System.currentTimeMillis();
    
    private final String id = generateId();
    
    public RedissonTransaction(CommandAsyncExecutor commandExecutor, TransactionOptions options) {
        super();
        this.options = options;
        this.commandExecutor = commandExecutor;
    }
    
    public RedissonTransaction(CommandAsyncExecutor commandExecutor, TransactionOptions options,
            List operations, Set localCaches) {
        super();
        this.commandExecutor = commandExecutor;
        this.options = options;
        this.operations = operations;
        this.localCaches = localCaches;
    }

    @Override
    public  RLocalCachedMap getLocalCachedMap(RLocalCachedMap fromInstance) {
        checkState();

        localCaches.add(fromInstance.getName());
        return new RedissonTransactionalLocalCachedMap(commandExecutor,
                operations, options.getTimeout(), executed, fromInstance, id);
    }
    
    @Override
    public  RBucket getBucket(String name) {
        checkState();
        
        return new RedissonTransactionalBucket(commandExecutor, name, operations, executed, id);
    }
    
    @Override
    public  RBucket getBucket(String name, Codec codec) {
        checkState();

        return new RedissonTransactionalBucket(codec, commandExecutor, name, operations, executed, id);
    }

    @Override
    public  RSet getSet(String name) {
        checkState();
        
        return new RedissonTransactionalSet(commandExecutor, name, operations, options.getTimeout(), executed, id);        
    }
    
    @Override
    public  RSet getSet(String name, Codec codec) {
        checkState();
        
        return new RedissonTransactionalSet(codec, commandExecutor, name, operations, options.getTimeout(), executed, id);
    }
    
    @Override
    public  RSetCache getSetCache(String name) {
        checkState();
        
        return new RedissonTransactionalSetCache(commandExecutor, name, operations, options.getTimeout(), executed, id);        
    }
    
    @Override
    public  RSetCache getSetCache(String name, Codec codec) {
        checkState();
        
        return new RedissonTransactionalSetCache(codec, commandExecutor, name, operations, options.getTimeout(), executed, id);
    }

    @Override
    public  RMap getMap(String name) {
        checkState();
        
        return new RedissonTransactionalMap(commandExecutor, name, operations, options.getTimeout(), executed, id);
    }

    @Override
    public  RMap getMap(String name, Codec codec) {
        checkState();
        
        return new RedissonTransactionalMap(codec, commandExecutor, name, operations, options.getTimeout(), executed, id);
    }

    @Override
    public  RMapCache getMapCache(String name) {
        checkState();
        
        return new RedissonTransactionalMapCache(commandExecutor, name, operations, options.getTimeout(), executed, id);
    }

    @Override
    public  RMapCache getMapCache(String name, Codec codec) {
        checkState();
        
        return new RedissonTransactionalMapCache(codec, commandExecutor, name, operations, options.getTimeout(), executed, id);
    }
    
    @Override
    public RFuture commitAsync() {
        checkState();
        
        checkTimeout();
        
        BatchOptions batchOptions = createOptions();
        
        final CommandBatchService transactionExecutor = new CommandBatchService(commandExecutor.getConnectionManager(), batchOptions);
        for (TransactionalOperation transactionalOperation : operations) {
            transactionalOperation.commit(transactionExecutor);
        }

        final String id = generateId();
        final RPromise result = new RedissonPromise();
        RFuture> future = disableLocalCacheAsync(id, localCaches, operations);
        future.addListener(new FutureListener>() {
            @Override
            public void operationComplete(Future> future) throws Exception {
                if (!future.isSuccess()) {
                    result.tryFailure(new TransactionException("Unable to execute transaction", future.cause()));
                    return;
                }
                
                final Map hashes = future.getNow();
                try {
                    checkTimeout();
                } catch (TransactionTimeoutException e) {
                    enableLocalCacheAsync(id, hashes);
                    result.tryFailure(e);
                    return;
                }
                                
                RFuture> transactionFuture = transactionExecutor.executeAsync();
                transactionFuture.addListener(new FutureListener() {
                    @Override
                    public void operationComplete(Future future) throws Exception {
                        if (!future.isSuccess()) {
                            result.tryFailure(new TransactionException("Unable to execute transaction", future.cause()));
                            return;
                        }
                        
                        enableLocalCacheAsync(id, hashes);
                        executed.set(true);
                        
                        result.trySuccess(null);
                    }
                });
            }
        });
        return result;
    }

    private BatchOptions createOptions() {
        int syncSlaves = 0;
        if (!commandExecutor.getConnectionManager().isClusterMode()) {
            MasterSlaveEntry entry = commandExecutor.getConnectionManager().getEntrySet().iterator().next();
            syncSlaves = entry.getAvailableClients() - 1;
        }
        
        BatchOptions batchOptions = BatchOptions.defaults()
                .syncSlaves(syncSlaves, options.getSyncTimeout(), TimeUnit.MILLISECONDS)
                .responseTimeout(options.getResponseTimeout(), TimeUnit.MILLISECONDS)
                .retryAttempts(options.getRetryAttempts())
                .retryInterval(options.getRetryInterval(), TimeUnit.MILLISECONDS)
                .atomic();
        return batchOptions;
    }

    @Override
    public void commit() {
        commit(localCaches, operations);
    }
    
    public void commit(Set localCaches, List operations) {
        checkState();
        
        checkTimeout();
        
        BatchOptions batchOptions = createOptions();
        
        CommandBatchService transactionExecutor = new CommandBatchService(commandExecutor.getConnectionManager(), batchOptions);
        for (TransactionalOperation transactionalOperation : operations) {
            transactionalOperation.commit(transactionExecutor);
        }

        String id = generateId();
        Map hashes = disableLocalCache(id, localCaches, operations);
        
        try {
            checkTimeout();
        } catch (TransactionTimeoutException e) {
            enableLocalCache(id, hashes);
            throw e;
        }

        try {
            
            transactionExecutor.execute();
        } catch (Exception e) {
            throw new TransactionException("Unable to execute transaction", e);
        }
        
        enableLocalCache(id, hashes);
        
        executed.set(true);
    }

    private void checkTimeout() {
        if (options.getTimeout() != -1 && System.currentTimeMillis() - startTime > options.getTimeout()) {
            rollbackAsync();
            throw new TransactionTimeoutException("Transaction was discarded due to timeout " + options.getTimeout() + " milliseconds");
        }
    }

    private RFuture> enableLocalCacheAsync(String requestId, Map hashes) {
        if (hashes.isEmpty()) {
            return RedissonPromise.newSucceededFuture(null);
        }
        
        RedissonBatch publishBatch = new RedissonBatch(null, commandExecutor.getConnectionManager(), BatchOptions.defaults());
        for (Entry entry : hashes.entrySet()) {
            String name = RedissonObject.suffixName(entry.getKey().getName(), RedissonLocalCachedMap.TOPIC_SUFFIX);
            RTopicAsync topic = publishBatch.getTopic(name, LocalCachedMessageCodec.INSTANCE);
            LocalCachedMapEnable msg = new LocalCachedMapEnable(requestId, entry.getValue().getKeyIds().toArray(new byte[entry.getValue().getKeyIds().size()][]));
            topic.publishAsync(msg);
        }
        
        return publishBatch.executeAsync();
    }
    
    private void enableLocalCache(String requestId, Map hashes) {
        if (hashes.isEmpty()) {
            return;
        }
        
        RedissonBatch publishBatch = new RedissonBatch(null, commandExecutor.getConnectionManager(), BatchOptions.defaults());
        for (Entry entry : hashes.entrySet()) {
            String name = RedissonObject.suffixName(entry.getKey().getName(), RedissonLocalCachedMap.TOPIC_SUFFIX);
            RTopicAsync topic = publishBatch.getTopic(name, LocalCachedMessageCodec.INSTANCE);
            LocalCachedMapEnable msg = new LocalCachedMapEnable(requestId, entry.getValue().getKeyIds().toArray(new byte[entry.getValue().getKeyIds().size()][]));
            topic.publishAsync(msg);
        }
        
        try {
            publishBatch.execute();
        } catch (Exception e) {
            // skip it. Disabled local cache entries are enabled once reach timeout.
        }
    }
    
    private Map disableLocalCache(String requestId, Set localCaches, List operations) {
        if (localCaches.isEmpty()) {
            return Collections.emptyMap();
        }
        
        Map hashes = new HashMap(localCaches.size());
        RedissonBatch batch = new RedissonBatch(null, commandExecutor.getConnectionManager(), BatchOptions.defaults());
        for (TransactionalOperation transactionalOperation : operations) {
            if (localCaches.contains(transactionalOperation.getName())) {
                MapOperation mapOperation = (MapOperation) transactionalOperation;
                RedissonLocalCachedMap map = (RedissonLocalCachedMap)mapOperation.getMap();
                
                HashKey hashKey = new HashKey(transactionalOperation.getName(), transactionalOperation.getCodec());
                byte[] key = map.toCacheKey(mapOperation.getKey()).getKeyHash();
                HashValue value = hashes.get(hashKey);
                if (value == null) {
                    value = new HashValue();
                    hashes.put(hashKey, value);
                }
                value.getKeyIds().add(key);

                String disabledKeysName = RedissonObject.suffixName(transactionalOperation.getName(), RedissonLocalCachedMap.DISABLED_KEYS_SUFFIX);
                RMultimapCacheAsync multimap = batch.getListMultimapCache(disabledKeysName, transactionalOperation.getCodec());
                LocalCachedMapDisabledKey localCacheKey = new LocalCachedMapDisabledKey(requestId, options.getResponseTimeout());
                multimap.putAsync(localCacheKey, ByteBufUtil.hexDump(key));
                multimap.expireKeyAsync(localCacheKey, options.getResponseTimeout(), TimeUnit.MILLISECONDS);
            }
        }

        try {
            batch.execute();
        } catch (Exception e) {
            throw new TransactionException("Unable to execute transaction over local cached map objects: " + localCaches, e);
        }
        
        final CountDownLatch latch = new CountDownLatch(hashes.size());
        List topics = new ArrayList();
        for (final Entry entry : hashes.entrySet()) {
            RTopic topic = new RedissonTopic(LocalCachedMessageCodec.INSTANCE, 
                    commandExecutor, RedissonObject.suffixName(entry.getKey().getName(), requestId + RedissonLocalCachedMap.DISABLED_ACK_SUFFIX));
            topics.add(topic);
            topic.addListener(Object.class, new MessageListener() {
                @Override
                public void onMessage(CharSequence channel, Object msg) {
                    AtomicInteger counter = entry.getValue().getCounter();
                    if (counter.decrementAndGet() == 0) {
                        latch.countDown();
                    }
                }
            });
        }
        
        RedissonBatch publishBatch = new RedissonBatch(null, commandExecutor.getConnectionManager(), BatchOptions.defaults());
        for (final Entry entry : hashes.entrySet()) {
            String disabledKeysName = RedissonObject.suffixName(entry.getKey().getName(), RedissonLocalCachedMap.DISABLED_KEYS_SUFFIX);
            RMultimapCacheAsync multimap = publishBatch.getListMultimapCache(disabledKeysName, entry.getKey().getCodec());
            LocalCachedMapDisabledKey localCacheKey = new LocalCachedMapDisabledKey(requestId, options.getResponseTimeout());
            multimap.removeAllAsync(localCacheKey);
            
            RTopicAsync topic = publishBatch.getTopic(RedissonObject.suffixName(entry.getKey().getName(), RedissonLocalCachedMap.TOPIC_SUFFIX), LocalCachedMessageCodec.INSTANCE);
            RFuture future = topic.publishAsync(new LocalCachedMapDisable(requestId, 
                    entry.getValue().getKeyIds().toArray(new byte[entry.getValue().getKeyIds().size()][]), options.getResponseTimeout()));
            future.addListener(new FutureListener() {
                @Override
                public void operationComplete(Future future) throws Exception {
                    if (!future.isSuccess()) {
                        return;
                    }
                    
                    int receivers = future.getNow().intValue();
                    AtomicInteger counter = entry.getValue().getCounter();
                    if (counter.addAndGet(receivers) == 0) {
                        latch.countDown();
                    }
                }
            });
        }

        try {
            publishBatch.execute();
        } catch (Exception e) {
            throw new TransactionException("Unable to execute transaction over local cached map objects: " + localCaches, e);
        }
        
        for (RTopic topic : topics) {
            topic.removeAllListeners();
        }
        
        try {
            latch.await(options.getResponseTimeout(), TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return hashes;
    }
    
    private RFuture> disableLocalCacheAsync(final String requestId, Set localCaches, List operations) {
        if (localCaches.isEmpty()) {
            return RedissonPromise.newSucceededFuture(Collections.emptyMap());
        }
        
        final RPromise> result = new RedissonPromise>();
        final Map hashes = new HashMap(localCaches.size());
        RedissonBatch batch = new RedissonBatch(null, commandExecutor.getConnectionManager(), BatchOptions.defaults());
        for (TransactionalOperation transactionalOperation : operations) {
            if (localCaches.contains(transactionalOperation.getName())) {
                MapOperation mapOperation = (MapOperation) transactionalOperation;
                RedissonLocalCachedMap map = (RedissonLocalCachedMap)mapOperation.getMap();
                
                HashKey hashKey = new HashKey(transactionalOperation.getName(), transactionalOperation.getCodec());
                byte[] key = map.toCacheKey(mapOperation.getKey()).getKeyHash();
                HashValue value = hashes.get(hashKey);
                if (value == null) {
                    value = new HashValue();
                    hashes.put(hashKey, value);
                }
                value.getKeyIds().add(key);

                String disabledKeysName = RedissonObject.suffixName(transactionalOperation.getName(), RedissonLocalCachedMap.DISABLED_KEYS_SUFFIX);
                RMultimapCacheAsync multimap = batch.getListMultimapCache(disabledKeysName, transactionalOperation.getCodec());
                LocalCachedMapDisabledKey localCacheKey = new LocalCachedMapDisabledKey(requestId, options.getResponseTimeout());
                multimap.putAsync(localCacheKey, ByteBufUtil.hexDump(key));
                multimap.expireKeyAsync(localCacheKey, options.getResponseTimeout(), TimeUnit.MILLISECONDS);
            }
        }

        RFuture> batchListener = batch.executeAsync();
        batchListener.addListener(new FutureListener>() {
            @Override
            public void operationComplete(Future> future) throws Exception {
                if (!future.isSuccess()) {
                    result.tryFailure(future.cause());
                    return;
                }
                
                final CountableListener> listener = 
                                new CountableListener>(result, hashes, hashes.size());
                RPromise subscriptionFuture = new RedissonPromise();
                final CountableListener subscribedFutures = new CountableListener(subscriptionFuture, null, hashes.size());
                
                final List topics = new ArrayList();
                for (final Entry entry : hashes.entrySet()) {
                    final String disabledAckName = RedissonObject.suffixName(entry.getKey().getName(), requestId + RedissonLocalCachedMap.DISABLED_ACK_SUFFIX);
                    RTopic topic = new RedissonTopic(LocalCachedMessageCodec.INSTANCE, 
                            commandExecutor, disabledAckName);
                    topics.add(topic);
                    RFuture topicFuture = topic.addListenerAsync(Object.class, new MessageListener() {
                        @Override
                        public void onMessage(CharSequence channel, Object msg) {
                            AtomicInteger counter = entry.getValue().getCounter();
                            if (counter.decrementAndGet() == 0) {
                                listener.decCounter();
                            }
                        }
                    });
                    topicFuture.addListener(new FutureListener() {
                        @Override
                        public void operationComplete(Future future) throws Exception {
                            subscribedFutures.decCounter();
                        }
                    });
                }
                
                subscriptionFuture.addListener(new FutureListener() {
                    @Override
                    public void operationComplete(Future future) throws Exception {
                        RedissonBatch publishBatch = new RedissonBatch(null, commandExecutor.getConnectionManager(), BatchOptions.defaults());
                        for (final Entry entry : hashes.entrySet()) {
                            String disabledKeysName = RedissonObject.suffixName(entry.getKey().getName(), RedissonLocalCachedMap.DISABLED_KEYS_SUFFIX);
                            RMultimapCacheAsync multimap = publishBatch.getListMultimapCache(disabledKeysName, entry.getKey().getCodec());
                            LocalCachedMapDisabledKey localCacheKey = new LocalCachedMapDisabledKey(requestId, options.getResponseTimeout());
                            multimap.removeAllAsync(localCacheKey);
                            
                            RTopicAsync topic = publishBatch.getTopic(RedissonObject.suffixName(entry.getKey().getName(), RedissonLocalCachedMap.TOPIC_SUFFIX), LocalCachedMessageCodec.INSTANCE);
                            RFuture publishFuture = topic.publishAsync(new LocalCachedMapDisable(requestId, 
                                    entry.getValue().getKeyIds().toArray(new byte[entry.getValue().getKeyIds().size()][]), options.getResponseTimeout()));
                            publishFuture.addListener(new FutureListener() {
                                @Override
                                public void operationComplete(Future future) throws Exception {
                                    if (!future.isSuccess()) {
                                        return;
                                    }
                                    
                                    int receivers = future.getNow().intValue();
                                    AtomicInteger counter = entry.getValue().getCounter();
                                    if (counter.addAndGet(receivers) == 0) {
                                        listener.decCounter();
                                    }
                                }
                            });
                        }
                        
                        RFuture> publishFuture = publishBatch.executeAsync();
                        publishFuture.addListener(new FutureListener>() {
                            @Override
                            public void operationComplete(Future> future) throws Exception {
                                result.addListener(new FutureListener>() {
                                    @Override
                                    public void operationComplete(Future> future)
                                            throws Exception {
                                        for (RTopic topic : topics) {
                                            topic.removeAllListeners();
                                        }
                                    }
                                });
                                
                                if (!future.isSuccess()) {
                                    result.tryFailure(future.cause());
                                    return;
                                }
                                
                                commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
                                    @Override
                                    public void run(Timeout timeout) throws Exception {
                                        result.tryFailure(new TransactionTimeoutException("Unable to execute transaction within " + options.getResponseTimeout() + "ms"));
                                    }
                                }, options.getResponseTimeout(), TimeUnit.MILLISECONDS);
                            }
                        });
                    }
                });
            }
        });
        
        return result;
    }
    
    protected static String generateId() {
        byte[] id = new byte[16];
        // TODO JDK UPGRADE replace to native ThreadLocalRandom
        PlatformDependent.threadLocalRandom().nextBytes(id);
        return ByteBufUtil.hexDump(id);
    }

    @Override
    public void rollback() {
        rollback(operations);
    }
    
    public void rollback(List operations) {
        checkState();

        CommandBatchService executorService = new CommandBatchService(commandExecutor.getConnectionManager());
        for (TransactionalOperation transactionalOperation : operations) {
            transactionalOperation.rollback(executorService);
        }

        try {
            executorService.execute();
        } catch (Exception e) {
            throw new TransactionException("Unable to rollback transaction", e);
        }

        operations.clear();
        executed.set(true);
    }
    
    @Override
    public RFuture rollbackAsync() {
        checkState();

        CommandBatchService executorService = new CommandBatchService(commandExecutor.getConnectionManager());
        for (TransactionalOperation transactionalOperation : operations) {
            transactionalOperation.rollback(executorService);
        }

        final RPromise result = new RedissonPromise();
        RFuture> future = executorService.executeAsync();
        future.addListener(new FutureListener() {
            @Override
            public void operationComplete(Future future) throws Exception {
                if (!future.isSuccess()) {
                    result.tryFailure(new TransactionException("Unable to rollback transaction", future.cause()));
                    return;
                }
                
                operations.clear();
                executed.set(true);
                result.trySuccess(null);
            }
        });
        return result;
    }
    
    public Set getLocalCaches() {
        return localCaches;
    }
    
    public List getOperations() {
        return operations;
    }

    protected void checkState() {
        if (executed.get()) {
            throw new IllegalStateException("Unable to execute operation. Transaction was finished!");
        }
    }
    
}