com.oodrive.nuage.dtx.DtxTestHelper.java Source code

Java tutorial

Introduction

Here is the source code for com.oodrive.nuage.dtx.DtxTestHelper.java

Source

package com.oodrive.nuage.dtx;

/*
 * #%L
 * Project eguan
 * %%
 * Copyright (C) 2012 - 2015 Oodrive
 * %%
 * 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.
 * #L%
 */

import static com.oodrive.nuage.dtx.DtxConstants.DEFAULT_LAST_TX_VALUE;
import static com.oodrive.nuage.dtx.DtxResourceManagerState.UP_TO_DATE;
import static com.oodrive.nuage.dtx.TransactionManager.newJournalFilePrefix;
import static com.oodrive.nuage.dtx.proto.TxProtobufUtils.fromUuid;
import static com.oodrive.nuage.dtx.proto.TxProtobufUtils.toUuid;
import static com.oodrive.nuage.proto.dtx.DistTxWrapper.TxJournalEntry.TxOpCode.START;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

import javax.annotation.Nonnull;
import javax.transaction.xa.XAException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Table;
import com.google.common.collect.TreeBasedTable;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.oodrive.nuage.configuration.ConfigValidationException;
import com.oodrive.nuage.configuration.MetaConfiguration;
import com.oodrive.nuage.configuration.ValidConfigurationContext.ContextTestHelper;
import com.oodrive.nuage.dtx.DtxEventListeners.StateCountListener;
import com.oodrive.nuage.dtx.config.DtxConfigurationContext;
import com.oodrive.nuage.dtx.config.TestValidDtxConfigurationContext;
import com.oodrive.nuage.dtx.journal.JournalRecord;
import com.oodrive.nuage.dtx.journal.JournalRotationManager;
import com.oodrive.nuage.dtx.journal.WritableTxJournal;
import com.oodrive.nuage.dtx.proto.TxProtobufUtils;
import com.oodrive.nuage.proto.Common.ProtocolVersion;
import com.oodrive.nuage.proto.Common.Uuid;
import com.oodrive.nuage.proto.dtx.DistTxWrapper.TxJournalEntry;
import com.oodrive.nuage.proto.dtx.DistTxWrapper.TxMessage;
import com.oodrive.nuage.proto.dtx.DistTxWrapper.TxNode;
import com.oodrive.nuage.utils.Strings;

/**
 * Helper class for all tests related to the DTX subsystem.
 * 
 * @author oodrive
 * @author pwehrle
 * @author ebredzinski
 * 
 */
public final class DtxTestHelper {

    private static final Logger LOGGER = LoggerFactory.getLogger(DtxTestHelper.class);

    /**
     * Default Hazelcast listening port.
     */
    public static final int DEFAULT_HZ_PORT = 12341;

    /**
     * Default offset to add to port numbers when instantiating several Hazelcast peers.
     */
    private static final int HZ_PORT_OFFSET = 10;

    private static final int HZ_PORT_UPPER_LIMIT = 30000;

    private static final int HZ_PORT_SEED = 5000;

    private static final AtomicInteger HZ_PORT;
    static {
        HZ_PORT = new AtomicInteger(DEFAULT_HZ_PORT + new Random(System.currentTimeMillis()).nextInt(HZ_PORT_SEED));
    }

    private static final long DEFAULT_TX_TIMEOUT_MS = 10000;

    private static final int PARTICIPANT_COUNT = 5;

    private static final int STATE_UPDATE_WAIT_TIMEOUT_S = 10;

    /**
     * Artificial transaction ID for testing purposes.
     */
    private static final AtomicLong TX_ID = new AtomicLong(0);

    /**
     * The default always available loopback network host address.
     */
    private static final InetAddress LOCALHOST = InetAddress.getLoopbackAddress();

    private static final MetaConfiguration DEFAULT_CONFIG;
    static {
        final Properties props = new TestValidDtxConfigurationContext().getTestHelper().getDefaultConfig();
        MetaConfiguration defaultConfig = null;
        try {
            defaultConfig = MetaConfiguration.newConfiguration(ContextTestHelper.getPropertiesAsInputStream(props),
                    DtxConfigurationContext.getInstance());
        } catch (NullPointerException | IllegalArgumentException | IOException | ConfigValidationException e) {
            LOGGER.error("Default test configuration initialization failed", e);
        } finally {
            DEFAULT_CONFIG = defaultConfig;
        }
    }

