/* $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 org.apache.manifoldcf.core.system.ManifoldCF; import java.util.*; /** Throttles for a bin. * An instance of this class keeps track of the information needed to bandwidth throttle access * to a url belonging to a specific bin. * * In order to calculate * the effective "burst" fetches per second and bytes per second, we need to have some idea what the window is. * For example, a long hiatus from fetching could cause overuse of the server when fetching resumes, if the * window length is too long. * * One solution to this problem would be to keep a list of the individual fetches as records. Then, we could * "expire" a fetch by discarding the old record. However, this is quite memory consumptive for all but the * smallest intervals. * * Another, better, solution is to hook into the start and end of individual fetches. These will, presumably, occur * at the fastest possible rate without long pauses spent doing something else. The only complication is that * fetches may well overlap, so we need to "reference count" the fetches to know when to reset the counters. * For "fetches per second", we can simply make sure we "schedule" the next fetch at an appropriate time, rather * than keep records around. The overall rate may therefore be somewhat less than the specified rate, but that's perfectly * acceptable. * * Some notes on the algorithms used to limit server bandwidth impact * ================================================================== * * In a single connection case, the algorithm we'd want to use works like this. On the first chunk of a series, * the total length of time and the number of bytes are recorded. Then, prior to each subsequent chunk, a calculation * is done which attempts to hit the bandwidth target by the end of the chunk read, using the rate of the first chunk * access as a way of estimating how long it will take to fetch those next n bytes. * * For a multi-connection case, which this is, it's harder to either come up with a good maximum bandwidth estimate, * and harder still to "hit the target", because simultaneous fetches will intrude. The strategy is therefore: * * 1) The first chunk of any series should proceed without interference from other connections to the same server. * The goal here is to get a decent quality estimate without any possibility of overwhelming the server. * * 2) The bandwidth of the first chunk is treated as the "maximum bandwidth per connection". That is, if other * connections are going on, we can presume that each connection will use at most the bandwidth that the first fetch * took. Thus, by generating end-time estimates based on this number, we are actually being conservative and * using less server bandwidth. * * 3) For chunks that have started but not finished, we keep track of their size and estimated elapsed time in order to schedule when * new chunks from other connections can start. * * NOTE WELL: This is entirely local in operation */ public class ThrottleBin { /** This signals whether the bin is alive or not. */ protected boolean isAlive = true; /** This is the bin name which this throttle belongs to. */ protected final String binName; /** Service type name */ protected final String serviceTypeName; /** The (anonymous) service name */ protected final String serviceName; /** The target calculation lock name */ protected final String targetCalcLockName; /** The minimum milliseconds per byte */ protected double minimumMillisecondsPerByte = Double.MAX_VALUE; /** The local minimum milliseconds per byte */ protected double localMinimum = Double.MAX_VALUE; /** This is the reference count for this bin (which records active references) */ protected volatile int refCount = 0; /** The inverse rate estimate of the first fetch, in ms/byte */ protected double rateEstimate = 0.0; /** Flag indicating whether a rate estimate is needed */ protected volatile boolean estimateValid = false; /** Flag indicating whether rate estimation is in progress yet */ protected volatile boolean estimateInProgress = false; /** The start time of this series */ protected long seriesStartTime = -1L; /** Total actual bytes read in this series; this includes fetches in progress */ protected long totalBytesRead = -1L; /** The service type prefix for throttle bins */ protected final static String serviceTypePrefix = "_THROTTLEBIN_"; /** The target calculation lock prefix */ protected final static String targetCalcLockPrefix = "_THROTTLEBINTARGET_"; /** Constructor. */ public ThrottleBin(IThreadContext threadContext, String throttlingGroupName, String binName) throws ManifoldCFException { this.binName = binName; this.serviceTypeName = buildServiceTypeName(throttlingGroupName, binName); this.targetCalcLockName = buildTargetCalcLockName(throttlingGroupName, binName); // Now, register and activate service anonymously, and record the service name we get. ILockManager lockManager = LockManagerFactory.make(threadContext); this.serviceName = lockManager.registerServiceBeginServiceActivity(serviceTypeName, null, null); } protected static String buildServiceTypeName(String throttlingGroupName, String binName) { return serviceTypePrefix + throttlingGroupName + "_" + binName; } protected static String buildTargetCalcLockName(String throttlingGroupName, String binName) { return targetCalcLockPrefix + throttlingGroupName + "_" + binName; } /** Get the bin name. */ public String getBinName() { return binName; } /** Update minimumMillisecondsPerBytePerServer */ public synchronized void updateMinimumMillisecondsPerByte(double min) { this.minimumMillisecondsPerByte = min; } /** Note the start of a fetch operation for a bin. Call this method just before the actual stream access begins. * May wait until schedule allows. */ public void beginFetch() { synchronized (this) { if (refCount == 0) { // Now, reset bandwidth throttling counters estimateValid = false; rateEstimate = 0.0; totalBytesRead = 0L; estimateInProgress = false; seriesStartTime = -1L; } refCount++; } } /** Abort the fetch. */ public void abortFetch() { synchronized (this) { refCount--; } } /** Note the start of an individual byte read of a specified size. Call this method just before the * read request takes place. Performs the necessary delay prior to reading specified number of bytes from the server. *@return false if the wait was interrupted due to the bin being shut down. */ public boolean beginRead(int byteCount, IBreakCheck breakCheck) throws InterruptedException, BreakException { synchronized (this) { while (true) { if (!isAlive) return false; if (estimateInProgress) { if (breakCheck == null) { wait(); } else { long amt = breakCheck.abortCheck(); wait(amt); } continue; } // Update the current time long currentTime = System.currentTimeMillis(); if (estimateValid == false) { seriesStartTime = currentTime; estimateInProgress = true; // Add these bytes to the estimated total totalBytesRead += (long)byteCount; // Exit early; this thread isn't going to do any waiting return true; } // If we haven't set a proper throttle yet, wait until we do. if (localMinimum == Double.MAX_VALUE) { if (breakCheck == null) { wait(); } else { long amt = breakCheck.abortCheck(); wait(amt); } continue; } // Estimate the time this read will take, and wait accordingly long estimatedTime = (long)(rateEstimate * (double)byteCount); // Figure out how long the total byte count should take, to meet the constraint long desiredEndTime = seriesStartTime + (long)(((double)(totalBytesRead + (long)byteCount)) * localMinimum); // The wait time is the difference between our desired end time, minus the estimated time to read the data, and the // current time. But it can't be negative. long waitTime = (desiredEndTime - estimatedTime) - currentTime; // If no wait is needed, go ahead and update what needs to be updated and exit. Otherwise, do the wait. if (waitTime <= 0L) { // Add these bytes to the estimated total totalBytesRead += (long)byteCount; return true; } if (breakCheck == null) { this.wait(waitTime); } else { long amt = breakCheck.abortCheck(); if (waitTime < amt) amt = waitTime; wait(amt); } // Back around again... } } } /** Abort a read in progress. */ public void abortRead() { synchronized (this) { if (estimateInProgress) { estimateInProgress = false; notifyAll(); } } } /** Note the end of an individual read from the server. Call this just after an individual read completes. * Pass the actual number of bytes read to the method. */ public void endRead(int originalCount, int actualCount) { synchronized (this) { totalBytesRead = totalBytesRead + (long)actualCount - (long)originalCount; if (estimateInProgress) { if (actualCount == 0) // Didn't actually get any bytes, so use 0.0 rateEstimate = 0.0; else rateEstimate = ((double)(System.currentTimeMillis() - seriesStartTime))/(double)actualCount; estimateValid = true; estimateInProgress = false; notifyAll(); } } } /** Note the end of a fetch operation. Call this method just after the fetch completes. */ public boolean endFetch() { synchronized (this) { refCount--; return (refCount == 0); } } /** Poll this bin */ public synchronized void poll(IThreadContext threadContext) throws ManifoldCFException { // Enter write lock ILockManager lockManager = LockManagerFactory.make(threadContext); lockManager.enterWriteLock(targetCalcLockName); try { // The cross-cluster apportionment of byte fetching goes here. // For byte-rate throttling, the apportioning algorithm is simple. First, it's done // in bytes per millisecond, which is the inverse of what we actually use for the // rest of this class. Each service posts its current value for the maximum bytes // per millisecond, and a target value for the same. // The target value is computed as follows: // (1) Target is summed cross-cluster, excluding our local service. This is GlobalTarget. // (2) MaximumTarget is computed, which is Maximum-GlobalTarget. // (3) FairTarget is computed, which is Maximum/numServices + rand(Maximum%numServices). // (4) Finally, we compute Target by taking the minimum of MaximumTarget, FairTarget. // Compute MaximumTarget SumClass sumClass = new SumClass(serviceName); lockManager.scanServiceData(serviceTypeName, sumClass); int numServices = sumClass.getNumServices(); if (numServices == 0) return; double globalTarget = sumClass.getGlobalTarget(); double globalMaxBytesPerMillisecond; double maximumTarget; double fairTarget; if (minimumMillisecondsPerByte == 0.0) { //System.out.println(binName+":Global minimum milliseconds per byte = 0.0"); globalMaxBytesPerMillisecond = Double.MAX_VALUE; maximumTarget = globalMaxBytesPerMillisecond; fairTarget = globalMaxBytesPerMillisecond; } else { globalMaxBytesPerMillisecond = 1.0 / minimumMillisecondsPerByte; //System.out.println(binName+":Global max bytes per millisecond = "+globalMaxBytesPerMillisecond); maximumTarget = globalMaxBytesPerMillisecond - globalTarget; if (maximumTarget < 0.0) maximumTarget = 0.0; // Compute FairTarget fairTarget = globalMaxBytesPerMillisecond / numServices; } // Now compute actual target double inverseTarget = maximumTarget; if (inverseTarget > fairTarget) inverseTarget = fairTarget; //System.out.println(binName+":Inverse target = "+inverseTarget+"; maximumTarget = "+maximumTarget+"; fairTarget = "+fairTarget); // Write these values to the service data variables. // NOTE that there is a race condition here; the target value depends on all the calculations above being accurate, and not changing out from under us. // So, that's why we have a write lock around the pool calculations. lockManager.updateServiceData(serviceTypeName, serviceName, pack(inverseTarget)); // Update our local minimum. double target; if (inverseTarget == 0.0) target = Double.MAX_VALUE; else target = 1.0 / inverseTarget; // Reset local minimum, if it has changed. if (target == localMinimum) return; //System.out.println(binName+":Updating local minimum to "+target); localMinimum = target; notifyAll(); } finally { lockManager.leaveWriteLock(targetCalcLockName); } } /** Shut down this bin. */ public synchronized void shutDown(IThreadContext threadContext) throws ManifoldCFException { isAlive = false; notifyAll(); ILockManager lockManager = LockManagerFactory.make(threadContext); lockManager.endServiceActivity(serviceTypeName, serviceName); } // Protected classes and methods protected static class SumClass implements IServiceDataAcceptor { protected final String serviceName; protected int numServices = 0; protected double globalTargetTally = 0; public SumClass(String serviceName) { this.serviceName = serviceName; } @Override public boolean acceptServiceData(String serviceName, byte[] serviceData) throws ManifoldCFException { numServices++; if (!serviceName.equals(this.serviceName)) { globalTargetTally += unpackTarget(serviceData); } return false; } public int getNumServices() { return numServices; } public double getGlobalTarget() { return globalTargetTally; } } protected static double unpackTarget(byte[] data) { if (data == null || data.length != 8) return 0.0; return Double.longBitsToDouble((((long)data[0]) & 0xffL) + ((((long)data[1]) << 8) & 0xff00L) + ((((long)data[2]) << 16) & 0xff0000L) + ((((long)data[3]) << 24) & 0xff000000L) + ((((long)data[4]) << 32) & 0xff00000000L) + ((((long)data[5]) << 40) & 0xff0000000000L) + ((((long)data[6]) << 48) & 0xff000000000000L) + ((((long)data[7]) << 56) & 0xff00000000000000L)); } protected static byte[] pack(double targetDouble) { long target = Double.doubleToLongBits(targetDouble); byte[] rval = new byte[8]; rval[0] = (byte)(target & 0xffL); rval[1] = (byte)((target >> 8) & 0xffL); rval[2] = (byte)((target >> 16) & 0xffL); rval[3] = (byte)((target >> 24) & 0xffL); rval[4] = (byte)((target >> 32) & 0xffL); rval[5] = (byte)((target >> 40) & 0xffL); rval[6] = (byte)((target >> 48) & 0xffL); rval[7] = (byte)((target >> 56) & 0xffL); return rval; } }