/* * Bitronix Transaction Manager * * Copyright (c) 2011, Juergen Kellerer. * * This copyrighted material is made available to anyone wishing to use, modify, * copy, or redistribute it subject to the terms and conditions of the GNU * Lesser General Public License, as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this distribution; if not, write to: * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA */ package bitronix.tm.journal.nio; import bitronix.tm.journal.nio.util.SequencedBlockingQueue; import bitronix.tm.journal.nio.util.SequencedQueueEntry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.ListIterator; import java.util.concurrent.Callable; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; /** * Synchronizes 'force' calls between the write requesting threads and the thread that does the actual write IO. * <p/> * This class is also responsible for transmitting failure cases back to the requester (if it is waiting * on force to complete). * <p/> * Note: This is a low level implementation that is not meant to be used externally. * * @author juergen kellerer, 2011-04-30 */ class NioForceSynchronizer { private static final Logger log = LoggerFactory.getLogger(NioForceSynchronizer.class); private final ReentrantLock forceLock = new ReentrantLock(); private final Condition performedForce = forceLock.newCondition(); private final AtomicLong latestForcedElement = new AtomicLong(), latestFailedElement = new AtomicLong(); private final List<FailedRange> failures = new CopyOnWriteArrayList<FailedRange>(); private final SequencedBlockingQueue pendingRecordsQueue; NioForceSynchronizer(SequencedBlockingQueue pendingRecordsQueue) { this.pendingRecordsQueue = pendingRecordsQueue; } /** * Wait on the latest, previously enlisted element to get forced or failed. * <p/> * Important: A call to this method measures the storage of all enlisted element that * were enlisted in the current thread under the assumption that elements are stored * in the order they were placed in the queue. * * @return returns true if the force operation succeeded and false if an IO error was reported. */ public boolean waitOnEnlisted() { long enlistedElementNumber = pendingRecordsQueue.getMaxElementSequenceNumberForCurrentThread(true); try { // Wait until we have our entry forced (may not require a wait at all if it already happened). forceLock.lockInterruptibly(); try { while (enlistedElementNumber > latestForcedElement.get()) { if (verifyIsInFailedRange(enlistedElementNumber)) return false; if (log.isDebugEnabled()) { log.debug("Waiting until entry with sequence " + enlistedElementNumber + " was forced."); } performedForce.await(); } } finally { forceLock.unlock(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(e); } // Check if we had an exception. if (verifyIsInFailedRange(enlistedElementNumber)) { return false; } else { if (log.isDebugEnabled()) { log.debug("Entry with sequence " + enlistedElementNumber + " was successfully forced (force ranges up to " + latestForcedElement.get() + ")."); } return true; } } /** * Returns the number of threads waiting on a force to happen. * * @return the number of threads waiting on a force to happen. */ public int getNumberOfWaitingThreads() { forceLock.lock(); try { return forceLock.getWaitQueueLength(performedForce); } finally { forceLock.unlock(); } } /** * Processes the enlisted elements with the given force command and notifies the waiting * threads on success or failure. The operation does nothing if no thread is waiting. * * @param forceCommand the command to run prior to notifying the threads. * @param elements the elements that are processed by command (and are the base for notification). * @return returns true if the command was called and threads have been notified. * @throws Exception the exception thrown by the command if it failed. */ public boolean processEnlistedIfRequired(Callable forceCommand, Collection<? extends SequencedQueueEntry> elements) throws Exception { forceLock.lock(); try { final int waitingThreads = forceLock.getWaitQueueLength(performedForce); if (waitingThreads > 0) { if (log.isDebugEnabled()) { log.debug("Found " + waitingThreads + " threads waiting on force to happen. Forcing " + elements + "log entries to disk now."); } processEnlisted(forceCommand, elements); return true; } } finally { forceLock.unlock(); } return false; } /** * Processes the enlisted elements with the given force command and notifies any waiting * threads on success or failure. * * @param forceCommand the command to run prior to notifying the threads. * @param elements the elements that are processed by command (and are the base for notification). * @throws Exception the exception thrown by the command if it failed. */ public void processEnlisted(Callable forceCommand, Collection<? extends SequencedQueueEntry> elements) throws Exception { forceLock.lock(); try { try { forceCommand.call(); recordSuccess(elements); } catch (Exception e) { recordFailures(elements); throw e; } finally { performedForce.signalAll(); } } finally { forceLock.unlock(); } } private void recordSuccess(Collection<? extends SequencedQueueEntry> elements) { long largestInList = 0, n; for (SequencedQueueEntry entry : elements) { n = entry.getSequenceNumber(); if (n > largestInList) largestInList = n; } if (incrementTo(latestForcedElement, largestInList)) { if (log.isDebugEnabled()) { log.debug("Set the latest forced element sequence to " + largestInList + "."); } } } private void recordFailures(Collection<? extends SequencedQueueEntry> elements) { final boolean debug = log.isDebugEnabled(); for (SequencedQueueEntry entry : elements) { final long elementSequenceNumber = entry.getSequenceNumber(); if (incrementTo(latestFailedElement, elementSequenceNumber)) if (debug) { log.debug("Set the latest failed element sequence number to " + elementSequenceNumber + ".");} FailedRange latestRange = failures.isEmpty() ? null : failures.get(failures.size() - 1); if (latestRange == null || !latestRange.addToRange(elementSequenceNumber)) { if (debug) { log.debug("Creating new failed range for failed element " + entry + "."); } failures.add(new FailedRange(elementSequenceNumber)); } } } private boolean verifyIsInFailedRange(long elementSequenceNumber) { if (latestFailedElement.get() >= elementSequenceNumber) { final boolean debug = log.isDebugEnabled(); for (ListIterator<FailedRange> i = failures.listIterator(failures.size()); i.hasPrevious(); ) { FailedRange failedRange = i.previous(); if (failedRange.isInRange(elementSequenceNumber)) { if (debug) { log.debug("Reporting that force failed on entry with sequence number " + elementSequenceNumber + " (" + failedRange + ")."); } return true; } } } return false; } private static boolean incrementTo(final AtomicLong number, final long toNumber) { long n; while ((n = number.get()) <= toNumber) { if (number.compareAndSet(n, toNumber)) return true; } return false; } /** * Keeps a range of failed elements. */ static class FailedRange { final long createTime = System.currentTimeMillis(); final AtomicLong firstFailedElement, lastFailedElement; FailedRange(long initialNumber) { this.firstFailedElement = new AtomicLong(initialNumber); this.lastFailedElement = new AtomicLong(initialNumber); } boolean isInRange(final long elementSequenceNumber) { final long lowerBound = firstFailedElement.get(), upperBound = lastFailedElement.get(); return elementSequenceNumber >= lowerBound && elementSequenceNumber <= upperBound; } boolean addToRange(final long elementSequenceNumber) { final long lowerBound = firstFailedElement.get(), upperBound = lastFailedElement.get(); if (elementSequenceNumber == upperBound + 1) lastFailedElement.compareAndSet(upperBound, elementSequenceNumber); else if (elementSequenceNumber == lowerBound - 1) firstFailedElement.compareAndSet(lowerBound, elementSequenceNumber); return isInRange(elementSequenceNumber); } @Override public String toString() { return "FailedRange{" + "createTime=" + new Date(createTime) + ", firstFailedElement=" + firstFailedElement + ", lastFailedElement=" + lastFailedElement + '}'; } } }