    /**
     * Gets the default configuration satisfying the requirements of {@link DtxConfigurationContext}.
     * 
     * @return a {@link MetaConfiguration} or <code>null</code> if its initialization failed
     */
    public static final MetaConfiguration getDefaultConfiguration() {
        return DEFAULT_CONFIG;
    }

    /**
     * Default transaction content.
     */
    public static final TxMessage DEFAULT_TX_MESSAGE = TxMessage.newBuilder().setVersion(ProtocolVersion.VERSION_1)
            .setTxId(DtxTestHelper.nextTxId()).setTaskId(toUuid(UUID.randomUUID()))
            .setInitiatorId(toUuid(UUID.randomUUID())).setResId(toUuid(UUID.randomUUID()))
            .setPayload(ByteString.copyFrom(DtxDummyRmFactory.DEFAULT_PAYLOAD)).setTimeout(DEFAULT_TX_TIMEOUT_MS)
            .build();

    /**
     * Private constructor.
     */
    private DtxTestHelper() {
        throw new AssertionError("Not instantiable");
    }

    /**
     * Gets the default minimal valid configuration for constructing a {@link DtxManager}.
     * 
     * @param journalDir
     *            the journal directory
     * 
     * @return a functional {@link DtxManagerConfig} instance
     */
    static final DtxManagerConfig newDtxManagerConfig(final Path journalDir) {
        final DtxNode localPeerAddr = new DtxNode(UUID.randomUUID(),
                new InetSocketAddress(LOCALHOST, nextHazelcastPort()));

        return newDtxManagerConfig(localPeerAddr, journalDir);
    }

    /**
     * Creates a {@link DtxManagerConfig} instance with the given local and remote peers.
     * 
     * @param localPeer
     *            a non-<code>null</code> {@link DtxNode}
     * @param journalDir
     *            the journal directory
     * @param remotePeers
     *            a list of {@link DtxNode}s describing the remote peers to which the local peer connects
     * @return a valid {@link DtxManagerConfig} instance
     */
    static final DtxManagerConfig newDtxManagerConfig(final DtxNode localPeer, final Path journalDir,
            final DtxNode... remotePeers) {

        /*
         * computes a unique hash from the sorted set of peers to avoid interfering with simultaneously running test
         * clusters
         */
        final Set<DtxNode> peerSet = new TreeSet<DtxNode>(new Comparator<DtxNode>() {
            @Override
            public int compare(final DtxNode o1, final DtxNode o2) {
                return o1.getNodeId().compareTo(o2.getNodeId());
            }
        });
        peerSet.addAll(Arrays.asList(remotePeers));
        peerSet.add(localPeer);
        String configKey = "";
        try {
            final MessageDigest digester = MessageDigest.getInstance("MD5");
            for (final DtxNode currNode : peerSet) {
                digester.update(currNode.toString().getBytes());
            }
            configKey = Strings.toHexString(digester.digest());
        } catch (final NoSuchAlgorithmException e) {
            LOGGER.warn("Exception computing cluster config hash", e);
            // nothing
        }

        return new DtxManagerConfig(getDefaultConfiguration(), journalDir, "testCluster-" + configKey, configKey,
                localPeer, remotePeers);

    }

    /**
     * Constructs a new Hazelcast cluster with random properties (node IDs and ports).
     * 
     * Note: All addresses will point to {@link #LOCALHOST} and use partly random port numbers designed to minimize
     * collisions when running tests in parallel.
     * 
     * @param nbOfNodes
     *            the total number of nodes in the cluster
     * @return a {@link Set} of distinct {@link DtxNode}s
     */
    static final Set<DtxNode> newRandomCluster(final int nbOfNodes) {
        final HashSet<DtxNode> result = new HashSet<DtxNode>(nbOfNodes);
        for (int i = 0; i < nbOfNodes; i++) {
            result.add(new DtxNode(UUID.randomUUID(), new InetSocketAddress(LOCALHOST, nextHazelcastPort())));
        }
        return result;
    }

    private static final int nextHazelcastPort() {
        final int oldPort = HZ_PORT.get();
        if (oldPort + HZ_PORT_OFFSET > HZ_PORT_UPPER_LIMIT) {
            HZ_PORT.set(DEFAULT_HZ_PORT + new Random(System.currentTimeMillis()).nextInt(HZ_PORT_SEED));
        }
        return HZ_PORT.addAndGet(HZ_PORT_OFFSET);
    }

