org.redisson.transaction.RedissonTransaction.java Source code

Java tutorial

Introduction

Here is the source code for org.redisson.transaction.RedissonTransaction.java

Source

/**
 * Copyright (c) 2013-2019 Nikita Koksharov
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.redisson.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.ThreadLocalRandom;
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.RBuckets;
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;

/**
 * 
 * @author Nikita Koksharov
 *
 */
public class RedissonTransaction implements RTransaction {

    private final CommandAsyncExecutor commandExecutor;
    private final AtomicBoolean executed = new AtomicBoolean();

    private final TransactionOptions options;
    private List<TransactionalOperation> operations = new CopyOnWriteArrayList<>();
    private Set<String> 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<TransactionalOperation> operations, Set<String> localCaches) {
        super();
        this.commandExecutor = commandExecutor;
        this.options = options;
        this.operations = operations;
        this.localCaches = localCaches;
    }

    @Override
    public <K, V> RLocalCachedMap<K, V> getLocalCachedMap(RLocalCachedMap<K, V> fromInstance) {
        checkState();

        localCaches.add(fromInstance.getName());
        return new RedissonTransactionalLocalCachedMap<K, V>(commandExecutor, operations, options.getTimeout(),
                executed, fromInstance, id);
    }

    @Override
    public <V> RBucket<V> getBucket(String name) {
        checkState();

        return new RedissonTransactionalBucket<V>(commandExecutor, options.getTimeout(), name, operations, executed,
                id);
    }

    @Override
    public <V> RBucket<V> getBucket(String name, Codec codec) {
        checkState();

        return new RedissonTransactionalBucket<V>(codec, commandExecutor, options.getTimeout(), name, operations,
                executed, id);
    }

    @Override
    public RBuckets getBuckets() {
        checkState();

        return new RedissonTransactionalBuckets(commandExecutor, options.getTimeout(), operations, executed, id);
    }

    @Override
    public RBuckets getBuckets(Codec codec) {
        checkState();

        return new RedissonTransactionalBuckets(codec, commandExecutor, options.getTimeout(), operations, executed,
                id);
    }

    @Override
    public <V> RSet<V> getSet(String name) {
        checkState();

        return new RedissonTransactionalSet<V>(commandExecutor, name, operations, options.getTimeout(), executed,
                id);
    }

    @Override
    public <V> RSet<V> getSet(String name, Codec codec) {
        checkState();

        return new RedissonTransactionalSet<V>(codec, commandExecutor, name, operations, options.getTimeout(),
                executed, id);
    }

    @Override
    public <V> RSetCache<V> getSetCache(String name) {
        checkState();

        return new RedissonTransactionalSetCache<V>(commandExecutor, name, operations, options.getTimeout(),
                executed, id);
    }

    @Override
    public <V> RSetCache<V> getSetCache(String name, Codec codec) {
        checkState();

        return new RedissonTransactionalSetCache<V>(codec, commandExecutor, name, operations, options.getTimeout(),
                executed, id);
    }

    @Override
    public <K, V> RMap<K, V> getMap(String name) {
        checkState();

        return new RedissonTransactionalMap<K, V>(commandExecutor, name, operations, options.getTimeout(), executed,
                id);
    }

    @Override
    public <K, V> RMap<K, V> getMap(String name, Codec codec) {
        checkState();

        return new RedissonTransactionalMap<K, V>(codec, commandExecutor, name, operations, options.getTimeout(),
                executed, id);
    }

    @Override
    public <K, V> RMapCache<K, V> getMapCache(String name) {
        checkState();

        return new RedissonTransactionalMapCache<K, V>(commandExecutor, name, operations, options.getTimeout(),
                executed, id);
    }

    @Override
    public <K, V> RMapCache<K, V> getMapCache(String name, Codec codec) {
        checkState();

        return new RedissonTransactionalMapCache<K, V>(codec, commandExecutor, name, operations,
                options.getTimeout(), executed, id);
    }

    @Override
    public RFuture<Void> commitAsync() {
        checkState();

        checkTimeout();

        BatchOptions batchOptions = createOptions();

        CommandBatchService transactionExecutor = new CommandBatchService(commandExecutor.getConnectionManager(),
                batchOptions);
        for (TransactionalOperation transactionalOperation : operations) {
            transactionalOperation.commit(transactionExecutor);
        }

        String id = generateId();
        RPromise<Void> result = new RedissonPromise<Void>();
        RFuture<Map<HashKey, HashValue>> future = disableLocalCacheAsync(id, localCaches, operations);
        future.onComplete((res, ex) -> {
            if (ex != null) {
                result.tryFailure(new TransactionException("Unable to execute transaction", ex));
                return;
            }

            Map<HashKey, HashValue> hashes = future.getNow();
            try {
                checkTimeout();
            } catch (TransactionTimeoutException e) {
                enableLocalCacheAsync(id, hashes);
                result.tryFailure(e);
                return;
            }

            RFuture<List<?>> transactionFuture = transactionExecutor.executeAsync();
            transactionFuture.onComplete((r, exc) -> {
                if (exc != null) {
                    result.tryFailure(new TransactionException("Unable to execute transaction", exc));
                    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<String> localCaches, List<TransactionalOperation> operations) {
        checkState();

        checkTimeout();

        BatchOptions batchOptions = createOptions();

        CommandBatchService transactionExecutor = new CommandBatchService(commandExecutor.getConnectionManager(),
                batchOptions);
        for (TransactionalOperation transactionalOperation : operations) {
            transactionalOperation.commit(transactionExecutor);
        }

        String id = generateId();
        Map<HashKey, HashValue> 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<BatchResult<?>> enableLocalCacheAsync(String requestId, Map<HashKey, HashValue> hashes) {
        if (hashes.isEmpty()) {
            return RedissonPromise.newSucceededFuture(null);
        }

        RedissonBatch publishBatch = new RedissonBatch(null, commandExecutor.getConnectionManager(),
                BatchOptions.defaults());
        for (Entry<HashKey, HashValue> 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<HashKey, HashValue> hashes) {
        if (hashes.isEmpty()) {
            return;
        }

        RedissonBatch publishBatch = new RedissonBatch(null, commandExecutor.getConnectionManager(),
                BatchOptions.defaults());
        for (Entry<HashKey, HashValue> 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<HashKey, HashValue> disableLocalCache(String requestId, Set<String> localCaches,
            List<TransactionalOperation> operations) {
        if (localCaches.isEmpty()) {
            return Collections.emptyMap();
        }

        Map<HashKey, HashValue> 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<LocalCachedMapDisabledKey, String> 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);
        }

        CountDownLatch latch = new CountDownLatch(hashes.size());
        List<RTopic> topics = new ArrayList<>();
        for (Entry<HashKey, HashValue> 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<Object>() {
                @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 (Entry<HashKey, HashValue> entry : hashes.entrySet()) {
            String disabledKeysName = RedissonObject.suffixName(entry.getKey().getName(),
                    RedissonLocalCachedMap.DISABLED_KEYS_SUFFIX);
            RMultimapCacheAsync<LocalCachedMapDisabledKey, String> 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<Long> future = topic.publishAsync(new LocalCachedMapDisable(requestId,
                    entry.getValue().getKeyIds().toArray(new byte[entry.getValue().getKeyIds().size()][]),
                    options.getResponseTimeout()));
            future.onComplete((res, e) -> {
                if (e != null) {
                    return;
                }

                int receivers = res.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<Map<HashKey, HashValue>> disableLocalCacheAsync(String requestId, Set<String> localCaches,
            List<TransactionalOperation> operations) {
        if (localCaches.isEmpty()) {
            return RedissonPromise.newSucceededFuture(Collections.emptyMap());
        }

        RPromise<Map<HashKey, HashValue>> result = new RedissonPromise<>();
        Map<HashKey, HashValue> 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<LocalCachedMapDisabledKey, String> 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<BatchResult<?>> batchListener = batch.executeAsync();
        batchListener.onComplete((res, e) -> {
            if (e != null) {
                result.tryFailure(e);
                return;
            }

            CountableListener<Map<HashKey, HashValue>> listener = new CountableListener<>(result, hashes,
                    hashes.size());
            RPromise<Void> subscriptionFuture = new RedissonPromise<>();
            CountableListener<Void> subscribedFutures = new CountableListener<>(subscriptionFuture, null,
                    hashes.size());

            List<RTopic> topics = new ArrayList<>();
            for (Entry<HashKey, HashValue> entry : hashes.entrySet()) {
                String disabledAckName = RedissonObject.suffixName(entry.getKey().getName(),
                        requestId + RedissonLocalCachedMap.DISABLED_ACK_SUFFIX);
                RTopic topic = new RedissonTopic(LocalCachedMessageCodec.INSTANCE, commandExecutor,
                        disabledAckName);
                topics.add(topic);
                RFuture<Integer> topicFuture = topic.addListenerAsync(Object.class, new MessageListener<Object>() {
                    @Override
                    public void onMessage(CharSequence channel, Object msg) {
                        AtomicInteger counter = entry.getValue().getCounter();
                        if (counter.decrementAndGet() == 0) {
                            listener.decCounter();
                        }
                    }
                });
                topicFuture.onComplete((r, ex) -> {
                    subscribedFutures.decCounter();
                });
            }

            subscriptionFuture.onComplete((r, ex) -> {
                RedissonBatch publishBatch = new RedissonBatch(null, commandExecutor.getConnectionManager(),
                        BatchOptions.defaults());
                for (Entry<HashKey, HashValue> entry : hashes.entrySet()) {
                    String disabledKeysName = RedissonObject.suffixName(entry.getKey().getName(),
                            RedissonLocalCachedMap.DISABLED_KEYS_SUFFIX);
                    RMultimapCacheAsync<LocalCachedMapDisabledKey, String> 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<Long> publishFuture = topic.publishAsync(new LocalCachedMapDisable(requestId,
                            entry.getValue().getKeyIds().toArray(new byte[entry.getValue().getKeyIds().size()][]),
                            options.getResponseTimeout()));
                    publishFuture.onComplete((receivers, exc) -> {
                        if (ex != null) {
                            return;
                        }

                        AtomicInteger counter = entry.getValue().getCounter();
                        if (counter.addAndGet(receivers.intValue()) == 0) {
                            listener.decCounter();
                        }
                    });
                }

                RFuture<BatchResult<?>> publishFuture = publishBatch.executeAsync();
                publishFuture.onComplete((res2, ex2) -> {
                    result.onComplete((res3, ex3) -> {
                        for (RTopic topic : topics) {
                            topic.removeAllListeners();
                        }
                    });

                    if (ex2 != null) {
                        result.tryFailure(ex2);
                        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];
        ThreadLocalRandom.current().nextBytes(id);
        return ByteBufUtil.hexDump(id);
    }

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

    public void rollback(List<TransactionalOperation> 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<Void> rollbackAsync() {
        checkState();

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

        RPromise<Void> result = new RedissonPromise<>();
        RFuture<List<?>> future = executorService.executeAsync();
        future.onComplete((res, e) -> {
            if (e != null) {
                result.tryFailure(new TransactionException("Unable to rollback transaction", e));
                return;
            }

            operations.clear();
            executed.set(true);
            result.trySuccess(null);
        });
        return result;
    }

    public Set<String> getLocalCaches() {
        return localCaches;
    }

    public List<TransactionalOperation> getOperations() {
        return operations;
    }

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

}