/* * JBoss, Home of Professional Open Source * Copyright 2009, Red Hat Middleware LLC, and individual contributors * as indicated by the @author tags. * See the copyright.txt in the distribution for a * full listing of individual contributors. * 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, v. 2.1. * This program is distributed in the hope that it will be useful, but WITHOUT A * 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, * v.2.1 along with this distribution; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * * (C) 2009, * @author JBoss by Red Hat. */ package com.arjuna.ats.internal.arjuna.coordinator; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /* * Encapsulation of a specialised data structure with API and performance characteristics * designed specifically for use by the transaction reaper. * * ReaperElements represent transactions which need timing out. To do this, the reaper needs * to wake periodically and process any timeouts that are due. New elements are added on transaction * creation and will be removed prior to their timeout if they terminate normally. * * For high concurrency, normal inserts and removes should not block. However, to determine the next element * which needs (will need) processing, the elements must be ordered or at least searched. These requirements * are in conflict, since ordering/searching requires stability i.e. locking. * * To achieve the desired performance characteristics, we combine two data structures: an unsorted, concurrent * collection and a sorted, non-threadsafe one which is guarded by the the ReaperElementManager instance lock. * * Inserts are done, potentially concurrently, to the unsorted hash set. Removes likewise check this first * and can return successfully without blocking if the element is found in this collection. Thus the insert/remove * are cheap operations. * * When it is required to know the smallest (i.e. earliest to timeout) element, the contents * of the unsorted set are moved to the sorted set. Since this happens infrequently compared to the insert/delete, * only a fraction of the elements inserted should ever be copied - most will be removed without ever migrating. * * Note that additional external synchronization will be needed to ensure first element does not change * between getFirst and any operation depending on its timeout value. This is the TransactionReaper's problem. * * The sorted set is maintained manually, rather than using Collections.sort or other comparator based structure. * This is because compareTo on reaper elements is relatively expensive and we wish to avoid liner scans to minimise * the number of such calls. Hence we prefer ArrayList with binary search, despite the higher insert/remove cost * compared to LinkedList. * * Pay careful attention to locking and performance characteristics if altering this class. * * * @author Jonathan Halliday (jonathan.halliday@redhat.com) 2009-10 */ public class ReaperElementManager { /** * @return the first (i.e. earliest to time out) element of the colleciton or null if empty */ public synchronized ReaperElement getFirst() { flushPending(); // we need to order the elements before we can tell which is first. if(elementsOrderedByTimeout.isEmpty()) { return null; } else { return elementsOrderedByTimeout.get(0); } } // Note - unsynchronized for performance. public void add(ReaperElement reaperElement) throws IllegalStateException { if(pendingInsertions.putIfAbsent(reaperElement, reaperElement) != null) { // note this is best effort - we'll allow double inserts if the element is also in the ordered set. throw new IllegalStateException(); } } /** * @param reaperElement the reaper element to reorder in the sorted set. * @param delayMillis the amount of time to increment the element's timeout by. * @return the new soonest timeout in the set (not necessarily that of the reordered element) */ public synchronized long reorder(ReaperElement reaperElement, long delayMillis) { // assume it must be in the sorted list, as it was likely obtained via getFirst... removeSorted(reaperElement); // we could add delay to the original timeout, but using current time is probably safer. reaperElement.setAbsoluteTimeout((System.currentTimeMillis() + delayMillis)); // reinsert into its new position. insertSorted(reaperElement); // getFirst takes care of flushing the pending set for us. return getFirst().getAbsoluteTimeout(); } // use only for testing, it's nasty from a performance perspective. public synchronized int size() { return (elementsOrderedByTimeout.size() + pendingInsertions.size()); } public synchronized boolean isEmpty() { return (elementsOrderedByTimeout.isEmpty() && pendingInsertions.isEmpty()); } // strange hack to force instant expire of tx during shutdown. public synchronized void setAllTimeoutsToZero() { flushPending(); for(ReaperElement reaperElement : elementsOrderedByTimeout) { reaperElement.setAbsoluteTimeout(0); } } // Note - mostly unsynchronized for performance. public void remove(ReaperElement reaperElement) { if(pendingInsertions.remove(reaperElement) != null) { return; } // we missed finding it in the unsorted set - perhaps it has already been copied to the sorted set... synchronized(this) { removeSorted(reaperElement); } } //////////// // Private methods and structures are guarded where needed by ReaperElementManager instance locks in the // public methods - see class header doc comments for concurrency/performance info. private final ArrayList<ReaperElement> elementsOrderedByTimeout = new ArrayList<ReaperElement>(); private final ConcurrentHashMap<ReaperElement, ReaperElement> pendingInsertions = new ConcurrentHashMap<ReaperElement, ReaperElement>(); private void removeSorted(ReaperElement reaperElement) { int location = Collections.binarySearch(elementsOrderedByTimeout, reaperElement); if(location >= 0) { elementsOrderedByTimeout.remove(location); } } private void insertSorted(ReaperElement reaperElement) { int location = Collections.binarySearch(elementsOrderedByTimeout, reaperElement); if(location >= 0) { throw new IllegalStateException(); } int insertionPoint = -(location + 1); elementsOrderedByTimeout.add(insertionPoint, reaperElement); } private void flushPending() { // purge the pending inserts before doing anything else. This is potentially expensive. // Future versions may prefer to insert only a portion of the pending set, or // iterate it each time to determine the smallest (head) element. Set<Map.Entry<ReaperElement,ReaperElement>> entrySet = pendingInsertions.entrySet(); if(entrySet != null) { Iterator<Map.Entry<ReaperElement, ReaperElement>> queueIter = entrySet.iterator(); // iterator is weakly consistent - will traverse elements present at its time of creation, // may or may not see later updates. while(queueIter.hasNext()) { Map.Entry<ReaperElement,ReaperElement> entry = queueIter.next(); ReaperElement element = entry.getValue(); // insert/remove not locked, so we are careful to check that we don't insert // an element that has been removed from the pending set by a concurrent thread. if(entrySet.remove(entry)) { insertSorted(element); } } } } }