    /**
     * Registers the given {@link DtxResourceManager} with the {@link TransactionManager}.
     * 
     * @param txMgr
     *            the {@link TransactionManager} to register with
     * @param resMgr
     *            the {@link DtxResourceManager} to register, defaults to
     *            {@link DtxDummyRmFactory#newResMgrThatDoesEverythingRight(UUID)} if given <code>null</code>
     * @param syncState
     *            the {@link DtxResourceManagerState} to set, defaults to {@link DtxResourceManagerState#UP_TO_DATE} if
     *            <code>null</code>
     * @return the registered {@link DtxResourceManager}'s {@link UUID}
     * @throws XAException
     *             if {@link DtxDummyRmFactory#newResMgrThatDoesEverythingRight(UUID)} fails
     */
    static final UUID registerResMgrWithTxMgr(final TransactionManager txMgr, final DtxResourceManager resMgr,
            final DtxResourceManagerState syncState) throws XAException {

        final DtxResourceManager targetResMgr = resMgr == null
                ? DtxDummyRmFactory.newResMgrThatDoesEverythingRight(null)
                : resMgr;

        final UUID resUuid = targetResMgr.getId();

        txMgr.registerResourceManager(targetResMgr, null);
        assertEquals(targetResMgr, txMgr.getRegisteredResourceManager(resUuid));

        txMgr.setResManagerSyncState(resUuid, syncState == null ? UP_TO_DATE : syncState);

        return resUuid;
    }

    /**
     * Builds a minimal transaction with {@link #DEFAULT_TX_MESSAGE} and the next available transaction ID.
     * 
     * @param resUuid
     *            the {@link DtxResourceManager}'s ID to include in the transaction
     * @return a valid {@link TxMessage}
     */
    static final TxMessage buildDefaultTransaction(@Nonnull final UUID resUuid) {

        return TxMessage.newBuilder(DEFAULT_TX_MESSAGE).setVersion(ProtocolVersion.VERSION_1).setTxId(nextTxId())
                .setResId(toUuid(resUuid)).build();
    }

    /**
     * Awaits the first change of the given resource manager to a target state.
     * 
     * @param targetDtxMgr
     *            the {@link DtxManager} having registered the resource manager
     * @param resMgrId
     *            the {@link UUID} of the resource manager
     * @param targetState
     *            the {@link DtxResourceManagerState} to wait for
     * @throws InterruptedException
     *             if interrupted while waiting
     * @throws TimeoutException
     *             if the target state has not been reached after a fixed timeout of
     *             {@value #STATE_UPDATE_WAIT_TIMEOUT_S} seconds.
     */
    static final void awaitStateUpdate(final DtxManager targetDtxMgr, final UUID resMgrId,
            final DtxResourceManagerState targetState) throws InterruptedException, TimeoutException {

        final DtxResourceManagerState currState = targetDtxMgr.getResourceManagerState(resMgrId);
        if (targetState == currState) {
            return;
        }

        final CountDownLatch targetStateLatch = new CountDownLatch(1);
        final HashMultimap<UUID, DtxManager> resMgrMap = HashMultimap.create();
        resMgrMap.put(resMgrId, targetDtxMgr);
        final StateCountListener upToDateListener = new StateCountListener(targetStateLatch, targetState,
                resMgrMap);
        targetDtxMgr.registerDtxEventListener(upToDateListener);

        try {
            if (!targetDtxMgr.isStarted()) {
                targetDtxMgr.start();
            }

            if (!targetStateLatch.await(STATE_UPDATE_WAIT_TIMEOUT_S, SECONDS)) {
                // check once more in case the listener failed to detect the status change
                if (targetState != targetDtxMgr.getResourceManagerState(resMgrId)) {
                    throw new TimeoutException("Waiting for target state timed out; targetState=" + targetState);
                }
            }
        } finally {
            targetDtxMgr.unregisterDtxEventListener(upToDateListener);
        }

    }

    /**
     * Gets the next valid transaction ID.
     * 
     * IDs produced by this method are guaranteed to be unique, positive and greater than all previous values (excluding
     * overflows).
     * 
     * @return a positive long
     */
    public static final long nextTxId() {
        return TX_ID.incrementAndGet();
    }

