/* $Id$ */ /** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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. */ package org.apache.manifoldcf.connectorcommon.throttler; import org.apache.manifoldcf.core.interfaces.*; import org.apache.manifoldcf.connectorcommon.interfaces.*; import java.util.*; import java.util.concurrent.atomic.*; /** A Throttler object creates a virtual pool of connections to resources * whose access needs to be throttled in number, rate of use, and byte rate. * This code is modeled on the code for distributed connection pools, and is intended * to work in a similar manner. Basically, a periodic assessment is done about what the * local throttling parameters should be (on a per-pool basis), and the local throttling * activities then adjust what they are doing based on the new parameters. A service * model is used to keep track of which pools have what clients working with them. * This implementation has the advantage that: * (1) Only local throttling ever takes place on a method-by-method basis, which makes * it possible to use throttling even in streams and background threads; * (2) Throttling resources are apportioned fairly, on average, between all the various * cluster members, so it is unlikely that any persistent starvation conditions can * arise. */ public class Throttler { public static final String _rcsid = "@(#)$Id$"; /** Throttle group hash table. Keyed by throttle group type, value is throttling groups */ protected final Map<String,ThrottlingGroups> throttleGroupsHash = new HashMap<String,ThrottlingGroups>(); /** Create a throttler instance. Usually there will be one of these per connector * type that needs throttling. */ public Throttler() { } // There are a lot of synchronizers to coordinate here. They are indeed hierarchical. It is not possible to simply // throw a synchronizer at every level, and require that we hold all of them, because when we wait somewhere in the // inner level, we will continue to hold locks and block access to all the outer levels. // // Instead, I've opted for a model whereby individual resources are protected. This is tricky to coordinate, though, // because (for instance) after a resource has been removed from the hash table, it had better be cleaned up // thoroughly before the outer lock is removed, or two versions of the resource might wind up coming into existence. // The general rule is therefore: // (1) Creation or deletion of resources involves locking the parent where the resource is being added or removed // (2) Anything that waits CANNOT also add or remove. /** Get all existing throttle groups for a throttle group type. * The throttle group type typically describes a connector class, while the throttle group represents * a namespace of bin names specific to that connector class. *@param throttleGroupType is the throttle group type. *@return the set of throttle groups for that group type. */ public Set<String> getThrottleGroups(IThreadContext threadContext, String throttleGroupType) throws ManifoldCFException { synchronized (throttleGroupsHash) { return throttleGroupsHash.keySet(); } } /** Remove a throttle group. *@param throttleGroupType is the throttle group type. *@param throttleGroup is the throttle group. */ public void removeThrottleGroup(IThreadContext threadContext, String throttleGroupType, String throttleGroup) throws ManifoldCFException { // Removal. Lock the whole hierarchy. synchronized (throttleGroupsHash) { ThrottlingGroups tg = throttleGroupsHash.get(throttleGroupType); if (tg != null) { tg.removeThrottleGroup(threadContext, throttleGroup); } } } /** Set or update throttle specification for a throttle group. This creates the * throttle group if it does not yet exist. *@param throttleGroupType is the throttle group type. *@param throttleGroup is the throttle group. *@param throttleSpec is the desired throttle specification object. */ public void createOrUpdateThrottleGroup(IThreadContext threadContext, String throttleGroupType, String throttleGroup, IThrottleSpec throttleSpec) throws ManifoldCFException { // Potential addition. Lock the whole hierarchy. synchronized (throttleGroupsHash) { ThrottlingGroups tg = throttleGroupsHash.get(throttleGroupType); if (tg == null) { tg = new ThrottlingGroups(throttleGroupType); throttleGroupsHash.put(throttleGroupType, tg); } tg.createOrUpdateThrottleGroup(threadContext, throttleGroup, throttleSpec); } } /** Construct connection throttler for connections with specific bin names. This object is meant to be embedded with a connection * pool of similar objects, and used to gate the creation of new connections in that pool. *@param throttleGroupType is the throttle group type. *@param throttleGroup is the throttle group. *@param binNames are the connection type bin names. *@return the connection throttling object, or null if the pool is being shut down. */ public IConnectionThrottler obtainConnectionThrottler(IThreadContext threadContext, String throttleGroupType, String throttleGroup, String[] binNames) throws ManifoldCFException { // No waiting, so lock the entire tree. synchronized (throttleGroupsHash) { ThrottlingGroups tg = throttleGroupsHash.get(throttleGroupType); if (tg != null) return tg.obtainConnectionThrottler(threadContext, throttleGroup, binNames); return null; } } /** Poll periodically. */ public void poll(IThreadContext threadContext, String throttleGroupType) throws ManifoldCFException { // No waiting, so lock the entire tree. synchronized (throttleGroupsHash) { ThrottlingGroups tg = throttleGroupsHash.get(throttleGroupType); if (tg != null) tg.poll(threadContext); } } /** Poll ALL bins periodically. */ public void poll(IThreadContext threadContext) throws ManifoldCFException { // No waiting, so lock the entire tree. synchronized (throttleGroupsHash) { for (ThrottlingGroups tg : throttleGroupsHash.values()) { tg.poll(threadContext); } } } /** Free unused resources. */ public void freeUnusedResources(IThreadContext threadContext) throws ManifoldCFException { // This potentially affects the entire hierarchy. // Go through the whole pool and clean it out synchronized (throttleGroupsHash) { Iterator<ThrottlingGroups> iter = throttleGroupsHash.values().iterator(); while (iter.hasNext()) { ThrottlingGroups p = iter.next(); p.freeUnusedResources(threadContext); } } } /** Shut down all throttlers and deregister them. */ public void destroy(IThreadContext threadContext) throws ManifoldCFException { // This affects the entire hierarchy, so lock the whole thing. // Go through the whole pool and clean it out synchronized (throttleGroupsHash) { Iterator<ThrottlingGroups> iter = throttleGroupsHash.values().iterator(); while (iter.hasNext()) { ThrottlingGroups p = iter.next(); p.destroy(threadContext); iter.remove(); } } } // Protected methods and classes protected static String buildThrottlingGroupName(String throttlingGroupType, String throttlingGroupName) { return throttlingGroupType + "_" + throttlingGroupName; } /** This class represents a throttling group pool */ protected class ThrottlingGroups { /** The throttling group type for this throttling group pool */ protected final String throttlingGroupTypeName; /** The pool of individual throttle group services for this pool, keyed by throttle group name */ protected final Map<String,ThrottlingGroup> groups = new HashMap<String,ThrottlingGroup>(); public ThrottlingGroups(String throttlingGroupTypeName) { this.throttlingGroupTypeName = throttlingGroupTypeName; } /** Update throttle specification */ public void createOrUpdateThrottleGroup(IThreadContext threadContext, String throttleGroup, IThrottleSpec throttleSpec) throws ManifoldCFException { synchronized (groups) { ThrottlingGroup g = groups.get(throttleGroup); if (g == null) { g = new ThrottlingGroup(threadContext, throttlingGroupTypeName, throttleGroup, throttleSpec); groups.put(throttleGroup, g); } else { g.updateThrottleSpecification(throttleSpec); } } } /** Obtain connection throttler. *@return the throttler, or null of the hierarchy has changed. */ public IConnectionThrottler obtainConnectionThrottler(IThreadContext threadContext, String throttleGroup, String[] binNames) throws ManifoldCFException { synchronized (groups) { ThrottlingGroup g = groups.get(throttleGroup); if (g == null) return null; return g.obtainConnectionThrottler(threadContext, binNames); } } /** Remove specified throttle group */ public void removeThrottleGroup(IThreadContext threadContext, String throttleGroup) throws ManifoldCFException { // Must synch the whole thing, because otherwise there would be a risk of someone recreating the // group right after we removed it from the map, and before we destroyed it. synchronized (groups) { ThrottlingGroup g = groups.remove(throttleGroup); if (g != null) { g.destroy(threadContext); } } } /** Poll this set of throttle groups. */ public void poll(IThreadContext threadContext) throws ManifoldCFException { synchronized (groups) { Iterator<String> iter = groups.keySet().iterator(); while (iter.hasNext()) { String throttleGroup = iter.next(); ThrottlingGroup p = groups.get(throttleGroup); p.poll(threadContext); } } } /** Free unused resources */ public void freeUnusedResources(IThreadContext threadContext) throws ManifoldCFException { synchronized (groups) { Iterator<ThrottlingGroup> iter = groups.values().iterator(); while (iter.hasNext()) { ThrottlingGroup g = iter.next(); g.freeUnusedResources(threadContext); } } } /** Destroy and shutdown all */ public void destroy(IThreadContext threadContext) throws ManifoldCFException { synchronized (groups) { Iterator<ThrottlingGroup> iter = groups.values().iterator(); while (iter.hasNext()) { ThrottlingGroup p = iter.next(); p.destroy(threadContext); iter.remove(); } } } } /** This class represents a throttling group, of a specific throttling group type. It basically * describes an entire self-consistent throttling environment. */ protected class ThrottlingGroup { /** The throttling group name */ protected final String throttlingGroupName; /** The current throttle spec */ protected IThrottleSpec throttleSpec; /** The connection bins */ protected final Map<String,ConnectionBin> connectionBins = new HashMap<String,ConnectionBin>(); /** The fetch bins */ protected final Map<String,FetchBin> fetchBins = new HashMap<String,FetchBin>(); /** The throttle bins */ protected final Map<String,ThrottleBin> throttleBins = new HashMap<String,ThrottleBin>(); // For synchronization, we use several in this class. // Modification to the connectionBins, fetchBins, or throttleBins hashes uses the appropriate local synchronizer. // Changes to other local variables use the main synchronizer. /** Constructor */ public ThrottlingGroup(IThreadContext threadContext, String throttlingGroupType, String throttleGroup, IThrottleSpec throttleSpec) throws ManifoldCFException { this.throttlingGroupName = buildThrottlingGroupName(throttlingGroupType, throttleGroup); this.throttleSpec = throttleSpec; // Once all that is done, perform the initial setting of all the bin cutoffs poll(threadContext); } /** Create a bunch of bins, corresponding to the bin names specified. * Note that this also registers them as services etc. *@param binNames describes the set of bins to create. */ public synchronized IConnectionThrottler obtainConnectionThrottler(IThreadContext threadContext, String[] binNames) throws ManifoldCFException { synchronized (connectionBins) { for (String binName : binNames) { ConnectionBin bin = connectionBins.get(binName); if (bin == null) { bin = new ConnectionBin(threadContext, throttlingGroupName, binName); connectionBins.put(binName, bin); } } } synchronized (fetchBins) { for (String binName : binNames) { FetchBin bin = fetchBins.get(binName); if (bin == null) { bin = new FetchBin(threadContext, throttlingGroupName, binName); fetchBins.put(binName, bin); } } } synchronized (throttleBins) { for (String binName : binNames) { ThrottleBin bin = throttleBins.get(binName); if (bin == null) { bin = new ThrottleBin(threadContext, throttlingGroupName, binName); throttleBins.put(binName, bin); } } } return new ConnectionThrottler(this, binNames); } /** Update the throttle spec. *@param throttleSpec is the new throttle spec for this throttle group. */ public synchronized void updateThrottleSpecification(IThrottleSpec throttleSpec) throws ManifoldCFException { this.throttleSpec = throttleSpec; } // IConnectionThrottler support methods /** Wait for a connection to become available. *@param poolCount is a description of how many connections * are available in the current pool, across all bins. *@return the IConnectionThrottler codes for results. */ public int waitConnectionAvailable(String[] binNames, AtomicInteger[] poolCounts, IBreakCheck breakCheck) throws InterruptedException, BreakException { // Each bin can signal something different. Bins that signal // CONNECTION_FROM_NOWHERE are shutting down, but there's also // apparently the conflicting possibilities of distinct answers of // CONNECTION_FROM_POOL and CONNECTION_FROM_CREATION. // However: the pool count we track is in fact N * the actual pool count, // where N is the number of bins in each connection. This means that a conflict // is ALWAYS due to two entities simultaneously calling waitConnectionAvailable(), // and deadlocking each other. The solution is therefore to back off and retry. // This is the retry loop while (true) { int currentRecommendation = IConnectionThrottler.CONNECTION_FROM_NOWHERE; boolean retry = false; // First, make sure all the bins exist, and reserve a slot in each int i = 0; while (i < binNames.length) { String binName = binNames[i]; ConnectionBin bin; synchronized (connectionBins) { bin = connectionBins.get(binName); } if (bin != null) { // Reserve a slot int result; try { result = bin.waitConnectionAvailable(poolCounts[i],breakCheck); } catch (Throwable e) { while (i > 0) { i--; binName = binNames[i]; synchronized (connectionBins) { bin = connectionBins.get(binName); } if (bin != null) bin.undoReservation(currentRecommendation, poolCounts[i]); } if (e instanceof BreakException) throw (BreakException)e; if (e instanceof InterruptedException) throw (InterruptedException)e; if (e instanceof Error) throw (Error)e; if (e instanceof RuntimeException) throw (RuntimeException)e; throw new RuntimeException("Unexpected exception of type '"+e.getClass().getName()+"': "+e.getMessage(),e); } if (result == IConnectionThrottler.CONNECTION_FROM_NOWHERE) { // Release previous reservations, and either return, or retry while (i > 0) { i--; binName = binNames[i]; synchronized (connectionBins) { bin = connectionBins.get(binName); } if (bin != null) bin.undoReservation(currentRecommendation, poolCounts[i]); } return result; } if (currentRecommendation != IConnectionThrottler.CONNECTION_FROM_NOWHERE && currentRecommendation != result) { // Release all previous reservations, including this one, and either return, or retry bin.undoReservation(result, poolCounts[i]); while (i > 0) { i--; binName = binNames[i]; synchronized (connectionBins) { bin = connectionBins.get(binName); } if (bin != null) bin.undoReservation(currentRecommendation, poolCounts[i]); } // Break out of the outer loop so we can retry retry = true; break; } if (currentRecommendation == IConnectionThrottler.CONNECTION_FROM_NOWHERE) currentRecommendation = result; } i++; } if (retry) continue; // Complete the reservation process (if that is what we decided) if (currentRecommendation == IConnectionThrottler.CONNECTION_FROM_CREATION) { // All reservations have been made! Convert them. for (String binName : binNames) { ConnectionBin bin; synchronized (connectionBins) { bin = connectionBins.get(binName); } if (bin != null) bin.noteConnectionCreation(); } } return currentRecommendation; } } public IFetchThrottler getNewConnectionFetchThrottler(String[] binNames) { return new FetchThrottler(this, binNames); } public boolean noteReturnedConnection(String[] binNames) { // If ANY of the bins think the connection should be destroyed, then that will be // the recommendation. synchronized (connectionBins) { boolean destroyConnection = false; for (String binName : binNames) { ConnectionBin bin = connectionBins.get(binName); if (bin != null) { destroyConnection |= bin.shouldReturnedConnectionBeDestroyed(); } } return destroyConnection; } } public boolean checkDestroyPooledConnection(String[] binNames, AtomicInteger[] poolCounts) { // Only if all believe we can destroy a pool connection, will we do it. // This is because some pools may be empty, etc. synchronized (connectionBins) { boolean destroyConnection = false; int i = 0; while (i < binNames.length) { String binName = binNames[i]; ConnectionBin bin = connectionBins.get(binName); if (bin != null) { int result = bin.shouldPooledConnectionBeDestroyed(poolCounts[i]); if (result == ConnectionBin.CONNECTION_POOLEMPTY) { // Give up now, and undo all the other bins while (i > 0) { i--; binName = binNames[i]; bin = connectionBins.get(binName); bin.undoPooledConnectionDecision(poolCounts[i]); } return false; } else if (result == ConnectionBin.CONNECTION_DESTROY) { destroyConnection = true; } } i++; } if (destroyConnection) return true; // Undo pool reservation, since everything is apparently within bounds. for (int j = 0; j < binNames.length; j++) { ConnectionBin bin = connectionBins.get(binNames[j]); if (bin != null) bin.undoPooledConnectionDecision(poolCounts[j]); } return false; } } /** Connection expiration is tricky, because even though a connection may be identified as * being expired, at the very same moment it could be handed out in another thread. So there * is a natural race condition present. * The way the connection throttler deals with that is to allow the caller to reserve a connection * for expiration. This must be called BEFORE the actual identified connection is removed from the * connection pool. If the value returned by this method is "true", then a connection MUST be removed * from the pool and destroyed, whether or not the identified connection is actually still available for * destruction or not. *@return true if a connection from the pool can be expired. If true is returned, noteConnectionDestruction() * MUST be called once the connection has actually been destroyed. */ public boolean checkExpireConnection(String[] binNames, AtomicInteger[] poolCounts) { synchronized (connectionBins) { int i = 0; while (i < binNames.length) { String binName = binNames[i]; ConnectionBin bin = connectionBins.get(binName); if (bin != null) { if (!bin.hasPooledConnection(poolCounts[i])) { // Give up now, and undo all the other bins while (i > 0) { i--; binName = binNames[i]; bin = connectionBins.get(binName); bin.undoPooledConnectionDecision(poolCounts[i]); } return false; } } i++; } return true; } } public void noteConnectionReturnedToPool(String[] binNames, AtomicInteger[] poolCounts) { synchronized (connectionBins) { for (int j = 0; j < binNames.length; j++) { ConnectionBin bin = connectionBins.get(binNames[j]); if (bin != null) bin.noteConnectionReturnedToPool(poolCounts[j]); } } } public void noteConnectionDestroyed(String[] binNames) { synchronized (connectionBins) { for (String binName : binNames) { ConnectionBin bin = connectionBins.get(binName); if (bin != null) bin.noteConnectionDestroyed(); } } } // IFetchThrottler support methods /** Get permission to fetch a document. This grants permission to start * fetching a single document, within the connection that has already been * granted permission that created this object. *@param binNames are the names of the bins. *@return false if being shut down */ public boolean obtainFetchDocumentPermission(String[] binNames, IBreakCheck breakCheck) throws InterruptedException, BreakException { // First, make sure all the bins exist, and reserve a slot in each int i = 0; while (i < binNames.length) { String binName = binNames[i]; FetchBin bin; synchronized (fetchBins) { bin = fetchBins.get(binName); } // Reserve a slot try { if (bin == null || !bin.reserveFetchRequest(breakCheck)) { // Release previous reservations, and return null while (i > 0) { i--; binName = binNames[i]; synchronized (fetchBins) { bin = fetchBins.get(binName); } if (bin != null) bin.clearReservation(); } return false; } } catch (BreakException e) { // Release previous reservations, and rethrow while (i > 0) { i--; binName = binNames[i]; synchronized (fetchBins) { bin = fetchBins.get(binName); } if (bin != null) bin.clearReservation(); } throw e; } i++; } // All reservations have been made! Convert them. // (These are guaranteed to succeed - but they may wait) i = 0; while (i < binNames.length) { String binName = binNames[i]; FetchBin bin; synchronized (fetchBins) { bin = fetchBins.get(binName); } if (bin != null) { try { if (!bin.waitNextFetch(breakCheck)) { // Undo the reservations we haven't processed yet while (i < binNames.length) { binName = binNames[i]; synchronized (fetchBins) { bin = fetchBins.get(binName); } if (bin != null) bin.clearReservation(); i++; } return false; } } catch (BreakException e) { // Undo the reservations we haven't processed yet while (i < binNames.length) { binName = binNames[i]; synchronized (fetchBins) { bin = fetchBins.get(binName); } if (bin != null) bin.clearReservation(); i++; } throw e; } } i++; } return true; } public IStreamThrottler createFetchStream(String[] binNames) { // Do a "begin fetch" for all throttle bins synchronized (throttleBins) { for (String binName : binNames) { ThrottleBin bin = throttleBins.get(binName); if (bin != null) bin.beginFetch(); } } return new StreamThrottler(this, binNames); } // IStreamThrottler support methods /** Obtain permission to read a block of bytes. This method may wait until it is OK to proceed. * The throttle group, bin names, etc are already known * to this specific interface object, so it is unnecessary to include them here. *@param byteCount is the number of bytes to get permissions to read. *@return true if the wait took place as planned, or false if the system is being shut down. */ public boolean obtainReadPermission(String[] binNames, int byteCount, IBreakCheck breakCheck) throws InterruptedException, BreakException { int i = 0; while (i < binNames.length) { String binName = binNames[i]; ThrottleBin bin; synchronized (throttleBins) { bin = throttleBins.get(binName); } try { if (bin == null || !bin.beginRead(byteCount, breakCheck)) { // End bins we've already done, and exit while (i > 0) { i--; binName = binNames[i]; synchronized (throttleBins) { bin = throttleBins.get(binName); } if (bin != null) bin.endRead(byteCount,0); } return false; } } catch (BreakException e) { // End bins we've already done, and exit while (i > 0) { i--; binName = binNames[i]; synchronized (throttleBins) { bin = throttleBins.get(binName); } if (bin != null) bin.endRead(byteCount,0); } throw e; } i++; } return true; } /** Note the completion of the read of a block of bytes. Call this after * obtainReadPermission() was successfully called, and bytes were successfully read. *@param origByteCount is the originally requested number of bytes to get permissions to read. *@param actualByteCount is the number of bytes actually read. */ public void releaseReadPermission(String[] binNames, int origByteCount, int actualByteCount) { synchronized (throttleBins) { for (String binName : binNames) { ThrottleBin bin = throttleBins.get(binName); if (bin != null) bin.endRead(origByteCount, actualByteCount); } } } /** Note the stream being closed. */ public void closeStream(String[] binNames) { synchronized (throttleBins) { for (String binName : binNames) { ThrottleBin bin = throttleBins.get(binName); if (bin != null) bin.endFetch(); } } } // Bookkeeping methods /** Call this periodically. */ public synchronized void poll(IThreadContext threadContext) throws ManifoldCFException { // Go through all existing bins and update each one. synchronized (connectionBins) { for (ConnectionBin bin : connectionBins.values()) { bin.updateMaxActiveConnections(throttleSpec.getMaxOpenConnections(bin.getBinName())); bin.poll(threadContext); } } synchronized (fetchBins) { for (FetchBin bin : fetchBins.values()) { bin.updateMinTimeBetweenFetches(throttleSpec.getMinimumMillisecondsPerFetch(bin.getBinName())); bin.poll(threadContext); } } synchronized (throttleBins) { for (ThrottleBin bin : throttleBins.values()) { bin.updateMinimumMillisecondsPerByte(throttleSpec.getMinimumMillisecondsPerByte(bin.getBinName())); bin.poll(threadContext); } } } /** Free unused resources. */ public synchronized void freeUnusedResources(IThreadContext threadContext) throws ManifoldCFException { // Does nothing; there are not really resources to free } /** Destroy this pool. */ public synchronized void destroy(IThreadContext threadContext) throws ManifoldCFException { synchronized (connectionBins) { Iterator<ConnectionBin> binIter = connectionBins.values().iterator(); while (binIter.hasNext()) { ConnectionBin bin = binIter.next(); bin.shutDown(threadContext); binIter.remove(); } } synchronized (fetchBins) { Iterator<FetchBin> binIter = fetchBins.values().iterator(); while (binIter.hasNext()) { FetchBin bin = binIter.next(); bin.shutDown(threadContext); binIter.remove(); } } synchronized (throttleBins) { Iterator<ThrottleBin> binIter = throttleBins.values().iterator(); while (binIter.hasNext()) { ThrottleBin bin = binIter.next(); bin.shutDown(threadContext); binIter.remove(); } } } } /** Connection throttler implementation class. * This class instance stores some parameters and links back to ThrottlingGroup. But each class instance * models a connection pool with the specified bins. But the description of each pool consists of more than just * the bin names that describe the throttling - it also may include connection parameters which we have * no insight into at this level. * * Thus, in order to do pool tracking properly, we cannot simply rely on the individual connection bin instances * to do all the work, since they cannot distinguish between different pools properly. So that leaves us with * two choices. (1) We can somehow push the separate pool instance parameters down to the connection bin * level, or (2) the connection bins cannot actually do any waiting or blocking. * * The benefit of having blocking take place in connection bins is that they are in fact designed to be precisely * the thing you would want to synchronize on. If we presume that the waits happen in those classes, * then we need the ability to send in our local pool count to them, and we need to be able to "wake up" * those underlying classes when the local pool count changes. */ protected static class ConnectionThrottler implements IConnectionThrottler { protected final ThrottlingGroup parent; protected final String[] binNames; protected final AtomicInteger[] poolCounts; // Keep track of local pool parameters. public ConnectionThrottler(ThrottlingGroup parent, String[] binNames) { this.parent = parent; this.binNames = binNames; this.poolCounts = new AtomicInteger[binNames.length]; for (int i = 0; i < poolCounts.length; i++) poolCounts[i] = new AtomicInteger(0); } /** Get permission to grab a connection for use. If this object believes there is a connection * available in the pool, it will update its pool size variable and return If not, this method * evaluates whether a new connection should be created. If neither condition is true, it * waits until a connection is available. *@return whether to take the connection from the pool, or create one, or whether the * throttler is being shut down. */ @Override public int waitConnectionAvailable() throws InterruptedException { try { return waitConnectionAvailable(null); } catch (BreakException e) { throw new RuntimeException("Unexpected break exception: "+e.getMessage(),e); } } /** Get permission to grab a connection for use. If this object believes there is a connection * available in the pool, it will update its pool size variable and return If not, this method * evaluates whether a new connection should be created. If neither condition is true, it * waits until a connection is available. *@return whether to take the connection from the pool, or create one, or whether the * throttler is being shut down. */ @Override public int waitConnectionAvailable(IBreakCheck breakCheck) throws InterruptedException, BreakException { return parent.waitConnectionAvailable(binNames, poolCounts, breakCheck); } /** For a new connection, obtain the fetch throttler to use for the connection. * If the result from waitConnectionAvailable() is CONNECTION_FROM_CREATION, * the calling code is expected to create a connection using the result of this method. *@return the fetch throttler for a new connection. */ @Override public IFetchThrottler getNewConnectionFetchThrottler() { return parent.getNewConnectionFetchThrottler(binNames); } /** For returning a connection from use, there is only one method. This method signals /* whether a formerly in-use connection should be placed back in the pool or destroyed. *@return true if the connection should NOT be put into the pool but should instead * simply be destroyed. If true is returned, the caller MUST call noteConnectionDestroyed() * (below) in order for the bookkeeping to work. */ @Override public boolean noteReturnedConnection() { return parent.noteReturnedConnection(binNames); } /** This method calculates whether a connection should be taken from the pool and destroyed /* in order to meet quota requirements. If this method returns /* true, you MUST remove a connection from the pool, and you MUST call /* noteConnectionDestroyed() afterwards. *@return true if a pooled connection should be destroyed. If true is returned, the * caller MUST call noteConnectionDestroyed() (below) in order for the bookkeeping to work. */ @Override public boolean checkDestroyPooledConnection() { return parent.checkDestroyPooledConnection(binNames, poolCounts); } /** Connection expiration is tricky, because even though a connection may be identified as * being expired, at the very same moment it could be handed out in another thread. So there * is a natural race condition present. * The way the connection throttler deals with that is to allow the caller to reserve a connection * for expiration. This must be called BEFORE the actual identified connection is removed from the * connection pool. If the value returned by this method is "true", then a connection MUST be removed * from the pool and destroyed, whether or not the identified connection is actually still available for * destruction or not. *@return true if a connection from the pool can be expired. If true is returned, noteConnectionDestruction() * MUST be called once the connection has actually been destroyed. */ @Override public boolean checkExpireConnection() { return parent.checkExpireConnection(binNames, poolCounts); } /** Note that a connection has been returned to the pool. Call this method after a connection has been * placed back into the pool and is available for use. */ @Override public void noteConnectionReturnedToPool() { parent.noteConnectionReturnedToPool(binNames, poolCounts); } /** Note that a connection has been destroyed. Call this method ONLY after noteReturnedConnection() * or checkDestroyPooledConnection() returns true, AND the connection has been already * destroyed. */ @Override public void noteConnectionDestroyed() { parent.noteConnectionDestroyed(binNames); } } /** Fetch throttler implementation class. * This basically stores some parameters and links back to ThrottlingGroup. */ protected static class FetchThrottler implements IFetchThrottler { protected final ThrottlingGroup parent; protected final String[] binNames; public FetchThrottler(ThrottlingGroup parent, String[] binNames) { this.parent = parent; this.binNames = binNames; } /** Get permission to fetch a document. This grants permission to start * fetching a single document, within the connection that has already been * granted permission that created this object. *@return false if the throttler is being shut down. */ @Override public boolean obtainFetchDocumentPermission() throws InterruptedException { try { return obtainFetchDocumentPermission(null); } catch (BreakException e) { throw new RuntimeException("Unexpected break exception: "+e.getMessage(),e); } } /** Get permission to fetch a document. This grants permission to start * fetching a single document, within the connection that has already been * granted permission that created this object. *@return false if the throttler is being shut down. */ @Override public boolean obtainFetchDocumentPermission(IBreakCheck breakCheck) throws InterruptedException, BreakException { return parent.obtainFetchDocumentPermission(binNames,breakCheck); } /** Open a fetch stream. When done (or aborting), call * IStreamThrottler.closeStream() to note the completion of the document * fetch activity. *@return the stream throttler to use to throttle the actual data access. */ @Override public IStreamThrottler createFetchStream() { return parent.createFetchStream(binNames); } } /** Stream throttler implementation class. * This basically stores some parameters and links back to ThrottlingGroup. */ protected static class StreamThrottler implements IStreamThrottler { protected final ThrottlingGroup parent; protected final String[] binNames; public StreamThrottler(ThrottlingGroup parent, String[] binNames) { this.parent = parent; this.binNames = binNames; } /** Obtain permission to read a block of bytes. This method may wait until it is OK to proceed. * The throttle group, bin names, etc are already known * to this specific interface object, so it is unnecessary to include them here. *@param byteCount is the number of bytes to get permissions to read. *@return true if the wait took place as planned, or false if the system is being shut down. */ @Override public boolean obtainReadPermission(int byteCount) throws InterruptedException { try { return obtainReadPermission(byteCount, null); } catch (BreakException e) { throw new RuntimeException("Unexpected break exception: "+e.getMessage(),e); } } /** Obtain permission to read a block of bytes. This method may wait until it is OK to proceed. * The throttle group, bin names, etc are already known * to this specific interface object, so it is unnecessary to include them here. *@param byteCount is the number of bytes to get permissions to read. *@param breakCheck is the break check object. *@return true if the wait took place as planned, or false if the system is being shut down. */ @Override public boolean obtainReadPermission(int byteCount, IBreakCheck breakCheck) throws InterruptedException, BreakException { return parent.obtainReadPermission(binNames, byteCount, breakCheck); } /** Note the completion of the read of a block of bytes. Call this after * obtainReadPermission() was successfully called, and bytes were successfully read. *@param origByteCount is the originally requested number of bytes to get permissions to read. *@param actualByteCount is the number of bytes actually read. */ @Override public void releaseReadPermission(int origByteCount, int actualByteCount) { parent.releaseReadPermission(binNames, origByteCount, actualByteCount); } /** Note the stream being closed. */ @Override public void closeStream() { parent.closeStream(binNames); } } }