/* $Id: Throttler.java 1865954 2019-08-26 20:27:25Z schuch $ */

/**
* 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: Throttler.java 1865954 2019-08-26 20:27:25Z schuch $";

  /** 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 poolCounts 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);
    }

  }
  
}