    /**
     * Generates a {@link Set} of {@link TxNode}s for journalled operations on {@link TransactionManager}s.
     * 
     * @return a {@link Set} with {@value #PARTICIPANT_COUNT} {@link DtxNode}s pointing to localhost on different ports
     *         with random IDs
     */
    public static final Set<TxNode> newRandomParticipantsSet() {
        final HashSet<TxNode> result = new HashSet<TxNode>();
        for (int i = 0; i < PARTICIPANT_COUNT; i++) {
            final DtxNode dtxNode = new DtxNode(UUID.randomUUID(),
                    new InetSocketAddress("127.0.0.1", DEFAULT_HZ_PORT + (i * HZ_PORT_OFFSET)));
            result.add(TxProtobufUtils.toTxNode(dtxNode));
        }
        return Collections.unmodifiableSet(result);
    }

    /**
     * Writes any number of complete transactions (i.e. start, [commit|rollback]) to the given journal.
     * 
     * @param target
     *            a {@link WritableTxJournal} to which to write
     * @param numberOfTx
     *            the number of transactions to write
     * @param resUuid
     *            the {@link UUID} to include as resource manager ID, random if <code>null</code>
     * @param participants
     *            the {@link Set} of participating TxNodes to include
     * @return the ID of the last written transaction
     * @throws IllegalStateException
     *             if the journal is not in a state that allows writing to it
     * @throws IOException
     *             if writing to the journal fails
     */
    public static final long writeCompleteTransactions(final WritableTxJournal target, final int numberOfTx,
            final UUID resUuid, final Set<TxNode> participants) throws IllegalStateException, IOException {
        long txId = 0;

        final Uuid resId = TxProtobufUtils.toUuid(resUuid == null ? UUID.randomUUID() : resUuid);

        for (int i = 0; i < numberOfTx; i++) {
            txId = DtxTestHelper.nextTxId();
            final TxMessage defTx = TxMessage.newBuilder(DEFAULT_TX_MESSAGE).setVersion(ProtocolVersion.VERSION_1)
                    .setTxId(txId).setResId(resId).setTaskId(TxProtobufUtils.toUuid(UUID.randomUUID())).build();

            target.writeStart(defTx, participants);

            // writes commits and rollbacks for every other transaction
            if (i % 2 == 0) {
                target.writeRollback(txId, -1, participants);
            } else {
                target.writeCommit(txId, participants);
            }
        }
        return txId;
    }

    /**
     * Read any number of complete transactions (i.e. start, [commit|rollback]) to the given journal.
     * 
     * @param target
     *            a {@link WritableTxJournal} to which to read
     * @return an array List with the task ID
     */
    public static final ArrayList<UUID> readCompleteTransactions(final WritableTxJournal target) {

        final ArrayList<UUID> taskLists = new ArrayList<UUID>();

        for (final JournalRecord currRecord : target) {
            TxJournalEntry currEntry;
            try {
                currEntry = TxJournalEntry.parseFrom(currRecord.getEntry());
            } catch (final InvalidProtocolBufferException e) {
                LOGGER.warn("Could not read journal entry; journal=" + target);
                continue;
            }
            if (currEntry.getOp().equals(START)) {
                final UUID taskId = fromUuid(currEntry.getTx().getTaskId());
                taskLists.add(taskId);
            }
        }
        return taskLists;
    }

