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_JOURNAL_FILE_PREFIX; import static io.eguan.dtx.DtxConstants.DEFAULT_LAST_TX_VALUE; import static io.eguan.dtx.DtxResourceManagerState.LATE; import static io.eguan.dtx.DtxResourceManagerState.POST_SYNC_PROCESSING; import static io.eguan.dtx.DtxResourceManagerState.SYNCHRONIZING; import static io.eguan.dtx.DtxResourceManagerState.UNDETERMINED; import static io.eguan.dtx.DtxResourceManagerState.UNREGISTERED; import static io.eguan.dtx.DtxResourceManagerState.UP_TO_DATE; import static io.eguan.dtx.DtxTaskStatus.COMMITTED; import static io.eguan.dtx.DtxTaskStatus.ROLLED_BACK; import static io.eguan.dtx.DtxTaskStatus.STARTED; import static io.eguan.dtx.DtxTaskStatus.UNKNOWN; import static io.eguan.dtx.DtxUtils.updateAtomicLongToAtLeast; import static io.eguan.dtx.proto.TxProtobufUtils.fromUuid; import io.eguan.dtx.DtxTaskApiAbstract.TaskLoader; import io.eguan.dtx.events.DtxResourceManagerEvent; 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.dtx.DistTxWrapper.TxJournalEntry; import io.eguan.proto.dtx.DistTxWrapper.TxJournalEntry.TxOpCode; import io.eguan.proto.dtx.DistTxWrapper.TxMessage; import io.eguan.proto.dtx.DistTxWrapper.TxNode; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.BitSet; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import javax.annotation.Nonnull; import javax.annotation.ParametersAreNonnullByDefault; import javax.annotation.concurrent.GuardedBy; import javax.transaction.xa.XAException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.protobuf.InvalidProtocolBufferException; /** * The transaction manager managing all transactions for all resource managers of a single node. * * @author oodrive * @author pwehrle * @author ebredzinski * */ final class TransactionManager { private static final Logger LOGGER = LoggerFactory.getLogger(TransactionManager.class.getName()); private static final List<DtxResourceManagerState> TX_AUTH_STATES = Arrays.asList(new DtxResourceManagerState[] { UP_TO_DATE, POST_SYNC_PROCESSING }); /** * Generates a prefix string for a journal file. * * @param nodeId * the ID of the DTX node to include in the result * @param resId * the ID of the {@link DtxResourceManager} for which the journal is written * @return a non-empty {@link String} */ static final String newJournalFilePrefix(final UUID nodeId, final UUID resId) { return DEFAULT_JOURNAL_FILE_PREFIX + nodeId + "_" + resId; } private final DtxManagerConfig dtxManagerConfig; private final ConcurrentHashMap<Long, DtxResourceManagerContext> contextMap = new ConcurrentHashMap<>(); /** * {@link ConcurrentHashMap} holding the {@link DtxResourceManager} instances registered with this * {@link DtxManager}. */ @GuardedBy("transactionLock") private final ConcurrentHashMap<UUID, DtxResourceManager> resourceManagers; @GuardedBy("transactionLock") private final ConcurrentHashMap<UUID, WritableTxJournal> journals; @GuardedBy("transactionLock") private final ConcurrentHashMap<UUID, DtxResourceManagerState> states; /** * Shared lock whose only purpose is to postpone shutdown until all transaction processing is done. */ private final ReentrantReadWriteLock transactionLock = new ReentrantReadWriteLock(); /** * Exclusive lock to be held for replaying transactions during synchronization. */ private final ReentrantLock syncReplayLock = new ReentrantLock(); /** * Exclusive lock to be held while modifying any resource manager's synchronization state. */ private final ReentrantLock syncStateLock = new ReentrantLock(); /** * Flag set once the {@link #shutdown()} method completes. * * This is used to refuse transaction operations after shutdown. */ private volatile boolean shutdown = true; /** * The last successfully started transaction's ID. */ private final AtomicLong lastFinishedTxId = new AtomicLong(DEFAULT_LAST_TX_VALUE); /** * The last successfully prepared transaction's ID. * * Note: This cannot be persisted, but will be reinitialized with the first {@link #prepare(long)}. */ private final AtomicLong lastPreparedTxId = new AtomicLong(DEFAULT_LAST_TX_VALUE); /** * The rotation manager handling all this instance's journal rotations. */ private final JournalRotationManager journalRotationMgr; /** * The {@link DtxManager} containing this instance. * * Note: This is only to be used for referencing the source of events. Directly calling its methods exposes to risks * of deadlocking as they might in turn call this instance. */ private final DtxManager dtxManager; /** * Constructs an instance associated to a {@link DtxManager}. * * The created instance will accept transaction processing step calls and forward them to the registered * {@link DtxResourceManager}s. * * @param configuration * the {@link DtxManagerConfig} used to instantiate the containing DTX node * @param dtxManager * containing {@link DtxManager} for reference */ @ParametersAreNonnullByDefault TransactionManager(@Nonnull final DtxManagerConfig configuration, final DtxManager dtxManager) { this.dtxManagerConfig = Objects.requireNonNull(configuration); this.dtxManager = Objects.requireNonNull(dtxManager); resourceManagers = new ConcurrentHashMap<UUID, DtxResourceManager>(); journals = new ConcurrentHashMap<UUID, WritableTxJournal>(); states = new ConcurrentHashMap<UUID, DtxResourceManagerState>(); // TODO: get parameter values from configuration journalRotationMgr = new JournalRotationManager(0); } /** * Starts the first phase of transaction processing. * * @param transaction * the complete {@link TxMessage} object representing a valid transaction. * @param participants * the set of participant {@link TxNode}s * @throws XAException * if the start operation did not complete, with the following return codes: * <ul> * <li>{@link XAException#XAER_PROTO} if the transaction is already started</li> * <li>{@link XAException#XAER_NOTA} if the transaction has an invalid/already executed ID</li> * <li>{@link XAException#XAER_INVAL} if the transaction is invalid</li> * <li>{@link XAException#XAER_RMFAIL} if the resource manager is unavailable</li> * <li>{@link XAException#XAER_RMERR} if an internal error occurred</li> * <li>{@link XAException#XA_RBROLLBACK} transaction must be rolled back for an unspecified reason</li> * <li>{@link XAException#XA_RBINTEGRITY} transaction must be rolled back as it violates resource * integrity</li> * <li>{@link XAException#XA_RBPROTO} transaction must be rolled back following an internal protocol * error</li> * </ul> * @throws IllegalStateException * if the {@link TransactionManager} was {@link #shutdown shut down} */ final void start(@Nonnull final TxMessage transaction, @Nonnull final Iterable<TxNode> participants) throws XAException, IllegalStateException { Objects.requireNonNull(transaction); final DtxResourceManager dtxResourceManager = checkedGetResourceManager(TxProtobufUtils.fromUuid(transaction .getResId())); final long txId = transaction.getTxId(); if (LOGGER.isTraceEnabled()) { LOGGER.trace("Starting transaction; txId=" + txId + ", resId=" + dtxResourceManager.getId().toString()); } final WritableTxJournal journal = journals.get(dtxResourceManager.getId()); // checks if transaction is already started if (this.contextMap.keySet().contains(Long.valueOf(txId)) && !syncReplayLock.isHeldByCurrentThread()) { throw new XAException(XAException.XAER_PROTO); } this.transactionLock.readLock().lock(); try { // checks if transaction has already been completed and we're not replaying transactions if (txId <= this.lastFinishedTxId.longValue() && !syncReplayLock.isHeldByCurrentThread()) { LOGGER.error("Invalid transaction ID; txId=" + txId + ", lastTxId=" + lastFinishedTxId.longValue()); throw new XAException(XAException.XAER_NOTA); } if (shutdown) { LOGGER.error("Shut down"); throw new IllegalStateException("Shut down."); } final DtxResourceManagerContext startedCtx = dtxResourceManager.start(transaction.getPayload() .toByteArray()); // logs to journal journal.writeStart(transaction, participants); // Create or update task in the task keeper setTask(dtxResourceManager, transaction, DtxTaskStatus.STARTED); contextMap.put(Long.valueOf(transaction.getTxId()), startedCtx); } catch (final IOException e) { LOGGER.error("Failed to write start to journal; resourceID=" + dtxResourceManager.getId().toString() + ",txID=" + txId, e); // TODO: add error treatment/return } finally { transactionLock.readLock().unlock(); } if (LOGGER.isTraceEnabled()) { LOGGER.trace("Started transaction; txId=" + txId); } } /** * Prepares transaction execution as first part of the 2-phase commit. * * Returning <code>true</code> means there is no obstacle to committing the transaction. * * @param txId * the transaction ID associated to the transaction context * @return {@link Boolean#TRUE} if the transaction can be committed, {@link Boolean#FALSE} otherwise * @throws XAException * if the prepare operation fails, with the following return codes: * <ul> * <li>{@link XAException#XAER_NOTA} if no transaction with the given transaction ID exists</li> * <li>{@link XAException#XAER_PROTO} if the given transaction is not in a valid state</li> * <li>{@link XAException#XAER_INVAL} if the transaction context is invalid</li> * <li>{@link XAException#XAER_RMFAIL} if the resource manager is unavailable</li> * <li>{@link XAException#XAER_RMERR} if an internal error occurred</li> * <li>{@link XAException#XA_RBROLLBACK} transaction must be rolled back for an unspecified reason</li> * <li>{@link XAException#XA_RBDEADLOCK} transaction must be rolled back due to a deadlock</li> * <li>{@link XAException#XA_RBINTEGRITY} transaction must be rolled back as it violates resource * integrity</li> * <li>{@link XAException#XA_RBPROTO} transaction must be rolled back following an internal protocol * error</li> * </ul> * @throws IllegalStateException * if the {@link TransactionManager} was {@link #shutdown shut down} */ final Boolean prepare(final long txId) throws XAException, IllegalStateException { final DtxResourceManagerContext dtxResourceManagerContext = getContext(txId); final DtxResourceManager dtxResourceManager = checkedGetResourceManager(dtxResourceManagerContext .getResourceManagerId()); if (LOGGER.isTraceEnabled()) { LOGGER.trace("Preparing transaction; txId=" + txId + ", resId=" + dtxResourceManager.getId().toString()); } try { transactionLock.readLock().lockInterruptibly(); } catch (final InterruptedException e) { throw new XAException(XAException.XAER_RMERR); } try { final boolean holdsSyncLock = syncReplayLock.isHeldByCurrentThread(); if (txId <= this.lastFinishedTxId.longValue() && !holdsSyncLock) { LOGGER.error("Invalid transaction ID; txId=" + txId + ", lastTxId=" + lastFinishedTxId.longValue()); throw new XAException(XAException.XAER_NOTA); } /* * Checks that all preceding transactions have been prepared. This relies on the initiator enforcing a * global prepare order to avoid invalidating transactions just to preserve commitment ordering. */ final long lastPrepd = lastPreparedTxId.longValue(); final long lastFinished = lastFinishedTxId.longValue(); if ((txId <= lastPrepd) && !holdsSyncLock) { LOGGER.error("Commitment order violation: Transaction ID is out of order; txId=" + txId + ", lastPreparedTxId=" + lastPrepd); throw new XAException(XAException.XAER_PROTO); } /* * Checks that this instance can currently execute a prepare without violating commitment ordering * constraints. */ if ((lastPrepd > lastFinished) && !holdsSyncLock) { LOGGER.error("Commitment order violation: Last prepared transaction is not finished; txId=" + txId + ", lastPreparedTxId=" + lastPrepd + ", lastFinishedTxId=" + lastFinished); throw new XAException(XAException.XAER_PROTO); } if (shutdown) { throw new IllegalStateException("Shut down"); } final Boolean result = dtxResourceManager.prepare(dtxResourceManagerContext); // Update task status in the task keeper updateTask(txId, DtxTaskStatus.PREPARED); // updates the last transaction ID updateAtomicLongToAtLeast(lastPreparedTxId, txId); return result; } finally { transactionLock.readLock().unlock(); } } /** * Commits the transaction as second part of the 2-phase commit. * * @param txId * the transaction ID associated to the transaction context * @param participants * the set of participant {@link TxNode}s * @throws XAException * if the commit operation fails, with the following return codes: * <ul> * <li>{@link XAException#XAER_NOTA} if no transaction with the given transaction ID exists</li> * <li>{@link XAException#XAER_PROTO} if the given transaction is not in a valid state</li> * <li>{@link XAException#XAER_INVAL} if the transaction context is invalid</li> * <li>{@link XAException#XAER_RMFAIL} if the resource manager is unavailable</li> * <li>{@link XAException#XAER_RMERR} if an internal error occurred</li> * <li>{@link XAException#XA_RBROLLBACK} transaction must be rolled back for an unspecified reason</li> * <li>{@link XAException#XA_RBDEADLOCK} transaction must be rolled back due to a deadlock</li> * <li>{@link XAException#XA_RBINTEGRITY} transaction must be rolled back as it violates resource * integrity</li> * <li>{@link XAException#XA_RBPROTO} transaction must be rolled back following an internal protocol * error</li> * </ul> * @throws IllegalStateException * if the {@link TransactionManager} was {@link #shutdown shut down} */ final void commit(final long txId, @Nonnull final Iterable<TxNode> participants) throws XAException, IllegalStateException { final DtxResourceManagerContext dtxResourceManagerContext = getContext(txId); final DtxResourceManager dtxResourceManager = checkedGetResourceManager(dtxResourceManagerContext .getResourceManagerId()); if (LOGGER.isTraceEnabled()) { LOGGER.trace("Committing transaction; txId=" + txId + ", resId=" + dtxResourceManager.getId().toString()); } final WritableTxJournal journal = journals.get(dtxResourceManager.getId()); transactionLock.readLock().lock(); try { if (shutdown) { throw new IllegalStateException("Shut down"); } dtxResourceManager.commit(dtxResourceManagerContext); // logs to journal journal.writeCommit(txId, participants); // Update task status in the task keeper updateTask(txId, DtxTaskStatus.COMMITTED); // updates the last transaction ID updateAtomicLongToAtLeast(lastFinishedTxId, txId); if (COMMITTED.equals(dtxResourceManagerContext.getTxStatus())) { contextMap.remove(Long.valueOf(txId)); } } catch (final IOException e) { LOGGER.error("Failed to write commit to journal; resourceID=" + dtxResourceManager.getId().toString() + ",txID=" + txId, e); // TODO: add error treatment/return } finally { transactionLock.readLock().unlock(); } } /** * Rolls back the transaction at any moment after its {@link #start(TxMessage) start} and before it is * {@link #commit(long) committed}. * * @param txId * the transaction ID associated to the transaction context * @param participants * the set of participant {@link TxNode}s * @throws XAException * if the roll-back operation fails, with the following return codes: * <ul> * <li>{@link XAException#XAER_NOTA} if no transaction with the given transaction ID exists</li> * <li>{@link XAException#XAER_PROTO} if the given context is not in a valid state</li> * <li>{@link XAException#XAER_INVAL} if the context is invalid</li> * <li>{@link XAException#XAER_RMFAIL} if the resource manager is unavailable</li> * <li>{@link XAException#XAER_RMERR} if an internal error occurred</li> * </ul> * @throws IllegalStateException * if the {@link TransactionManager} was {@link #shutdown shut down} */ final void rollback(final long txId, @Nonnull final Iterable<TxNode> participants) throws XAException, IllegalStateException { final DtxResourceManagerContext dtxResourceManagerContext = getContext(txId); final DtxResourceManager dtxResourceManager = checkedGetResourceManager(dtxResourceManagerContext .getResourceManagerId()); if (LOGGER.isTraceEnabled()) { LOGGER.trace("Rolling back transaction; txId=" + txId + ", resId=" + dtxResourceManager.getId().toString()); } final WritableTxJournal journal = journals.get(dtxResourceManager.getId()); transactionLock.readLock().lock(); try { if (shutdown) { throw new IllegalStateException("Shut down"); } dtxResourceManager.rollback(dtxResourceManagerContext); // logs to journal journal.writeRollback(txId, 0, participants); // Update task status in the task keeper updateTask(txId, DtxTaskStatus.ROLLED_BACK); // updates the last transaction ID updateAtomicLongToAtLeast(lastFinishedTxId, txId); if (ROLLED_BACK.equals(dtxResourceManagerContext.getTxStatus())) { contextMap.remove(Long.valueOf(txId)); } } catch (final IOException e) { LOGGER.error("Failed to write rollback to journal; resourceID=" + dtxResourceManager.getId().toString() + ",txID=" + txId, e); // TODO: add error treatment/return } finally { transactionLock.readLock().unlock(); } } /** * Gets the transactional (read) lock status for the current thread. * * The lock status returned applies primarily to the shared read lock taken by {@link #start(TxMessage, Iterable)}, * {@link #prepare(long)}, {@link #commit(long, Iterable)} and {@link #rollback(long, Iterable)}, but it also * returns <code>true</code> if the transaction lock is write locked exclusively by the current thread. This should * be checked for all methods acquiring locks on DTX runtime states and which might be called from within a * transaction (e.g. {@link DtxManager#registerResourceManager(DtxResourceManager)}). * * @return <code>true</code> if the current thread holds the transaction lock, <code>false</code> otherwise */ final boolean holdsTransactionLock() { return transactionLock.getReadHoldCount() > 0 || transactionLock.isWriteLockedByCurrentThread(); } /** * Gets the {@link DtxResourceManagerContext} from the {@link #contextMap} and throws an appropriate * {@link XAException} if none was found. * * @param txId * the transaction ID to search * @return a non-<code>null</code> {@link DtxResourceManagerContext} * @throws XAException * {@link XAException#XAER_NOTA} if no transaction context was found for this ID */ @Nonnull private final DtxResourceManagerContext getContext(final long txId) throws XAException { final DtxResourceManagerContext result = contextMap.get(Long.valueOf(txId)); if (result == null) { throw new XAException(XAException.XAER_NOTA); } return result; } /** * Gets the {@link DtxResourceManager} from the {@link #resourceManagers} collection and throws an appropriate * {@link XAException} if none was found. * * @param resourceId * the ID under which the {@link DtxResourceManager} is registered * @return a non-<code>null</code> {@link DtxResourceManager} * @throws XAException * {@link XAException#XAER_RMFAIL} (resource manager unavailable) if no {@link DtxResourceManager} was * found for this ID */ private final DtxResourceManager checkedGetResourceManager(final UUID resourceId) throws XAException { final DtxResourceManager result = this.getRegisteredResourceManager(resourceId); if (result == null) { throw new XAException(XAException.XAER_RMFAIL); } final DtxResourceManagerState syncState = states.get(resourceId); if (syncState == null || (!TX_AUTH_STATES.contains(syncState) && !syncReplayLock.isHeldByCurrentThread())) { throw new XAException(XAException.XAER_RMFAIL); } return result; } /** * Starts the instance. * * @param txInit * the {@link TransactionInitiator} to update if necessary * @throws IllegalThreadStateException * if the current thread is currently executing a transaction, however improbable this may sound */ final void startUp(final TransactionInitiator txInit) throws IllegalThreadStateException { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Starting transaction manager"); } // avoid deadlocks (and other niceties) by refusing to act from within a transaction if (holdsTransactionLock()) { throw new IllegalThreadStateException("Startup attempted from within a transaction."); } transactionLock.writeLock().lock(); try { if (!shutdown) { return; } journalRotationMgr.start(); for (final WritableTxJournal currJournal : journals.values()) { try { currJournal.start(); updateAtomicLongToAtLeast(lastFinishedTxId, currJournal.getLastFinishedTxId()); readTasksFromJournal(currJournal.newReadOnlyTxJournal()); } catch (final IOException e) { LOGGER.error("Error while starting journal", e); throw new IllegalStateException(e); } if (txInit != null) { txInit.mergeLastTxCounters(lastFinishedTxId.get()); } } dtxManager.startPurgeTaskKeeper(); // reset all resource manager synchronization states for (final UUID currStateKey : states.keySet()) { setResManagerSyncState(currStateKey, UNDETERMINED); } shutdown = false; } finally { transactionLock.writeLock().unlock(); } if (LOGGER.isDebugEnabled()) { LOGGER.debug("Started transaction manager; registered resource managers=" + resourceManagers); } } /** * Shuts down the instance properly. * * @throws IllegalThreadStateException * if the calling thread is executing a transaction and thus trying to shoot itself in the foot */ final void shutdown() throws IllegalThreadStateException { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Shutting down transaction manager"); } // avoid deadlocks (and other niceties) by refusing to act from within a transaction if (holdsTransactionLock()) { throw new IllegalThreadStateException("Shutdown attempt from within a transaction."); } syncReplayLock.lock(); try { transactionLock.writeLock().lock(); try { if (shutdown) { return; } // stops the rotation journalRotationMgr.stop(); // closes journals for (final WritableTxJournal currJournal : journals.values()) { try { currJournal.stop(); } catch (final IOException e) { LOGGER.error("Error while closing journal", e); } } dtxManager.stopPurgeTaskKeeper(); // reset all resource manager synchronization states for (final UUID currStateKey : states.keySet()) { setResManagerSyncState(currStateKey, UNDETERMINED); } this.shutdown = true; } finally { transactionLock.writeLock().unlock(); } } finally { syncReplayLock.unlock(); } } /** * Gets the shutdown state of this instance. * * @return <code>true</code> if this {@link TransactionManager} is {@link #shutdown() shut down}, <code>false</code> * otherwise */ final boolean isShutdown() { return shutdown; } /** * Registers a {@link DtxResourceManager} with this instance. * * @param resourceManager * the {@link DtxResourceManager} to register * @param txInit * the {@link TransactionInitiator} to update if necessary, may be <code>null</code> if this instance was * not started * @throws IllegalArgumentException * if a resource manager with identical ID is already registered * @throws IllegalStateException * if the journal for the resource manager could not be initialized */ final void registerResourceManager(final DtxResourceManager resourceManager, final TransactionInitiator txInit) throws IllegalArgumentException, IllegalStateException { // releases all read locks and waits for the write lock // Note: excludes a deadlock caused by having this method called within a transaction phase final int readHoldCount = transactionLock.getReadHoldCount(); while (transactionLock.getReadHoldCount() > 0) { transactionLock.readLock().unlock(); } // takes the write lock as we're about to modify the resource manager list transactionLock.writeLock().lock(); try { // validate the input parameter final UUID resId = resourceManager.getId(); if (resourceManagers.keySet().contains(resId)) { throw new IllegalArgumentException("Resource Manager already registered with ID=" + resId.toString()); } // create and start the journal final String filename = newJournalFilePrefix(dtxManagerConfig.getLocalPeer().getNodeId(), resId); // TODO: add configuration value for rotation threshold final WritableTxJournal newJournal = new WritableTxJournal(dtxManagerConfig.getJournalDirectory().toFile(), filename, 0, journalRotationMgr); // starts the journal only if the transaction manager is already started if (!shutdown) { try { newJournal.start(); updateAtomicLongToAtLeast(lastFinishedTxId, newJournal.getLastFinishedTxId()); } catch (final IOException e) { LOGGER.error("Could not start journal for resource manager", e); throw new IllegalStateException(e); } if (txInit != null) { txInit.mergeLastTxCounters(lastFinishedTxId.get()); } } this.journals.put(resId, newJournal); this.states.put(resId, UNDETERMINED); this.resourceManagers.put(resId, resourceManager); // Update task in the task keeper only if transaction manager is started if (!shutdown) { readTasksFromJournal(newJournal.newReadOnlyTxJournal()); } dtxManager.postEvent(new DtxResourceManagerEvent(dtxManager, resourceManager.getId(), UNREGISTERED, UNDETERMINED), true); } finally { // on the way out, re-acquire all previously held read locks before releasing the write lock try { for (int i = 0; i < readHoldCount; i++) { transactionLock.readLock().lockInterruptibly(); } } catch (final InterruptedException e) { throw new IllegalStateException(e); } finally { transactionLock.writeLock().unlock(); } } } /** * Unregisters a resource manager. * * @param resourceManagerId * the {@link UUID} of the {@link DtxResourceManager} to unregister * @throws IllegalStateException * if the journal for the resource manager does not exist or cannot be stopped */ final void unregisterResourceManager(final UUID resourceManagerId) throws IllegalStateException { // if we're in a transaction, release all read locks and wait for the write lock // Note: excludes a deadlock caused by having this method called within a transaction phase final int readHoldCount = transactionLock.getReadHoldCount(); while (transactionLock.getReadHoldCount() > 0) { transactionLock.readLock().unlock(); } // takes the write lock as we're about to modify the resource manager list transactionLock.writeLock().lock(); try { if (!resourceManagers.keySet().contains(resourceManagerId)) { return; } final WritableTxJournal resJournal = journals.get(resourceManagerId); if (resJournal == null) { throw new IllegalStateException("No journal instance for resource manager; ID=" + resourceManagerId.toString()); } try { resJournal.stop(); } catch (final IOException e) { LOGGER.error("Could not stop journal for resource manager", e); throw new IllegalStateException(e); } this.journals.remove(resourceManagerId); final DtxResourceManagerState oldState = this.states.get(resourceManagerId); this.states.remove(resourceManagerId); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Unregistering resource manager; resId=" + resourceManagerId); } this.resourceManagers.remove(resourceManagerId); dtxManager.postEvent(new DtxResourceManagerEvent(dtxManager, resourceManagerId, oldState, UNREGISTERED), true); } finally { // on the way out, re-acquire all previously held read locks before releasing the write lock try { for (int i = 0; i < readHoldCount; i++) { transactionLock.readLock().lockInterruptibly(); } } catch (final InterruptedException e) { throw new IllegalStateException(e); } finally { transactionLock.writeLock().unlock(); } } } /** * Gets one of the registered {@link DtxResourceManager}s. * * @param resMgrId * the {@link UUID} of the {@link DtxResourceManager} to get * @return a {@link DtxResourceManager} instance or <code>null</code> if none was found * @throws NullPointerException * if the argument is <code>null</code> */ final DtxResourceManager getRegisteredResourceManager(@Nonnull final UUID resMgrId) throws NullPointerException { transactionLock.readLock().lock(); try { return resourceManagers.get(resMgrId); } finally { transactionLock.readLock().unlock(); } } /** * Gets all registered {@link DtxResourceManager}s. * * @return a (possibly empty) {@link Collection} of {@link DtxResourceManager}s */ final Collection<DtxResourceManager> getRegisteredResourceManagers() { transactionLock.readLock().lock(); try { return Collections.unmodifiableCollection(resourceManagers.values()); } finally { transactionLock.readLock().unlock(); } } /** * Create or update a task in the task keeper corresponding to a given transaction. * * @param dtxResourceManager * the {@link DtxResourceManager} of the task * @param transaction * the {@link TxMessage} to use * @param newStatus * the {@link DtxTaskStatus} to set * @param timestamp * a timestamp */ private final void setTask(final DtxResourceManager dtxResourceManager, final TxMessage transaction, final DtxTaskStatus newStatus) { final UUID taskId = fromUuid(transaction.getTaskId()); // Check if the resource manager is not null and if the task info was not already set if ((dtxResourceManager != null) && !dtxManager.isDtxTaskInfoSet(taskId)) { // dtx task info is created only if the resource manager is registered final DtxTaskInfo info = dtxResourceManager.createTaskInfo(transaction.getPayload().toByteArray()); dtxManager.setTask(taskId, transaction.getTxId(), fromUuid(transaction.getResId()), newStatus, info); } else { // set a task with no dtx task info dtxManager.setTask(taskId, transaction.getTxId(), fromUuid(transaction.getResId()), newStatus, null); } } /** * Create or update a task in the task keeper corresponding to a given transaction. * * @param dtxResourceManager * the {@link DtxResourceManager} of the task * @param transaction * the {@link TxMessage} to use * @param newStatus * the {@link DtxTaskStatus} to set * @param timestamp * a timestamp */ final void loadTask(final DtxResourceManager dtxResourceManager, final TxMessage transaction, final DtxTaskStatus newStatus, final long timestamp) { try { final UUID taskId = fromUuid(transaction.getTaskId()); // Check if the resource manager is not null and if the task info was not already set if (dtxResourceManager != null) { // dtx task info is created only if the resource manager is registered final DtxTaskInfo info = dtxResourceManager.createTaskInfo(transaction.getPayload().toByteArray()); dtxManager.loadTask(taskId, transaction.getTxId(), fromUuid(transaction.getResId()), newStatus, info, timestamp); } else { // set a task with no dtx task info dtxManager.loadTask(taskId, transaction.getTxId(), fromUuid(transaction.getResId()), newStatus, null, timestamp); } } catch (final IllegalArgumentException e) { LOGGER.warn("Could not load task: " + e); } } /** * Update a task status in the task keeper corresponding to a given transaction ID. * * @param txId * the transaction id of the task * @param newStatus * the {@link DtxTaskStatus} to use */ private final void updateTask(final long txId, final DtxTaskStatus newStatus) { dtxManager.setTaskStatus(txId, newStatus); } /** * Update the tasks in the task keeper by reading the journal. * * @param journal * the {@link JournalRecord} to read */ final void readTasksFromJournal(final Iterable<JournalRecord> journal) { final Map<Long, TxJournalEntry> startedEntries = new HashMap<>(); for (final JournalRecord currRecord : journal) { TxJournalEntry currEntry; TxJournalEntry startedEntry; try { currEntry = TxJournalEntry.parseFrom(currRecord.getEntry()); } catch (final InvalidProtocolBufferException e) { LOGGER.warn("Could not read journal entry; journal=" + journal); continue; } final long txId = currEntry.getTxId(); switch (currEntry.getOp()) { case START: startedEntries.put(Long.valueOf(txId), currEntry); break; case COMMIT: startedEntry = startedEntries.get(Long.valueOf(txId)); if (startedEntry != null) { loadTask(dtxManager.getRegisteredResourceManager(fromUuid(startedEntry.getTx().getResId())), startedEntry.getTx(), DtxTaskStatus.COMMITTED, startedEntry.getTimestamp()); startedEntries.remove(Long.valueOf(txId)); } else { LOGGER.warn("Transaction: " + currEntry.getTx() + " was not started before"); } break; case ROLLBACK: startedEntry = startedEntries.get(Long.valueOf(txId)); if (startedEntry != null) { loadTask(dtxManager.getRegisteredResourceManager(fromUuid(startedEntry.getTx().getResId())), startedEntry.getTx(), DtxTaskStatus.ROLLED_BACK, startedEntry.getTimestamp()); startedEntries.remove(Long.valueOf(txId)); } else { LOGGER.warn("Transaction: " + currEntry.getTx() + " was not started before"); } break; default: // nothing break; } } // Add tasks with only the started status for (final Entry<Long, TxJournalEntry> entry : startedEntries.entrySet()) { loadTask(dtxManager.getRegisteredResourceManager(fromUuid(entry.getValue().getTx().getResId())), entry .getValue().getTx(), DtxTaskStatus.STARTED, entry.getValue().getTimestamp()); } } /** * Read a complete task in the journal. Return a task with status unknown if the task is not found * * @param taskId * the task {@link UUID} to read * @return a {@link TaskLoader} */ final TaskLoader readTask(final UUID taskId) { TaskLoader result = null; try { transactionLock.readLock().lockInterruptibly(); } catch (final InterruptedException e) { throw new IllegalStateException("Interrupted while searching task; taskID=" + taskId); } try { // searches writable journals, limited to the current file for (final WritableTxJournal currJournal : journals.values()) { result = readTaskFromJournal(taskId, currJournal); if (result != null) { return result; } } // read all journals from the start including all backups for (final WritableTxJournal currJournal : journals.values()) { result = readTaskFromJournal(taskId, currJournal.newReadOnlyTxJournal()); if (result != null) { return result; } } LOGGER.warn("Could not find task status, returning; taskId=" + taskId + ", status=" + result); return TaskLoader.createUnknownTask(taskId); } finally { transactionLock.readLock().unlock(); } } /** * Read a complete task in the journal. Return null if not found * * @param taskId * the task {@link UUID} to read * @param journal * the journal {@link JournalRecord} to read * @return a {@link TaskLoader} or null */ private final TaskLoader readTaskFromJournal(final UUID taskId, final Iterable<JournalRecord> journal) { long foundTxId = DEFAULT_LAST_TX_VALUE; TaskLoader result = null; if (LOGGER.isTraceEnabled()) { LOGGER.trace("Reading journal; wanted taskId=" + taskId + ", journal=" + journal); } TxJournalEntry resultEntry = null; for (final JournalRecord currRecord : journal) { TxJournalEntry currEntry; try { currEntry = TxJournalEntry.parseFrom(currRecord.getEntry()); } catch (final InvalidProtocolBufferException e) { LOGGER.warn("Could not read journal entry; journal=" + journal); continue; } switch (currEntry.getOp()) { case START: // checks if the task ID in the transaction message matches if (taskId.equals(fromUuid(currEntry.getTx().getTaskId()))) { resultEntry = currEntry; foundTxId = currEntry.getTxId(); if (LOGGER.isTraceEnabled()) { LOGGER.trace("Found start in journal; taskId=" + taskId + ", txId=" + foundTxId); } } break; case COMMIT: // checks if the commit belongs to the identified transaction if ((resultEntry != null) && TxOpCode.START.equals(resultEntry.getOp()) && (foundTxId == currEntry.getTxId())) { result = createTaskLoader(resultEntry, DtxTaskStatus.COMMITTED); if (LOGGER.isTraceEnabled()) { LOGGER.trace("Found commit in journal; taskId=" + taskId + ", txId=" + foundTxId); } return result; } break; case ROLLBACK: // checks if the rollback belongs to the identified transaction if ((resultEntry != null) && TxOpCode.START.equals(resultEntry.getOp()) && (foundTxId == currEntry.getTxId())) { result = createTaskLoader(resultEntry, DtxTaskStatus.ROLLED_BACK); if (LOGGER.isTraceEnabled()) { LOGGER.trace("Found rollback in journal; taskId=" + taskId + ", txId=" + foundTxId); } return result; } break; default: // nothing } } if (resultEntry != null) { result = createTaskLoader(resultEntry, DtxTaskStatus.STARTED); } return result; } private TaskLoader createTaskLoader(final TxJournalEntry resultEntry, final DtxTaskStatus status) { DtxTaskInfo info = null; final DtxTaskAdm taskAdm = new DtxTaskAdm(fromUuid(resultEntry.getTx().getTaskId()), null, null, fromUuid(resultEntry.getTx().getResId()), status); final DtxResourceManager resourceManager = dtxManager.getRegisteredResourceManager(fromUuid(resultEntry.getTx() .getResId())); if (resourceManager != null) { info = resourceManager.createTaskInfo(resultEntry.getTx().getPayload().toByteArray()); } return new TaskLoader(taskAdm, info); } /** * Searches for a given task in all available journals and returns its status. * * @param taskId * the {@link UUID} of the requested task * @return a valid {@link DtxTaskStatus}, {@link DtxTaskStatus#UNKNOWN} if it was not found */ final DtxTaskStatus searchTaskStatus(final UUID taskId) { DtxTaskStatus result = UNKNOWN; try { transactionLock.readLock().lockInterruptibly(); } catch (final InterruptedException e) { throw new IllegalStateException("Interrupted while searching task; taskID=" + taskId); } try { // searches writable journals, limited to the current file for (final WritableTxJournal currJournal : journals.values()) { result = readTaskStatusFromJournal(taskId, currJournal); if (!UNKNOWN.equals(result)) { return result; } } // read all journals from the start including all backups for (final WritableTxJournal currJournal : journals.values()) { result = readTaskStatusFromJournal(taskId, currJournal.newReadOnlyTxJournal()); if (!UNKNOWN.equals(result)) { return result; } } LOGGER.warn("Could not find task status, returning; taskId=" + taskId + ", status=" + result); return result; } finally { transactionLock.readLock().unlock(); } } private final DtxTaskStatus readTaskStatusFromJournal(final UUID taskId, final Iterable<JournalRecord> journal) { DtxTaskStatus result = UNKNOWN; long foundTxId = DEFAULT_LAST_TX_VALUE; boolean keepSearching = true; if (LOGGER.isTraceEnabled()) { LOGGER.trace("Reading journal; wanted taskId=" + taskId + ", journal=" + journal); } for (final JournalRecord currRecord : journal) { TxJournalEntry currEntry; try { currEntry = TxJournalEntry.parseFrom(currRecord.getEntry()); } catch (final InvalidProtocolBufferException e) { LOGGER.warn("Could not read journal entry; journal=" + journal); continue; } switch (currEntry.getOp()) { case START: // checks if the task ID in the transaction message matches if (taskId.equals(fromUuid(currEntry.getTx().getTaskId()))) { result = STARTED; foundTxId = currEntry.getTxId(); if (LOGGER.isTraceEnabled()) { LOGGER.trace("Found start in journal; taskId=" + taskId + ", txId=" + foundTxId); } } break; case COMMIT: // checks if the commit belongs to the identified transaction if (STARTED.equals(result) && (foundTxId == currEntry.getTxId())) { result = COMMITTED; keepSearching = false; if (LOGGER.isTraceEnabled()) { LOGGER.trace("Found commit in journal; taskId=" + taskId + ", txId=" + foundTxId); } } break; case ROLLBACK: // checks if the rollback belongs to the identified transaction if (STARTED.equals(result) && (foundTxId == currEntry.getTxId())) { result = ROLLED_BACK; keepSearching = false; if (LOGGER.isTraceEnabled()) { LOGGER.trace("Found rollback in journal; taskId=" + taskId + ", txId=" + foundTxId); } } break; default: // nothing } if (!keepSearching) { break; } } return result; } /** * Gets the ID of the last completed transaction for this instance. * * @return a non-negative transaction ID or {@value #DEFAULT_LAST_TX_VALUE} if non was executed yet * @throws IllegalStateException * if this instance is not {@link #startUp() started} */ final long getLastCompleteTxId() throws IllegalStateException { if (shutdown) { throw new IllegalStateException("Not started"); } return lastFinishedTxId.longValue(); } /** * Gets the last complete transaction for a given {@link #registerResourceManager(DtxResourceManager) registered} * resource manager. * * @param resId * the {@link UUID} of the {@link DtxResourceManager} to search for * @return a positive transaction ID or {@value #DEFAULT_LAST_TX_VALUE} if no transaction has been completed yet or * the resource manager is not registered with this instance * @throws IllegalStateException * if the resource manager is unknown or processing is interrupted */ final long getLastCompleteTxIdForResMgr(final UUID resId) throws IllegalStateException { try { transactionLock.readLock().lockInterruptibly(); } catch (final InterruptedException e) { // wraps the interruption into a runtime exception throw new IllegalStateException("Interrupted on waiting for lock", e); } try { final WritableTxJournal targetJournal = journals.get(resId); if (targetJournal == null) { return DEFAULT_LAST_TX_VALUE; } return targetJournal.getLastFinishedTxId(); } finally { transactionLock.readLock().unlock(); } } /** * Gets a registered resource manager's status. * * @param resUuid * the requested resource manager's {@link UUID} * @return a valid {@link DtxResourceManagerState} if the resource manager is registered, <code>null</code> * otherwise */ final DtxResourceManagerState getResourceManagerState(@Nonnull final UUID resUuid) { try { transactionLock.readLock().lockInterruptibly(); } catch (final InterruptedException e) { throw new IllegalStateException("Interrupted."); } try { return states.get(resUuid); } finally { transactionLock.readLock().unlock(); } } /** * Gets the information of the journal for a given {@link #registerResourceManager(DtxResourceManager) registered} * resource manager. * * @param resId * the {@link UUID} of the {@link DtxResourceManager} to search for * @return a String describing the journal * * @throws IllegalStateException * if the resource manager is unknown or processing is interrupted */ final String getJournalPathForResMgr(final UUID resId) throws IllegalStateException { try { transactionLock.readLock().lockInterruptibly(); } catch (final InterruptedException e) { // wraps the interruption into a runtime exception throw new IllegalStateException("Interrupted on waiting for lock", e); } try { final WritableTxJournal targetJournal = journals.get(resId); if (targetJournal == null) { return ""; } return targetJournal.getJournalFilename(); } finally { transactionLock.readLock().unlock(); } } /** * Gets the status of the journal for a given {@link #registerResourceManager(DtxResourceManager) registered} * resource manager. * * @param resId * the {@link UUID} of the {@link DtxResourceManager} to search for * @return true if the journal is started. * * @throws IllegalStateException * if the resource manager is unknown or processing is interrupted */ final boolean getJournalStatusForResMgr(final UUID resId) throws IllegalStateException { try { transactionLock.readLock().lockInterruptibly(); } catch (final InterruptedException e) { // wraps the interruption into a runtime exception throw new IllegalStateException("Interrupted on waiting for lock", e); } try { final WritableTxJournal targetJournal = journals.get(resId); if (targetJournal == null) { return false; } return targetJournal.isStarted(); } finally { transactionLock.readLock().unlock(); } } /** * Gets the configured local node from the configuration. * * @return a {@link DtxNode} representing the local node * @throws IllegalStateException * if this instance is {@link #shutdown() shut down} */ final DtxNode getLocalNode() throws IllegalStateException { transactionLock.readLock().lock(); try { if (shutdown) { throw new IllegalStateException("Not started"); } return this.dtxManagerConfig.getLocalPeer(); } finally { transactionLock.readLock().unlock(); } } /** * Evaluates and updates the synchronization state of the given resource manager according to the given discovered * last transaction ID and the presence of a quorum. * * @param resUuid * the target resource manager's {@link UUID} * @param lastDiscTxId * the last discovered transaction ID relative to which the synchronization status is to be updated * @param quorumPresent * <code>true</code> if the last transaction ID was discovered with responses from a quorum of nodes, * <code>false</code> otherwise */ final void evaluateResManagerSyncState(final UUID resUuid, final long lastDiscTxId, final boolean quorumPresent) { syncStateLock.lock(); try { if (!resourceManagers.containsKey(resUuid)) { return; } final DtxResourceManagerState oldState; final DtxResourceManagerState newState; transactionLock.readLock().lock(); try { final long localLastTxId = getLastCompleteTxIdForResMgr(resUuid); oldState = states.get(resUuid); if (localLastTxId < lastDiscTxId) { // check if we're already synchronizing if (SYNCHRONIZING == oldState || LATE == oldState) { // don't revert to LATE or trigger any additional event return; } if (POST_SYNC_PROCESSING == oldState) { dtxManager.getPostSyncProcessor().resetAfterPostProc(resUuid); return; } newState = LATE; } else { if (quorumPresent) { // transitions to up-to-date only if a quorum responded newState = UP_TO_DATE == oldState ? UP_TO_DATE : POST_SYNC_PROCESSING; } else { // leave things unchanged newState = oldState; } } if ((oldState == null) || (oldState == newState)) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Keeping old synchronization status; status=" + oldState + ", node=" + dtxManagerConfig.getLocalPeer() + ", resId=" + resUuid); } return; } if (LOGGER.isDebugEnabled()) { LOGGER.debug("Setting new synchronization status; previous=" + oldState + ", update=" + newState + ", node=" + dtxManagerConfig.getLocalPeer()); } states.put(resUuid, newState); } finally { transactionLock.readLock().unlock(); } dtxManager.postEvent(new DtxResourceManagerEvent(dtxManager, resUuid, oldState, newState), true); } finally { syncStateLock.unlock(); } } /** * Sets a new state for a resource manager. * * This does nothing if the resource manager is not registered or already in the target state. * * @param resUuid * the target resource manager's {@link UUID} * @param newState * the new {@link DtxResourceManagerState} after the transition */ final void setResManagerSyncState(final UUID resUuid, final DtxResourceManagerState newState) { syncStateLock.lock(); try { final DtxResourceManagerState oldState; transactionLock.readLock().lock(); try { oldState = states.get(resUuid); if ((oldState == null) || (oldState == newState)) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Keeping old synchronization status; status=" + oldState + ", node=" + dtxManagerConfig.getLocalPeer() + ", resId=" + resUuid); } return; } if (LOGGER.isDebugEnabled()) { LOGGER.debug("Setting new synchronization status; previous=" + oldState + ", update=" + newState + ", node=" + dtxManagerConfig.getLocalPeer()); } states.put(resUuid, newState); } finally { transactionLock.readLock().unlock(); } dtxManager.postEvent(new DtxResourceManagerEvent(dtxManager, resUuid, oldState, newState), true); } finally { syncStateLock.unlock(); } } /** * Gets the IDs and of all resource managers whose state is {@link DtxResourceManagerState#UNDETERMINED}. * * @return a (possibly empty) {@link Map} of {@link UUID} and last complete transaction ID */ final Map<UUID, Long> getUndeterminedResourceManagers() { transactionLock.readLock().lock(); try { final HashMap<UUID, Long> result = new HashMap<UUID, Long>(); for (final UUID currId : states.keySet()) { if (UNDETERMINED == states.get(currId)) { result.put(currId, Long.valueOf(getLastCompleteTxIdForResMgr(currId))); } } return result; } finally { transactionLock.readLock().unlock(); } } /** * Replay the given transactions on the specified resource manager. * * @param resMgrId * the target resource manager's {@link UUID} * @param journalDiff * the set of transactions to replay in iteration order * @param expTxNb * expected number of transactions to be replayed * @return the transaction ID of the last successfully replayed transaction */ @ParametersAreNonnullByDefault final long replayTransactions(final UUID resMgrId, final Iterable<TxJournalEntry> journalDiff, final int expTxNb) { long result = DEFAULT_LAST_TX_VALUE; try { syncReplayLock.lockInterruptibly(); } catch (final InterruptedException e) { throw new IllegalStateException("Interrupted on waiting for sync lock", e); } try { result = getLastCompleteTxIdForResMgr(resMgrId); checkedGetResourceManager(resMgrId); final BitSet failList = new BitSet(expTxNb); for (final TxJournalEntry currEntry : journalDiff) { final int failIndex = (int) (currEntry.getTxId() % expTxNb); final long currTxId = currEntry.getTxId(); // skips ahead if the current transaction has already been replayed if (currTxId <= result) { continue; } switch (currEntry.getOp()) { case START: try { // executes start if its context is not already registered final DtxResourceManagerContext txCxt = contextMap.get(Long.valueOf(currTxId)); if (txCxt == null) { start(currEntry.getTx(), currEntry.getTxNodesList()); prepare(currTxId); break; } // handles the existing context case switch (txCxt.getTxStatus()) { case UNKNOWN: case PENDING: start(currEntry.getTx(), currEntry.getTxNodesList()); //$FALL-THROUGH$ case STARTED: prepare(currTxId); break; case PREPARED: case COMMITTED: case ROLLED_BACK: default: // nothing } } catch (final XAException xe) { LOGGER.warn("Exception while replaying first phase; errorCode=" + xe.errorCode); failList.set(failIndex); } break; case COMMIT: if (failList.get(failIndex)) { // does not commit if previous phases failed throw new IllegalStateException("Commit of failed transaction; txId=" + currTxId); } commit(currTxId, currEntry.getTxNodesList()); if (LOGGER.isTraceEnabled()) { LOGGER.trace("Committed transaction; txId=" + currTxId); } break; case ROLLBACK: if (failList.get(failIndex)) { // ignores rolled back transactions if start or prepare failed updateAtomicLongToAtLeast(lastFinishedTxId, currTxId); failList.clear(failIndex); break; } rollback(currTxId, currEntry.getTxNodesList()); if (LOGGER.isTraceEnabled()) { LOGGER.trace("Rolled back transaction; txId=" + currTxId); } break; default: // nothing } result = getLastCompleteTxIdForResMgr(resMgrId); } if (!failList.isEmpty()) { throw new IllegalStateException("There are failed transactions on replay"); } if (LOGGER.isDebugEnabled()) { LOGGER.debug("Replayed transactions; nodeId=" + dtxManager.getNodeId() + ", lastTxId=" + result); } return result; } catch (final IllegalStateException e) { LOGGER.warn("Exception while replaying", e); return result; } catch (final XAException e) { LOGGER.warn("XAException while replaying; code=" + e.errorCode); return result; } finally { syncReplayLock.unlock(); } } /** * Extract transaction journal records between two given transaction IDs. * * @param resMgrId * the target resource manager's * @param firstTxId * the transaction ID from which to extract (exclusive) * @param lastTxId * the last transaction ID to which to extract * @return an {@link Iterable<JournalRecord>} containing all or a subset of the requested transactions */ final Iterable<TxJournalEntry> extractTransactions(final UUID resMgrId, final long firstTxId, final long lastTxId) { transactionLock.readLock().lock(); try { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Extracting transactions; resourceId=" + resMgrId + ", firstTxId=" + firstTxId + ", lastTxId=" + lastTxId); } final ArrayList<TxJournalEntry> result = new ArrayList<TxJournalEntry>((int) (2 * (lastTxId - firstTxId))); final WritableTxJournal targetJournal = journals.get(resMgrId); if (targetJournal == null) { return result; } for (final JournalRecord currRecord : targetJournal.newReadOnlyTxJournal()) { final TxJournalEntry currEntry = TxJournalEntry.parseFrom(currRecord.getEntry()); final long txId = currEntry.getTxId(); if ((firstTxId < txId) && (txId <= lastTxId)) { result.add(currEntry); } } return result; } catch (final InvalidProtocolBufferException e) { LOGGER.warn("Exception reading journal", e); return new ArrayList<TxJournalEntry>(); } finally { transactionLock.readLock().unlock(); } } }