package io.eguan.dtx; /* * #%L * Project eguan * %% * Copyright (C) 2012 - 2017 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 io.eguan.dtx.DtxConstants.DEFAULT_LAST_TX_VALUE; import static io.eguan.dtx.DtxResourceManagerState.UP_TO_DATE; import static io.eguan.dtx.TransactionManager.newJournalFilePrefix; import static io.eguan.dtx.proto.TxProtobufUtils.fromUuid; import static io.eguan.dtx.proto.TxProtobufUtils.toUuid; import static io.eguan.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 io.eguan.configuration.ConfigValidationException; import io.eguan.configuration.MetaConfiguration; import io.eguan.configuration.ValidConfigurationContext.ContextTestHelper; import io.eguan.dtx.DtxConstants; import io.eguan.dtx.DtxManager; import io.eguan.dtx.DtxManagerConfig; import io.eguan.dtx.DtxNode; import io.eguan.dtx.DtxResourceManager; import io.eguan.dtx.DtxResourceManagerState; import io.eguan.dtx.TransactionManager; import io.eguan.dtx.DtxEventListeners.StateCountListener; import io.eguan.dtx.config.DtxConfigurationContext; import io.eguan.dtx.config.TestValidDtxConfigurationContext; import io.eguan.dtx.journal.JournalRecord; import io.eguan.dtx.journal.JournalRotationManager; import io.eguan.dtx.journal.WritableTxJournal; import io.eguan.dtx.proto.TxProtobufUtils; import io.eguan.proto.Common.ProtocolVersion; import io.eguan.proto.Common.Uuid; import io.eguan.proto.dtx.DistTxWrapper.TxJournalEntry; import io.eguan.proto.dtx.DistTxWrapper.TxMessage; import io.eguan.proto.dtx.DistTxWrapper.TxNode; import io.eguan.utils.Strings; 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; /** * 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(); } }