    /**
     * Prepares, i.e. writes transactions to a set of journals according to the information given as a {@link Table}.
     * 
     * @param dtxMgrTxTable
     *            (sorted) {@link TreeBasedTable} mapping resource manager {@link UUID}s, last transaction IDs to
     *            {@link DtxManager} instances
     * @param journalDirMap
     *            a {@link Map} providing the temporary directories for each {@link DtxManager}
     * @param setupRotMgr
     *            a central {@link JournalRotationManager} used to write the prepared journals
     * @return a {@link Table} of non-failing mock {@link DtxResourceManager}s with their last transaction ID and
     *         journal file directories
     * @throws IllegalStateException
     *             if writing to journals fails due to their internal state
     * @throws IOException
     *             if writing or reading data fails
     * @throws XAException
     *             if mock setup fails
     */
    public static final Table<DtxResourceManager, Long, Path> prepareExistingJournals(
            final TreeBasedTable<Long, UUID, DtxManager> dtxMgrTxTable, final Map<DtxManager, Path> journalDirMap,
            final JournalRotationManager setupRotMgr) throws IllegalStateException, IOException, XAException {

        // reference map to order resource managers by their last tx ID
        final Map<DtxResourceManager, Long> rankMap = new HashMap<DtxResourceManager, Long>();

        final Comparator<DtxResourceManager> rowComp = new Comparator<DtxResourceManager>() {

            @Override
            public final int compare(final DtxResourceManager o1, final DtxResourceManager o2) {
                // compare last tx IDs before falling back to classic comparison
                final Long rank1 = rankMap.get(o1);
                final Long rank2 = rankMap.get(o2);
                final int rankComp = Long.compare(rank1.longValue(), rank2.longValue()) * -1;
                if (rankComp != 0) {
                    return rankComp;
                }

                // fall back to comparing IDs
                final UUID id1 = o1.getId();
                final UUID id2 = o2.getId();
                final int idComp = id1.compareTo(id2);

                // maintain coherence with equals()
                if (idComp == 0) {
                    return Integer.compare(o1.hashCode(), o2.hashCode());
                }
                return idComp;
            }
        };

        final Comparator<Long> columnComp = Collections.reverseOrder();

        final Table<DtxResourceManager, Long, Path> result = TreeBasedTable.create(rowComp, columnComp);

        final HashMap<UUID, DtxManager> previousLog = new HashMap<UUID, DtxManager>();

        long lastTxId = TX_ID.get();

        for (final Long currTargetTxId : dtxMgrTxTable.rowKeySet()) {

            final long targetTxId = currTargetTxId.longValue();
            if (targetTxId <= lastTxId) {
                throw new IllegalArgumentException(
                        "Not increasing transaction IDs; lastTxId=" + lastTxId + ", targetTxId=" + targetTxId);
            }

            final Set<TxNode> participants = newRandomParticipantsSet();

            final SortedMap<UUID, DtxManager> currRow = dtxMgrTxTable.row(currTargetTxId);

            // / insert here
            for (final UUID currResMgrId : currRow.keySet()) {

                final DtxResourceManager currResMgr = DtxDummyRmFactory
                        .newResMgrThatDoesEverythingRight(currResMgrId);

                final DtxManager currDtxMgr = currRow.get(currResMgrId);
                final Path currTmpDir = journalDirMap.get(currDtxMgr);

                final String journalFilename = newJournalFilePrefix(currDtxMgr.getNodeId(), currResMgrId);

                final WritableTxJournal targetJournal = new WritableTxJournal(currTmpDir.toFile(), journalFilename,
                        0, setupRotMgr);

                targetJournal.start();
                assertEquals(DEFAULT_LAST_TX_VALUE, targetJournal.getLastFinishedTxId());

                final WritableTxJournal prevJournal;
                final DtxManager prevDtxMgr = previousLog.get(currResMgrId);
                if (prevDtxMgr != null) {
                    prevJournal = new WritableTxJournal(journalDirMap.get(prevDtxMgr).toFile(),
                            newJournalFilePrefix(prevDtxMgr.getNodeId(), currResMgrId), 0, setupRotMgr);
                } else {
                    prevJournal = null;
                }

                // re-reads the previous journal and copies it to the new target
                if (prevJournal != null) {
                    prevJournal.start();
                    for (final JournalRecord currRecord : prevJournal.newReadOnlyTxJournal()) {
                        final TxJournalEntry currEntry = TxJournalEntry.parseFrom(currRecord.getEntry());
                        switch (currEntry.getOp()) {
                        case START:
                            targetJournal.writeStart(currEntry.getTx(), currEntry.getTxNodesList());
                            break;
                        case COMMIT:
                            targetJournal.writeCommit(currEntry.getTxId(), currEntry.getTxNodesList());
                            break;
                        case ROLLBACK:
                            targetJournal.writeRollback(currEntry.getTxId(), currEntry.getErrCode(),
                                    currEntry.getTxNodesList());
                            break;
                        default:
                            // nothing
                        }
                    }
                    prevJournal.stop();
                }

                final int nbToWrite = Long
                        .valueOf(lastTxId == DEFAULT_LAST_TX_VALUE ? targetTxId - 1 : targetTxId - lastTxId)
                        .intValue();
                lastTxId = DtxTestHelper.writeCompleteTransactions(targetJournal, nbToWrite, currResMgrId,
                        participants);
                assertEquals(targetTxId, lastTxId);
                assertEquals(lastTxId, targetJournal.getLastFinishedTxId());
                targetJournal.stop();

                previousLog.put(currResMgrId, currDtxMgr);

                rankMap.put(currResMgr, currTargetTxId);
                result.put(currResMgr, currTargetTxId, currTmpDir);
            }

        }
        return result;
    }

    /**
     * Performs multiple checks to ensure all {@link TransactionManager}s recorded the same transactions.
     * 
     * @param resUuid
     *            the resource manager {@link UUID} to check
     * @param txManagers
     *            a {@link List} of {@link TransactionManager}s
     * @param nbOfTx
     *            the maximum number of transactions that can be checked
     */
    public static final void checkJournalSync(final UUID resUuid, final List<TransactionManager> txManagers,
            final int nbOfTx) {
        final int nbOfTxMgrs = txManagers.size();
        final HashMap<DtxNode, Long> lateTxMgrs = new HashMap<DtxNode, Long>(nbOfTxMgrs);

        long lastTxId = DtxConstants.DEFAULT_LAST_TX_VALUE;
        for (final TransactionManager currTxMgr : txManagers) {
            lastTxId = Math.max(lastTxId, currTxMgr.getLastCompleteTxIdForResMgr(resUuid));
        }

        for (final TransactionManager currTxMgr : txManagers) {
            final long lastCompleteTx = currTxMgr.getLastCompleteTxIdForResMgr(resUuid);
            if (lastTxId > lastCompleteTx) {
                lateTxMgrs.put(currTxMgr.getLocalNode(), Long.valueOf(lastCompleteTx));
            }
        }
        if (!lateTxMgrs.isEmpty()) {
            throw new AssertionError("Some transaction managers are late; expected last tx=" + lastTxId
                    + ", late list=" + lateTxMgrs);
        }

        // references set for commits and rollbacks on any of the transaction managers
        final BitSet refCommitSet = new BitSet(nbOfTx);
        final BitSet refRollbackSet = new BitSet(nbOfTx);

        final HashMap<TransactionManager, BitSet> commitMap = new HashMap<TransactionManager, BitSet>();
        final HashMap<TransactionManager, BitSet> rollbackMap = new HashMap<TransactionManager, BitSet>();

        for (final TransactionManager currTxMgr : txManagers) {
            final BitSet commitSet = new BitSet(nbOfTx);
            final BitSet rollbackSet = new BitSet(nbOfTx);
            commitMap.put(currTxMgr, commitSet);
            rollbackMap.put(currTxMgr, rollbackSet);
            for (final TxJournalEntry currTxEntry : currTxMgr.extractTransactions(resUuid,
                    DtxConstants.DEFAULT_LAST_TX_VALUE, lastTxId)) {
                final long currTxId = currTxEntry.getTxId();
                final int currIndex = Long.valueOf(currTxId % nbOfTx).intValue();
                switch (currTxEntry.getOp()) {
                case START:
                    break;
                case COMMIT:
                    commitSet.set(currIndex);
                    refCommitSet.set(currIndex);
                    break;
                case ROLLBACK:
                    rollbackSet.set(currIndex);
                    refRollbackSet.set(currIndex);
                    break;
                default:
                    // nothing
                }
            }
        }

        // check single commits and rollbacks
        for (final TransactionManager currTxMgr : txManagers) {
            final BitSet commitSet = commitMap.get(currTxMgr);
            commitSet.xor(refCommitSet);
            if (!commitSet.isEmpty()) {
                throw new AssertionError("Incoherence in commit log; offending transaction offsets="
                        + extractSetBitsMsgFromTxSet(commitSet));
            }

            final BitSet rollbackSet = rollbackMap.get(currTxMgr);
            rollbackSet.xor(refRollbackSet);
            if (!rollbackSet.isEmpty()) {
                throw new AssertionError("Incoherence in rollback log; offending transaction offsets="
                        + extractSetBitsMsgFromTxSet(rollbackSet));
            }

            assertTrue(rollbackSet.isEmpty());
        }
    }

    private static final String extractSetBitsMsgFromTxSet(final BitSet txSet) {
        if (txSet.isEmpty()) {
            return null;
        }

        final StringBuffer result = new StringBuffer();
        final int lastIndex = txSet.size() - 1;
        int setIndex = txSet.nextSetBit(0);
        do {
            result.append(setIndex);
            setIndex = txSet.nextSetBit(setIndex + 1);
            if (setIndex > 0) {
                result.append(",");
            }
        } while ((setIndex > 0) && (setIndex < lastIndex));

        return result.toString();
    }
}